繁体   English   中英

.NET 是否在新的不同线程池线程上恢复等待继续,还是重用先前恢复的线程?

[英]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 Taskawait将捕获“上下文”并使用它来恢复异步方法 这个“上下文”是SynchronizationContext.Current ,除非它是null ,在这种情况下上下文是TaskScheduler.Current 在您的示例代码中,上下文是线程池上下文。

谜题的另一部分没有记录: await使用TaskContinuationOptions.ExecuteSynchronously标志 这意味着当Task.Run任务完成时(由线程4完成),它的延续会立即同步运行 - 如果可能的话 在您的示例代码中,延续可能会同步运行,因为线程4上有足够的堆栈,并且延续应该在线程池线程上运行,而线程4是线程池线程。

同样,当AsyncThree完成时, AsyncTwo的延续会立即同步运行 - 再次在线程4上运行,因为它满足所有条件。

这是一种优化,在像 ASP.NET 这样的场景中特别有用,在这些场景中,通常有一系列async方法并完成一个任务(例如,读取数据库)来完成整个链并发送响应。 在这些情况下,您希望避免不必要的线程切换。

一个有趣的副作用是你最终得到了一个“反向调用堆栈”:线程池线程4运行了你的代码,然后完成了AsyncThree ,然后是AsyncTwoAsyncOne ,并且这些完成中的每一个都在实际调用中堆。 如果您在AsyncOneWriteLine上放置断点(并查看外部代码),您可以看到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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM