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

标签 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 个锁可以一起正常工作...

谢谢大家!

最佳答案

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

首先,您的 Log.StartMessageGeneration实际上并不是每秒都生成一条日志消息,因为你不是 await正在执行 Task.Delay 返回的任务- 你基本上只是非常快速地(毫无意义地)创建了一千个计时器。日志生成仅受 Invoke 限制。 .使用 Thread.SleepTask.Delay 的阻塞替代方案如果你不想使用 Task小号,await等。当然,这就是你最大的问题 - StartMessageGeneration相对于 UI 线程不是异步的!

其次,使用 System.Threading.Timer 没有什么意义在你的表格上。相反,只需使用 Windows 窗体计时器 - 它完全在 UI 线程上,因此无需将代码编码回 UI 线程。由于您的 TimerProcessor不做任何 CPU 工作,它只会阻塞很短的时间,这是更直接的解决方案。

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

调试器中的一个简单暂停将很容易告诉您程序卡在何处 - 学会使用好调试器,它将为您节省大量时间。但是,让我们从逻辑上看一下。 StartMessageGeneration在 UI 线程上运行,而 System.Threading.Timer使用线程池线程。当计时器锁定时syncLock , StartMessageGeneration当然,不能进入同一把锁——没关系。但是你Send到 UI 线程,并且... UI 线程不能做任何事情,因为它被 StartMessageGeneration 阻塞了,它永远不会给用户界面做任何事情的机会。和 StartMessageGeneration无法继续,因为它正在等待锁定。此“有效”的唯一情况是 StartMessageGeneration运行速度足够快,可以在你的计时器触发之前完成(从而释放 UI 线程来完成它的工作)——这很可能是因为你不正确地使用了 Task.Delay .

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

还有其他一些因素可以让编写这样的代码变得更简单。当您可以只使用 Action<string> 时,声明自己的委托(delegate)毫无意义。 , 例如;使用 await在处理混合 UI/非 UI 代码以及管理任何类型的异步代码时有很大帮助。您不需要使用 event一个简单的函数就足够了 - 如果有意义的话,您可以将该委托(delegate)传递给需要它的函数,并且不是允许调用多个事件处理程序可能是完全合理的。如果您决定继续参加事件,至少要确保它符合 EventHandler代表。

要展示如何重写您的代码以使其更新并实际工作:

void Main()
{
  Application.Run(new LogForm());
}

public static class Log
{
  public static async Task GenerateMessagesAsync(Action<string> logEvent, 
                                            CancellationToken cancel)
  {
    for (int i = 0; i < 1000; i++)
    {
      cancel.ThrowIfCancellationRequested();

      logEvent(i.ToString());

      await Task.Delay(1000, cancel);
    }
  }
}

public partial class LogForm : Form
{
  private readonly List<string> messages;
  private readonly Button btnStart;
  private readonly Button btnStop;
  private readonly TextBox tbxLog;
  private readonly System.Windows.Forms.Timer timer;

  public LogForm()
  {
    messages = new List<string>();

    btnStart = new Button { Text = "Start" };
    btnStart.Click += btnStart_Click;
    Controls.Add(btnStart);

    btnStop = 
      new Button { Text = "Stop", Location = new Point(80, 0), Enabled = false };
    Controls.Add(btnStop);

    tbxLog = new TextBox { Height = 200, Multiline = true, Dock = DockStyle.Bottom };
    Controls.Add(tbxLog);

    timer = new System.Windows.Forms.Timer { Interval = 500 };
    timer.Tick += TimerProcessor;
    timer.Start();
  }

  private void TimerProcessor(object sender, EventArgs e)
  {
    foreach (var message in messages)
    {
      tbxLog.AppendText(message + Environment.NewLine);
    }

    messages.Clear();
  }

  private async void btnStart_Click(object sender, EventArgs e)
  {
    btnStart.Enabled = false;
    var cts = new CancellationTokenSource();
    EventHandler stopAction = (_, __) => cts.Cancel();
    btnStop.Click += stopAction;
    btnStop.Enabled = true;

    try
    {
      await Log.GenerateMessagesAsync(message => messages.Add(message), cts.Token);
    }
    catch (TaskCanceledException)
    {
      messages.Add("Cancelled.");
    }
    finally
    {
      btnStart.Enabled = true;
      btnStop.Click -= stopAction;
      btnStop.Enabled = false;
    }
  }

  protected override void Dispose(bool disposing)
  {
    if (disposing)
    {
      timer.Dispose();
      btnStart.Dispose();
      btnStop.Dispose();
      tbxLog.Dispose();
    }

    base.Dispose(disposing);
  }
}

关于c# - 从工作线程更新 UI 控件时出现死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36616893/

相关文章:

c# - 通用存储库 - IRepository<T> 或 IRepository

c# - 我需要确认我对 IQueryable 和 IEnumerable 的理解

c# - 非等待的 Task.Run 和 Task.RunSynchronously 之间有什么区别

c# - FileNotFoundException 未处理 - 但文件在那里

c# - Entity Framework 6 代码优先并发

c# - 如何使用 Selenium WebDriver C# 从下拉列表中选择一个选项?

c# - 如何在 C# 中使用私有(private)构造函数实例化对象?

c# - PDFsharp 看不到文档中的页面

c# - 从 Windows 窗体应用程序 C# 控制控制台应用程序

C# WPF - 在按钮单击事件中从另一个页面访问变量