[英]Does .NET resume an await continuation on a new different thread pool thread or reuse the thread from a previous resumption?
.NET 是否在新的不同线程池线程上恢复等待继续,还是重用先前恢复的线程?
让我们在下面的 .NET Core 控制台应用程序中的 C# 代码中想象一下:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NetCoreResume
{
class Program
{
static async Task AsyncThree()
{
await Task.Run(() =>
{
Console.WriteLine($"AsyncThree Task.Run thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
});
Console.WriteLine($"AsyncThree continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
}
static async Task AsyncTwo()
{
await AsyncThree();
Console.WriteLine($"AsyncTwo continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
}
static async Task AsyncOne()
{
await AsyncTwo();
Console.WriteLine($"AsyncOne continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
}
static void Main(string[] args)
{
AsyncOne().Wait();
Console.WriteLine("Press any key to end...");
Console.ReadKey();
}
}
}
它会输出:
AsyncThree Task.Run thread id:4
AsyncThree continuation thread id:4
AsyncTwo continuation thread id:4
AsyncOne continuation thread id:4
Press any key to end...
我试图在每个 await Task
之后添加ConfigureAwait(false)
,但它会得到相同的结果。
正如我们所见,似乎所有的 await 延续都重用了在AsyncThree()
方法的Task.Run
中创建的线程。 我想问一下.NET是否会一直在之前的恢复线程上恢复等待继续,或者在某些情况下它会从线程池中应用一个新的不同线程?
我知道有答案,继续将在下面的讨论中的线程池线程上恢复:
让我们排除上面链接中的SynchronizationContext
情况,因为我们现在正在讨论 .NET 控制台应用程序。 但是我想问一下,我的例子中的线程池线程似乎总是thread id 4
,我不知道是不是因为thread id 4
在线程池中总是空闲的,所以每个延续都巧合地重用它,或者.NET有机制会尽量复用之前的恢复线程吗?
每个延续是否有可能在不同的线程池线程上恢复,如下所示?
AsyncThree Task.Run thread id:4
AsyncThree continuation thread id:5
AsyncTwo continuation thread id:6
AsyncOne continuation thread id:7
Press any key to end...
.NET 是否在新的不同线程池线程上恢复等待继续,还是重用先前恢复的线程?
两者都不。 默认情况下,当await
Task
, await
将捕获“上下文”并使用它来恢复异步方法。 这个“上下文”是SynchronizationContext.Current
,除非它是null
,在这种情况下上下文是TaskScheduler.Current
。 在您的示例代码中,上下文是线程池上下文。
谜题的另一部分没有记录: await
使用TaskContinuationOptions.ExecuteSynchronously
标志。 这意味着当Task.Run
任务完成时(由线程4
完成),它的延续会立即同步运行 - 如果可能的话。 在您的示例代码中,延续可能会同步运行,因为线程4
上有足够的堆栈,并且延续应该在线程池线程上运行,而线程4
是线程池线程。
同样,当AsyncThree
完成时, AsyncTwo
的延续会立即同步运行 - 再次在线程4
上运行,因为它满足所有条件。
这是一种优化,在像 ASP.NET 这样的场景中特别有用,在这些场景中,通常有一系列async
方法并完成一个任务(例如,读取数据库)来完成整个链并发送响应。 在这些情况下,您希望避免不必要的线程切换。
一个有趣的副作用是你最终得到了一个“反向调用堆栈”:线程池线程4
运行了你的代码,然后完成了AsyncThree
,然后是AsyncTwo
和AsyncOne
,并且这些完成中的每一个都在实际调用中堆。 如果您在AsyncOne
的WriteLine
上放置断点(并查看外部代码),您可以看到ThreadPoolWorkQueue.Dispatch
(间接)调用AsyncThree
(间接)调用AsyncTwo
(间接)调用AsyncOne
。
正如您所看到的, 为什么在等待方法之后的代码上没有使用初始线程? 根据目前可用的内容,很有可能在另一个线程上恢复。
在异步编程中,当与 async await 一起使用时,没有明确使用特定线程。 您只知道将从线程池中挑选一个可用线程。
在您的情况下,由于执行几乎是连续的,线程被释放并且您得到数字 4。
基于线程池文档https://docs.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool每个进程的线程池是唯一的,所以我希望使用要使用的第一个可用线程。 所以当你没有其他并发操作时,线程 4 每次都会被重用。 但是没有任何保证。
.NET 是否在新的不同线程池线程上恢复等待继续,还是重用先前恢复的线程?
大多数时候它将使用相同的线程,但这不能保证。 This answer to a question where OP 希望强制它到同一线程提供了一些细节。
继续运行的位置取决于TaskScheduler 。 我没有看过,但我想它会在同一个线程上运行延续,只是为了避免在使用线程池时不必要的开销。
每个延续是否有可能在不同的线程池线程上恢复,如下所示?
是的,有一种可能性,但很可能你不会看到这个,同样是因为调度程序。 您可能必须自己编写以强制改变,而我个人不知道您为什么要这样做。 (不是说这是你的意图)。
您的示例代码中没有真正的异步调用,即。 一个 i/o 调用,因此很可能将延续作为优化内联在原始线程上。 在您的异步方法中,如果您添加 await Task.Delay,您可能会观察到延续可能在不同的线程上运行。 底线永远不会对延续运行的线程做出任何假设,假设它在另一个线程上运行。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.