考虑在 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
将完成并运行尝试访问已处理表单上的控件的延续。发生异常。
有没有更好的方法来处理这个问题或简化这个模式?我用await
UI 代码中的大量内容,而且检查起来都很丑陋IsDisposed
每次都这样,如果我忘记了,就很容易出错。
EDIT:
有一些建议的解决方案不符合要求,因为它们改变了功能。
这会让用户感到沮丧。而且它仍然允许进行潜在昂贵的 GUI 工作,这些工作浪费时间、损害性能并且不再相关。在我几乎总是做后台工作的情况下,这可能会阻止表单关闭很长一段时间。
这具有阻止表单关闭的所有问题,除了不会让用户感到沮丧。执行昂贵的 GUI 工作的延续仍然会运行。它还增加了跟踪所有任务完成时的复杂性,然后在表单隐藏时关闭表单。
- Use a
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
.
澄清
原始代码示例是exactly我想要的功能。这只是一个丑陋的模式,“等待后台工作然后更新 GUI”是一个非常常见的用例。从技术上讲,我只想在表单被处理后根本不运行延续。示例代码就是这样做的,但不够优雅,并且容易出错(如果我忘记检查IsDisposed
在每一个await
我正在引入一个错误)。理想情况下,我想编写一个包装器、扩展方法等来封装这个基本设计。但我想不出办法来做到这一点。
另外,我想我必须声明性能是首要考虑因素。例如,抛出异常的成本非常高,原因我不会详细说明。所以我也不想只是尝试捕捉ObjectDisposedException
每当我做一个await
。甚至更丑陋的代码也会损害性能。看起来就像只是做了一个IsDisposed
每次检查是最好的解决方案,但我希望有更好的方法。
EDIT #2
关于性能 - 是的,这都是相对的。我知道绝大多数开发人员并不关心抛出异常的成本。抛出异常的真正成本是题外话。其他地方有大量关于此的信息。可以说它比普通的要贵很多数量级if (IsDisposed)
查看。对我来说,不必要地抛出异常的成本是不可接受的。在这种情况下我说没必要,因为我已经有了一个不会抛出异常的解决方案。再次,让延续抛出一个ObjectDisposedException
不是一个可接受的解决方案并且exactly我试图避免什么。