加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-15 23:32:17 所属栏目:百科 来源:网络整理
导读:为了简化我遇到的奇怪行为的解释,我有一个名为Log的简单类,它每1000毫秒触发1个日志事件. public static class Log{ public delegate void LogDel(string msg); public static event LogDel logEvent; public static void StartMessageGeneration () { for (
为了简化我遇到的奇怪行为的解释,我有一个名为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,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实际上并不是每秒都会生成一条日志消息,因为你不是在等待Task.Delay返回的任务 – 你基本上只是非常快速(毫无意义地)创建了一千个定时器.日志生成仅受Invoke的限制.使用Thread.Sleep是Task.Delay的阻止替代,如果你不想使用Tasks,等待等等.当然,这是你最大的问题 – StartMessageGeneration与UI线程不是异步的!

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

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

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

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

还有其他一些东西使编写这样的代码变得更简单.例如,当您可以使用Action< string>时,声明自己的委托是没有意义的;在处理混合UI /非UI代码以及管理任何类型的异步代码时,使用await会有很大帮助.您不需要使用简单函数就足够的事件 – 如果有意义的话,您可以将该委托传递给需要它的函数,并且可以完全理解不允许调用多个事件处理程序.如果您决定继续使用该事件,请至少确保它符合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);
  }
}

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读