Когда 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, Это вызвало бы проблемы, если бы это было сделано. Вот почему он никогда не делает это.

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