默认SynchronizationContext与默认TaskScheduler

nos*_*tio 20 .net c# multithreading task-parallel-library async-await

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

我在想默认任务scheduler(ThreadPoolTaskScheduler)的行为与默认的" ThreadPool" SynchronizationContext(后者可以通过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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

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,因此应该捕获它并且延迟应该异步发生(即,在排队的不同池线程上SynchronizationContext.Post).依此类推,如果我从一个WinForms应用程序内运行测试#1,它会被异步后继续await tcs.Task,对WinFormsSynchronizationContext在消息循环的未来迭代.

但这不是测试#1中发生的事情.出于好奇,我改变ConfigureAwait(true) ConfigureAwait(false),这对输出没有任何影响.我正在寻找对此的解释.

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

我实际上更喜欢测试#2的行为,因为它为副作用(可能是死锁)留下了更少的间隙,这可能是由同步延续触发引起的tcs.SetResult,即使它是以额外的线程切换为代价的.但是,我不完全理解为什么这样的线程切换发生在哪里ConfigureAwait(false).

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

TaskCompletionSource
并行编程的本质:任务调度程序和同步上下文
并行编程:TaskScheduler.FromCurrentSynchronizationContext
它是关于SynchronizationContext的全部内容


[更新]我的观点是,在线程命中await tcs.Task测试#1中的第一个之前,默认同步上下文对象已显式安装在主线程上.IMO,它不是一个GUI同步上下文的事实并不意味着它不应该被捕获继续之后await.这就是为什么我希望继续tcs.SetResult发生在不同的线程上ThreadPool(在那里排队SynchronizationContext.Post),而主线程可能仍然被阻止TcsTest(...).Wait().这与此处描述的场景非常相似.

所以,我继续实施哑同步上下文类 TestSyncContext,这是只是一个包装周围SynchronizationContext.它现在安装而不是SynchronizationContext自己:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

神奇的是,事情发生了变化!这是新的输出:

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现在按预期运行(await tcs.Task异步排队到池线程).#2似乎也没问题.让我们ConfigureAwait(true)改为ConfigureAwait(false):

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(false)使await tcs.Task忽略同步上下文(TestSyncContext.Post调用了),所以现在同步之后继续tcs.SetResult.

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

现在,我还是无法解释测试#2的行为ConfigureAwait(false).这是一个较少的TestSyncContext.Post电话,这是理解.但是,await tcs.Task仍然会在不同的线程上继续tcs.SetResult(与#1不同),这不是我所期望的.我仍然在寻找这个原因.

Ste*_*ary 19

当您开始深入了解实现细节时,区分记录/可靠行为和未记录的行为非常重要.而且,SynchronizationContext.Current设置为真的并不合适new SynchronizationContext(); 某些类型的.NET治疗null作为默认的调度,以及其他类型的治疗null new SynchronizationContext()作为默认的调度.

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

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

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

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

  • StartNew 使用默认任务调度程序(在线程10上)启动新任务.
  • SetResult同步执行由await tcs.Task.设置的连续.
  • StartNew任务结束时,它同步执行设置的延续await task.

在您原来的测试#2中:

  • StartNew使用任务调度程序包装器为默认构造的同步上下文(在线程10上)启动新任务.请注意,线程10上的任务已TaskScheduler.Current设置为a,SynchronizationContextTaskSchedulerm_synchronizationContext实例是由new SynchronizationContext(); 创建的; 但是,那个线程SynchronizationContext.Currentnull.
  • SetResult尝试await tcs.Task在当前任务调度程序上同步执行延续; 但是,它可以因为无法SynchronizationContextTaskScheduler看到该线程10拥有SynchronizationContext.Currentnull是需要一段时间new SynchronizationContext().因此,它异步地调度连续(在线程11上).
  • 类似的情况发生在StartNew任务结束时; 在这种情况下,我认为await task继续在同一个线程上是巧合.

总之,我必须强调,取决于无证实施细节并不明智.如果你想让你的async方法继续在一个线程池线程,然后将其包装在一个Task.Run.这将使您的代码的意图更加清晰,并使您的代码对未来的框架更新更具弹性.此外,不要设置SynchronizationContext.Currentnew SynchronizationContext(),因为那种情况下的处理是不一致的.

  • 我同意这不是最好的设计。有许多本地用途,例如 `localContext = SynchronizationContext.Current ?? 新的 SynchronizationContext()`。但我还没有经历整个框架。:) (2认同)