Skip to content

Instantly share code, notes, and snippets.

@eterekhin
Last active January 2, 2020 08:55
Show Gist options
  • Save eterekhin/e161149e5fdc579b01e3bdb86c04e530 to your computer and use it in GitHub Desktop.
Save eterekhin/e161149e5fdc579b01e3bdb86c04e530 to your computer and use it in GitHub Desktop.

Немного о ThreadPool'e

  1. ThreadPool стартует задачи по очереди, поэтому возможна ситуация, когда задачи выполнятся друг за другом несмотря на то, что были запланированы в ThreadPool, смотрим:
 public static void ThreadPoolTest()
        {
            Console.WriteLine("ThreadPoolTest " + Thread.CurrentThread.ManagedThregadId);
        }

        public static void ThreadPoolTest1()
        {
            Console.WriteLine("ThreadPoolTest1 " + Thread.CurrentThread.ManagedThreadId);
        }

        public static void ThreadPoolTest2()
        {
            Console.WriteLine("ThreadPoolTest2 " + Thread.CurrentThread.ManagedThreadId);
        }

        static async Task Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(x => ThreadPoolTest()); // ThreadPoolTest 4
            Thread.Sleep(1000);
            ThreadPool.QueueUserWorkItem(x => ThreadPoolTest1()); // ThreadPoolTest1 4
            Thread.Sleep(1000);
            ThreadPool.QueueUserWorkItem(x => ThreadPoolTest2()); // ThreadPoolTest2 5
        }

Это все потому что поток работающий над задачей ThreadPoolTest уже завершает свою работу и возвращается в pool, перед тем, как будет запланирована задача ThreadPoolTest1

Task'и

Можно создать два вида тасков - таски с делегатом и без(Promise)
Рассмотрим продолжения тасков:

var t = Task.Delay(1000);
t.ContinueWith((x) => {},   TaskContinuationOptions.ExecuteSynchronously);
t.ContinueWith((x) => {},   TaskContinuationOptions.ExecuteSynchronously);
t.ContinueWith((x) => {},   TaskContinuationOptions.ExecuteSynchronously);

Console.ReadKey();

В данном случае создается таск t, и к нему три продолжения. Все продолжение независимы друг от друга, но в данном случае будут запущены последовательно (ExecuteSynchronously), в процессе того, как они зарегистрированы
Что интересно, если мы перепишем код таким образом:

var t = Task.Delay(1000)
 .ContinueWith((x) => {})
 .ContinueWith((x) => {})
 .ContinueWith((x) => {});

Console.ReadKey();

То методы также будут выполнены синхронно друг за другом, сначала это вызывает недоумение, разберемся:
В первом случае создается 1 основной Task, и регистрируются три продолжения (внутри таска приватное поле m_continuationObject - становится листом типа StandartTaskContinuation в котором три элемента.

Когда таск t завершается - они вызываются сихронно, друг за другом в цикле, вызов необычен, он происходит через _taskScheduler.TryRunInline, если не получается(такое возможно, когда делегат вызвается на TaskSchedulerFromSynchronizationContext, например в WPF, тогда продолжение планируется в TaskScheduler)

Я в данный момент работаю в консольном приложении, поэтому такого поведение не ощущаю
Но, если бы мы не указали TaskContinuationOptions.ExecuteSynchronously, эти continuation'ы были бы назначены TaskScheduler'у на выполнение асинхронно

(Лямбда, переданная в Task.Run, хранится в Delegate? m_action)

Теперь рассмотрим второй вариант каждый вызов ContinueWith создает новый Task, а именно ContinuationTaskFromTask, а предыдущему Task'у добавляется функция продолжения в m_continuationObject, в итоге t - это ContinuationTaskFromTask который мы ожидаем, очевидно он выполнится последним (сначала Task.Delay, потом первый continatuation, а потом второй, а потом уже и t), они также будут выполнены сихронно хоть и назначены в TaskScheduler через вызов m_taskScheduler.InternalQueueTask но поскольку они являюся продолжениями друг друга, то, при отсутсвии других потоков в программе будут выполнены на одном потоке, но через вызов InternalQueueTask.

Сравниваем ContinueWith и продолжение после await

Теперь рассмотрим, какая разница между вызовом ContinueWith и await Сравним два этих кода:

 public static Task ContinueWith()
 {
  return Task.Delay(1000).ContinueWith(x => Console.WriteLine("continuation"));
 }

 public static async Task Await()
 {
  await Task.Delay(1000);
  Console.WriteLine("continuation");
 }


  static void Main(string[] args)
  {
   Await();
   Console.ReadKey();
  }

Рассмотрим await;

  1. Генерируется StateMachine
  2. Если awaiter (выражение стоящее около await в методе) не завершен вызывается метод AwaitUnsafeOnCompleted, в котором назначается Continuzation, если есть syncContext и он не дефолтный, то назначается continuation, который будет обращаться к нему, если нет, то смотрится TaskScheduler, а именно TaskScheduler.Current - если он не дефолтный(ThreadPoolTaskScheduler) продолжение планируется туда, иначе, пробуем добавить продолжение без планирования(в m_continuationObject передается сама stateMachine), сихронно сразу после завершения Task'a, если не получилось - Task уже завершен, и мы ставим continuation в ThreadPool. Если awaiter уже завершен - у awaiter'a забирается результат и continuation не планируется
  3. После того, как Task отработал - он вызывает FinishContinuation, всего каждый случай который был описан в пункте два обрабатывается, соответсвенно, если Task был запланирован на SyncContext - выполняется на SyncContext'e, на TaskScheduler - на TaskScheduler'e, передана stateMachine - метод либо будет выполнен на текущем потоке(через прямой вызов метода MoveNext у stateMachine), либо может уйти в ThreadPool (обычно все-таки выполняется синхронно)

Это отличается от вызова ContinueWith:

  1. пишем .ContinueWith(x => {..})
  2. в m_continuationObject всегда добавляется только StandartTaskContinuation, этот объект создается и в него передается сам ContinuationTask(который создается из делегата, передаваемого в ContinueWith), опции таска и TaskScheduler
  3. Когда основной Task отрабатывает, вызывается FinishContinuations, который обрабатывает StandartTaskContinuation таким образом: Если завершенному Task'у не установлена опция RunContinuationsAsynchronously, а continuation'у была передана опция RunSynchronously, скорее всего он выполнится синхронно, если же нет, то будет заскедулено в TaskScheduler и выполнено асинхронно Т.e в случае с ContinueWith вообще нет никакого обращения к SyncContext, но в опции в ContinueWith можно передать SchedulerFromSynchronizationContext, и уже он отрегулирует вызов Task'a.

Когда мы пишем await - генерируется asyncStateMachine, далее если в методе несколько await'ов, все таски создаются и ожидаются(через TaskAwaiter), когда Task завершен, также вызывается метод FinishContinuations, в котором вызывается метод MoveNext машины состояний( снова попадаем в тот же метод, но в другом состоянии и так на каждый await)

Тут тоже есть continuation, но это не StandartTaskContinuation, это может быть либо IAsyncStateMachineBox, интерфейс через который можно вызвать метод MoveNext машины состояний, в итоге вызывается метод MoveNext AsyncMethodBuilder, который берез ExecutionContext который был до вызова await и снова ставит его, в итоге вызывается callback метода MoveNext машины состояний синхронно.

Либо, SynchronizationContextTaskContinuation, который запустит continuation выполнив метод Post SynchronizationContext'a. Этот continuation прицепляется к Task'у, когда вызывается метод m_builder.AwaitUnsafeOnCompleted - там проверяется наличие syncContext'a и если он существует регистрируется такое продолжение, если не существует, то будет зарегистрировано продолжение, которое обращается к TaskScheduler'у.

Либо может быть TaskSchedulerAwaitTaskContinuation, и SynchronizationContextTaskContinuation и TaskSchedulerAwaitTaskContinuation оба наследуются от типа AwaitTaskContinuation, в котором переопределяют виртуальный метод Run(вызывается, когда продолжение планируется), TaskSchedulerAwaitTaskContinuation в методе Run, проверяет можно ли вызвать продолжение синхронно, если можно вызывается, нельзя - планирует

Continuation после ContinueWith будет планироваться либо на TaskScheduler(если передать его в вызов), либо на TaskScheduler.Current - это ThreadPool.

TaskCompletionSourse вызыват continuation после SetResult - по умолчанию синхронно, это можно переопределить передав в его конструктор флаг TaskCreationOptions.RunContinuationsAsynchronously

ConfigureAwait

Этот метод подменяет awaiter, на тот, который игнорирует syncContext, taskScheduler и выполнится в том же потоке, если не получается(когда продолжение планируется в момент, когда Task уже выполнен), планирует в ThreadPool, Разберемся в последовательности, продолжение планируется, когда вызывается метод TaskMethodBuilder'a AwaitUnsafeOnCompleted, этот метод в свою очередь вызывает метод awaiter'a OnCompleted,ContinueWith возвращает свой awaiter, в котором вызывается метод OnCompletedInternal, с параметром m_continueOnCapturedContext = false.Метод OnCompletedInternal в свою очередь обращается к Task'y и вызывает у него SetContinuationForAwait, а этот метод показан на предыдущем скрине

В итоге - ContinueWithAwaiter игнорирует TaskScheduler и SyncContext, и выполняет продолжение синхронно, либо планирует в ThreadPool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment