默认SynchronizationContext与默认TaskScheduler

时间:2014-01-06 02:59:36

标签: c# .net multithreading task-parallel-library async-await

这会有点长,所以请耐心等待。

我认为默认任务调度程序(ThreadPoolTaskScheduler)的行为与默认“ThreadPoolSynchronizationContext的行为非常相似(后者可以通过{隐式引用) {1}}或明确地通过await)。它们都安排在随机TaskScheduler.FromCurrentSynchronizationContext()线程上执行的任务。实际上,ThreadPool只是调用SynchronizationContext.Post

然而,当ThreadPool.QueueUserWorkItem在默认TaskCompletionSource.SetResult上排队的任务使用时,SynchronizationContext的工作方式存在细微但重要的区别。这是一个简单的控制台应用程序说明它:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleTcs
{
    class Program
    {
        static async Task TcsTest(TaskScheduler taskScheduler)
        {
            var tcs = new TaskCompletionSource<bool>();

            var task = Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(1000);
                    Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
                    tcs.SetResult(true);
                    Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(2000);
                },
                CancellationToken.None,
                TaskCreationOptions.None,
                taskScheduler);

            Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
            await tcs.Task.ConfigureAwait(true);
            Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);

            await task.ConfigureAwait(true);
            Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId);
        }

        // Main
        static void Main(string[] args)
        {
            // SynchronizationContext.Current is null
            // install default SynchronizationContext on the thread
            SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());

            // use TaskScheduler.Default for Task.Factory.StartNew
            Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId);
            TcsTest(TaskScheduler.Default).Wait();

            // use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew
            Console.WriteLine("\nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId);
            TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait();

            Console.WriteLine("\nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId);
            Console.ReadLine();
        }
    }
}

输出:

Test #1, thread: 9
before await tcs.Task, thread: 9
before tcs.SetResult, thread: 10
after await tcs.Task, thread: 10
after tcs.SetResult, thread: 10
after await task, thread: 10

Test #2, thread: 9
before await tcs.Task, thread: 9
before tcs.SetResult, thread: 10
after tcs.SetResult, thread: 10
after await tcs.Task, thread: 11
after await task, thread: 11

Press enter to exit, thread: 9

这是一个控制台应用程序,默认情况下其Main线程没有任何同步上下文,因此我在开始运行测试之前显式安装默认值:SynchronizationContext.SetSynchronizationContext(new SynchronizationContext())

最初,我认为我在测试#1期间完全理解了执行工作流程(其中任务是使用TaskScheduler.Default安排的)。 tcs.SetResult同步调用第一个延续部分(await tcs.Task),然后执行点返回tcs.SetResult并继续同步,包括第二个await task。这对我来说很有意义,直到我意识到以下。由于我们现在在执行await tcs.Task的线程上安装了默认同步上下文,因此应该捕获并继续异步(即,在由{{排队]的另一个池线程上1}})。通过类比,如果我在WinForms应用程序中运行测试#1,它将在SynchronizationContext.Post之后异步地继续,在await tcs.Task之后在消息循环的未来迭代中继续。

但这不是测试#1中发生的事情。出于好奇,我将WinFormsSynchronizationContext 更改为ConfigureAwait(true) 对输出产生任何影响。我正在寻找对此的解释。

现在,在测试#2(任务调度为ConfigureAwait(false))期间,与#1相比,确实还有一个线程切换。从输出中可以看出,TaskScheduler.FromCurrentSynchronizationContext()触发的await tcs.Task延续确实在另一个池线程上异步发生。我也试过tcs.SetResult,也没有改变任何东西。我还尝试在开始测试#2 之前立即安装ConfigureAwait(false),而不是在开始时。这导致了完全相同的输出。

我实际上更喜欢测试#2的行为,因为它可以避免由SynchronizationContext触发的同步延续引起的副作用(以及潜在的死锁)的差距,即使它来了以一个额外的线程开关的价格。但是,我并不完全理解为什么这样的线程切换,无论tcs.SetResult如何。

我熟悉以下关于这个主题的优秀资源,但我仍然在寻找对测试#1和#2中所见行为的一个很好的解释。 有人可以详细说明吗?

The Nature of TaskCompletionSource
Parallel Programming: Task Schedulers and Synchronization Context
Parallel Programming: TaskScheduler.FromCurrentSynchronizationContext
It's All About the SynchronizationContext

<小时/>

[更新] 我的观点是,在线程命中测试#1中的第一个ConfigureAwait(false)之前,默认同步上下文对象已显式安装在主线程上。 IMO,它不是GUI同步上下文这一事实并不意味着它不应该在await tcs.Task之后被继续捕获。这就是为什么我期望await之后的继续发生在与tcs.SetResult不同的线程上(由ThreadPool排队),而主线程可能仍被{{1}阻止}。这与one described here非常相似。

所以我继续前进,实现了一个愚蠢的同步上下文类 SynchronizationContext.Post,它只是{em>围绕TcsTest(...).Wait()的包装。它现在安装而不是TestSyncContext本身:

SynchronizationContext

奇怪的是,事情发生了更好的变化!以下是新的输出:

Test #1, thread: 10
before await tcs.Task, thread: 10
before tcs.SetResult, thread: 6
TestSyncContext.Post, thread: 6
after tcs.SetResult, thread: 6
after await tcs.Task, thread: 11
after await task, thread: 6

Test #2, thread: 10
TestSyncContext.Post, thread: 10
before await tcs.Task, thread: 10
before tcs.SetResult, thread: 11
TestSyncContext.Post, thread: 11
after tcs.SetResult, thread: 11
after await tcs.Task, thread: 12
after await task, thread: 12

Press enter to exit, thread: 10

现在,测试#1现在按预期运行(SynchronizationContext异步排队到池线程)。 #2似乎也没问题。我们将using System; using System.Threading; using System.Threading.Tasks; namespace ConsoleTcs { public class TestSyncContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { Console.WriteLine("TestSyncContext.Post, thread: " + Thread.CurrentThread.ManagedThreadId); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine("TestSyncContext.Send, thread: " + Thread.CurrentThread.ManagedThreadId); base.Send(d, state); } }; class Program { static async Task TcsTest(TaskScheduler taskScheduler) { var tcs = new TaskCompletionSource<bool>(); var task = Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId); tcs.SetResult(true); Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(2000); }, CancellationToken.None, TaskCreationOptions.None, taskScheduler); Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId); await tcs.Task.ConfigureAwait(true); Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId); await task.ConfigureAwait(true); Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId); } // Main static void Main(string[] args) { // SynchronizationContext.Current is null // install default SynchronizationContext on the thread SynchronizationContext.SetSynchronizationContext(new TestSyncContext()); // use TaskScheduler.Default for Task.Factory.StartNew Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId); TcsTest(TaskScheduler.Default).Wait(); // use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew Console.WriteLine("\nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId); TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait(); Console.WriteLine("\nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId); Console.ReadLine(); } } } 更改为await tcs.Task

Test #1, thread: 9
before await tcs.Task, thread: 9
before tcs.SetResult, thread: 10
after await tcs.Task, thread: 10
after tcs.SetResult, thread: 10
after await task, thread: 10

Test #2, thread: 9
TestSyncContext.Post, thread: 9
before await tcs.Task, thread: 9
before tcs.SetResult, thread: 11
after tcs.SetResult, thread: 11
after await tcs.Task, thread: 10
after await task, thread: 10

Press enter to exit, thread: 9

测试#1仍然按预期正常运行:ConfigureAwait(true)使ConfigureAwait(false)忽略同步上下文(ConfigureAwait(false)调用已消失),所以现在它在{{1}之后继续同步}。

为什么这与使用默认await tcs.Task的情况不同?我仍然很想知道。也许,默认任务调度程序(负责TestSyncContext.Post个连续)检查线程同步上下文的运行时类型信息,并对tcs.SetResult进行一些特殊处理?

现在,我还是无法解释SynchronizationContext时测试#2的行为。这是一个较少await电话,这是理解的。但是,SynchronizationContext仍在ConfigureAwait(false)的不同主题上继续(与#1不同),这不是我所期望的。我仍在寻找原因。

2 个答案:

答案 0 :(得分:18)

当您开始深入了解实施细节时,区分记录/可靠行为和未记录的行为非常重要。此外,将SynchronizationContext.Current设置为new SynchronizationContext()并不合适; .NET中的某些类型将null视为默认调度程序,其他类型将null new SynchronizationContext()视为默认调度程序。

await不完整Task时,TaskAwaiter默认会捕获当前SynchronizationContext - 除非它是null(或GetType } {return typeof(SynchronizationContext)},在这种情况下,TaskAwaiter会捕获当前的TaskScheduler。此行为主要记录在案(GetType子句不是AFAIK)。但请注意,这描述了TaskAwaiter的行为,而不是TaskScheduler.DefaultTaskFactory.StartNew

在捕获上下文(如果有)之后,await计划继续。这个延续计划using ExecuteSynchronously,如我的博客所述(此行为未记录)。但请注意ExecuteSynchronously does not always execute synchronously;特别是,如果一个continuation有一个任务调度程序,它只会请求在当前线程上同步执行,并且任务调度程序可以选择拒绝同步执行它(也没有记录)。

最后,请注意,可以请求TaskScheduler同步执行任务,但SynchronizationContext不能。因此,如果await捕获自定义SynchronizationContext,那么它必须始终异步执行延续。

所以,在你原来的测试#1中:

  • StartNew使用默认任务计划程序(在主题10上)启动新任务。
  • SetResult同步执行await tcs.Task设置的延续。
  • StartNew任务结束时,它会同步执行await task设置的续集。

在原来的测试#2中:

  • StartNew使用任务调度程序包装器为默认构造的同步上下文(在线程10上)启动新任务。请注意,线程10上的任务将TaskScheduler.Current设置为SynchronizationContextTaskScheduler,其m_synchronizationContext是由new SynchronizationContext()创建的实例;但是,该帖子的SynchronizationContext.Currentnull
  • SetResult尝试在当前任务调度程序上同步执行await tcs.Task延续;但是,它不能因为SynchronizationContextTaskScheduler看到线程10有SynchronizationContext.Current null而需要new SynchronizationContext()。因此,它以异步方式安排延续(在线程11上)。
  • 类似的情况发生在StartNew任务结束时;在这种情况下,我认为await task继续在同一个线程上是巧合。

总之,我必须强调,取决于无证件的实施细节并不明智。如果您希望在线程池线程上继续使用async方法,请将其包装在Task.Run中。这将使您的代码的意图更加清晰,并使您的代码对未来的框架更新更具弹性。另外,请勿将SynchronizationContext.Current设置为new SynchronizationContext(),因为该方案的处理方式不一致。

答案 1 :(得分:4)

SynchronizationContext总是简单地在帖子上调用ThreadPool.QueueUserWorkItem - 这解释了为什么你总是在测试#2中看到不同的主题。

在测试#1中,您使用的是更智能的TaskSchedulerawait应该在同一个线程(或"stay on the current thread")上继续。在控制台应用程序中,没有办法像在基于消息队列的UI框架中那样“安排”返回主线程。控制台应用程序中的await必须阻塞主线程,直到工作完成(让主线程无事可做)才能在同一个线程上继续。如果调度程序知道这个,那么它也可以在同一个线程上同步运行代码,因为它可以获得相同的结果而不必创建另一个线程并冒险进行上下文切换。

可在此处找到更多信息:http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx

<强>更新: 就ConfigureAwait而言。控制台应用程序无法“编组”回主线程,因此,假设ConfigureAwait(false)在控制台应用程序中没有任何意义。

另请参阅:http://msdn.microsoft.com/en-us/magazine/jj991977.aspx