首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >意外任务取消行为

意外任务取消行为
EN

Stack Overflow用户
提问于 2019-03-30 21:19:38
回答 1查看 811关注 0票数 6

我创建了一个简单的.NET框架4.7.2WPF应用程序,有两个控件-一个文本框和一个按钮。下面是我的代码:

代码语言:javascript
复制
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文本框中看到以下结果:

代码语言:javascript
复制
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(),那么输出看起来和预期的一样:

代码语言:javascript
复制
12:06:59.5583 [1] Before cancellation.
12:06:59.5603 [1] After cancellation.
12:07:02.5998 [5] Task delay has been cancelled.

更新

有趣的是当我替换

代码语言:javascript
复制
await Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token);

使用

代码语言:javascript
复制
while (true)
{
    await Task.Delay(TimeSpan.FromMilliseconds(100));
    cancellationTokenSource.Token.ThrowIfCancellationRequested();
}

.NET使后台线程处于繁忙状态,并且输出再次如愿以偿:

代码语言:javascript
复制
12:08:15.7259 [5] Before cancellation.
12:08:15.7289 [5] After cancellation.
12:08:18.8418 [7] Task delay has been cancelled..

更新2

我稍微更新了代码示例,希望能更清楚一些。

请注意,这不是一个纯粹的假设问题,而是一个实际的问题,我花了相当长的时间来理解我们的生产代码。但为了简洁起见,我创建了这个极其简化的代码示例,说明了相同的行为。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2019-04-01 17:25:06

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

这是因为标志。我还认为这种行为令人惊讶,并最初将其报告为bug (以“通过设计”结束)。

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

要通过它:

  • 一些线程池线程"7“运行cancellationTokenSource.Cancel()
  • 这将导致CancellationTokenSource进入已取消的状态并运行其回调。
  • 其中一个回调是Task.Delay内部的。该回调不是特定于线程的,因此它在线程7上执行。
  • 这将导致从Task返回的Task.Delay被取消。await已经从线程池线程中安排了它的继续,并且线程池线程都被认为是兼容的,所以async继续直接在线程7上执行。

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

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

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

这是因为UI上下文是,而不是与线程池上下文兼容的。因此,当从Task返回的Task.Delay被取消时,await将看到它位于一个UI上下文中,而不是线程池上下文中,因此它将它的延续排队到线程池,而不是直接执行它。

有趣的是,当我将Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token)替换为cancellationTokenSource.Token.ThrowIfCancellationRequested()时,.NET使后台线程处于繁忙状态,并且输出再次如愿以偿

这不是因为线程“忙”。因为再也没有回音了。因此,观察方法是轮询而不是通知。

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

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

奖金:一个为你的队友做测试

票数 7
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/55435757

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档