【问题标题】:Async/Await equivalent to .ContinueWith with CancellationToken and TaskScheduler.FromCurrentSynchronizationContext() schedulerAsync/Await 等效于 .ContinueWith 与 CancellationToken 和 TaskScheduler.FromCurrentSynchronizationContext() 调度程序
【发布时间】:2014-12-05 23:40:10
【问题描述】:

这是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)

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

  1. CancellationToken 被取消时,updateUITask 最终会尽快取消(即LongRunningAndMightThrow 的工作可能还要持续一段时间)。

  2. 在运行 UpdateUI lambda 之前,在 UI 线程上检查ct CancellationToken 是否取消(请参阅this answer)。

  3. task 完成或出错的某些情况下,updateUITask 最终会被取消(因为在执行 UpdateUI lambda 之前在 UI 线程上检查了 ct CancellationToken。

    李>
  4. 在 UI 线程上检查 CancellationToken 和运行 UpdateUI lambda 之间没有中断。也就是说,如果CancellationTokenSource 在UI 线程上 被取消,那么在CancellationToken 的检查和UpdateUI lambda 的运行之间没有竞争条件--没有可能会在这两个事件之间触发CancellationToken,因为在这两个事件之间没有放弃 UI 线程。

讨论:

  • 我将其移至 async/await 的主要目标之一是让 UpdateUI 工作输出 lambda(为了便于阅读/调试)。

  • 上面的#1 可以通过Stephen Toub's WithCancellation task extension method 解决。 (您可以随意在答案中使用)。

  • 如果不将 UpdateUI 作为 lambda 传递,其他要求似乎很难封装到辅助方法中,因为在检查 CancellationToken 和执行UpdateUI(因为我假设我不能依赖 await 使用 ExecuteSynchronously as mentioned here 的实现细节。这似乎是拥有神话般的 Task 扩展方法 .ConfigureAwait(CancellationToken) Stephen 谈到的有用。

  • 我已经发布了目前最好的答案,但我希望有人能提出更好的答案。

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

相关链接:

【问题讨论】:

    标签: c# task-parallel-library async-await cancellation


    【解决方案1】:

    以下应该是等价的:

    var task = Task.Run(() => LongRunningAndMightThrow());
    
    m_cts = new CancellationTokenSource();
    CancellationToken ct = m_cts.Token;
    
    try
    {
        await task.WithCancellation(ct);
    }
    finally
    {
        ct.ThrowIfCancellationRequested();
        UpdateUI(task);
    }
    

    请注意,try/finallyLongRunningAndMightThrow 方法出错的情况下是必需的,但当我们返回 UI 线程时,CancellationToken 已被触发。没有它,返回的外部 Task 将出现故障,而在原始 ContinueWith 情况下,它将被取消。

    【讨论】:

      【解决方案2】:

      编写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&lt;bool&gt;,它应该只接受布尔值。在调用UpdateUI之前,应该从任务中解包该值。

      【讨论】:

      • 我喜欢新的 oneliner WithCancellation。我想知道为什么斯蒂芬没有使用它。
      • 您的解决方案与 ContinueWith 场景的行为不匹配:(1) 不一定在 UI 线程上检查 CancellationToken,而且在检查 CancellationToken 和运行之间的 UI 上可能会发生其他事情更新用户界面。
      • (2) 外部任务将(在某些情况下)最终处于故障状态,而 ContinueWith 案例最终会被取消。
      • @MattSmith 1) 取消令牌不需要在 UI 线程中检查;没有理由这样做。 2) 如果这两个操作几乎同时发生,为什么会出现这个问题。无论您如何编码它会首先发生,这都是一种竞争条件。这里没有产生真正的问题。 3)这个你自己的代码做同样的事情;它只会在成功的情况下调用UpdateUI。这不会改变这一点。如果您有要在出现错误时运行的代码,请使用try/catch 并将错误处理代码放入其中。
      • @MattSmith 你没有考虑到我描述的所有情况。可以单击取消按钮,并且在该单击事件处理程序被 UI 线程执行之前,任务可能最终出现故障。用户单击按钮与实际发生相应操作之间存在延迟。在此期间可能会发生一些事情,例如此任务出错(或正常完成)。另一方面,您的任务可能会出错或正常完成,安排您必须做的事情的延续,然后让用户在此期间单击取消。
      猜你喜欢
      • 2012-02-04
      • 2014-01-27
      • 2019-09-06
      • 1970-01-01
      • 2020-11-19
      • 2017-09-18
      • 2013-05-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多