Continued:
part III .
The last note about async (
part I ) was an introduction. In this, I will continue the topic I have started: I will talk about how async interacts with SynchronizationContext, and how this affects the development of asynchronous graphical applications.
The test site will be an example of DiningPhilosophers, which comes with an asynchronous programming extension. This program is a visualization of Dijkstra’s famous task of the dining philosophers (
link ). Before reading further, it is better to get acquainted with the conditions of the problem.
')

The program is short, so look at it. At the very beginning, classes are defined that represent the main essence of the task: the state of the philosopher, the philosopher and the fork.
The forks use the BufferBlock class, which lies in the System.Threading.Tasks.Dataflow namespace. This space allows you to write multi-threaded applications based on data flow. The simplest example of this approach is the channels that are used in Limbo, Go, Axum; their essence is that two streams interact through channels (analogue queues), in which you can write and read, if the stream tries to read from the channel, and the channel is empty, then the stream is blocked until data appears in the channel. The refusal of common objects and the use of channels for data exchange and synchronization tools allows you to write more understandable and secure code. BufferBlock is such a channel, the Post method adds data, Receive receives, and ReceiveAsync is an extension method that receives data asynchronously. The essence of the task is ideally to fall on this class: an accessible fork is described by a channel in which something is there, a occupied fork is an empty channel, if the philosopher is a stream of execution, he will continue to perform when addressed to a free fork (channel) to wait.
In the program, the class Philosopher represents not the philosopher himself, but his condition, which is visualized. In this case, it is the standard WPF primitive - Ellipse and the different state of the philosopher is represented by different colors. It is important that since this is a graphic object, it can only be accessed from one stream.
As I already wrote, the philosopher himself will represent the flow of execution (the RunPhilosopherAsync method).
using Philosopher = Ellipse; using Fork = BufferBlock<bool>;
The MainWindow method is practically not interesting, it is the initialization of class structures. The only thing that can be noticed about him is that he calls a method whose signature contains async void, calling such methods we start an asynchronous operation and lose control over it, for example, we cannot wait for its completion.
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] ); } }
The RunPhilosopherAsync code describes the actions of the action of the philosopher, it is quite straightforward especially for the asynchronous: we think, wait for the fork, eat, put the fork back and think again. Pauses (TaskEx.Delay) are set so that you can watch different stages.
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); } }
What can you say about this code?
Firstly, it does not work (deadlock) in the case when the number of philosophers is two because of the line:
i < philosophers.Length - 1 ? forks[i] : forks[1]
It is necessary to replace forks [1] with forks [0]. But they are cavils, even if the number of philosophers is five.
Secondly, the code should not work, since we are accessing the gui element from another thread, but the strangest thing is that it works. If you get rid of async / await in the code, replacing everywhere “await x” with “x.Wait ()” and “RunPhilosopherAsync (...)” with “new Task (() => RunPhilosopherAsync (...)). Start () ”and removing the async token, then we get the expected InvalidOperationException.

Now it's clear that all the magic is in await.
Interaction with gui-streams in .Net Framework
Recall how to interact with gui-threads in the .Net Framework.
In the case of WinForms, it is enough for any control to call the Invoke method and pass a delegate to it with the code that needs to be executed in the gui stream. In the case of WPF, you need to refer to the Dispatcher property of any control and call the Invoke method, again passing a delegate to it.
This is reminiscent of the beginning of the classic definition of the pattern by Christopher: the problem is repeated again and again. The problem itself can be described as the need to execute code in a specific thread, when the initializer of execution is code from another thread. In fact, this problem is not specific to gui, it also pops up in COM and WCF ... It is logical that it should have a solution, the stream should provide a means to run code in it, just like WinForms and WPF controls do. . There is such a solution; an object of the SynchronizationContext type provides the Send and Post (asynchronous) methods for executing code on another thread. To gain access to an object of the SynchronizationContext type of any thread, you need to refer to SynchronizationContext.Current (per thread singleton) in this thread.
It is important that the SynchronizationContext and the thread are consistent, for example, if the stream runs on an event loop, the SynchronizationContext must have access to the event queue. That is, each event loop implementation needs to inherit and override the SynchronizationContext methods. If you create a SynchronizationContext object, it will use a thread from ThreadPool when calling Send and Post.
Now we can assume that the result of the asynchronous operation is performed by await using the SynchronizationContext object, which it receives on the first call. This hypothesis is easy to test: we will install our 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(); } }
At startup, the “MyContext.Post” will be displayed several times, hence await actually refers to the SynchronizationContext.
Conclusion
The fact that await uses SynchronizationContext at work makes writing asynchronous graphical applications even easier, since you don’t need to worry about the flow in which we are accessing graphic elements.
By the way, SynchronizationContext is a good example when singleton is not an absolute evil.
I was prompted to write this post by a blog post on
ikvm.net on November 1. And the article “Understanding SynchronizationContext” (
Part I ,
Part II and
Part III ) helped me understand the topic.