为了简化我遇到的奇怪行为的解释,我有一个名为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个锁可以正常工作......
谢谢大家!
答案 0 :(得分:4)
您的代码存在一些问题,还有一些......愚蠢的事情。
首先,您的Log.StartMessageGeneration
实际上并不会每秒生成一条日志消息,因为您await
没有Task.Delay
返回任务 - 您基本上只是创建了一个千万计时器很快(毫无意义)。日志生成仅受Invoke
的限制。如果您不想使用Thread.Sleep
,Task.Delay
等,使用Task
是await
的阻止替代方案。当然,这是您最大的问题 - {{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;重构..