As promised in a previous post, it's time to talk about the SynchronizationContext, something that at first I found a bit confusing. Indeed it's not that complex, but for sure having some notes to come back to when needed will come handy.
The main point is, in what thread will the "continuation code" (the one after await or passed by to ContinueWith) run?. Sometimes we don't care, but in other occasions we need it to run in a specific thread (basically the UI thread).
This article is essential to understand the whole thing. If you are using await, the compiler already takes care of running the continuation in the correct Thread (if necessary). It does so by capturing the current context (when the Task is created, so it captures the context for the calling thread) and then when the continuation must run, if this captured context is not null the continuation code will be dispatched to it. Think of the SynchronizationContext as a sort of Task Scheduler. If you are not using await, but directly ContinueWith, you don't get this for free, you'll have to pass it in one of the ContinueWith overloads.
As explained in that article, you can think of the following code:
await FooAsync(); RestOfMethod();
as being similar in nature to this:
var t = FooAsync(); var currentContext = SynchronizationContext.Current; t.ContinueWith(delegate { if (currentContext == null) RestOfMethod(); else currentContext.Post(delegate { RestOfMethod(); }, null); }, TaskScheduler.Current);
After reading this impressive post about how the waiting for an I/O operation to complete works, one question comes to mind, the "if" conditional in the ContinueWith, in what thread does it run?
Since the library/BCL is using the standard P/Invoke overlapped I/O system, it has already registered the handle with the I/O Completion Port (IOCP), which is part of the thread pool. So an I/O thread pool thread is borrowed briefly to execute the APC, which notifies the task that it’s complete.
The task has captured the UI context, so it does not resume the async method directly on the thread pool thread. Instead, it queues the continuation of that method onto the UI context, and the UI thread will resume executing that method when it gets around to it.
The another fundamental element to the SynchronizationContext is the ConfigureAwait method. If you call it with false, the dispatch to the captured context will not be done. This is important for library code. If you are writing code that creates tasks in your library, you should not care there about capturing the synchronization context and dispatching, this is a concern that should be only at an upper level (the one managing the UI). Let's see:
//UI, button handler string st = await new Downloader(this.logger).DownloadAndFormat(address); this.ResultDisplay.Text = st; //library code (in the Downloader class) public async TaskDownloadAndFormat(string address) { HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync(address); string formatted = await response.ReadAsStringAsync(); return formatted.Trim().ToUpper(); }
So the UI thread calls into DownloadAndFormat and it continues executing the await client.GetAsync call. As we've seen the compiler will capture the synchronizationContext and execute the continuation in it. As the Task creation happens in the UI thread, the continuation will be dispatched there also, and the same will happen with the next continuation (the one ofter ReadAsStringAsync). In principle we should not care about those continuations running in the UI Thread, it's not necessary, but should not cause trouble, or yes? Yes, as you can see in different articles, if the caller (the code in the UI handler) did something a bit weird, we would get into a deadlock. How?
//UI, button handler string st = new Downloader(this.logger).DownloadAndFormat(address).Result; this.ResultDisplay.Text = st;
The above code seems a bit strange, rather than await it's getting blocked in the .Result access. Weird, but valid code. So there the UI thread is blocked waiting for the Result of the Task, but in the DoanloadAndFormat method, as the continuation is being dispatched to the captured context, we are also blocked waiting for the UI thread, hence, we have a deadlock!
To avoid this, our library code should ensure the continuation is not dispatched to the UI thread, by doing:
//library code (in the Downloader class) public async Task<string> DownloadAndFormat(string address) { HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync(address).ConfigureAwait(false);; string formatted = await response.ReadAsStringAsync().ConfigureAwait(false);; return formatted.Trim().ToUpper(); }
Apart from the risks of deadlocks, dispatching code to the UI thread when it can just run in a thread pool thread can hit performance (your UI thread can alread have enough real work to do). You can read about all this here
No comments:
Post a Comment