需要第二(和第三)意见我对此Winforms竞争条件的修复

时间:2011-07-22 21:39:22

标签: c# .net winforms multithreading .net-3.5

在博客等中有一百个关于如何实现记录或给前景GUI元素提供状态的后台工作者的例子。其中大多数包括处理产生工作线程和使用ShowDialog()创建前景对话之间存在的竞争条件的方法。但是,我发现一个简单的方法是强制在表单构造函数中创建句柄,这样线程就不能在句柄创建之前触发表单上的Invoke / BeginInvoke调用。

考虑一个使用后台工作线程记录到前台的Logger类的简单示例。

另外,假设我们不希望NLog或其他重型框架做一些如此简单和轻量级的事情。

我的记录器窗口由前台线程使用ShowDialog()打开,但仅在启动后台“worker”线程之后。工作线程调用logger.Log(),它本身使用logForm.BeginInvoke()在前台线程上正确更新日志控件。

  public override void Log(string s)
  {
     form.BeginInvoke(logDelegate, s);
  }

其中logDelegate只是“form.Log()”的简单包装或其他可能更新进度条的代码。

问题在于存在的竞争条件;当后台工作线程在调用前景ShowDialog()之前开始记录时,表单的句柄尚未创建,因此BeginInvoke()调用失败。

我熟悉各种方法,包括使用Form OnLoad事件和计时器来创建工作人员任务暂停,直到OnLoad事件生成一个计时器消息,一旦表单显示就启动任务,或者,如上所述,使用队列进行消息。但是,我认为只是强制对话框的句柄尽早创建(在构造函数中)可确保没有竞争条件,假设线程是由创建对话框的同一线程生成的。

http://msdn.microsoft.com/en-us/library/system.windows.forms.control.handle(v=vs.71).aspx

MSDN说:“如果尚未创建句柄,则引用此属性将强制创建句柄。”

所以我的记录器包装了一个表单,它的构造函数执行:

   public SimpleProgressDialog() {
       var h = form.Handle; // dereference the handle
   }

解决方案似乎太简单了。我特别感兴趣的是为什么看似过于简单的解决方案使用起来或不安全。

有何评论?我错过了别的什么吗?

编辑:我不是在寻求其他选择。如果我没有询问如何使用NLog或Log4net等,我会写一个关于这个应用程序的所有客户限制的页面等。

根据赞成票的数量,还有很多其他人也想知道答案。

4 个答案:

答案 0 :(得分:3)

我的两分钱:如果日志框架只是在未创建句柄的情况下维护未显示日志条目的缓冲区,则不需要强制提前创建句柄。它可以实现为Queue或许多其他内容。搞乱.NET中句柄创建的顺序让我感到娇气。

我认为唯一的危险就是性能下降。在winforms中延迟处理创建以加快速度。但是,由于这听起来像是一次性操作,因此听起来并不昂贵,所以我认为你的方法很好。

答案 1 :(得分:3)

如果您担心引用Control.Handle依赖于副作用来创建句柄,您只需调用Control.CreateControl()即可创建它。但是,引用该属性的好处是如果它已经存在则不进行初始化。

假设创建句柄是否安全,你是正确的:只要你在同一个线程上产生后台任务之前创建句柄,就可以避免竞争条件。

答案 2 :(得分:2)

您始终可以检查表单的IsHandleCreated属性,看看是否已构建句柄;但是,有一些警告。我和你的一个类似的地方,在那里动态地创建/销毁winforms控件,并进行大量的多线程处理。我们使用的模式有点像这样:

private void SomeEventHandler(object sender, EventArgs e) // called from a bg thread
{
    MethodInvoker ivk = delegate
    {
        if(this.IsDisposed)
            return; // bail out!  Run away!

        // maybe look for queued stuff if it exists?

        // the code to run on the UI thread
    };

    if(this.IsDisposed)
        return; // run away!  killer rabbits with pointy teeth!

    if(!this.IsHandleCreated) // handle not built yet, do something in the meantime
        DoSomethingToQueueTheCall(ivk);
    else
        this.BeginInvoke(ivk);
}

这里的重要教训是,如果您在处理完表单后尝试与表单进行交互,则需要使用kaboom。不要依赖InvokeRequired,因为如果尚未创建控件的句柄,它将在任何线程上返回false。此外,不要仅仅依赖于IsHandleCreated,因为在控件处理完毕后,它将返回false。

基本上,你有三个标志,其状态将告诉你你需要知道的关于控件的初始化状态以及你是否在相对于控件的BG线程上。

控件可以处于以下三种初始化状态之一:

  • 未初始化,尚未创建句柄
    • InvokeRequired每个主题
    • 上返回false
    • IsHandleCreated返回false
    • IsDisposed返回false
  • 初始化,准备就绪,活跃
    • InvokeRequired执行文档说的内容
    • IsHandleCreated返回true
    • IsDisposed返回false
  • 处理完毕
    • InvokeRequired每个主题
    • 上返回false
    • IsHandleCreated返回false
    • IsDisposed返回true

希望这有帮助。

答案 3 :(得分:1)

由于您在调用线程上创建了窗口,因此最终会出现死锁。如果创建窗口的线程没有运行您的BeginInvoke的消息泵,则会将您的委托调用添加到永远不会被清空的消息队列中,如果您在处理窗口消息的同一线程上没有Application.Run() 。

发送每条日志消息的窗口消息也很慢。拥有一个生产者消费者模型要好得多,你的日志记录线程会将消息添加到另一个线程清空的队列< string> 。您需要锁定的唯一时间是您将邮件入队或出列。当发出事件信号或超时(例如100ms)时,消费者线程可以等待超时事件开始处理下一条消息。

可以找到线程安全阻塞队列here