为了账号安全,请及时绑定邮箱和手机立即绑定

意外的任务取消行为

意外的任务取消行为

C#
哆啦的时光机 2022-11-21 15:46:34
我创建了一个简单的 .NET Framework 4.7.2 WPF 应用程序,其中包含两个控件 - 一个文本框和一个按钮。这是我的代码:private async void StartTest_Click(object sender, RoutedEventArgs e){    Output.Clear();    var cancellationTokenSource = new CancellationTokenSource();    // Fire and forget    Task.Run(async () => {        try        {            await Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token);        }        catch (OperationCanceledException)        {            Task.Delay(TimeSpan.FromSeconds(3)).Wait();            Print("Task delay has been cancelled.");        }    });    await Task.Delay(TimeSpan.FromSeconds(1));    await Task.Run(() =>    {        Print("Before cancellation.");        cancellationTokenSource.Cancel();        Print("After cancellation.");    });}private void Print(string message){    var threadId = Thread.CurrentThread.ManagedThreadId;    var time = DateTime.Now.ToString("HH:mm:ss.ffff");    Dispatcher.Invoke(() =>    {        Output.AppendText($"{ time } [{ threadId }] { message }\n");    });}按下按钮后,我在文本框中StartTest看到以下结果:Output12:05:54.1508 [7] Before cancellation.12:05:57.2431 [7] Task delay has been cancelled.12:05:57.2440 [7] After cancellation.我的问题是为什么[7] Task delay has been cancelled.在请求令牌取消的同一线程中执行?我希望看到的是[7] Before cancellation.然后是[7] After cancellation.然后Task delay has been cancelled.。或者至少Task delay has been cancelled.在另一个线程中执行。请注意,如果我cancellationTokenSource.Cancel()从主线程执行,那么输出看起来像预期的那样:12:06:59.5583 [1] Before cancellation.12:06:59.5603 [1] After cancellation.12:07:02.5998 [5] Task delay has been cancelled.
查看完整描述

1 回答

?
Qyouu

TA贡献1786条经验 获得超11个赞

我的问题是为什么[7] Task delay has been cancelled.在请求令牌取消的同一线程中执行?

这是因为用flagawait安排它的任务延续ExecuteSynchronously。我也认为这种行为令人惊讶,并且最初将其报告为一个错误(作为“按设计”关闭)。

更具体地说,await捕获一个上下文,如果该上下文与正在完成任务的当前上下文兼容,则async延续将直接在完成该任务的线程上执行。

要逐步完成它:

  • 某些线程池线程“7”运行cancellationTokenSource.Cancel()

  • 这会导致CancellationTokenSource进入取消状态并运行其回调。

  • 其中一个回调是Task.Delay. 该回调不是特定于线程的,因此它在线程 7 上执行。

  • 这会导致Task返回的 fromTask.Delay被取消。已经从一个线程池线程安排了它的awaitcontinuation,线程池线程都被认为是相互兼容的,所以asynccontinuation直接在线程7上执行。

提醒一下,线程池线程仅在有代码要运行时使用。当您使用awaitto发送异步代码时Task.Run,它可以在一个线程上运行第一部分(直到await),然后在另一个线程上运行另一部分(在 之后await)。

因此,由于线程池线程是可互换的,因此线程 7 在;之后继续执行该async方法并不是“错误的”。await这只是一个问题,因为现在 之后的代码在那个延续Cancel上被阻塞了。async

请注意,如果我从主线程执行 cancellationTokenSource.Cancel() 那么输出看起来像预期的那样

这是因为 UI 上下文被认为与线程池上下文不兼容。因此,当Task返回的 fromTask.Delay被取消时,await将看到它在 UI 上下文中而不是线程池上下文中,因此它将其继续排队到线程池而不是直接执行它。

有趣的是,当我替换Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token)cancellationTokenSource.Token.ThrowIfCancellationRequested().NET 时,后台线程一直处于忙碌状态,并且输出再次符合预期

这不是因为线程“忙”。这是因为没有回调了。所以观察方法是轮询而不是被通知

该代码设置一个计时器(通过Task.Delay),然后将线程返回到线程池。当定时器计时结束后,从线程池中抓取一个线程,检查取消令牌源是否被取消;如果不是,则设置另一个计时器并将线程再次返回到线程池。本段的要点是,Task.Run它不仅仅代表“一个线程”;它在执行代码时只有一个线程(即不在 an 中await),并且线程可以在任何await.


除非您混合使用阻塞代码和异步代码,否则await使用的一般问题通常不是问题。ExecuteSynchronously在那种情况下,最好的解决方案是将阻塞代码更改为异步代码。如果你不能这样做,那么你需要小心如何继续你async在 之后阻塞的方法await。这主要是TaskCompletionSource<T>和的问题CancellationTokenSourceTaskCompletionSource<T>有一个很好的RunContinuationsAsynchronously选项可以覆盖ExecuteSynchronously标志;不幸的是,CancellationTokenSource没有;你必须使用排队你Cancel对线程池的调用Task.Run

奖励:为您的队友准备的测验


查看完整回答
反对 回复 2022-11-21
  • 1 回答
  • 0 关注
  • 93 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信