Продолжение: часть III.
Прошлая заметка о async (часть I) была введением. В этой я продолжу начатую тему: я расскажу о том, что async взаимодействует с SynchronizationContext, и как это влияет на разработку асинхронных графических приложений.
Тестовым полигоном будет пример DiningPhilosophers, который идет вместе с расширением для асинхронного программирования. Это программа — визуализация знаменитой задачи Дейкстры об обедающих философах (ссылка). Прежде, чем читать дальше, лучше ознакомиться с условиями задачи.

Программа короткая, поэтому просмотрим её. В самом начале определяются классы представляющие основные сущности задачи: состояние философа, философ и вилка.
В качестве вилки используется класс BufferBlock, который лежит в пространстве имен System.Threading.Tasks.Dataflow. Это пространство позволяет писать многопоточные приложения на основе потока данных. Самый простой пример этого подхода — каналы, которые используются в Limbo, Go, Axum; их суть в том, что два потока взаимодействуют через каналы (аналог очереди), в которые можно писать и читать, если поток пытается прочитать из канала, а канал пуст, то поток блокируется до того момента, пока в канале не появятся данные. Отказ от общих объектов и использование каналов для обмена данными и средства синхронизации позволяет писать более понятный и безопасный код. BufferBlock является так раз таким каналом, метод Post добавляет данные, Receive получает, а ReceiveAsync является методом-расширением получающим данные асинхронно. Сущность задачи идеально ложиться на этот класс: доступная вилка описывается каналом, в котором что-то есть, занятая вилка — пустым каналом, если философ — поток выполнения, то при обращении к свободной вилке (каналу) он продолжит выполнение, а к занятой — будет ждать.
В программе класс Philosopher представляет не самого философа, а его состояние, которое визуализируется. В данном случае это стандартный примитив WPF — Ellipse и разное состояние философа представлено разным цветом. Важно, что раз это графический объект, то обращаться к нему можно только из одного потока.
Как я уже написал, самого философа будет представлять поток выполнения (метод RunPhilosopherAsync).
Метод MainWindow практически не интересен, в нем происходит инициализация структур класса. Единственное, что можно заметить про него, так это то, что он вызывает метод, в сигнатуре которого встречается async void, вызывая такие методы мы запускаем асинхронную операцию и теряем контром над ней, например, мы не можем дождаться её завершения.
Код RunPhilosopherAsync описывает действия действия философа, он достаточно прямолинеен особенно для асинхронного: думаем, ждем вилки, едим, кладем вилки обратно и думаем снова. Паузы (TaskEx.Delay) расставлены, чтобы можно было наблюдать разные стадии.
Во-первых, он не работает (deadlock) в случае когда число философов два из-за строчки:
Нужно forks[1] заменить на forks[0]. Но это придирки, пусть число философов — пять.
Во-вторых, код не должен работать, так как обращаемся к элементу gui из другого потока, но самое странное, что он работает. Если избавиться от async/await в коде, заменив везде «await x» на «x.Wait()», а «RunPhilosopherAsync(...)» на «new Task(() => RunPhilosopherAsync(...)).Start()» и удалив маркер async, то мы получим ожидаемый InvalidOperationException.

Теперь ясно, что вся магия заключена в await.
Вспомним, как принято взаимодействовать с gui-потоками в .Net Framework.
В случае WinForms, достаточно у любого контрола вызвать метод Invoke и передать ему делегат с кодом, который необходимо выполнить в gui потоке. В случае WPF, нужно обратиться к свойству Dispatcher любого контрола и вызвать метод Invoke, опять же передав ему делегат.
Это напоминает начало классического определения паттерна по Кристоферу: проблема повторяющаяся снова и снова. Саму проблему можно описать, как необходимость выполнять код в определенном потоке, когда инициализатор выполнения — код из другого потока. На самом деле это проблема не специфична для gui, так же она всплывает и в COM, и в WCF… Логично, что у неё должно быть решение, поток должен предоставлять средство запуска кода в нем, подобно тому, как это делают контролы WinForms и WPF. Такое решение есть, объект типа SynchronizationContext предоставляет методы Send и Post (асинхронный) для выполнения кода на другом потоке. Для получения доступа к объект типа SynchronizationContext какого-либо потока, нужно в этом потоке обратиться к SynchronizationContext.Current (per thread singleton).
Важно, чтобы SynchronizationContext и поток были согласованы, например, если поток работает на основе event loop, SynchronizationContext должен иметь доступ к очереди событий. То есть каждой реализации event loop нужно наследовать и переопределить методы SynchronizationContext. Если создать объект SynchronizationContext, то при вызовах Send и Post он будет использовать поток из ThreadPool.
Теперь можно предположить, что результат асинхронной операции await выполняет с помощью объекта SynchronizationContext, который он получает при первом вызове. Эту гипотезу легко проверить: установим свой SynchronizationContext:
При запуске будет выведено несколько раз «MyContext.Post», следовательно await действительно обращается к SynchronizationContext.
Тот факт, что await использует SynchronizationContext при работе, позволяет писать асинхронные графические приложения еще проще, так как не нужно беспокоиться о том, в каком потоке мы обращаемся к графическим элементам.
Кстати, SynchronizationContext — хороший пример, когда singleton не является абсолютным злом.
Написать эту заметку меня побудил пост в блоге ikvm.net от 1 ноября. А разобраться в теме мне помогли статьи «Understanding SynchronizationContext» (Part I, Part II и Part III).
Прошлая заметка о async (часть I) была введением. В этой я продолжу начатую тему: я расскажу о том, что async взаимодействует с SynchronizationContext, и как это влияет на разработку асинхронных графических приложений.
Тестовым полигоном будет пример DiningPhilosophers, который идет вместе с расширением для асинхронного программирования. Это программа — визуализация знаменитой задачи Дейкстры об обедающих философах (ссылка). Прежде, чем читать дальше, лучше ознакомиться с условиями задачи.

Программа короткая, поэтому просмотрим её. В самом начале определяются классы представляющие основные сущности задачи: состояние философа, философ и вилка.
В качестве вилки используется класс BufferBlock, который лежит в пространстве имен System.Threading.Tasks.Dataflow. Это пространство позволяет писать многопоточные приложения на основе потока данных. Самый простой пример этого подхода — каналы, которые используются в Limbo, Go, Axum; их суть в том, что два потока взаимодействуют через каналы (аналог очереди), в которые можно писать и читать, если поток пытается прочитать из канала, а канал пуст, то поток блокируется до того момента, пока в канале не появятся данные. Отказ от общих объектов и использование каналов для обмена данными и средства синхронизации позволяет писать более понятный и безопасный код. BufferBlock является так раз таким каналом, метод Post добавляет данные, Receive получает, а ReceiveAsync является методом-расширением получающим данные асинхронно. Сущность задачи идеально ложиться на этот класс: доступная вилка описывается каналом, в котором что-то есть, занятая вилка — пустым каналом, если философ — поток выполнения, то при обращении к свободной вилке (каналу) он продолжит выполнение, а к занятой — будет ждать.
В программе класс Philosopher представляет не самого философа, а его состояние, которое визуализируется. В данном случае это стандартный примитив WPF — Ellipse и разное состояние философа представлено разным цветом. Важно, что раз это графический объект, то обращаться к нему можно только из одного потока.
Как я уже написал, самого философа будет представлять поток выполнения (метод RunPhilosopherAsync).
using Philosopher = Ellipse; using Fork = BufferBlock<bool>;
Метод MainWindow практически не интересен, в нем происходит инициализация структур класса. Единственное, что можно заметить про него, так это то, что он вызывает метод, в сигнатуре которого встречается async void, вызывая такие методы мы запускаем асинхронную операцию и теряем контром над ней, например, мы не можем дождаться её завершения.
public MainWindow() { for (int i = 0; i < philosophers.Length; i++) { diningTable.Children.Add(philosophers[i] = CreatePhilosopher()); forks[i] = new Fork(); forks[i].Post(true); } // Разрешаем философам думать и есть for (int i = 0; i < philosophers.Length; i++) { RunPhilosopherAsync( philosophers[i], i < philosophers.Length - 1 ? forks[i] : forks[1], i < philosophers.Length - 1 ? forks[i + 1] : forks[i] ); } }
Код RunPhilosopherAsync описывает действия действия философа, он достаточно прямолинеен особенно для асинхронного: думаем, ждем вилки, едим, кладем вилки обратно и думаем снова. Паузы (TaskEx.Delay) расставлены, чтобы можно было наблюдать разные стадии.
private async void RunPhilosopherAsync(Philosopher philosopher, Fork fork1, Fork fork2) { // Думает, ждет вилки, есть, и все по кругу while (true) { // Думает (желтый) philosopher.Fill = Brushes.Yellow; await TaskEx.Delay(_rand.Next(10) * TIMESCALE); // Ждет вилки (Красный) philosopher.Fill = Brushes.Red; await fork1.ReceiveAsync(); await fork2.ReceiveAsync(); // Ест (Зеленый) philosopher.Fill = Brushes.Green; await TaskEx.Delay(_rand.Next(10) * TIMESCALE); // После еды кладет вилки на стол fork1.Post(true); fork2.Post(true); } }
Что можно сказать про этот код?
Во-первых, он не работает (deadlock) в случае когда число философов два из-за строчки:
i < philosophers.Length - 1 ? forks[i] : forks[1]
Нужно forks[1] заменить на forks[0]. Но это придирки, пусть число философов — пять.
Во-вторых, код не должен работать, так как обращаемся к элементу gui из другого потока, но самое странное, что он работает. Если избавиться от async/await в коде, заменив везде «await x» на «x.Wait()», а «RunPhilosopherAsync(...)» на «new Task(() => RunPhilosopherAsync(...)).Start()» и удалив маркер async, то мы получим ожидаемый InvalidOperationException.

Теперь ясно, что вся магия заключена в await.
Взаимодействие с gui-потоками в .Net Framework
Вспомним, как принято взаимодействовать с gui-потоками в .Net Framework.
В случае WinForms, достаточно у любого контрола вызвать метод Invoke и передать ему делегат с кодом, который необходимо выполнить в gui потоке. В случае WPF, нужно обратиться к свойству Dispatcher любого контрола и вызвать метод Invoke, опять же передав ему делегат.
Это напоминает начало классического определения паттерна по Кристоферу: проблема повторяющаяся снова и снова. Саму проблему можно описать, как необходимость выполнять код в определенном потоке, когда инициализатор выполнения — код из другого потока. На самом деле это проблема не специфична для gui, так же она всплывает и в COM, и в WCF… Логично, что у неё должно быть решение, поток должен предоставлять средство запуска кода в нем, подобно тому, как это делают контролы WinForms и WPF. Такое решение есть, объект типа SynchronizationContext предоставляет методы Send и Post (асинхронный) для выполнения кода на другом потоке. Для получения доступа к объект типа SynchronizationContext какого-либо потока, нужно в этом потоке обратиться к SynchronizationContext.Current (per thread singleton).
Важно, чтобы SynchronizationContext и поток были согласованы, например, если поток работает на основе event loop, SynchronizationContext должен иметь доступ к очереди событий. То есть каждой реализации event loop нужно наследовать и переопределить методы SynchronizationContext. Если создать объект SynchronizationContext, то при вызовах Send и Post он будет использовать поток из ThreadPool.
Теперь можно предположить, что результат асинхронной операции await выполняет с помощью объекта SynchronizationContext, который он получает при первом вызове. Эту гипотезу легко проверить: установим свой SynchronizationContext:
class MyContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { Console.WriteLine("MyContext.Post"); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine("MyContext.Send"); base.Send(d, state); } } class Program { static async Task SavePage(string file, string a) { using (var stream = File.AppendText(file)) { var html = await new WebClient().DownloadStringTaskAsync(a); await stream.WriteAsync(html); } } static void Main(string[] args) { SynchronizationContext.SetSynchronizationContext(new MyContext()); var task = SavePage("habrahabr", "http://habrahabr.ru"); task.Wait(); } }
При запуске будет выведено несколько раз «MyContext.Post», следовательно await действительно обращается к SynchronizationContext.
Заключение
Тот факт, что await использует SynchronizationContext при работе, позволяет писать асинхронные графические приложения еще проще, так как не нужно беспокоиться о том, в каком потоке мы обращаемся к графическим элементам.
Кстати, SynchronizationContext — хороший пример, когда singleton не является абсолютным злом.
Написать эту заметку меня побудил пост в блоге ikvm.net от 1 ноября. А разобраться в теме мне помогли статьи «Understanding SynchronizationContext» (Part I, Part II и Part III).
