考虑在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"是一个非常常见的用例。从技术上讲,我只是希望如果表格被处理,延续就不会运行。示例代码就是这样做但不优雅并且容易出错(如果我忘记在每个IsDisposed
上检查await
我引入了一个错误)。理想情况下,我想编写一个封装器,扩展方法等,可以封装这个基本设计。但我无法想办法做到这一点。
另外,我想我必须说明性能是一流的考虑因素。例如,抛出异常是非常昂贵的,因为我不会进入。因此,每当我执行ObjectDisposedException
时,我也不想尝试捕捉await
。甚至更丑陋的代码也会伤害性能。似乎只是每次进行IsDisposed
检查是最好的解决方案,但我希望有更好的方法。
编辑#2
关于表现 - 是的,它都是相对的。据我所知,绝大多数开发人员都不关心抛出异常的成本。抛出异常的真实成本是偏离主题的。在其他地方有很多可用的信息。我只想说它比if (IsDisposed)
支票要贵很多个数量级。对我来说,不必要地抛出异常的代价是不可接受的。在这种情况下我说不用,因为我已经有了一个不会抛出异常的解决方案。再一次,让延续抛出ObjectDisposedException
不是一个可接受的解决方案而完全我试图避免的。
答案 0 :(得分:2)
表单拥有CancellationTokenSource
应该非常简单,并在表单关闭时调用表单Cancel
。
然后,您的async
方法可以观察CancellationToken
。
答案 1 :(得分:2)
在这种情况下,我还使用IsDisposed
来检查控件的状态。虽然它有点冗长,但处理这种情况并不是必要的冗长 - 而且根本不会让人感到困惑。像F#这样的函数式语言可能有助于monad - 我不是专家 - 但这看起来和C#一样好。
答案 2 :(得分:0)
我曾经通过不关闭表单解决了类似的问题。相反,我一开始就把它藏起来,只有当所有杰出的工作都完成后才真正关闭它。当然,我必须以Task
变量的形式跟踪这项工作。
我发现这是一个干净的解决方案,因为根本不会出现处理问题。然而,用户可以立即关闭表单。