为了简化对我遇到的奇怪行为的解释,我有一个名为 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.Sleep
是 Task.Delay
的阻塞替代方案如果你不想使用 Task
小号,await
等。当然,这就是你最大的问题 - StartMessageGeneration
相对于 UI 线程不是异步的!
其次,使用 System.Threading.Timer
没有什么意义在你的表格上。相反,只需使用 Windows 窗体计时器 - 它完全在 UI 线程上,因此无需将代码编码回 UI 线程。由于您的 TimerProcessor
不做任何 CPU 工作,它只会阻塞很短的时间,这是更直接的解决方案。
如果您决定继续使用 System.Threading.Timer
无论如何,手动处理同步上下文是没有意义的 - 只需使用 BeginInvoke
在表格上;同样,将表单作为参数传递给方法是没有意义的,因为该方法不是静态的。 this
是你的形式。你实际上可以看到这种情况,因为你省略了 myForm
在listOfMessages.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/