В этой статье говорится, что 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()
будет выполняться в контексте пользовательского интерфейса (блокируя поток пользовательского интерфейса), как описано в статье?