Простой класс безопасного журнала 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, поэтому даже если код выдает исключение, семафор освобождается правильно.