ConfigureAwait подталкивает продолжение в stream пула

Вот код WinForms:

async void Form1_Load(object sender, EventArgs e) { // on the UI thread Debug.WriteLine(new { where = "before", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); var tcs = new TaskCompletionSource(); this.BeginInvoke(new MethodInvoker(() => tcs.SetResult(true))); await tcs.Task.ContinueWith(t => { // still on the UI thread Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); // on a pool thread Debug.WriteLine(new { where = "after", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); } 

Выход:

 {where = before, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = after, ManagedThreadId = 11, IsThreadPoolThread = True}

Почему ConfigureAwait активно продвигает await продолжения в stream пула здесь?

Документы MSDN говорят:

continueOnCapturedContext … true, чтобы попытаться упорядочить продолжение в исходном контексте; в противном случае – false.

Я понимаю, что в текущем streamе установлен WinFormsSynchronizationContext . Тем не менее, попытки маршала не предпринимаются, точка исполнения уже существует.

Таким образом, это больше похоже на «никогда не продолжать в исходном контексте, захваченном»

Как и ожидалось, нет нитка, если точка выполнения уже находится в streamе пула без контекста синхронизации:

 await Task.Delay(100).ContinueWith(t => { // on a pool thread Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); 
 {where = before, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = ContinueWith, ManagedThreadId = 6, IsThreadPoolThread = True}
 {where = after, ManagedThreadId = 6, IsThreadPoolThread = True}

Я собираюсь посмотреть на реализацию ConfiguredTaskAwaitable для ответов.

Обновлен , еще один тест, чтобы увидеть, есть ли какая-либо синхронизация. контекст недостаточно хорош для продолжения (а не оригинального). Это действительно так:

 class DumbSyncContext: SynchronizationContext { } // ... Debug.WriteLine(new { where = "before", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); var tcs = new TaskCompletionSource(); var thread = new Thread(() => { Debug.WriteLine(new { where = "new Thread", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread}); SynchronizationContext.SetSynchronizationContext(new DumbSyncContext()); tcs.SetResult(true); Thread.Sleep(1000); }); thread.Start(); await tcs.Task.ContinueWith(t => { Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread}); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); Debug.WriteLine(new { where = "after", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); 
 {where = before, ManagedThreadId = 9, IsThreadPoolThread = False}
 {where = new Thread, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False}
 {where = after, ManagedThreadId = 6, IsThreadPoolThread = True}

Почему ConfigureAwait активно продвигает ожидание продолжения в stream пула здесь?

Он не «подталкивает его к streamу пула streamов» так же, как говорит «не заставляйте себя возвращаться к предыдущему SynchronizationContext ».

Если вы не фиксируете существующий контекст, то продолжение, которое обрабатывает код после этого await будет выполняться только в streamе пула streamов, так как нет никакого контекста для возврата назад.

Теперь это тонко отличается от «нажатия на пул streamов», так как нет гарантии, что он будет запущен в пуле streamов, когда вы выполните ConfigureAwait(false) . Если вы звоните:

 await FooAsync().ConfigureAwait(false); 

Возможно, что FooAsync() будет выполняться синхронно, и в этом случае вы никогда не покинете текущий контекст. В этом случае ConfigureAwait(false) не имеет реального эффекта, поскольку конечный автомат, созданный функцией await будет иметь короткое замыкание и просто запускаться напрямую.

Если вы хотите увидеть это в действии, сделайте asynchronous метод следующим образом:

 static Task FooAsync(bool runSync) { if (!runSync) await Task.Delay(100); } 

Если вы назовете это так:

 await FooAsync(true).ConfigureAwait(false); 

Вы увидите, что вы остаетесь в основном streamе (при условии, что это был текущий контекст до ожидания), так как в коде отсутствует фактический код асинхронного кода. Тот же вызов с FooAsync(false).ConfigureAwait(false); однако, это приведет к тому, что он перейдет в stream пула streamов после выполнения.

Вот объяснение этого поведения, основанного на копании .NET Reference Source .

Если используется ConfigureAwait(true) , продолжение выполняется через TaskSchedulerAwaitTaskContinuation который использует SynchronizationContextTaskScheduler , все ясно в этом случае.

Если ConfigureAwait(false) используется (или нет контекста синхронизации), он выполняется через AwaitTaskContinuation , который AwaitTaskContinuation пытается AwaitTaskContinuation задачу продолжения, а затем использует ThreadPool для очереди, если встраивание невозможно.

Inlining определяется IsValidLocationForInlining , которая никогда не ставит задачу в streamе с настраиваемым контекстом синхронизации. Однако он делает все возможное, чтобы встроить его в текущий stream streamа. Это объясняет, почему в первом случае мы Task.Delay(100) stream пула и остаемся в том же streamе пула во втором случае (с Task.Delay(100) ).

Я думаю, что это проще всего по-другому.

Допустим, у вас есть:

 await task.ConfigureAwait(false); 

Во-первых, если task уже завершена, то, как заметил Рид, ConfigureAwait фактически игнорируется и выполнение продолжается (синхронно, в том же streamе).

В противном случае await приостанавливает метод. В этом случае, когда await возобновляется и видит, что ConfigureAwait имеет значение false , существует специальная логика для проверки наличия кода SynchronizationContext и возобновления в пуле streamов, если это так. Это недокументированное, но не неправильное поведение. Поскольку он недокументирован, я рекомендую вам не зависеть от поведения; если вы хотите что-то запустить в пуле streamов, используйте Task.Run . ConfigureAwait(false) довольно буквально означает «Мне все равно, в каком контексте этот метод возобновляется».

Обратите внимание, что ConfigureAwait(true) (по умолчанию) продолжит метод текущего SynchronizationContext или TaskScheduler . Хотя ConfigureAwait(false) будет продолжать метод в любом streamе, кроме одного с SynchronizationContext . Они не совсем противоположны друг другу.

Interesting Posts
Давайте будем гением компьютера.