这是this question的后续措施。

问题:用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);


我主要对UI SynchronizationContext的情况感兴趣(例如,对于Winforms)

请注意,此行为具有以下所有所需行为:


取消CancellationToken后,updateUITask会尽快被取消(即LongRunningAndMightThrow工作可能仍会进行一段时间)。
在运行UpdateUI lambda之前,请在UI线程上检查ct CancellationToken是否取消(请参见this answer)。
updateUITask完成或出现故障的某些情况下,task将最终被取​​消(因为在执行UpdateUI lambda之前,已在UI线程上检查了ct CancellationToken)。
在UI线程上检查CancellationToken和运行UpdateUI lambda之间,流程没有中断。也就是说,如果CancellationTokenSource仅在UI线程上被取消,则在CancellationToken的检查与UpdateUI lambda的运行之间不存在竞争条件-任何事情都不会触发CancellationToken在这两个事件之间切换,因为在这两个事件之间不放弃UI线程。


讨论:


我将其移到异步/等待中的主要目的之一是使UpdateUI工作脱离lambda(为便于阅读/调试)。
上面的#1可以用Stephen Toub's WithCancellation task extension method寻址。 (您可以在答案中随意使用)。
在不通过UpdateUI作为lambda的情况下,其他要求似乎很难封装到辅助方法中,因为在await的检查和CancellationToken的执行之间我不能有一个中断(即UpdateUI)(因为我假设我不能依赖await使用ExecuteSynchronously as mentioned here的实现细节,这似乎是Stephen讨论的神话般的Task扩展方法.ConfigureAwait(CancellationToken)很有用的地方。
我已经发布了目前最好的答案,但我希望有人会提出更好的建议。


示例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的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;
}


相关链接:


Equivalent of ContinueWith(delegate, CancellationToken) with await continuation
http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx
https://stackoverflow.com/a/15673072/495262
https://stackoverflow.com/a/17560746/495262

最佳答案

只需一行代码,编写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);
}


至于您要执行的操作,只需使用await代替ContinueWith听起来就很容易。您将ContinueWith替换为await。大部分小东西虽然可以清理很多。

m_cts.Cancel();
m_cts = new CancellationTokenSource();
var result = await Task.Run(() => LongRunningAndMightThrow())
    .WithCancellation(m_cts.Token);
UpdateUI(result);


更改虽然不大,但是仍然存在。您[可能]要在开始新操作时取消上一个操作。如果该要求不存在,请删除相应的行。取消逻辑已由WithCancellation处理,如果请求取消,则无需显式抛出,因为这已经发生。真正不需要将任务或取消令牌存储为局部变量。 UpdateUI不应接受Task<bool>,而应仅接受布尔值。在调用UpdateUI之前,应从任务中解开该值。

关于c# - Async/Await等效于带有CancellationToken和TaskScheduler.FromCurrentSynchronizationContext()调度程序的.ContinueWith,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/26305569/

10-13 04:35