【问题标题】:Getting away from BackgroundWorker towards TPL for a logging class从 BackgroundWorker 转向 TPL 以获取日志记录类
【发布时间】:2015-08-26 14:57:59
【问题描述】:

我目前在旧的Backgroundworker 类的视图中编写了一个简单的事件记录器。我正在尝试将其转换为 TPL 实现。

我在 C# 中没有足够的线程使用来真正喜欢其中一个而不是另一个,但我知道 TPL 正变得越来越受欢迎,我想尽可能地坚持使用它。另一个原因是,使用当前代码,我找不到使EventLog 类线程安全的简单方法。我发现自己使用BeginInvoke 从非 UI 线程写入日志,这对我来说似乎很乱。

所以这里是原始代码。

public class EventLog
{
    public String LogPath { get; set; }
    public List<LogEvent> Events { get; private set; }

    public static EventLog Instance { get { return lazyInstance.Value; } }
    private static readonly Lazy<EventLog> lazyInstance = new Lazy<EventLog>(() => new EventLog());

    private EventLog()
    {
        Events = new List<LogEvent>();
        LogPath = Assembly.GetExecutingAssembly().CodeBase;
        LogPath = Path.GetDirectoryName(LogPath);
        LogPath = LogPath.Replace("file:\\", "");
        LogPath = LogPath + "\\Log.txt";
    }

    public override void publish(LogEvent newEvent)
    {
        Events.Add(newEvent);
        if (!LogEventWriter.Instance.IsBusy)
            LogEventWriter.Instance.RunWorkerAsync(LogPath);
        LogEventWriter.Instance.LogEvents.Add(newEvent);
    }
}

internal class LogEventWriter : BackgroundWorker
{
    public BlockingCollection<LogEvent> LogEvents { get; set; }

    public static LogEventWriter Instance { get { return lazyInstance.Value; } }
    private static readonly Lazy<LogEventWriter> lazyInstance = new Lazy<LogEventWriter>(() => new LogEventWriter());

    private LogEventWriter()
    {
        WorkerSupportsCancellation = true;
        LogEvents = new BlockingCollection<LogEvent>();
    }

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        if (e.Argument != null && e.Argument is String)
        {
            String logPath = (String)e.Argument;
            using (StreamWriter logFile = new StreamWriter(logPath, true))
            {
                while (!CancellationPending)
                {
                    LogEvent anEvent = LogEvents.Take();
                    logFile.WriteLine(anEvent.Message);
                    logFile.Flush();
                    if (anEvent.Message.Contains("Application Terminated"))
                        break;
                }
                logFile.Close();
            }
        }
        e.Cancel = true;
    }
}

我目前对日志的思路是在系统发生故障时尽快将日志写入文件,以便日志包含尽可能多的信息。这就是Backgroundworker 的用途。我还只是在EventLog 类中保留了一个List&lt;LogEvent&gt;,以便用户可以在当前日志中搜索特定事件(未完全实现/完善)。

这是我目前的 TPL 解决方案。我已尽我所能将日志记录功能包装到Tasks 中,但我仍然觉得我应该有一个类似于publish 的功能,而不必直接将LogEvents 放入BlockingCollection&lt;&gt; 中,这样我可以在主 UI 的单独线程上运行日志记录。

还有一种更简洁的方法来阻止Tasks,而不必从他们的循环中向break 发送“特殊”LogEvent 给他们?

public class EventLog
{
    public static EventLog Instance { get { return lazyInstance.Value; } }
    private static readonly Lazy<EventLog> lazyInstance = new Lazy<EventLog>(() => new EventLog());

    public String LogPath { get; set; }
    public ConcurrentQueue<LogEvent> Events { get; set; }

    private EventLog()
    {
        Events = new ConcurrentQueue<LogEvent>();
        WriteQueue = new BlockingCollection<LogEvent>();
        LogEventQueue = new BlockingCollection<LogEvent>();

        LogPath = Assembly.GetExecutingAssembly().CodeBase;
        LogPath = Path.GetDirectoryName(LogPath);
        LogPath = LogPath.Replace("file:\\", "");
        LogPath = LogPath + "\\LogASDF.txt";

        StartManager();
        StartWriter();
    }

    public BlockingCollection<LogEvent> LogEventQueue { get; set; }
    private void StartManager()
    {
        var writeTask = Task.Factory.StartNew(() =>
        {
            while (true)
            {
                LogEvent anEvent = LogEventQueue.Take();
                Events.Enqueue(anEvent);
                WriteQueue.Add(anEvent);
                if (anEvent.Message.Contains("Application Terminated"))
                    break;
            }
        });
    }

    private BlockingCollection<LogEvent> WriteQueue { get; set; }
    private void StartWriter()
    {
        var writeTask = Task.Factory.StartNew(() =>
        {
            using (StreamWriter logFile = new StreamWriter(LogPath, true))
            {
                while(true)
                {
                    LogEvent anEvent = WriteQueue.Take();
                    logFile.WriteLine(anEvent.Message);
                    logFile.Flush();
                    if (anEvent.Message.Contains("Application Terminated"))
                        break;
                }
                logFile.Close();
            }
        });
    }
}
  1. 如何正确使用CancellationToken 取消这两个任务?我不明白如果BlockingCollection 被阻塞,我总是必须“脉冲”集合以使其解除阻塞。
  2. 是否有一种“更简洁”的方式可以将LogEvent 插入日志而不必直接将其插入LogEventQueue

【问题讨论】:

  • LogEventEventLog 对我来说是不好的命名......
  • 是的,我知道.. 我还没有完全完善这段代码。

标签: c# multithreading task-parallel-library backgroundworker


【解决方案1】:

现在你的代码不是thread-safe,因为你有这个:

public List<LogEvent> Events { get; private set; }

List&lt;T&gt; 不是线程安全的,可以从外部代码更改。而且我根本看不到它是否被使用过。

另外,你真的应该在你的代码中使用CancellationToken,因为在其他情况下你可能会遇到麻烦:例如,你有 5 条消息在队列中,你决定取消你的工作。在这种情况下,Shutdown Log 的检查将仅在一段时间后才会中断循环,这会让您班级的最终用户感到困惑。

此外,BlockingCollection&lt;T&gt;.Take 方法与 CancellationToken 存在重载,但如果取消,您将捕获 OperationCanceledException

try
{
    LogEvent anEvent = WriteQueue.Take(CancellationPending);
}
catch (OperationCanceledException ex)
{
    // handle stop here;
}

无限循环在多线程中非常糟糕的做法,我建议不要使用它。

【讨论】:

  • 是的,我正要发布一个编辑,我用另一个ConcurrentCollection 代替Events。 // 我想这就是我的问题。当BlockingCollection 阻塞时,我如何使用CacellationToken?我总是必须“脉冲”BlockingCollection 以使其停止阻塞,以便我什至可以检查令牌。
  • 啊,我没有看到Take 的那个论点,不知道我是怎么错过的。在我原来的帖子中,我有第二个问题,你愿意解决它吗? // 另外,您能否建议一种不使用while(true) 不断等待/写入日志的方法?
  • 我知道如何解决它。当我使用CancellationTokens 时,我可以将它们用于循环而不是true
  • 你的帖子还有问题吗?
  • 不,你帮我解决了这个问题,谢谢!
【解决方案2】:

以下是我使用 .net 4.5 处理此问题的方法。对事件队列的所有访问都是同步的,因此不需要锁定或同步:

public class EventLog
{
    public String LogPath { get; set; }
    public List<LogEvent> Events {get;set;}
    private isProcessing = false;
    public CancellationTokenSource cts = new CancellationTokenSource();
    private CancellationToken _token;

    public static EventLog Instance { get { return lazyInstance.Value; } }
    private static readonly Lazy<EventLog> lazyInstance = new Lazy<EventLog>(() => new EventLog());

    private EventLog()
    {
        Events = new List<LogEvent>();
        Events.CollectionChanged += Events_CollectionChanged;
        LogPath = Assembly.GetExecutingAssembly().CodeBase;
        LogPath = Path.GetDirectoryName(LogPath);
        LogPath = LogPath.Replace("file:\\", "");
        LogPath = LogPath + "\\Log.txt";
        _token = cts.Token; 
    }

    public override void publish(LogEvent newEvent)
    {
        Events.Add(newEvent);
        if (!isProcessing)
            ProcessLog();
    }

    private async void ProcessLog()
    {
        while (Events.Count > 0)
        {
            isProcessing = true;
            LogEvent e = EventLogs.First();
            await Task.Run (() => { WriteLog(e,token); },_token);
            EventLogs.Remove(e);
            if (_token.IsCancellationRequested == true)
                EventLogs.Clear();
        }
        isProcessing = false;
    }

    private void WriteLog(LogEvent e,CancellationToken token)
    {
        using (StreamWriter logFile = new StreamWriter(LogPath, true))
        {
            if (token.IsCancellationRequested == false)
            {
                logFile.WriteLine(e.Message);
                logFile.Flush();
            }
        }
    }
}

编辑:添加了取消标记。 编辑 2:添加了 WriteLog 功能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-09-06
    • 1970-01-01
    • 2017-10-03
    • 2021-06-17
    • 2011-11-11
    • 2012-06-04
    • 1970-01-01
    • 2020-08-18
    相关资源
    最近更新 更多