- 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
Можно создать два вида тасков - таски с делегатом и без(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 Сравним два этих кода:
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;
- Генерируется StateMachine
- Если awaiter (выражение стоящее около await в методе) не завершен вызывается метод AwaitUnsafeOnCompleted, в котором назначается Continuzation, если есть syncContext и он не дефолтный, то назначается continuation, который будет обращаться к нему, если нет, то смотрится TaskScheduler, а именно TaskScheduler.Current - если он не дефолтный(ThreadPoolTaskScheduler) продолжение планируется туда, иначе, пробуем добавить продолжение без планирования(в m_continuationObject передается сама stateMachine), сихронно сразу после завершения Task'a, если не получилось - Task уже завершен, и мы ставим continuation в ThreadPool.
Если awaiter уже завершен - у awaiter'a забирается результат и continuation не планируется
- После того, как Task отработал - он вызывает FinishContinuation, всего каждый случай который был описан в пункте два обрабатывается, соответсвенно, если Task был запланирован на SyncContext - выполняется на SyncContext'e, на TaskScheduler - на TaskScheduler'e, передана stateMachine - метод либо будет выполнен на текущем потоке(через прямой вызов метода MoveNext у stateMachine), либо может уйти в ThreadPool (обычно все-таки выполняется синхронно)
Это отличается от вызова ContinueWith:
- пишем .ContinueWith(x => {..})
- в m_continuationObject всегда добавляется только StandartTaskContinuation, этот объект создается и в него передается сам ContinuationTask(который создается из делегата, передаваемого в ContinueWith), опции таска и TaskScheduler
- Когда основной 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
Этот метод подменяет 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