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. Они не совсем противоположны друг другу.