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