【问题标题】:Task cancellation with async task使用异步任务取消任务
【发布时间】:2023-09-19 15:35:01
【问题描述】:

我正在尝试使用this FAQ 中描述的取消令牌。这是我最初的想法:

private async void OnLoginButtonClicked(object sender, EventArgs e)
{
    if (this.cancelToken == null)
    {
        this.cancelToken = new CancellationTokenSource();
    }

    try
    {
        bool loginSuccess = await AsyncLoginTask(this.cancelToken.Token);

        if (loginSuccess)
        {
            // Show main page
        }
    }
    catch (OperationCanceledException ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
    finally
    {
        this.cancelToken = null;
    }
}

private async Task<bool> AsyncLoginTask(CancellationToken cancellationToken = default(CancellationToken))
{
    // Pass the token to HttpClient()
}

现在我对其进行了调整,结果如下:

private async void OnLoginButtonClicked(object sender, EventArgs e)
{
    this.cancelToken?.Dispose();
    this.cancelToken = new CancellationTokenSource();

    try
    {
        var ui = TaskScheduler.FromCurrentSynchronizationContext();
        var loginTask = Task.Factory.StartNew(async () =>
        {
            bool loginSuccess = await AsyncLoginTask(this.cancelToken.Token);
        }, this.cancelToken.Token);

        var displayResults = loginTask.ContinueWith(resultTask =>
                            {
                                // How do I know if the login was successful?
                                // Because AsyncLoginTask() returns bool.
                                System.Diagnostics.Debug.WriteLine("done");
                            },
                             CancellationToken.None,
                             TaskContinuationOptions.OnlyOnRanToCompletion,
                             ui);

        var displayCancelledTasks = loginTask.ContinueWith(resultTask =>
                                    {
                                        System.Diagnostics.Debug.WriteLine("canceled");
                                    },
                                   CancellationToken.None,
                                   TaskContinuationOptions.OnlyOnCanceled, ui);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
}

问题:

  • 如何知道登录是否成功?因为AsyncLoginTask() 返回bool
  • 如何正确创建和销毁令牌以允许多次启动和取消操作?
  • 如何处理任务中的任务?控制台中显示“完成”,而任务 (AsyncLoginTask) 尚未完成。

【问题讨论】:

  • How do I know if the login was successful? Because AsyncLoginTask() returns bool 好吧,您可以在该任务中返回 loginSuccess,这将给 loginTask 一个结果。但是...我的问题是为什么要将登录任务包装在任务中?
  • 我的旧代码 (AsyncLoginTask) 是这样构建的(异步/等待返回 true/false)。现在我希望用户可以取消操作。在链接的文章中,这是处理任务和ContinueWith 的推荐方式。所以我完成了我的任务。我应该继续抓OperationCanceledException吗?
  • 好吧,我还没有深入研究链接的文章,我必须说我不是这方面的专家......但是,由于登录已经可以选择提供取消令牌,稍后您的逻辑需要登录结果,一个简单的await 对我来说似乎更直观。您可以以适当的方式处理该取消。你能解释一下你的实际目标以及为什么你认为这次重构将有助于实现它吗?
  • @testing,查看这篇博文:Async re-entrancy, and the patterns to deal with it
  • 内部任务退出,因为这就是await 的工作方式——它从第一个await 返回,保存上下文并释放线程。你应该抓住OperationCanceledException,它是基类*.com/a/13040503/213550

标签: c# asynchronous async-await task cancellation


【解决方案1】:

我正在尝试使用此常见问题解答中描述的取消令牌。

那篇博文使用的是动态任务并行(StartNewContinueWith)。动态任务并行是指您有很多 CPU 密集型操作要做,而您在处理它们之前不知道有多少(即,您处理的每个操作都可以添加零个或多个附加任务到同一个任务中)过程)。

在您的情况下,您有一个异步操作。因此,该文章中的方法对于您的用例来说是完全错误的。你原来的想法要正确得多。

你会更想这样做:

private async void OnLoginButtonClicked(object sender, EventArgs e)
{
  // Cancel the previous attempt (if any) and start a new one.
  this.cts?.Cancel();
  this.cts = new CancellationTokenSource();

  try
  {
    bool loginSuccess = await AsyncLoginTask(this.cts.Token);
    // Resolve race condition where user cancels just as it completed.
    this.cts.Token.ThrowIfCancellationRequested();
    if (loginSuccess)
    {
      // Show main page
    }
  }
  catch (OperationCanceledException ex)
  {
    System.Diagnostics.Debug.WriteLine(ex.Message);
  }
  catch (Exception ex)
  {
    System.Diagnostics.Debug.WriteLine(ex.Message);
  }
}

private async Task<bool> AsyncLoginTask(CancellationToken cancellationToken = default(CancellationToken))
{
  // Pass the token to HttpClient()
}

【讨论】:

  • 感谢您的回答。如果再次按下登录按钮,是否必须创建新的取消令牌?
  • 是的,因为任何已经存在的 CTS 都是为了更早的点击。