当使用TPL时,如何确定方法将在哪个线程上执行

本文关键字:线程 执行 方法 何确定 TPL | 更新日期: 2023-09-27 18:08:07

我知道TPL是面向任务的,而经典的线程模型是面向工作者的。任务让你主要关注你想要解决的问题,而不是如何解决的机制它会完成的。但是当涉及到线程和任务的关系时,我仍然有点困惑。

下面是一个演示代码:
namespace AsyncUnderTheHood
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main Start : {0}", Thread.CurrentThread.ManagedThreadId);
            AwaitTest();
            Console.WriteLine("Main End : {0}", Thread.CurrentThread.ManagedThreadId);
            Console.ReadLine();
        }
        public static void DoWork()
        {
            Console.WriteLine("DoWork Start: {0}", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(5000);
            Console.WriteLine("DoWork End: {0}", Thread.CurrentThread.ManagedThreadId);
        }
        public async static void AwaitTest()
        {
            Console.WriteLine("AwaitTest Start : {0}", Thread.CurrentThread.ManagedThreadId);
            Task t = new Task(DoWork);
            t.Start();
            await t;
            Console.WriteLine("AwaitTest Done : {0}", Thread.CurrentThread.ManagedThreadId);
        }
    }
}

输出如下:

Main Start : 1
AwaitTest Start : 1   <------------ A
DoWork Start: 3
Main End : 1
DoWork End: 3
AwaitTest Done : 3    <------------ B

我的问题是,为什么A和B在不同的线程?

相同的方法在不同的线程上执行,当线程亲和性很重要时,这会导致问题吗?

当使用TPL时,如何确定方法将在哪个线程上执行

为什么A和B在不同的线程?

首先,如果您的Task是由默认调度器调度的,那么无法保证哪个Thread将在Task上运行。AwaitTest()的各个部分是单独执行的,因此不能保证它们将在同一个线程上运行。

第二,默认调度程序使用ThreadPool来执行Task。每个async方法的第一部分同步运行。在您的示例中,这意味着AwaitTest()的第一部分将在主线程上运行,第二部分将在某个ThreadPool线程上运行。因此,实际上可以保证它们不会在同一个线程上运行。

当线程亲和性很重要时,这会导致问题吗?

当然可以。但是在线程亲和性很重要的最常见的情况下:GUI编程中,它将正确工作。这是因为GUI应用程序设置了SynchronizationContext,这意味着如果async方法的第一部分在UI线程上运行,那么第二部分也将在那里运行(除非您通过使用ConfigureAwait(false)禁用它)。

但在其他情况下,它会引起问题。例如,使用以下代码:

Monitor.Enter(lockObject);
await someTask;
Monitor.Exit(lockObject);

这段代码不能在控制台应用程序中工作(Exit()很可能会抛出SynchronizationLockException),因为Exit()可以运行在与Enter()不同的线程上。

您已经要求系统"等待"一个任务。你真正要问的是,调用await的线程应该继续运行,await之后的一切都是一个"延续",当任务完成时,它将异步运行。由于控制台应用程序中没有"消息泵",因此没有简单的方法将其封送回"主"线程,因此延续只是继续使用异步Task的线程。如果在WinForm或WPF应用程序中执行相同的测试,则延续将在UI线程上运行。

这篇博文很好地描述了TPL如何使用带有工作窃取的多任务队列在现有线程池之上分层以获得最佳性能。