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 пула здесь?
- Каков правильный способ отмены асинхронной операции, которая не принимает CancellationToken?
- Лучший способ конвертировать метод async на основе обратного вызова в ожидаемую задачу
- WaitAll vs WhenAll
- В чем разница между возвратом пустоты и возвратом задачи?
- Отмена ожидающей задачи синхронно в 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}
- Шаблон для самоотдачи и перезапуска задачи
- Почему это асинхронное действие зависает?
- Использовать Task.Run () в синхронном методе, чтобы избежать тупиковой остановки в асинхронном методе?
- Когда использовать Task.Delay, когда использовать Thread.Sleep?
- Должны ли мы переключиться на использование асинхронного ввода-вывода по умолчанию?
- Выполнение задач параллельно
- Разница между TPL и async / await (Обработка streamов)
- Как ограничить количество одновременных операций асинхронного ввода-вывода?
Почему 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
. Они не совсем противоположны друг другу.