Подтвердить что ты не робот

Синхронизация по умолчаниюContext по умолчанию TaskScheduler

Это будет немного длиннее, поэтому, пожалуйста, несите меня.

Я думал, что поведение планировщика задач по умолчанию (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();
        }
    }
}

Выход:

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). По аналогии, если бы я запустил тест # 1 из приложения WinForms, он был бы продолжен асинхронно после 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. Кто-нибудь может подумать над этим?

Природа объекта TaskCompletion
Параллельное программирование: планировщики заданий и контекст синхронизации
Параллельное программирование: TaskScheduler.FromCurrentSynchronizationContext
Это все о SynchronizationContext


[ОБНОВЛЕНИЕ]. Моя точка зрения: объект контекста синхронизации по умолчанию был явно установлен в основном потоке, прежде чем поток попадает в первый await tcs.Task в тесте # 1. IMO, тот факт, что он не является контекстом синхронизации графического интерфейса, не означает, что он не должен быть записан для продолжения после 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();
        }
    }
}

Волшебно, все изменилось лучше! Здесь новый вывод:

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), это не то, что я ожидаю. Я все еще ищу причины для этого.

4b9b3361

Ответ 1

Когда вы начинаете погружаться глубоко в детали реализации, важно различать документированное/надежное поведение и недокументированное поведение. Кроме того, на самом деле не считается правильным установить SynchronizationContext.Current на new SynchronizationContext(); некоторые типы в .NET рассматривают null как планировщик по умолчанию, а другие типы рассматривают null или new SynchronizationContext() как планировщик по умолчанию.

Когда вы await не завершены Task, TaskAwaiter по умолчанию захватывает текущий SynchronizationContext - если он не является null (или его GetType возвращает typeof(SynchronizationContext)), и в этом случае TaskAwaiter фиксирует текущий TaskScheduler. Это поведение в основном документировано (предложение GetType не AFAIK). Однако учтите, что это описывает поведение TaskAwaiter, а не TaskScheduler.Default или TaskFactory.StartNew.

После того, как контекст (если есть) будет захвачен, тогда await планирует продолжение. Это продолжение запланировано с помощью ExecuteSynchronously, как описано в моем блоге (это поведение недокументировано). Однако обратите внимание, что ExecuteSynchronously не всегда выполняется синхронно; в частности, если в продолжении есть планировщик задач, он будет запрашивать только синхронное выполнение текущего потока, и планировщик задач имеет возможность отказаться от его выполнения синхронно (также недокументирован).

Наконец, обратите внимание, что a 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.Current равен null.
  • SetResult пытается выполнить продолжение await tcs.Task синхронно в текущем планировщике задач; однако он не может, потому что SynchronizationContextTaskScheduler видит, что поток 10 имеет SynchronizationContext.Current null, в то время как для него требуется new SynchronizationContext(). Таким образом, он планирует асинхронное продолжение (в потоке 11).
  • Аналогичная ситуация возникает в конце задачи StartNew; в этом случае я считаю совпадением, что await task продолжается в одном и том же потоке.

В заключение я должен подчеркнуть, что в зависимости от недокументированной реализации детали не являются разумными. Если вы хотите, чтобы ваш метод async продолжался в потоке пула потоков, заверните его в Task.Run. Это значительно упростит ваш код, а также сделает ваш код более устойчивым к будущим обновлениям фреймворка. Кроме того, не устанавливайте SynchronizationContext.Current в new SynchronizationContext(), так как обработка этого сценария несовместима.

Ответ 2

SynchronizationContext всегда просто вызывает ThreadPool.QueueUserWorkItem в сообщении, что объясняет, почему вы всегда видите другой поток в тесте # 2.

В тесте # 1 вы используете умнее TaskScheduler. await предполагается продолжать в том же потоке (или "оставаться в текущем потоке" ). В приложении консоли нет способа "запланировать" возврат к основному потоку, как в инфраструктуре пользовательского интерфейса на основе сообщений. Приложению 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