创建简单的多线程安全日志记录类的最佳方法是什么?这样的事情足够吗?如何在最初创建日志时清除日志?
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();
}
}
答案 0 :(得分:22)
以下是使用BlockingCollection通过Producer / Consumer模式(使用.Net 4)实现的Log的示例。界面是:
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();
}
}
}
在此示例中,用于通过BlockingCollection将信息传递给写入线程的内部Param类是:
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;
}
}
}
答案 1 :(得分:12)
不,每次调用方法时都会创建一个新的锁对象。如果要确保一次只有一个线程可以执行该函数中的代码,则将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 :(得分:7)
使用单个监视器(锁定)创建线程安全的日志记录实现不太可能产生积极的结果。虽然您可以正确地执行此操作,并且已经发布了几个答案,显示如何,但它会对性能产生显着的负面影响,因为每个执行日志记录的对象都必须与执行日志记录的每个其他对象同步。获得多于一个或两个线程同时执行此操作,突然间您可能会花费更多时间等待处理。
使用单一监视器方法遇到的另一个问题是,您无法保证线程将按照它们最初请求的顺序获取锁定。因此,日志条目可能基本上不按顺序出现。如果你将它用于跟踪日志记录,这可能会令人沮丧。
多线程很难。轻轻接近它总会导致错误。
解决这个问题的一种方法是实现Producer/Consumer pattern,其中记录器的调用者只需要写入内存缓冲区并立即返回而不是等待记录器写入磁盘,从而大大减少了性能惩罚。日志记录框架将在一个单独的线程上使用日志数据并保留它。
答案 3 :(得分:5)
您需要在类级声明同步对象:
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建议。
答案 4 :(得分:0)
该问题使用的不是异步方法File.AppendText
,其他答案正确显示使用lock
是做到这一点的方法。
但是,在许多实际情况下,首选使用异步方法,因此调用方不必等待它被编写。 lock
在这种情况下没有用,因为它会阻塞线程,并且async
块内也不允许使用lock
方法。
在这种情况下,您可以使用信号量(C#中的SemaphoreSlim
类)来实现相同的功能,但是它具有异步性并允许在锁定区域内调用异步函数的优点。
下面是使用SemaphoreSlim
作为异步锁的快速示例:
// a semaphore as a private field in Logging class:
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
// Inside WriteToLog method:
try
{
await semaphore.WaitAsync();
// Code to write log to file asynchronously
}
finally
{
semaphore.Release();
}
请注意,始终在try..finally
块中使用信号量是一个好习惯,因此,即使代码抛出异常,信号量也会被正确释放。