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

Другие вопросы по тегам