sha*_*ner 1 c# asynchronous winforms thread-synchronization
在创建简单的样本使用期间async/await,我发现,一些示例只是在Button1_Click类似的方法上说明模式,并直接从async方法中自由更新GUI控件.因此可以将此视为安全机制.但是,我的测试代码是不断地对崩溃TargetInvocationException的例外在mscorlib.dll与内部异常,如:NullReference,ArgumentOutOfRange等.关于堆栈跟踪,一切似乎都指向WinForms.StatusStrip显示结果(直接从驱动标签async绑定到按钮事件处理方法).Control.Invoke在访问GUI控件时使用旧学校时,崩溃似乎已得到解决.
问题是:我错过了重要的事情吗?异步方法是否与先前用于长期操作的线程/后台工作程序相同,因此Invoke是推荐的解决方案?直接从async方法错误驱动GUI的代码片段是什么?
编辑:对于缺失源的downvoters:创建一个包含三个按钮的简单表单和一个包含两个标签的StatusStrip ...
//#define OLDSCHOOL_INVOKE
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AsyncTests
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void LongTermOp()
{
int delay;
int thisId;
lock (mtx1)
{
delay = rnd.Next(2000, 10000);
thisId = firstCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"Generating first run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++firstPending;
}
await Task.Delay(delay);
lock (mtx1)
{
--firstPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"First run #{thisId} completed, {firstPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
}
private async Task LongTermOpAsync()
{
await Task.Run((Action)LongTermOp);
}
private readonly Random rnd = new Random();
private readonly object mtx1 = new object();
private readonly object mtx2 = new object();
private int firstCount;
private int firstPending;
private int secondCount;
private int secondPending;
private async void buttonRound1_Click(object sender, EventArgs e)
{
await LongTermOpAsync();
}
private async void buttonRound2_Click(object sender, EventArgs e)
{
await Task.Run(async () =>
{
int delay;
int thisId;
lock (mtx2)
{
delay = rnd.Next(2000, 10000);
thisId = secondCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Generating second run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++secondPending;
}
await Task.Delay(delay);
lock (mtx2)
{
--secondPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Second run #{thisId} completed, {secondPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
});
}
private void buttonRound12_Click(object sender, EventArgs e)
{
buttonRound1_Click(sender, e);
buttonRound2_Click(sender, e);
}
private bool isRunning = false;
private async void buttonCycle_Click(object sender, EventArgs e)
{
isRunning = !isRunning;
await Task.Run(() =>
{
while (isRunning)
{
buttonRound12_Click(sender, e);
Application.DoEvents();
}
});
}
}
}
Run Code Online (Sandbox Code Playgroud)
在这方面也Task没有await给你任何保证.您需要考虑创建任务的上下文以及发布延续的位置.
如果您await在winforms事件处理程序中使用,则捕获同步上下文,并且continuation返回到UI线程(实际上,它几乎调用Invoke给定的代码块).但是,如果你刚开始一个新的任务Task.Run,或者你await从其他的同步环境,这已不再适用.解决方案是在winforms同步上下文中运行适当的任务调度程序上运行continuation.
但是,应该指出,它仍然不一定意味着async事件将正常工作.例如,Winforms还将事件用于诸如CellPainting实际依赖于它们同步运行的事物.如果你await在这样的事件中使用,它几乎可以保证不能正常工作 - 延续仍将发布到UI线程,但这并不一定使它安全.例如,假设控件具有如下代码:
using (var graphics = NewGraphics())
{
foreach (var cell in cells)
CellPainting(cell, graphics);
}
Run Code Online (Sandbox Code Playgroud)
当你的继续运行时,graphics实例已经完全被处理掉了.甚至可能细胞不再是控制的一部分,或者控制本身不再存在.
同样重要的是,代码可能取决于您的代码更改事物 - 例如,您可以在其中设置一些值EventArgs以指示例如成功或提供一些返回值的事件.同样,这意味着你不能await在内部使用- 只要调用者知道,函数只是在你执行时返回await(除非它同步完成).