Il thread separato per l'operazione I \ O sembra ragionevole.
Ad esempio, non sarebbe utile registrare quali pulsanti l'utente ha premuto nello stesso thread dell'interfaccia utente. Tale UI si bloccherà a caso e avrà prestazioni percepite lentamente .
La soluzione è disaccoppiare l'evento dalla sua elaborazione.
Ecco molte informazioni sul problema produttore-consumatore e la coda degli eventi dal mondo dello sviluppo di giochi
Spesso c'è un codice simile
///Never do this!!!
public void WriteLog_Like_Bastard(string msg)
{
lock (_lockBecauseILoveThreadContention)
{
File.WriteAllText("c:\\superApp.log", msg);
}
}
Questo approccio porterà alla discussione sul thread. Tutti i thread di elaborazione combatteranno per poter ottenere il blocco e scrivere nello stesso file in una sola volta.
Alcuni potrebbero provare a rimuovere i blocchi.
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...")
}
}
}
Non è possibile prevedere il risultato se 2 thread accederanno al metodo contemporaneamente.
Quindi alla fine gli sviluppatori disabiliteranno la registrazione a tutti ...
È possibile riparare?
Sì.
Diciamo che abbiamo un'interfaccia:
public interface ILogger
{
void Debug(string message);
// ... etc
void Fatal(string message);
}
Invece di attendere il blocco e di eseguire l'operazione di blocco dei file ogni volta che ILogger
viene chiamato, aggiungeremo il nuovo LogMessage alla coda dei messaggi Penging e torneremo alle cose più importanti:
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));
}
}
Abbiamo fatto con questo semplice Logger asincrono .
Il prossimo passo è elaborare i messaggi in arrivo.
Per semplicità, avvia il nuovo thread e attendi per sempre fino alla chiusura dell'applicazione o il Logger asincrono aggiungerà un nuovo messaggio alla coda in sospeso .
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);
}
}
}
}
Sto passando una catena di ascoltatori personalizzati. Probabilmente potresti voler semplicemente inviare il framework di registrazione delle chiamate ( log4net
, ecc ...)
Ecco il resto del codice:
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();
}
Punto d'entrata:
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;
}
}