Когда Task.Run передает SynchronizationContext с 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); }); }
Вот что моя ментальная модель говорит мне, что произойдет с этим кодом. Пользователь нажимает кнопку 1, в результате чего инфраструктура пользовательского интерфейса вызывает button1_Click в потоке пользовательского интерфейса. Затем код запускает рабочий элемент для запуска в ThreadPool (через Task.Run). Этот рабочий элемент начинает некоторую загрузку и асинхронно ожидает ее завершения. Последующий рабочий элемент в ThreadPool затем выполняет некоторую ресурсоемкую операцию с результатом этой загрузки и возвращает результат, вызывая завершение задачи, ожидаемой в потоке пользовательского интерфейса. В этот момент поток пользовательского интерфейса обрабатывает оставшуюся часть этого метода button1_Click, сохраняя результат вычисления в свойстве Text button1.
Мои ожидания верны, если SynchronizationContext не передается как часть ExecutionContext. Однако если это произойдет, я буду сильно разочарован. Task.Run захватывает ExecutionContext при вызове и использует его для запуска делегата, переданного ему. Это означает, что пользовательский интерфейс SynchronizationContext, который был текущим при вызове Task.Run, будет перетекать в задачу и будет текущим при вызове DownloadAsync и ожидании результирующей задачи. Это означает, что ожидающий увидит текущий SynchronizationContext и опубликует остаток асинхронного метода как продолжение для запуска в потоке пользовательского интерфейса. А это значит, что мой метод Compute, скорее всего, будет работать в потоке пользовательского интерфейса, а не в ThreadPool, что вызовет проблемы с отзывчивостью для моего приложения.
Теперь история становится немного более запутанной: ExecutionContext на самом деле имеет два метода Capture, но только один из них является общедоступным. Внутренний (внутренний для mscorlib) - это тот, который используется большинством асинхронных функций, предоставляемых из mscorlib, и он дополнительно позволяет вызывающей стороне подавлять захват SynchronizationContext как части ExecutionContext; в соответствии с этим существует также внутренняя перегрузка метода Run, который поддерживает игнорирование SynchronizationContext, который хранится в ExecutionContext, фактически притворяясь, что он не был захвачен (это, опять же, перегрузка, используемая большинством функций в mscorlib). Это означает, что практически любая асинхронная операция, базовая реализация которой находится в mscorlib, не будет передавать SynchronizationContext как часть ExecutionContext, но любая асинхронная операция, базовая реализация которой находится где-либо еще, будет передавать SynchronizationContext как часть ExecutionContext. Ранее я упоминал, что "строителями" для асинхронных методов были типы, отвечающие за выполнение ExecutionContext в асинхронных методах, и эти сборщики действительно живут в mscorlib, и они используют внутренние перегрузки… как таковые, SynchronizationContext не передается как часть ExecutionContext через ожидает (опять же, это отдельно от того, как ожидающие задачи поддерживают захват SynchronizationContext и Post'ing обратно к нему). Чтобы помочь справиться со случаями, когда 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()
работать в контексте пользовательского интерфейса (блокируя поток пользовательского интерфейса), как обсуждалось в статье?
1 ответ
Когда Task.Run передает SynchronizationContext с ExecutionContext?
Никогда.
Смысл этой статьи в том, что (публичный API для) ExecutionContext
будет течь SynchronizationContext
, Но Task.Run
(и "почти любая асинхронная операция, базовая реализация которой находится в mscorlib") никогда не сделает этого.
Абзац, начинающийся с "Мои ожидания верны, если" является гипотетическим. Он описывает, что произойдет, если Task.Run
использовать публичный API для потока ExecutionContext
, Это вызвало бы проблемы, если бы это было сделано. Вот почему он никогда не делает это.