Async/Await等同于.ContinueWith和CancellationToken以及TaskScheduler.FromCurrentSynchronizationContext()调度程序

Mat*_*ith 1 c# task-parallel-library cancellation async-await

这是这个问题的后续行动.

问题:使用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);
Run Code Online (Sandbox Code Playgroud)

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

请注意,该行为具有以下所有期望的行为:

  1. CancellationToken取消时,updateUITask最终会尽快取消(即LongRunningAndMightThrow工作可能仍会持续相当长的一段时间).

  2. ct的CancellationToken被检查取消在UI线程上运行UpdateUI拉姆达之前(见这个答案).

  3. updateUITask最终会取消在某些情况下task完成的或者是错误的(因为ct的CancellationToken在执行UpdateUI拉姆达之前在UI线程检查.

  4. CancellationToken在UI线程的检查和UpdateUIlambda 的运行之间没有中断流.也就是说,如果CancellationTokenSource唯一曾经在UI线程上取消了,再有就是的检查之间不存在竞争条件CancellationToken和的运行UpdateUI拉姆达-没有什么可以触发CancellationToken这两个事件之间,因为UI线程不放弃这两个事件之间.

讨论:

  • 将其转换为异步/等待的主要目标之一是将UpdateUI工作 lambda中取出(为了便于阅读/调试).

  • 上面的#1可以通过Stephen Toub的WithCancellation任务扩展方法来解决.(你可以在答案中随意使用).

  • 其他要求似乎很难封装成一个辅助方法没有通过UpdateUI的拉姆达因为我不能有一个突破(即await)的检查之间CancellationToken和执行UpdateUI(因为我以为我可以不依赖于实现细节await使用ExecuteSynchronously 如前所述这里似乎有斯蒂芬谈到的神话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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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; 
}
Run Code Online (Sandbox Code Playgroud)

相关链接:

Ser*_*rvy 5

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);
}
Run Code Online (Sandbox Code Playgroud)

至于你想要做的操作,只需使用await而不是ContinueWith听起来那么简单; 你ContinueWith用一个替换await.尽管如此,大多数小件都可以清理干净.

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

变化并不大,但他们在那里.您[可能]想要在开始新操作时取消之前的操作.如果该要求不存在,请删除相应的行.取消逻辑全部已经处理WithCancellation,如果请求取消则不需要明确抛出,因为已经发生了.没有必要将任务或取消令牌存储为局部变量. UpdateUI不应该接受a Task<bool>,它应该只接受一个布尔值.在调用之前,应该从任务中解开该值UpdateUI.