从工作线程更新UI控件时出现死锁

时间:2016-04-14 07:52:41

标签: c#

为了简化我遇到的奇怪行为的解释,我有一个名为Log的简单类,它每1000毫秒触发1个日志事件。

public static class Log
{
    public delegate void LogDel(string msg);
    public static event LogDel logEvent;

    public static void StartMessageGeneration ()
    {
        for (int i = 0; i < 1000; i++)
        {
            logEvent.Invoke(i.ToString());
            Task.Delay(1000);
        }
    }
}

我有下面的Form类,它订阅了Log类的日志事件,因此它可以处理它们并显示在一个简单的文本框中。 一旦日志消息到达,它就会被添加到列表中。每500毫秒,一个计时器对象访问该列表,以便其内容可以显示在文本框中。

public partial class Form1 : Form
{
    private SynchronizationContext context;
    private System.Threading.Timer guiTimer = null;
    private readonly object syncLock = new object();
    private List<string> listOfMessages = new List<string>();

    public Form1()
    {
        InitializeComponent();
        context = SynchronizationContext.Current;
        guiTimer = new System.Threading.Timer(TimerProcessor, this, 0, 500);
        Log.logEvent += Log_logEvent;
    }

    private void Log_logEvent(string msg)
    {
        lock (syncLock)
            listOfMessages.Add(msg);
    }

    private void TimerProcessor(object obj)
    {
        Form1 myForm = obj as Form1;
        lock (myForm.syncLock)
        {
            if (myForm.listOfMessages.Count == 0)
                return;

            myForm.context.Send(new SendOrPostCallback(delegate
            {
                foreach (string item in myForm.listOfMessages)
                    myForm.textBox1.AppendText(item + "\n");
            }), null);

            listOfMessages.Clear();
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Log.StartMessageGeneration();
    }
}

我看到的问题是,有时会出现死锁(应用程序卡住)。似乎2个锁(第一个用于添加到列表中,第一个用于从列表中“检索”)以某种方式阻塞彼此。

提示: 1)降低从1秒到200毫秒发送消息的速度似乎有帮助(不知道为什么) 2)当返回GUI线程(使用同步上下文)和访问GUI控件时,会发生某种情况。如果我没有返回GUI线程,那么2个锁可以正常工作......

谢谢大家!

2 个答案:

答案 0 :(得分:4)

您的代码存在一些问题,还有一些......愚蠢的事情。

首先,您的Log.StartMessageGeneration实际上并不会每秒生成一条日志消息,因为您await没有Task.Delay返回任务 - 您基本上只是创建了一个千万计时器很快(毫无意义)。日志生成仅受Invoke的限制。如果您不想使用Thread.SleepTask.Delay等,使用Taskawait的阻止替代方案。当然,这是您最大的问题 - {{1关于UI线程,它不是异步的!

其次,在表单上使用StartMessageGeneration没什么意义。相反,只需使用Windows窗体计时器 - 它完全在UI线程上,因此不需要将代码编组回UI线程。由于您的System.Threading.Timer不执行任何CPU工作且只能在很短的时间内阻塞,因此它是更直接的解决方案。

如果您决定继续使用TimerProcessor,那么手动处理同步上下文毫无意义 - 只需在表单上使用System.Threading.Timer;同样地,将表单作为参数传递给方法也没有意义,因为该方法不是静态的。 BeginInvoke是您的表格。您实际上可以看到这种情况,因为您在this中省略了myForm - 两个实例相同,listOfMessages.Clear()是多余的。

调试器中的一个简单暂停将很容易告诉您程序挂起的位置 - 学习如何使用调试器,这将为您节省大量时间。但是,让我们从逻辑上看一下。 myForm在UI线程上运行,而StartMessageGeneration使用线程池线程。当计时器锁定System.Threading.Timer时,syncLock无法进入同一个锁,当然 - 这没关系。但后来你StartMessageGeneration到了UI线程,并且...... UI线程无法做任何事情,因为它被Send阻止了,它永远不会让UI有机会做任何事情。并且StartMessageGeneration无法继续,因为它正在等待锁定。这个“工作”的唯一情况是StartMessageGeneration在计时器触发之前运行得足够快(从而释放UI线程来完成其工作) - 这很可能是由于你错误地使用{{1 }}

现在让我们用我们所知道的“提示”来看看。 1)只是你在测量中的偏见。因为你永远不会以任何方式等待StartMessageGeneration,所以改变间隔绝对没有任何意义(如果延迟为零,则会有微小的变化)。 2)当然 - 这就是你的僵局所在。依赖于共享资源的两段代码,同时它们都需要占用另一个资源。这是一个非常典型的僵局。线程1正在等待A释放B,并且线程2正在等待B释放A(在这种情况下,A是Task.Delay而B是UI线程)。当您删除Task.Delay(或将其替换为syncLock)时,线程1不再需要等待B,并且死锁消失。

还有其他一些东西可以让编写这样的代码变得更简单。例如,当您可以使用Send时,宣布自己的委托是没有意义的;在处理混合UI /非UI代码以及管理任何类型的异步代码时,使用Post会有很大帮助。你不需要在一个简单的函数就足够的情况下使用Action<string> - 如果有意义的话,你可以将该委托传递给需要它的函数,它可能完全合理 not 允许调用多个事件处理程序。如果您决定继续活动,请至少确保它符合await代表。

显示如何重写代码以使其更新并实际工作:

event

答案 1 :(得分:3)

SynchronizationContext.Send同步运行。当您调用它时,实际上会阻止UI线程,直到操作完成。但是如果UI线程已经处于lock状态,那么你就陷入了僵局。

您可以使用SynchronizationContext.Post来避免这种情况。

我只是回答你的问题,但事实是你的代码需要一个小小的&#34;重构..