【发布时间】:2015-09-12 18:20:46
【问题描述】:
考虑在 UI 线程上运行的这段代码:
dividends = await Database.GetDividends();
if (IsDisposed)
return;
//Do expensive UI work here
earnings = await Database.GetEarnings();
if (IsDisposed)
return;
//Do expensive UI work here
//etc...
请注意,每次我await 时,我也会检查IsDisposed。这是必要的,因为我说我await 长期运行Task。同时,用户在完成之前关闭表单。 Task 将完成并运行一个尝试访问已处置表单上的控件的延续。发生异常。
有没有更好的方法来处理或简化这种模式?我在 UI 代码中大量使用await,每次检查IsDisposed 都很难看,如果我忘记了也容易出错。
编辑:
有一些建议的解决方案不符合要求,因为它们改变了功能。
- 在后台任务完成之前防止表单关闭
这会让用户感到沮丧。而且它还允许发生可能会浪费时间、损害性能并且不再相关的昂贵的 GUI 工作。在我几乎总是在做后台工作的情况下,这可能会阻止表单关闭很长时间。
- 隐藏表单并在所有任务完成后关闭它
这有阻止表单关闭的所有问题,除非不会让用户感到沮丧。执行昂贵的 GUI 工作的延续仍然会运行。它还增加了跟踪所有任务完成时的复杂性,然后在隐藏表单时关闭表单。
- 在表单关闭时使用
CancellationTokenSource取消所有任务
这甚至不能解决问题。事实上,我已经这样做了(也没有必要浪费后台资源)。这不是一个解决方案,因为由于隐含的竞争条件,我仍然需要检查 IsDisposed。下面的代码演示了竞态条件。
public partial class NotMainForm : Form
{
private readonly CancellationTokenSource tokenSource = new CancellationTokenSource();
public NotMainForm()
{
InitializeComponent();
FormClosing += (sender, args) => tokenSource.Cancel();
Load += NotMainForm_Load;
Shown += (sender, args) => Close();
}
async void NotMainForm_Load(object sender, EventArgs e)
{
await DoStuff();
}
private async Task DoStuff()
{
try
{
await Task.Run(() => SimulateBackgroundWork(tokenSource.Token), tokenSource.Token);
}
catch (TaskCanceledException)
{
return;
}
catch (OperationCanceledException)
{
return;
}
if (IsDisposed)
throw new InvalidOperationException();
}
private void SimulateBackgroundWork(CancellationToken token)
{
Thread.Sleep(1);
token.ThrowIfCancellationRequested();
}
}
竞争条件发生在任务已经完成、表单已关闭且延续仍在运行时。你会看到InvalidOperationException 偶尔被抛出。取消任务是个好习惯,当然,但这并不能减轻我检查IsDisposed 的麻烦。
澄清
原始代码示例正是我想要的功能。这只是一个丑陋的模式,做“等待后台工作然后更新 GUI”是一个很常见的用例。从技术上讲,如果表单被处理,我只是希望继续不运行。示例代码就是这样做的,但并不优雅,而且容易出错(如果我忘记在每个 await 上检查 IsDisposed,我正在引入一个错误)。理想情况下,我想编写一个可以封装这个基本设计的包装器、扩展方法等。但我想不出办法。
另外,我想我必须说明性能是一流的考虑因素。例如,抛出异常是非常昂贵的,因为我不会进入。所以我也不想在每次执行await 时尝试捕获ObjectDisposedException。即使是丑陋的代码也会损害性能。似乎只做一次IsDisposed 检查是最好的解决方案,但我希望有更好的方法。
编辑#2
关于性能 - 是的,这都是相对的。我了解绝大多数开发人员并不关心抛出异常的成本。抛出异常的真正成本是偏离主题的。在其他地方有很多关于这方面的信息。可以说它比if (IsDisposed) 支票贵了许多数量级。对我来说,不必要地抛出异常的代价是不可接受的。在这种情况下我说没必要,因为我已经有了一个不会引发异常的解决方案。同样,让延续抛出 ObjectDisposedException 不是一个可接受的解决方案,正是我试图避免的。
【问题讨论】:
-
@HansPassant 当然,我可以做到。但是我只会有一群愤怒的用户等待更新他们想要关闭的表单。此外,这个例子也大大简化了。我可以在任何给定时间运行许多捕获 UI 上下文的任务。上面的代码在功能上是我想要的,但现在我的代码库中出现了这种“如果不处理则继续”的模式。
-
@Loathing 我也想过这个问题。但它增加了跟踪正在运行的任务的复杂性。更重要的是,UI 代码最终仍会运行(包括上面的后续等待),这在许多方面都是昂贵且浪费的。检查您提到的布尔值与检查
IsDisposed相同。 -
您似乎严重高估了在此处使用异常的性能成本。你有一个例外情况,这种行为正是例外情况的类型。在用户已经关闭应用程序后,在进程被拆除时抛出然后捕获一个异常根本不应该是性能问题。鉴于 UI 已关闭,用户甚至不会意识到清理仍在进行中,或者关心是否需要额外的毫秒才能完成。
-
@Zer0 “非常昂贵”是一个相对术语。在您的应用程序用户的人类时间尺度上,抛出和捕获一个异常的成本是微不足道的;甚至感觉不到。现在,如果您不断地每秒抛出和捕获数万次异常,那么它可能会成为一个相关的性能问题。关闭表单时执行一次或两次远非如此。即使表单关闭不是应用程序的结束,它仍然不会以任何人能够感知的方式阻碍用户体验。
-
@Zer0 哎呀,即使您提出的使用任务取消来解决此问题的解决方案正在使用异常。它在工作完成时抛出异常,只是为了稍后捕获它。它将具有解决方案的所有相同性能特征,而您出于性能原因完全忽略了这些特征。
标签: c# winforms asynchronous async-await