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 });
}
Выход:
{ where = before, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = after, ManagedThreadId = 11, IsThreadPoolThread = True }
Почему ConfigureAwait активно продвигает продолжение await
в поток пула здесь?
Документы MSDN говорят:
continueOnCapturedContext... true, чтобы попытаться маршалировать продолжение обратно в исходный контекст; в противном случае - false.
Я понимаю, что там 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);
{ 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<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 });
{ 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 }
Ответы
Ответ 1
Почему ConfigureAwait pro-активно подталкивает ожидание продолжения в поток пула здесь?
Он не "подталкивает его к потоку пула потоков" так же, как говорит "не заставляйте себя возвращаться к предыдущему SynchronizationContext
".
Если вы не фиксируете существующий контекст, то продолжение, которое обрабатывает код после этого await
, будет запускаться только в потоке пула потоков, поскольку нет контекста для возврата назад.
Теперь это тонко отличается от "нажмите на пул потоков", так как нет гарантии, что он будет запускаться в пуле потоков, когда вы выполняете 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);
Вы увидите, что вы остаетесь в основном потоке (при условии, что это был текущий контекст до ожидания), так как в коде кода нет действительного кода асинхронного кода. Тот же вызов с FooAsync(false).ConfigureAwait(false);
заставит его перейти к потоку пула потоков после выполнения, однако.
Ответ 2
Вот объяснение этого поведения, основанного на копании .NET Reference Source.
Если используется ConfigureAwait(true)
, продолжение выполняется через TaskSchedulerAwaitTaskContinuation
, который использует SynchronizationContextTaskScheduler
, все ясно в этом случае.
Если используется ConfigureAwait(false)
(или нет контекста синхронизации), это делается через AwaitTaskContinuation
, который сначала пытается встроить задачу продолжения, затем использует ThreadPool
для очереди, если вложение невозможно.
Вложение определяется IsValidLocationForInlining
, которое никогда не строит задачу в потоке с настраиваемым контекстом синхронизации. Однако он делает все возможное, чтобы встроить его в текущий поток потока. Это объясняет, почему в первом случае мы вставляем поток пула и остаемся в том же потоке пула во втором случае (с Task.Delay(100)
).
Ответ 3
Я думаю, что это проще всего по-разному думать.
Скажем, у вас есть:
await task.ConfigureAwait(false);
Во-первых, если task
уже завершено, то, как указал Рид, ConfigureAwait
фактически игнорируется и выполнение продолжается (синхронно, в том же потоке).
В противном случае await
остановит метод. В этом случае, когда await
возобновляет и видит, что ConfigureAwait
- false
, существует специальная логика для проверки того, имеет ли код SynchronizationContext
и возобновление в пуле потоков, если это так. Это недокументированное, но не неправильное поведение. Поскольку он не документирован, я рекомендую вам не зависеть от поведения; если вы хотите что-то запустить в пуле потоков, используйте Task.Run
. ConfigureAwait(false)
вполне буквально означает "Мне все равно, в каком контексте этот метод возобновляется".
Обратите внимание, что ConfigureAwait(true)
(по умолчанию) продолжит метод текущего SynchronizationContext
или TaskScheduler
. Пока ConfigureAwait(false)
продолжит метод в любом потоке, кроме одного с SynchronizationContext
. Они не совсем противоположны друг другу.