Простой класс безопасного журнала MultiThread
Каков наилучший подход к созданию простого многопоточного безопасного журнала? Достаточно ли этого? Как я могу очистить журнал при его первоначальном создании?
public class Logging
{
public Logging()
{
}
public void WriteToLog(string message)
{
object locker = new object();
lock(locker)
{
StreamWriter SW;
SW=File.AppendText("Data\\Log.txt");
SW.WriteLine(message);
SW.Close();
}
}
}
public partial class MainWindow : Window
{
public static MainWindow Instance { get; private set; }
public Logging Log { get; set; }
public MainWindow()
{
Instance = this;
Log = new Logging();
}
}
Ответы
Ответ 1
Нет, вы создаете новый объект блокировки каждый раз при вызове метода. Если вы хотите, чтобы только один поток за один раз мог выполнить код в этой функции, переместите locker
из функции либо в экземпляр, либо в статический член. Если этот класс создается каждый раз, когда запись должна быть записана, значит, locker
должен быть статическим.
public class Logging
{
public Logging()
{
}
private static readonly object locker = new object();
public void WriteToLog(string message)
{
lock(locker)
{
StreamWriter SW;
SW=File.AppendText("Data\\Log.txt");
SW.WriteLine(message);
SW.Close();
}
}
}
Ответ 2
Вот пример для журнала, реализованного с шаблоном Producer/Consumer (с .Net 4) с использованием BlockingCollection. Интерфейс:
namespace Log
{
public interface ILogger
{
void WriteLine(string msg);
void WriteError(string errorMsg);
void WriteError(string errorObject, string errorAction, string errorMsg);
void WriteWarning(string errorObject, string errorAction, string errorMsg);
}
}
и полный код класса:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Log
{
// Reentrant Logger written with Producer/Consumer pattern.
// It creates a thread that receives write commands through a Queue (a BlockingCollection).
// The user of this log has just to call Logger.WriteLine() and the log is transparently written asynchronously.
public class Logger : ILogger
{
BlockingCollection<Param> bc = new BlockingCollection<Param>();
// Constructor create the thread that wait for work on .GetConsumingEnumerable()
public Logger()
{
Task.Factory.StartNew(() =>
{
foreach (Param p in bc.GetConsumingEnumerable())
{
switch (p.Ltype)
{
case Log.Param.LogType.Info:
const string LINE_MSG = "[{0}] {1}";
Console.WriteLine(String.Format(LINE_MSG, LogTimeStamp(), p.Msg));
break;
case Log.Param.LogType.Warning:
const string WARNING_MSG = "[{3}] * Warning {0} (Action {1} on {2})";
Console.WriteLine(String.Format(WARNING_MSG, p.Msg, p.Action, p.Obj, LogTimeStamp()));
break;
case Log.Param.LogType.Error:
const string ERROR_MSG = "[{3}] *** Error {0} (Action {1} on {2})";
Console.WriteLine(String.Format(ERROR_MSG, p.Msg, p.Action, p.Obj, LogTimeStamp()));
break;
case Log.Param.LogType.SimpleError:
const string ERROR_MSG_SIMPLE = "[{0}] *** Error {1}";
Console.WriteLine(String.Format(ERROR_MSG_SIMPLE, LogTimeStamp(), p.Msg));
break;
default:
Console.WriteLine(String.Format(LINE_MSG, LogTimeStamp(), p.Msg));
break;
}
}
});
}
~Logger()
{
// Free the writing thread
bc.CompleteAdding();
}
// Just call this method to log something (it will return quickly because it just queue the work with bc.Add(p))
public void WriteLine(string msg)
{
Param p = new Param(Log.Param.LogType.Info, msg);
bc.Add(p);
}
public void WriteError(string errorMsg)
{
Param p = new Param(Log.Param.LogType.SimpleError, errorMsg);
bc.Add(p);
}
public void WriteError(string errorObject, string errorAction, string errorMsg)
{
Param p = new Param(Log.Param.LogType.Error, errorMsg, errorAction, errorObject);
bc.Add(p);
}
public void WriteWarning(string errorObject, string errorAction, string errorMsg)
{
Param p = new Param(Log.Param.LogType.Warning, errorMsg, errorAction, errorObject);
bc.Add(p);
}
string LogTimeStamp()
{
DateTime now = DateTime.Now;
return now.ToShortTimeString();
}
}
}
В этом примере внутренний класс Param, используемый для передачи информации в поток записи через BlockingCollection:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Log
{
internal class Param
{
internal enum LogType { Info, Warning, Error, SimpleError };
internal LogType Ltype { get; set; } // Type of log
internal string Msg { get; set; } // Message
internal string Action { get; set; } // Action when error or warning occurs (optional)
internal string Obj { get; set; } // Object that was processed whend error or warning occurs (optional)
internal Param()
{
Ltype = LogType.Info;
Msg = "";
}
internal Param(LogType logType, string logMsg)
{
Ltype = logType;
Msg = logMsg;
}
internal Param(LogType logType, string logMsg, string logAction, string logObj)
{
Ltype = logType;
Msg = logMsg;
Action = logAction;
Obj = logObj;
}
}
}
Ответ 3
Создание потоковой реализации регистрации с использованием одного монитора (блокировки) вряд ли даст положительные результаты. Хотя вы можете сделать это правильно, и было опубликовано несколько ответов, показывающих, как это повлияло бы на производительность, поскольку каждый объект, выполняющий ведение журнала, должен будет синхронизироваться с любым другим объектом, выполняющим ведение журнала. Получите более одного или двух потоков, делая это в одно и то же время, и внезапно вы можете потратить больше времени на ожидание, чем на обработку.
Другая проблема, с которой вы сталкиваетесь с подходом с одним монитором, заключается в том, что у вас нет гарантии, что потоки получат блокировку в том порядке, в котором они первоначально запросили ее. Таким образом, записи журнала могут по существу выглядеть неработоспособными. Это может расстраивать, если вы используете это для ведения журнала трассировки.
Многопоточность жесткая. Приближение к нему легко приведет к ошибкам.
Одним из подходов к этой проблеме было бы реализовать образец производителя/потребителя, в котором вызывающим абонентам только нужно записывать в буфер памяти и немедленно возвращаться, а не ждать, пока регистратор напишет на диск, тем самым резко снизив эффективность. Структура журнала в отдельном потоке потребляет данные журнала и сохраняет его.
Ответ 4
вам нужно объявить объект синхронизации на уровне класса:
public class Logging
{
private static readonly object locker = new object();
public Logging()
{
}
public void WriteToLog(string message)
{
lock(locker)
{
StreamWriter SW;
SW=File.AppendText("Data\\Log.txt");
SW.WriteLine(message);
SW.Close();
SW.Dispose();
}
}
}
Может быть, лучше объявить ваш класс ведения журнала как static
, и предложит объект блокировки как @Adam Robinson.
Ответ 5
Этот вопрос использует File.AppendText
который не является асинхронным методом, и другие ответы правильно показывают, что использование lock
- это способ сделать это.
Однако во многих реальных случаях использование асинхронного метода является предпочтительным, поэтому вызывающему не нужно ждать, пока это будет записано. lock
не является полезным в том случае, поскольку он блокирует нить, а также async
методы не допускается внутри lock
блока.
В такой ситуации вы можете использовать Semaphores (класс SemaphoreSlim
в С#) для достижения того же самого, но с преимуществом асинхронности и возможности вызова асинхронных функций внутри зоны блокировки.
Вот краткий пример использования SemaphoreSlim
в качестве асинхронной блокировки:
// a semaphore in Logging class
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
// Inside log method
try
{
await semaphore.WaitAsync();
// Code to write log to file asynchronously
}
finally
{
semaphore.Release();
}
Обратите внимание, что рекомендуется всегда использовать семафоры в блоках try..finally
, поэтому даже если код выдает исключение, семафор освобождается правильно.