【问题标题】:Thread safe logging Form线程安全日志记录表单
【发布时间】:2011-06-23 12:06:28
【问题描述】:

我有 Form 负责将所有线程中的所有必要信息记录到它的richTextBox 中。当我想从不同的线程访问它的控件(将文本附加到richTextBox)时,我使用 Invoke 并且它运行良好。

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

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

你有什么提示吗?

【问题讨论】:

    标签: c# multithreading forms logging


    【解决方案1】:

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

    在您创建主窗口时创建窗口并且从不关闭它。添加一个 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 的输出。由于实现是自同步的,因此可以在多个线程上调用 this 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;当用户界面开始做这件事时,它会清除标志,然后一次附加所有排队的文本。然而,由于所有进行日志记录的线程只是按照它们进入记录器的任何顺序转储它们的消息,因此具有大量小日志语句的可读性可能非常糟糕;最好一次记录尽可能大的字符串,以确保它不会被另一个线程消息打断;如果你这样做,那么无论如何你将不会有很多 BeginInvoke 和性能。将不再是一个问题。

    【讨论】:

    • 感谢您的回答。如果第一条消息由创建日志记录表单实例的同一线程附加,那么一切都很好。但是,如果我想在其他线程中调用 Show() 将在片刻后终止,则日志表单仅在这片刻出现,并在线程结束时消失。除了让其他线程保持活动状态(我想避免)之外,有什么方法可以保持框架可见?
    • 那就别那样做了;当 InvokeRequired 返回 false 时,使用 BeginInvoke 安排 ShowHide 在 GUI 线程上运行。永远不要从另一个线程对 GUI 对象执行任何操作
    • 我已编辑我的答案以包括 InvokeRequired/BeginInvoke 请注意我尚未测试此版本。此外,我还添加了一些关于您可能需要根据您的日志记录模式进行的重大优化的简介。
    • @Daryn: Console 是线程安全的;大概包括SetOut
    • @EamonNerbonne,谢谢,我最终提出了一个新问题,我担心stackoverflow.com/questions/29312141/…
    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多