c# – 从工作线程更新UI控件时出现死锁
为了简化我遇到的奇怪行为的解释,我有一个名为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类的日志事件,因此它可以处理它们并显示在一个简单的文本框中. 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个锁(第一个用于添加到列表而第二个用于从列表中“检索”)以某种方式阻塞彼此. 提示: 感谢大家! 解决方法
您的代码存在一些问题,还有一些……愚蠢的事情.
首先,你的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); } } (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |