gpt4 book ai didi

c# - 意外的任务取消行为

转载 作者:太空狗 更新时间:2023-10-29 23:19:52 24 4
gpt4 key购买 nike

我创建了一个简单的 .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. 是在请求​​ token 取消的同一线程中执行的?

我希望看到的是 [7] Before cancellation.,然后是 [7] After cancellation.,然后是 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.

更新

有趣的是当我替换

await Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token);

while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
cancellationTokenSource.Token.ThrowIfCancellationRequested();
}

.NET 使后台线程保持忙碌,并且输出再次符合预期:

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

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

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

最佳答案

My question is why [7] Task delay has been cancelled. is executed in the same thread where token cancellation is being requested?

这是因为 await schedules its task continuations with the ExecuteSynchronously flag .我也认为这种行为令人惊讶,最初将其报告为一个错误(作为“设计”关闭)。

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

单步执行:

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

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

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

Note that if I execute cancellationTokenSource.Cancel() from the main thread then the output looks as expected

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

Interestingly when I replace Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token) with cancellationTokenSource.Token.ThrowIfCancellationRequested() .NET keeps that background thread busy and the output is again as expected

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

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


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

奖金:quiz for your teammates .

关于c# - 意外的任务取消行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55435757/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com