线程安全日志表单

时间:2011-06-23 12:06:28

标签: c# multithreading forms logging

我有Form负责将所有线程中的所有必要信息记录到其richTextBox中。当我想从不同的线程访问它的控件(将文本追加到richTextBox)时,我使用Invoke,它完美地工作。

但是当第一条消息要附加到richTextBox时我需要显示这个日志形式,我不知道哪个来自我的线程会首先执行此操作。 此外,当我关闭日志表单时,我希望在下一条消息到来时再次显示它(在这种情况下,我仍然不知道哪个线程会先调用它。)

我尝试在新线程中通过Application.Run(ApplicationContext)创建此表单,但这些解决方案都没有。

你有任何提示吗?

2 个答案:

答案 0 :(得分:3)

不是关闭/重新生成窗口,而是简单地隐藏和取消隐藏通常更容易。

创建主窗口时创建窗口,从不关闭它。添加onbeforeclose(或任何被再次调用的...)事件处理程序,取消用户启动的关闭并改为隐藏窗口。

现在,您已经在使用线程安全的调度程序来修改窗口内容:只需在该事件处理程序中添加一行以取消隐藏窗口(如果它已经可见,这将不会执行任何操作),并且您是很高兴去!

顺便提一下,在这些场景中有用的是自定义TextWriter子类。你可以按如下方式制作一个:

public abstract class AbstractTextWriter : TextWriter {
    protected abstract void WriteString(string value);
    public override Encoding Encoding { get { return Encoding.Unicode; } }
    public override void Write(char[] buffer, int index, int count) {
        WriteString(new string(buffer, index, count));
    }
    public override void Write(char value) {
        WriteString(value.ToString(FormatProvider));
    }
    public override void Write(string value) { WriteString(value); }
    //subclasses might override Flush, Dispose and Encoding
}
public class DelegateTextWriter : AbstractTextWriter {
    readonly Action<string> OnWrite;
    readonly Action OnClose;
    static void NullOp() { }
    public DelegateTextWriter(Action<string> onWrite, Action onClose = null) { 
        OnWrite = onWrite; 
        OnClose = onClose ?? NullOp; 
    }
    protected override void WriteString(string value) { OnWrite(value); }
    protected override void Dispose(bool disposing) { 
        OnClose(); base.Dispose(disposing); 
    }
}

这样你就可以将表单更新逻辑粘贴到这样的......

var threadSafeLogWriter =  new DelegateTextWriter(str => {
        Action updateCmd = ()=>{
            myControl...//append text to whatever control here
            myControl.Show();
        };
        if(myControl.InvokeRequired) myControl.BeginInvoke(updateCmd);
        else updateCmd();
    });

...并在任何地方使用它,甚至可能Console.SetOut将输出捕获到Console.Write。可以在多个线程上调用 TextWriter上的Write,因为实现是自同步的。

你可以像这样测试......

Console.SetOut(threadSafeLogWriter);
Parallel.For(0,100, i=>{
    threadSafeLogWriter.Write("Hello({0}) ", i);
    Thread.Yield();
    Console.WriteLine("World({0})!",i);
});

如果您执行批量的小邮件日志记录,最终可能会遇到很多BeginInvoke调用,而这些调用很慢。作为优化,您可以将所有日志消息排队到ConcurrentQueue或其他一些同步结构,如果队列为空,则只有BeginInvoke才能排队。这样,您通常只在UI更新之间执行一个BeginInvoke;当UI完成它的操作时,它会清除该标志,然后立即附加所有排队的文本。但是,由于所有执行日志记录的线程只是按照它们进入记录器的顺序转储它们的消息,因此可读性非常糟糕,因为它具有大量的小日志语句。最好一次记录尽可能大的字符串,以确保它不会被另一个线程消息中断;如果你这样做,那么无论如何你都不会有很多BeginInvoke和perf。将不再是一个问题。

答案 1 :(得分:1)

this other reply类似,我会将消息排入队列并让表格取消它们,因此订单将被保留。必须提供一些并发策略,让线程在表单出列时让队列进入队列,但是您确保保留事件的顺序,并且可以在第一个看到的事件时启动表单。