I / O 작업을위한 별도의 스레드가 합리적입니다.
예를 들어, 동일한 UI 스레드에서 사용자가 누른 버튼을 기록하는 것은 좋지 않습니다. 이러한 UI는 임의로 중단되며 성능 이 느려 집니다 .
해결책은 이벤트를 처리에서 분리하는 것입니다.
게임 개발 세계의 프로듀서-소비자 문제 및 이벤트 큐에 대한 많은 정보가 있습니다.
종종 같은 코드가 있습니다
///Never do this!!!
public void WriteLog_Like_Bastard(string msg)
{
lock (_lockBecauseILoveThreadContention)
{
File.WriteAllText("c:\\superApp.log", msg);
}
}
이 접근 방식은 스레드 경합으로 이어집니다. 모든 처리 스레드는 잠금을 확보하고 동일한 파일에 한 번에 쓸 수 있도록 싸우고 있습니다.
일부는 잠금 장치를 제거하려고 할 수 있습니다.
public void Log_Like_Dumbass(string msg)
{
try
{ File.Append("c:\\superApp.log", msg); }
catch (Exception ex)
{
MessageBox.Show("Log file may be locked by other process...")
}
}
}
2 개의 스레드가 동시에 메소드에 들어가면 결과를 예측할 수 없습니다.
결국 개발자는 전혀 로깅을 비활성화합니다 ...
고칠 수 있습니까?
예.
인터페이스가 있다고 가정 해 봅시다.
public interface ILogger
{
void Debug(string message);
// ... etc
void Fatal(string message);
}
ILogger
호출 될 때마다 잠금 대기 및 파일 차단 작업을 수행하는 대신 Penging Messages Queue에 새 LogMessage 를 추가 하고 더 중요한 항목으로 돌아갑니다.
public class AsyncLogger : ILogger
{
private readonly BlockingCollection<LogMessage> _pendingMessages;
private readonly Type _loggerFor;
private readonly IThreadAdapter _threadAdapter;
public AsyncLogger(BlockingCollection<LogMessage> pendingMessages, Type loggerFor, IThreadAdapter threadAdapter)
{
_pendingMessages = pendingMessages;
_loggerFor = loggerFor;
_threadAdapter = threadAdapter;
}
public void Debug(string message)
{
Push(LoggingLevel.Debug, message);
}
public void Fatal(string message)
{
Push(LoggingLevel.Fatal, message);
}
private void Push(LoggingLevel importance, string message)
{
// since we do not know when our log entry will be written to disk, remember current time
var timestamp = DateTime.Now;
var threadId = _threadAdapter.GetCurrentThreadId();
// adds message to the queue in lock-free manner and immediately returns control to caller
_pendingMessages.Add(LogMessage.Create(timestamp, importance, message, _loggerFor, threadId));
}
}
우리는이 간단한 비동기 로거를 사용했습니다 .
다음 단계는 들어오는 메시지를 처리하는 것입니다.
간단하게하기 위해 새 스레드를 시작 하고 응용 프로그램이 종료 될 때까지 기다리거나 Asynchronous Logger 가 보류중인 큐에 새 메시지를 추가 합니다.
public class LoggingQueueDispatcher : IQueueDispatcher
{
private readonly BlockingCollection<LogMessage> _pendingMessages;
private readonly IEnumerable<ILogListener> _listeners;
private readonly IThreadAdapter _threadAdapter;
private readonly ILogger _logger;
private Thread _dispatcherThread;
public LoggingQueueDispatcher(BlockingCollection<LogMessage> pendingMessages, IEnumerable<ILogListener> listeners, IThreadAdapter threadAdapter, ILogger logger)
{
_pendingMessages = pendingMessages;
_listeners = listeners;
_threadAdapter = threadAdapter;
_logger = logger;
}
public void Start()
{
// Here I use 'new' operator, only to simplify example. Should be using interface '_threadAdapter.CreateBackgroundThread' to allow unit testing
Thread thread = new Thread(MessageLoop);
thread.Name = "LoggingQueueDispatcher Thread";
thread.IsBackground = true;
thread.Start();
_logger.Debug("Asked to start log message Dispatcher ");
_dispatcherThread = thread;
}
public bool WaitForCompletion(TimeSpan timeout)
{
return _dispatcherThread.Join(timeout);
}
private void MessageLoop()
{
_logger.Debug("Entering dispatcher message loop...");
var cancellationToken = new CancellationTokenSource();
LogMessage message;
while (_pendingMessages.TryTake(out message, Timeout.Infinite, cancellationToken.Token))
{
// !!!!! Now it is safe to use File.AppendAllText("c:\\my.log") without ever using lock or forcing important threads to wait.
// this is example, do not use in production
foreach (var listener in _listeners)
{
listener.Log(message);
}
}
}
}
맞춤 리스너 체인을 전달하고 있습니다. 통화 로깅 프레임 워크 ( log4net
등)를 보내려고 할 수도 있습니다.
나머지 코드는 다음과 같습니다.
public enum LoggingLevel
{
Debug,
// ... etc
Fatal,
}
public class LogMessage
{
public DateTime Timestamp { get; private set; }
public LoggingLevel Importance { get; private set; }
public string Message { get; private set; }
public Type Source { get; private set; }
public int ThreadId { get; private set; }
private LogMessage(DateTime timestamp, LoggingLevel importance, string message, Type source, int threadId)
{
Timestamp = timestamp;
Message = message;
Source = source;
ThreadId = threadId;
Importance = importance;
}
public static LogMessage Create(DateTime timestamp, LoggingLevel importance, string message, Type source, int threadId)
{
return new LogMessage(timestamp, importance, message, source, threadId);
}
public override string ToString()
{
return string.Format("{0} [TID:{4}] {1:h:mm:ss} ({2})\t{3}", Importance, Timestamp, Source, Message, ThreadId);
}
}
public class LoggerFactory : ILoggerFactory
{
private readonly BlockingCollection<LogMessage> _pendingMessages;
private readonly IThreadAdapter _threadAdapter;
private readonly ConcurrentDictionary<Type, ILogger> _loggersCache = new ConcurrentDictionary<Type, ILogger>();
public LoggerFactory(BlockingCollection<LogMessage> pendingMessages, IThreadAdapter threadAdapter)
{
_pendingMessages = pendingMessages;
_threadAdapter = threadAdapter;
}
public ILogger For(Type loggerFor)
{
return _loggersCache.GetOrAdd(loggerFor, new AsyncLogger(_pendingMessages, loggerFor, _threadAdapter));
}
}
public class ThreadAdapter : IThreadAdapter
{
public int GetCurrentThreadId()
{
return Thread.CurrentThread.ManagedThreadId;
}
}
public class ConsoleLogListener : ILogListener
{
public void Log(LogMessage message)
{
Console.WriteLine(message.ToString());
Debug.WriteLine(message.ToString());
}
}
public class SimpleTextFileLogger : ILogListener
{
private readonly IFileSystem _fileSystem;
private readonly string _userRoamingPath;
private readonly string _logFileName;
private FileStream _fileStream;
public SimpleTextFileLogger(IFileSystem fileSystem, string userRoamingPath, string logFileName)
{
_fileSystem = fileSystem;
_userRoamingPath = userRoamingPath;
_logFileName = logFileName;
}
public void Start()
{
_fileStream = new FileStream(_fileSystem.Path.Combine(_userRoamingPath, _logFileName), FileMode.Append);
}
public void Stop()
{
if (_fileStream != null)
{
_fileStream.Dispose();
}
}
public void Log(LogMessage message)
{
var bytes = Encoding.UTF8.GetBytes(message.ToString() + Environment.NewLine);
_fileStream.Write(bytes, 0, bytes.Length);
}
}
public interface ILoggerFactory
{
ILogger For(Type loggerFor);
}
public interface ILogListener
{
void Log(LogMessage message);
}
public interface IThreadAdapter
{
int GetCurrentThreadId();
}
public interface IQueueDispatcher
{
void Start();
}
진입 지점:
public static class Program
{
public static void Main()
{
Debug.WriteLine("[Program] Entering Main ...");
var pendingLogQueue = new BlockingCollection<LogMessage>();
var threadAdapter = new ThreadAdapter();
var loggerFactory = new LoggerFactory(pendingLogQueue, threadAdapter);
var fileSystem = new FileSystem();
var userRoamingPath = GetUserDataDirectory(fileSystem);
var simpleTextFileLogger = new SimpleTextFileLogger(fileSystem, userRoamingPath, "log.txt");
simpleTextFileLogger.Start();
ILogListener consoleListener = new ConsoleLogListener();
ILogListener[] listeners = new [] { simpleTextFileLogger , consoleListener};
var loggingQueueDispatcher = new LoggingQueueDispatcher(pendingLogQueue, listeners, threadAdapter, loggerFactory.For(typeof(LoggingQueueDispatcher)));
loggingQueueDispatcher.Start();
var logger = loggerFactory.For(typeof(Console));
string line;
while ((line = Console.ReadLine()) != "exit")
{
logger.Debug("you have entered: " + line);
}
logger.Fatal("Exiting...");
Debug.WriteLine("[Program] pending LogQueue will be stopped now...");
pendingLogQueue.CompleteAdding();
var logQueueCompleted = loggingQueueDispatcher.WaitForCompletion(TimeSpan.FromSeconds(5));
simpleTextFileLogger.Stop();
Debug.WriteLine("[Program] Exiting... logQueueCompleted: " + logQueueCompleted);
}
private static string GetUserDataDirectory(FileSystem fileSystem)
{
var roamingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var userDataDirectory = fileSystem.Path.Combine(roamingDirectory, "Async Logging Sample");
if (!fileSystem.Directory.Exists(userDataDirectory))
fileSystem.Directory.CreateDirectory(userDataDirectory);
return userDataDirectory;
}
}