Подтвердить что ты не робот

Ожидание не возобновляет контекст после операции async?

Я прочитал этот вопрос от Noseratio, который показывает поведение, когда TaskScheduler.Current не то же самое после того, как ожидаемый завершил свою работу.

В ответе указывается, что:

Если фактическая задача не выполняется, то TaskScheduler.Currentсовпадает с TaskScheduler.Default

Это правда. Я уже видел это здесь:

  • TaskScheduler.Default
    • Возвращает экземпляр ThreadPoolTaskScheduler
  • TaskScheduler.Current
    • Если вызываемый из исполняющей задачи возвращает TaskSchedulerвыполняемой в настоящее время задачи
    • Если вызов из любого другого места вернет TaskScheduler.Default

Но потом я подумал: если да, то создайте фактический Task (а не только Task.Yield()) и протестируйте его:

async void button1_Click_1(object sender, EventArgs e)
{
    var ts = TaskScheduler.FromCurrentSynchronizationContext();
    await Task.Factory.StartNew(async () =>
    {
        MessageBox.Show((TaskScheduler.Current == ts).ToString()); //True

           await new WebClient().DownloadStringTaskAsync("http://www.google.com");

        MessageBox.Show((TaskScheduler.Current == ts).ToString());//False

    }, CancellationToken.None, TaskCreationOptions.None,ts).Unwrap();
}

Первый Messagebox - "True", второй - "False"

Вопрос:

Как вы можете видеть, я создал фактическую задачу.

Я могу понять, почему первый MessageBox дает True. Это из-за:

Если вызов из задачи выполнения возвратит TaskScheduler выполняемой в настоящее время задачи

И у этой задачи есть ts, который является отправленным TaskScheduler.FromCurrentSynchronizationContext()

Но почему контекст не сохраняется во втором MessageBox? Для меня это было непонятно из ответа Стефана.

Дополнительная информация:

Если я напишу вместо этого (второго сообщения):

MessageBox.Show((TaskScheduler.Current == TaskScheduler.Default).ToString());

Это дает True. Но почему?

4b9b3361

Ответ 1

Причины путаницы:

  • Пользовательский интерфейс не имеет "специального" TaskScheduler. Случай по умолчанию для кода, запущенного в потоке пользовательского интерфейса, состоит в том, что TaskScheduler.Current хранит ThreadPoolTaskScheduler и SynchronizationContext.Current сохраняет WindowsFormsSynchronizationContext (или соответствующий в других приложениях пользовательского интерфейса)
  • ThreadPoolTaskScheduler в TaskScheduler.Current не обязательно означает, что TaskScheduler используется для запуска текущей части кода. Это также означает TaskSchdeuler.Current == TaskScheduler.Default и поэтому "не используется TaskScheduler" .
  • TaskScheduler.FromCurrentSynchronizationContext() не возвращает "acutal" TaskScheduler. Он возвращает "прокси", который отправляет задания прямо на захваченный SynchronizationContext.

Итак, если вы запустите свой тест перед запуском задачи (или в любом другом месте), вы получите тот же результат, что и после ожидания:

MessageBox.Show(TaskScheduler.Current == TaskScheduler.FromCurrentSynchronizationContext()); // False

Потому что TaskScheduler.Current есть ThreadPoolTaskScheduler, а TaskScheduler.FromCurrentSynchronizationContext() возвращает SynchronizationContextTaskScheduler.

Это поток вашего примера:

  • Вы создаете новый SynchronizationContextTaskScheduler из пользовательского интерфейса SynchronizationContext (т.е. WindowsFormsSynchronizationContext).
  • Запланируйте задачу, созданную с помощью Task.Factory.StartNew, на этом TaskScheduler. Поскольку это просто "прокси", он помещает делегата в WindowsFormsSynchronizationContext, который вызывает его в потоке пользовательского интерфейса.
  • Синхронная часть (часть до первого ожидания) этого метода выполняется в потоке пользовательского интерфейса, будучи связана с SynchronizationContextTaskScheduler.
  • Метод достигает ожидания и приостанавливается при захвате WindowsFormsSynchronizationContext.
  • Когда продолжение возобновляется после ожидания, оно отправляется на WindowsFormsSynchronizationContext, а не SynchronizationContextTaskScheduler, поскольку SynchronizationContext имеет приоритет (это можно увидеть в Task.SetContinuationForAwait). Затем он регулярно запускается в потоке пользовательского интерфейса без каких-либо специальных TaskScheduler, поэтому TaskScheduler.Current == TaskScheduler.Default.

Итак, созданная задача выполняется на прокси-сервере TaskScheduler, который использует SynchronizationContext, но продолжение после ожидания отправляется на этот SynchronizationContext, а не TaskScheduler.