C# 简单多线程安全日志类

C# 简单多线程安全日志类,c#,C#,创建简单的多线程安全日志类的最佳方法是什么?这样就足够了吗?最初创建日志时,如何清除日志 public class Logging { public Logging() { } public void WriteToLog(string message) { object locker = new object(); lock(locker) { StreamWriter SW;

创建简单的多线程安全日志类的最佳方法是什么?这样就足够了吗?最初创建日志时,如何清除日志

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();
    }
}

您需要在类级别声明同步对象:

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建议的锁定对象。

否,每次调用该方法时都会创建一个新的锁定对象。如果要确保一次只有一个线程可以执行该函数中的代码,请将
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();
        }
    }
}

使用单个监视器(锁)创建线程安全日志记录实现不太可能产生积极的结果。虽然您可以正确地做到这一点,并且已经发布了几个答案来说明如何做到这一点,但这将对性能产生巨大的负面影响,因为每个进行日志记录的对象都必须与其他进行日志记录的对象同步。让一个或两个以上的线程同时执行此操作,您可能会突然花更多的时间等待而不是处理

使用单监视器方法遇到的另一个问题是,无法保证线程将按照最初请求的顺序获取锁。因此,日志条目可能基本上是无序的。如果您将其用于跟踪日志记录,这可能会令人沮丧

多线程是很难的。轻率地接近它总是会导致错误


解决此问题的一种方法是实现,其中记录器的调用方只需写入内存缓冲区并立即返回,而不是等待记录器写入磁盘,从而大大降低了性能损失。日志框架将在单独的线程上使用日志数据并将其持久化。

以下是使用BlockingCollection使用生产者/消费者模式(使用.Net 4)实现的日志示例。界面为:

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();
        }

    }
}

问题使用了
File.AppendText
,这不是一种异步方法,其他答案正确地表明使用
lock
是一种方法

然而,在许多实际情况下,使用异步方法是首选的,这样调用者就不必等待它被写入。在这种情况下,
lock
没有用处,因为它会阻塞线程,而且
lock
块中不允许使用
async
方法

在这种情况下,您可以使用信号量(C#中的类)来实现同样的功能,但其好处是异步,并允许在锁定区域内调用异步函数

下面是一个使用
信号量lim
作为异步锁的快速示例:

// 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
块中始终使用信号量是一种很好的做法,因此即使代码抛出异常,信号量被正确释放。

如何清除构造函数中的日志?我已经更新了代码,以说明如何使用日志类。@Robert:那么您应该按照我编写的代码(一个带有静态锁定变量的非静态类)。虽然在其他地方可以有多个
Logging
类的实例,但您仍然希望一次只能有一个实例写入该文件。因此,如果其他线程跳过它们仍在记录日志,或者该工作将被跳过,而不是编写您自己的日志实现,会发生什么情况,确保你已经看过了。如果这是一个只Windows应用程序和并行性能是一个问题,也可以考虑。非常好的例子,如何解决多线程问题。这应该被更多的投票支持。很容易扩展。感谢@efdummy花时间发布此消息。你是说efdummy的答案是正确的吗?@Perdi Estaquel在我发布三年后发布的该答案提出了一个特定的生产者/消费者解决方案。我还没有评估具体的实现,但我支持我在回答中所述的方法。
// 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();
}