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