意外的任务取消行为

我创建了一个简单的 .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看到以下结果:Output


12: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.


哆啦的时光机
浏览 94回答 1
1回答

Qyouu

我的问题是为什么[7] Task delay has been cancelled.在请求令牌取消的同一线程中执行?这是因为用flagawait安排它的任务延续ExecuteSynchronously。我也认为这种行为令人惊讶,并且最初将其报告为一个错误(作为“按设计”关闭)。更具体地说,await捕获一个上下文,如果该上下文与正在完成任务的当前上下文兼容,则async延续将直接在完成该任务的线程上执行。要逐步完成它:某些线程池线程“7”运行cancellationTokenSource.Cancel()。这会导致CancellationTokenSource进入取消状态并运行其回调。其中一个回调是Task.Delay.&nbsp;该回调不是特定于线程的,因此它在线程 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>和的问题CancellationTokenSource。TaskCompletionSource<T>有一个很好的RunContinuationsAsynchronously选项可以覆盖ExecuteSynchronously标志;不幸的是,CancellationTokenSource没有;你必须使用排队你Cancel对线程池的调用Task.Run。奖励:为您的队友准备的测验。
打开App,查看更多内容
随时随地看视频慕课网APP