[英]Async/Await equivalent to .ContinueWith with CancellationToken and TaskScheduler.FromCurrentSynchronizationContext() scheduler
This is a follow-up to this question . 这是该问题的后续措施。
Question : What would be a succinct way to express the following using async
/ await
instead of .ContinueWith()
?: 问题 :使用async
/ await
而不是.ContinueWith()
来表达以下内容的简洁方式是什么?
var task = Task.Run(() => LongRunningAndMightThrow());
m_cts = new CancellationTokenSource();
CancellationToken ct = m_cts.Token;
var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task updateUITask = task.ContinueWith(t => UpdateUI(t), ct, TaskContinuationOptions.None, uiTaskScheduler);
I'm mainly interested in the case of a UI SynchronizationContext (eg for Winforms) 我主要对UI SynchronizationContext的情况感兴趣(例如,对于Winforms)
Note with this that the behavior has all the following desired behaviors: 请注意,此行为具有以下所有所需行为:
When the CancellationToken
is cancelled, the updateUITask
ends up cancelled as soon as possible (ie the LongRunningAndMightThrow
work may still be going on for quite some time). CancellationToken
, updateUITask
最终会尽快取消(即LongRunningAndMightThrow
工作可能仍会持续一段时间)。
The ct
CancellationToken gets checked for cancellation on the UI thread prior to running the UpdateUI lambda (see this answer ). 在运行UpdateUI lambda之前,请在用户界面线程上检查ct
CancellationToken是否取消(请参见此答案 )。
The updateUITask
will end up cancelled in some cases where the task
completed or faulted (since the ct
CancellationToken is checked on the UI thread before executing the UpdateUI lambda. 在某些情况下,如果task
已完成或发生故障,则updateUITask
最终将被取消(因为在执行UpdateUI lambda之前已在UI线程上检查了ct
CancellationToken)。
There is no break in flow between the check of the CancellationToken
on the UI thread and the running of the UpdateUI
lambda. 在UI线程上检查CancellationToken
和运行UpdateUI
lambda之间,流程没有中断。 That is, if the CancellationTokenSource
is only ever cancelled on the UI thread, then there is no race condition between the checking of the CancellationToken
and the running of the UpdateUI
lambda--nothing could trigger the CancellationToken
in between those two events because the UI thread is not relinquished in between those two events. 也就是说,如果CancellationTokenSource
是永远只能在UI线程上取消了,再有就是的检查之间不存在竞争条件CancellationToken
和的运行UpdateUI
拉姆达-没有什么可以触发CancellationToken
在这两个事件之间,因为UI线程在这两个事件之间不被放弃。
Discussion: 讨论:
One of my main goals in moving this to async/await is to get the UpdateUI
work out of a lambda (for ease of readability/debuggability). 我将其移到异步/等待中的主要目的之一是使UpdateUI
工作不再是lambda(为了易于阅读/调试)。
#1 above can be addressed by Stephen Toub's WithCancellation
task extension method . 上面的#1可以通过Stephen Toub的WithCancellation
任务扩展方法解决 。 (which you can feel free to use in the answers). (您可以在答案中随意使用)。
The other requirements seemed difficult to encapsulate into a helper method without passing UpdateUI
as a lambda since I cannot have a break (ie an await
) between the checking of the CancellationToken
and the executing of UpdateUI
(because I assume I cannot rely on the implementation detail that await
uses ExecuteSynchronously
as mentioned here . This is where it seems that having the mythical Task
extension method .ConfigureAwait(CancellationToken)
that Stephen talks about would be very useful. 在不通过UpdateUI
作为lambda的情况下,其他要求似乎很难封装到辅助方法中,因为我无法在CancellationToken
的检查与UpdateUI
的执行之间有一个中断(即await
)(因为我假设我不能依赖实现细节) 就像这里提到的 , await
使用ExecuteSynchronously
,这似乎对拥有Stephen讨论的神话般的Task
扩展方法.ConfigureAwait(CancellationToken)
很有用。
I've posted the best answer I have right now, but I'm hoping that someone will come up with something better. 我已经发布了目前最好的答案,但我希望有人会提出更好的建议。
Sample Winforms Application demonstrating the usage: 示例Winforms应用程序演示了用法:
public partial class Form1 : Form
{
CancellationTokenSource m_cts = new CancellationTokenSource();
private void Form1_Load(object sender, EventArgs e)
{
cancelBtn.Enabled = false;
}
private void cancelBtn_Click(object sender, EventArgs e)
{
m_cts.Cancel();
cancelBtn.Enabled = false;
doWorkBtn.Enabled = true;
}
private Task DoWorkAsync()
{
cancelBtn.Enabled = true;
doWorkBtn.Enabled = false;
var task = Task.Run(() => LongRunningAndMightThrow());
m_cts = new CancellationTokenSource();
CancellationToken ct = m_cts.Token;
var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task updateUITask = task.ContinueWith(t => UpdateUI(t), ct, TaskContinuationOptions.None, uiTaskScheduler);
return updateUITask;
}
private async void doWorkBtn_Click(object sender, EventArgs e)
{
try
{
await DoWorkAsync();
MessageBox.Show("Completed");
}
catch (OperationCanceledException)
{
MessageBox.Show("Cancelled");
}
catch
{
MessageBox.Show("Faulted");
}
}
private void UpdateUI(Task<bool> t)
{
// We *only* get here when the cancel button was *not* clicked.
cancelBtn.Enabled = false;
doWorkBtn.Enabled = true;
// Update the UI based on the results of the task (completed/failed)
// ...
}
private bool LongRunningAndMightThrow()
{
// Might throw, might complete
// ...
return true;
}
}
Stephen Toub's WithCancellation
extension method: Stephen Toub的WithCancellation
扩展方法:
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using(cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
return await task;
}
Related Links: 相关链接:
Writing a WithCancellation
method can be done much simpler, in just one line of code: 只需一行代码,编写WithCancellation
方法就可以更加简单:
public static Task WithCancellation(this Task task,
CancellationToken token)
{
return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
public static Task<T> WithCancellation<T>(this Task<T> task,
CancellationToken token)
{
return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
As for the operation you want to do, just using await
instead of ContinueWith
is just as easy as it sounds; 至于您要执行的操作,只需使用await
而不是ContinueWith
听起来就很容易。 you replace the ContinueWith
with an await
. 您将await
替换为ContinueWith
。 Most of the little pieces can be cleaned up a lot though. 大部分小东西虽然可以清理很多。
m_cts.Cancel();
m_cts = new CancellationTokenSource();
var result = await Task.Run(() => LongRunningAndMightThrow())
.WithCancellation(m_cts.Token);
UpdateUI(result);
The changes are not huge, but they're there. 更改虽然不大,但是仍然存在。 You [probably] want to cancel the previous operation when starting a new one. 您[可能]要在开始新操作时取消上一个操作。 If that requirement doesn't exist, remove the corresponding line. 如果该要求不存在,请删除相应的行。 The cancellation logic is all already handled by WithCancellation
, there is no need to throw explicitly if cancellation is requested, as that will already happen. 取消逻辑已由WithCancellation
处理,如果请求取消,则无需显式抛出,因为这已经发生。 There's no real need to store the task, or the cancellation token, as local variables. 真正不需要将任务或取消令牌存储为局部变量。 UpdateUI
shouldn't accept a Task<bool>
, it should just accept a boolean. UpdateUI
不应接受Task<bool>
,而应仅接受布尔值。 The value should be unwrapped from the task before calling UpdateUI
. 在调用UpdateUI
之前,应将该值从任务中解包。
The following should be equivalent: 以下内容应等效:
var task = Task.Run(() => LongRunningAndMightThrow());
m_cts = new CancellationTokenSource();
CancellationToken ct = m_cts.Token;
try
{
await task.WithCancellation(ct);
}
finally
{
ct.ThrowIfCancellationRequested();
UpdateUI(task);
}
Notice that the try/finally
is required for the case where the LongRunningAndMightThrow
method faults, but by the time we return to the UI thread the CancellationToken
has been triggered. 请注意,对于LongRunningAndMightThrow
方法出错的情况,需要try/finally
,但是到我们返回UI线程时, CancellationToken
已被触发。 Without it the returned outer Task
would be faulted where in the original ContinueWith
case, it would have been cancelled. 没有它,返回的外部Task
将会出错,在原始ContinueWith
情况下,它将被取消。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.