使用 async await 乱序执行的 WinForms 事件
WinForms events executing out of order with async await
WinForms 事件 运行 乱序。我想我明白为什么,但我很好奇解决它的最佳方法。
在下面的(设计的)代码中,首先调用 Load
,但是当达到 await
时它放弃控制,然后调用 Shown
。 Shown
完成后,Load
终于可以完成了。
private async void SomeForm_Load(object sender, EventArgs e)
{
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
}
private void SomeForm_Shown(object sender, EventArgs e)
{
someLabel.Text = "Shown";
}
private async Task SomeMethod()
{
await Task.Delay(1).ConfigureAwait(false);
}
我需要确保 Load
在 Shown
可以执行之前完成。
这是我目前的解决方案:
Task _isLoadedTask;
private async void SomeForm_Load(object sender, EventArgs e)
{
var tcs = new TaskCompletionSource();
_isLoadedTask = tcs.Task;
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
tcs.TrySetResult();
}
private async void SomeForm_Shown(object sender, EventArgs e)
{
await _isLoadedTask;
someLabel.Text = "Shown";
}
这样做的明显缺点是我必须手动设置 boundaries/dependecies。确保事件按顺序执行的最佳方法是什么?
在这种情况下,覆盖 OnLoad
或订阅 Load
具有相同的效果。
但是,订阅 Load
会导致委托实例化和调用,而覆盖 OnLoad
不会,而且会给予对执行内容和执行顺序的更多控制。
试试这个:
private Task isLoadedTask;
protected override void OnLoad(EventArgs e)
{
this.isLoadedTask = OnLoadAsync();
base.OnLoad(e);
}
private async Task LoadAsync()
{
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
}
protected override async void OnShown(EventArgs e)
{
base.OnShown(e);
await _isLoadedTask;
someLabel.Text = "Shown";
}
您以正确的顺序获取事件。事件处理程序中的 async void
和 await
混淆了这个问题。从 Form
class 的角度来看,当它调用你的 Load
事件处理程序时,事件处理程序方法 returns 当 SomeMethod
中的 await 被命中时。当 SomeMethod
任务完成时,Load 事件处理程序中的其余代码将作为 continuation 在 UI 线程上执行 - 尽管它看起来像仍在 Load 事件处理程序中,因为那是您的代码所在的位置,而不是编译器发出的内容。当然,有点混乱。 Jon Skeet 的书 C# 深入:第四版 包含了我所见过的最好的解释之一。
此外,尽可能避免使用 async void
方法,因为抛出异常会使您的进程崩溃。为了解决这两个问题,并增加一些清晰度,我会使用 Load 事件来启动任务,然后在 Shown 中为它附加一个延续。这确保了当加载中的异步内容完成并显示时,您想要 运行 的代码。
private Task _loadTask = Task.CompletedTask;
private Stopwatch _sw = Stopwatch.StartNew();
private void Form1_Load(object sender, EventArgs e)
{
Trace.TraceInformation("[{0}] {1} Form1_Load - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
_loadTask = DoAsyncLoadStuff();
Trace.TraceInformation("[{0}] {1} Form1_Load - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
}
private void Form1_Shown(object sender, EventArgs e)
{
Trace.TraceInformation("[{0}] {1} Form1_Shown - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
_loadTask.ContinueWith(DoStuffAfterShownAndAsyncLoadStuff, null, TaskScheduler.FromCurrentSynchronizationContext());
Trace.TraceInformation("[{0}] {1} Form1_Shown - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
}
private void DoStuffAfterShownAndAsyncLoadStuff(Task asyncLoadTask, object context)
{
Trace.TraceInformation("[{0}] {1} DoStuffAfterShownAndAsyncLoadStuff. Task was {2}", Thread.CurrentThread.ManagedThreadId,
_sw.ElapsedMilliseconds, asyncLoadTask.Status);
}
private async Task DoAsyncLoadStuff()
{
await Task.Delay(5000).ConfigureAwait(false);
Trace.TraceInformation("[{0}] {1} DoAsyncLoadStuff - Returning", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
//throw new NotImplementedException();
}
上面的示例将在 Visual Studio 中输出线程 ID 和经过的毫秒数到输出 window。调整延迟时间并取消注释抛出异常。请注意 DoStuffAfterShownAndAsyncLoadStuff
将始终在 UI 线程上执行。
所有解决方案都需要一些东西来阻止显示继续,直到加载完成。 TaskCompletionSource
似乎是最优雅的实现方式。
Task _isLoadedTask;
private async void SomeForm_Load(object sender, EventArgs e)
{
var tcs = new TaskCompletionSource();
_isLoadedTask = tcs.Task;
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
tcs.TrySetResult();
}
private async void SomeForm_Shown(object sender, EventArgs e)
{
await _isLoadedTask;
someLabel.Text = "Shown";
}
WinForms 事件 运行 乱序。我想我明白为什么,但我很好奇解决它的最佳方法。
在下面的(设计的)代码中,首先调用 Load
,但是当达到 await
时它放弃控制,然后调用 Shown
。 Shown
完成后,Load
终于可以完成了。
private async void SomeForm_Load(object sender, EventArgs e)
{
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
}
private void SomeForm_Shown(object sender, EventArgs e)
{
someLabel.Text = "Shown";
}
private async Task SomeMethod()
{
await Task.Delay(1).ConfigureAwait(false);
}
我需要确保 Load
在 Shown
可以执行之前完成。
这是我目前的解决方案:
Task _isLoadedTask;
private async void SomeForm_Load(object sender, EventArgs e)
{
var tcs = new TaskCompletionSource();
_isLoadedTask = tcs.Task;
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
tcs.TrySetResult();
}
private async void SomeForm_Shown(object sender, EventArgs e)
{
await _isLoadedTask;
someLabel.Text = "Shown";
}
这样做的明显缺点是我必须手动设置 boundaries/dependecies。确保事件按顺序执行的最佳方法是什么?
在这种情况下,覆盖 OnLoad
或订阅 Load
具有相同的效果。
但是,订阅 Load
会导致委托实例化和调用,而覆盖 OnLoad
不会,而且会给予对执行内容和执行顺序的更多控制。
试试这个:
private Task isLoadedTask;
protected override void OnLoad(EventArgs e)
{
this.isLoadedTask = OnLoadAsync();
base.OnLoad(e);
}
private async Task LoadAsync()
{
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
}
protected override async void OnShown(EventArgs e)
{
base.OnShown(e);
await _isLoadedTask;
someLabel.Text = "Shown";
}
您以正确的顺序获取事件。事件处理程序中的 async void
和 await
混淆了这个问题。从 Form
class 的角度来看,当它调用你的 Load
事件处理程序时,事件处理程序方法 returns 当 SomeMethod
中的 await 被命中时。当 SomeMethod
任务完成时,Load 事件处理程序中的其余代码将作为 continuation 在 UI 线程上执行 - 尽管它看起来像仍在 Load 事件处理程序中,因为那是您的代码所在的位置,而不是编译器发出的内容。当然,有点混乱。 Jon Skeet 的书 C# 深入:第四版 包含了我所见过的最好的解释之一。
此外,尽可能避免使用 async void
方法,因为抛出异常会使您的进程崩溃。为了解决这两个问题,并增加一些清晰度,我会使用 Load 事件来启动任务,然后在 Shown 中为它附加一个延续。这确保了当加载中的异步内容完成并显示时,您想要 运行 的代码。
private Task _loadTask = Task.CompletedTask;
private Stopwatch _sw = Stopwatch.StartNew();
private void Form1_Load(object sender, EventArgs e)
{
Trace.TraceInformation("[{0}] {1} Form1_Load - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
_loadTask = DoAsyncLoadStuff();
Trace.TraceInformation("[{0}] {1} Form1_Load - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
}
private void Form1_Shown(object sender, EventArgs e)
{
Trace.TraceInformation("[{0}] {1} Form1_Shown - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
_loadTask.ContinueWith(DoStuffAfterShownAndAsyncLoadStuff, null, TaskScheduler.FromCurrentSynchronizationContext());
Trace.TraceInformation("[{0}] {1} Form1_Shown - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
}
private void DoStuffAfterShownAndAsyncLoadStuff(Task asyncLoadTask, object context)
{
Trace.TraceInformation("[{0}] {1} DoStuffAfterShownAndAsyncLoadStuff. Task was {2}", Thread.CurrentThread.ManagedThreadId,
_sw.ElapsedMilliseconds, asyncLoadTask.Status);
}
private async Task DoAsyncLoadStuff()
{
await Task.Delay(5000).ConfigureAwait(false);
Trace.TraceInformation("[{0}] {1} DoAsyncLoadStuff - Returning", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
//throw new NotImplementedException();
}
上面的示例将在 Visual Studio 中输出线程 ID 和经过的毫秒数到输出 window。调整延迟时间并取消注释抛出异常。请注意 DoStuffAfterShownAndAsyncLoadStuff
将始终在 UI 线程上执行。
所有解决方案都需要一些东西来阻止显示继续,直到加载完成。 TaskCompletionSource
似乎是最优雅的实现方式。
Task _isLoadedTask;
private async void SomeForm_Load(object sender, EventArgs e)
{
var tcs = new TaskCompletionSource();
_isLoadedTask = tcs.Task;
someLabel.Text = "Start Load";
await SomeMethod();
someLabel.Text = "Finish Load";
tcs.TrySetResult();
}
private async void SomeForm_Shown(object sender, EventArgs e)
{
await _isLoadedTask;
someLabel.Text = "Shown";
}