Когда выполняется синхронизация потока Task.Run с ExecutionContext?

В этой статье говорится, что SynchronizationContext может течь с ExecutionContext:

private void button1_Click(object sender, EventArgs e)  { 
    button1.Text = await Task.Run(async delegate 
    { 
        string data = await DownloadAsync(); 
        return Compute(data); 
    });  
}

Вот что моя ментальная модель говорит мне, что произойдет с этим кодом. Пользователь нажимает кнопку button1, в результате чего инфраструктура пользовательского интерфейса вызывает button1_Click в потоке пользовательского интерфейса. Затем код запускает рабочий элемент для выполнения в ThreadPool (через Task.Run). Этот рабочий элемент запускает некоторую загрузку и асинхронно ожидает ее завершения. Последующий рабочий элемент в ThreadPool затем выполняет некоторые ресурсоемкие операции с результатом этой загрузки и возвращает результат, в результате чего задача, которая ожидалась в потоке пользовательского интерфейса, завершилась. В этот момент поток пользовательского интерфейса обрабатывает оставшуюся часть этого метода button1_Click, сохраняя результат вычисления в свойстве Text button1.

Мое ожидание справедливо, если SynchronizationContext не является частью ExecutionContext. Однако, если он все-таки потечет, я буду очень разочарован. Task.Run захватывает ExecutionContext при вызове и использует его для запуска переданного ему делегата. Это означает, что UI SynchronizationContext, который был текущим при вызове Task.Run, будет передаваться в Task и будет текущим при вызове DownloadAsync и ожидании результирующей задачи. Это означает, что ожидание увидит текущий контекст синхронизации и отправит оставшуюся часть асинхронного метода в качестве продолжения для выполнения в потоке пользовательского интерфейса. А это означает, что мой метод Compute, скорее всего, будет работать в потоке пользовательского интерфейса, а не в ThreadPool, что вызовет проблемы с быстродействием моего приложения.

Теперь история становится немного запутанной: ExecutionContext фактически имеет два метода Capture, но только один из них является общедоступным. Внутренний (внутренний для mscorlib) - это тот, который используется большинством асинхронных функций, предоставляемых из mscorlib, и он дополнительно позволяет вызывающей стороне подавлять захват SynchronizationContext как часть ExecutionContext; В соответствии с этим существует также внутренняя перегрузка метода Run, которая поддерживает игнорирование SynchronizationContext, хранящегося в ExecutionContext, фактически делая вид, что он не был захвачен (это, опять же, перегрузка, используемая большинством функций в mscorlib). Это означает, что практически любая асинхронная операция, основная реализация которой находится в mscorlib, не будет передавать SynchronizationContext как часть ExecutionContext, но любая асинхронная операция, основная реализация которой находится где-либо еще, будет передавать SynchronizationContext как часть ExecutionContext. Я ранее упоминал, что «построители» для асинхронных методов - это типы, ответственные за передачу ExecutionContext в асинхронных методах, и эти построители действительно живут в mscorlib, и они действительно используют внутренние перегрузки ... как таковые, SynchronizationContext не передается как часть ExecutionContext через awaits (это опять же отдельно от того, как ожидающие задачи поддерживают захват SynchronizationContext и отправку обратно в него). Чтобы помочь справиться со случаями, когда ExecutionContext действительно передает SynchronizationContext, инфраструктура асинхронного метода пытается игнорировать SynchronizationContexts, заданные как Current из-за потока.

Однако мне не совсем ясно, когда это может произойти. Похоже, что это произойдет, когда используется общедоступный ExecutionContext.Capture метод и внутренняя Task.Run перегрузка, подавляющая поток SynchronizationContext с ExecutionContext, не используется, но я не знаю, когда это произойдет.

В моем тестировании на .NET 4.5 Task.Run, похоже, не передает SynchronizationContext с ExecutionContext:

private async void button1_Click(object sender, EventArgs e) {
    Console.WriteLine("Click context:" + SynchronizationContext.Current);
    button1.Text = await Task.Run(async delegate {

        // In my tests this always returns false
        Console.WriteLine("SynchronizationContext was flowed: " + (SynchronizationContext.Current != null));

        string data = await DownloadAsync();
        return Compute(data);
    });
}

Итак, мой вопрос: при каких обстоятельствах Compute() будет выполняться в контексте пользовательского интерфейса (блокируя поток пользовательского интерфейса), как описано в статье?


person Lawrence Johnston    schedule 23.10.2013    source источник


Ответы (1)


Когда выполняется синхронизация потока Task.Run с ExecutionContext?

Никогда.

Суть этой статьи в том, что (публичный API для) потока ExecutionContext будет потоком SynchronizationContext. Но Task.Run (и «практически любая асинхронная операция, основная реализация которой находится в mscorlib») никогда этого не сделает.

Абзац, начинающийся с «Мое ожидание действительно, если» является гипотетическим. Он описывает, что произойдет, если Task.Run будет использовать общедоступный API для потока ExecutionContext. Это вызовет проблемы, если это произойдет. Вот почему он никогда этого не делает.

person Stephen Cleary    schedule 23.10.2013