ConfigureAwait передает продолжение в поток пула
Вот код 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<bool>();
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 });
}
Выход:
{где = раньше, ManagedThreadId = 10, IsThreadPoolThread = False } {где = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False } {где = после, ManagedThreadId = 11, IsThreadPoolThread = True }
Почему ConfigureAwait активно выдвигает await
продолжение нити пула здесь?
Документы MSDN говорят:
continueOnCapturedContext... true, чтобы попытаться перенаправить продолжение обратно в исходный захваченный контекст; иначе ложно.
Я понимаю, что естьWinFormsSynchronizationContext
установлен на текущий поток. Тем не менее,нет попытки сделать маршал, точка исполнения уже есть.
Таким образом, это больше похоже на то, что"никогда не продолжать исходный контекст"...
Как и ожидалось, нет переключателя потока, если точка выполнения уже находится в потоке пула без контекста синхронизации:
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);
{где = раньше, ManagedThreadId = 10, IsThreadPoolThread = False } {где = ContinueWith, ManagedThreadId = 6, IsThreadPoolThread = True } {где = после, ManagedThreadId = 6, IsThreadPoolThread = True}
Я собираюсь посмотреть на реализациюConfiguredTaskAwaitable
за ответы.
Обновлен еще один тест для проверки синхронизации. контекст не достаточно хорош для продолжения (а не оригинальный). Это действительно так:
class DumbSyncContext: SynchronizationContext
{
}
// ...
Debug.WriteLine(new { where = "before",
Thread.CurrentThread.ManagedThreadId,
Thread.CurrentThread.IsThreadPoolThread });
var tcs = new TaskCompletionSource<bool>();
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 });
{где = раньше, ManagedThreadId = 9, IsThreadPoolThread = False } {где = новый поток, ManagedThreadId = 10, IsThreadPoolThread = False } {где = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False } {где = после, ManagedThreadId = 6, IsThreadPoolThread = True}
3 ответа
Почему ConfigureAwait проактивно помещает ожидание продолжения в поток пула здесь?
Он не "толкает его в поток пула потоков", а, скажем, "не заставляет себя возвращаться к предыдущему". SynchronizationContext
".
Если вы не захватите существующий контекст, то продолжение, которое обрабатывает код после этого await
вместо этого будет работать только в потоке пула потоков, поскольку нет контекста, в который можно было бы выполнить маршал.
Теперь это немного отличается от "push to the pool pool", так как нет гарантии, что он будет работать в пуле потоков, когда вы это сделаете ConfigureAwait(false)
, Если вы позвоните:
await FooAsync().ConfigureAwait(false);
Возможно, что FooAsync()
будет выполняться синхронно, в этом случае вы никогда не покинете текущий контекст. В таком случае, ConfigureAwait(false)
не имеет никакого реального эффекта, так как конечный автомат, созданный await
Функция будет короткого замыкания и просто работать напрямую.
Если вы хотите увидеть это в действии, создайте асинхронный метод следующим образом:
static Task FooAsync(bool runSync)
{
if (!runSync)
await Task.Delay(100);
}
Если вы называете это как:
await FooAsync(true).ConfigureAwait(false);
Вы увидите, что вы остаетесь в главном потоке (при условии, что он был текущим контекстом до await), поскольку в пути кода не выполняется фактический асинхронный код. Тот же звонок с FooAsync(false).ConfigureAwait(false);
однако после выполнения он перейдет к потоку пула потоков.
Вот объяснение этого поведения, основанное на копании .NET Reference Source.
Если ConfigureAwait(true)
используется, продолжение выполняется через TaskSchedulerAwaitTaskContinuation
который использует SynchronizationContextTaskScheduler
, с этим делом все понятно.
Если ConfigureAwait(false)
используется (или если нет контекста синхронизации для захвата), это делается с помощью AwaitTaskContinuation
, который сначала пытается выполнить задачу продолжения, а затем использует ThreadPool
поставить его в очередь, если встраивание невозможно.
Подкладка определяется IsValidLocationForInlining
, который никогда не вставляет задачу в поток с пользовательским контекстом синхронизации. Однако он делает все возможное, чтобы встроить его в текущий поток пула. Это объясняет, почему мы помещаемся в поток пула в первом случае, и остаемся в том же потоке пула во втором случае (с Task.Delay(100)
).
Я думаю, что проще всего думать об этом немного по-другому.
Допустим, у вас есть:
await task.ConfigureAwait(false);
Во-первых, если task
уже завершено, то, как указал Рид, ConfigureAwait
фактически игнорируется, и выполнение продолжается (синхронно, в том же потоке).
Иначе, await
приостановит метод. В том случае, когда await
возобновляет и видит, что ConfigureAwait
является false
, есть специальная логика, чтобы проверить, есть ли код SynchronizationContext
и возобновить работу пула потоков, если это так. Это недокументированное, но не неправильное поведение. Поскольку это недокументировано, я рекомендую вам не зависеть от поведения; если вы хотите что-то запустить в пуле потоков, используйте Task.Run
, ConfigureAwait(false)
буквально означает "мне все равно, в каком контексте этот метод возобновляется".
Обратите внимание, что ConfigureAwait(true)
(по умолчанию) будет продолжать метод на текущем SynchronizationContext
или же TaskScheduler
, В то время как ConfigureAwait(false)
продолжит метод в любом потоке, кроме одного с SynchronizationContext
, Они не совсем противоположны друг другу.