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