在多线程应用

时间:2015-07-05 07:49:21

标签: c# multithreading winforms thread-safety

我正在尝试创建一个WinForms多线程应用程序,它可以在两个不同的线程中无休止地生成异常。

一个线程使用GenerateDllNotFoundExc()方法,另一个方法使用另一个方法,它基本相同,但只是生成另一个异常。

然后将异常消息写入队列,然后从队列写入文本框。

然而,GUI总是在1秒后冻结,它会将消息写入文本框并冻结。我尝试调试它,代码本身工作,但GUI冻结。

有人可以给我一个关于我做错的提示吗?

private delegate void GetQueueElem();
private event GetQueueElem getqueuelem;     

private void GenerateDllNotFoundExc()
{
    Action<String> addelem = new Action<String>(AddToQueue);

    string exdll = string.Empty;

    while (shouldgeneratemore)
    {
        try 
        {
            throw new DllNotFoundException();
        }
        catch (Exception ex) 
        {
            exdll = ex.Message;
        }

        this.Invoke(addelem, exdll);
    }
}

private void AddToQueue(string exmess)
{            
    lock (lockobject)            
        queue.Enqueue(exmess);

    getqueuelem.Invoke();
}        

private void AddToTextBox()
{           
     while (queue.Count > 0)
     {
         string s = queue.Dequeue() +"\t" + Thread.CurrentThread.Name 
             + "\t" + Thread.CurrentThread.ManagedThreadId + "\t";

         lock (lockobject)
             textBox1.Text += s;                
     }                     
}       

1 个答案:

答案 0 :(得分:12)

这个问题具有教育意义,它显示出所有三个主要线程错误的证据。将它们大致按顺序排列:

  1. 线程竞赛错误。当一个线程读取由另一个线程修改的变量时跳闸。需要锁定以避免引起问题。此代码使用 lock 关键字,但未正确使用它。 Queue类不是线程安全的,在此代码中,使用不安全的Count属性和Dequeue()方法都没有锁定。然而,这里不是实际问题,使用Queue的代码实际上并不在多个线程上运行。换句话说,实际上并不需要 lock

  2. 死锁。代码以不可预测的顺序获取锁时发生。特别讨厌在程序的UI线程上运行的代码,它经常获取不可见的锁,内置在.NET Framework,操作系统或各种第三方钩子中。屏幕阅读器例如。 Invoke()方法特别容易出现死锁,应该强烈避免,BeginInvoke()始终是首选。你实际上不需要 Invoke(),你不关心返回值。然而,不是这个程序中的实际错误,即使它看起来像死锁一样很多,你可以使用调试器并看到UI线程正在执行代码而不是在锁上停止。

  3. 一个消防水带虫。当产生结果的线程比处理它们的线程消耗得快时,就会发生消防。这种bug会产生各种各样的痛苦,它看起来很像僵局。最终,当内存耗尽时,这样的程序总是会崩溃,由包含太多尚未处理的结果的队列消耗。需要一段时间,.NET程序可以提供大量内存。

  4. 这个节目中的第3位。 UI线程需要执行多个职责并以高优先级处理调用请求。在这种情况下调度调用的方法AddToQueue()。它从内部队列读取调用请求,并尝试在执行其他较低优先级任务之前先清空队列。当队列无法清空时会出错,因为工作线程以高于UI线程可以清空队列的速率向队列添加条目。换句话说,UI线程永远不会跟上,它只调度调用请求而不会做任何其他事情。

    例如,在任务管理器中非常明显,您将看到您的程序燃烧100%核心。所以你知道它实际上并不是死锁。在你的UI中非常明显,你可以敲击停止按钮,但它没有任何效果。并且绘画不再发生,被视为低优先级任务,只有在没有更重要的事情需要发生时才执行。 看起来完全冻结,即使UI线程像大佬一样运行。

    一个火管漏洞很容易绊倒,每秒只需要一千多次调用请求。取决于UI线程需要做多少工作。通常很多,更新UI通常非常昂贵。设置TextBox的Text属性没有什么非常微妙的,很多工作都在幕后进行。那个看上去无辜的+ =运算符会烧掉一个批次的周期。除了SendMessage()的静态开销与本机TextBox通信之外,还经常需要重新分配内部文本缓冲区。比较String vs StringBuilder。或者换句话说,即使您最初没有绊倒防火软管错误,也可以保证您迟早会因为TextBox包含太多需要从一个缓冲区移动到另一个缓冲区的文本。在你的情况下更快。

    最终这样的消防水带虫是一个平衡的错误。您正在以远远高于人类可以观察到的的速率更新UI。这不是一个有用的用户界面。对于这个程序没有实用的建议,它太合成了,故意减慢工作线程将是一个解决方法。