Context of synchronization

As previously discussed in a previous post, let’s delve into the SynchronizationContext, a concept that can initially seem a bit puzzling. It’s not overly complicated, but having a reference to refer back to can be quite helpful.

The key point is to understand where the “continuation code” (the part after await or within ContinueWith) executes. While sometimes the execution thread doesn’t matter, there are cases where we need it to run on a specific thread, typically the UI thread.

This article provides crucial context for grasping this concept. When using await, the compiler manages the execution thread for the continuation. It achieves this by capturing the current context during Task creation and, if this context isn’t null, dispatches the continuation code to it. You can think of the SynchronizationContext as a Task Scheduler. However, with ContinueWith (without await), you need to explicitly pass the context.

Illustrating this, consider the following code:

1
2
await FooAsync();
RestOfMethod();

This code behaves similarly to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var t = FooAsync();

var currentContext = SynchronizationContext.Current;

t.ContinueWith(delegate

{

if (currentContext == null)

RestOfMethod();

else

currentContext.Post(delegate { RestOfMethod(); }, null);

}, TaskScheduler.Current);

After reviewing this impressive post and its explanation of I/O operation waiting, a question arises: in which thread does the “if” condition within ContinueWith run?

The standard P/Invoke overlapped I/O system, employed by the library/BCL, registers the handle with the thread pool’s I/O Completion Port (IOCP). An I/O thread pool thread briefly executes the APC, signaling task completion.

Instead of directly resuming the async method on the thread pool thread, the task, having captured the UI context, queues the method continuation onto the UI context. The UI thread then resumes execution when possible.

While one might assume execution occurs on the I/O thread pool thread, this would imply that “RestOfMethod” also runs there if the context is null, which seems illogical. Therefore, it’s plausible (though not confirmed) that the I/O thread pool thread invokes another thread for the condition, subsequently either continuing or dispatching it to the appropriate context thread.

Another crucial aspect of the SynchronizationContext is the ConfigureAwait method. Calling it with ‘false’ prevents dispatch to the captured context. This is vital for library code, as it shouldn’t concern itself with synchronization contexts; that responsibility lies at a higher level (e.g., UI management). Let’s illustrate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//UI, button handler
string st = await new Downloader(this.logger).DownloadAndFormat(address);
this.ResultDisplay.Text = st;

//library code (in the Downloader class)
public async Task DownloadAndFormat(string address)
{ 
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync(address);
string formatted = await response.ReadAsStringAsync();
return formatted.Trim().ToUpper();
} 

The UI thread calls DownloadAndFormat and continues with await client.GetAsync. The compiler captures the synchronizationContext and uses it for the continuation. Since Task creation occurs on the UI thread, the continuation is dispatched there, as is the subsequent one after ReadAsStringAsync. While these continuations running on the UI thread might seem unnecessary, they shouldn’t pose issues unless the UI handler code does something unusual, potentially leading to a deadlock. Let’s see how:

1
2
3
4
5
//UI, button handler
string st = new Downloader(this.logger).DownloadAndFormat(address).Result;
this.ResultDisplay.Text = st;
 
 

Instead of using await, this code blocks on the .Result access, which is unusual but valid. This blocks the UI thread while waiting for the Task result. Simultaneously, within DownloadAndFormat, the continuation dispatch to the captured context waits for the UI thread, resulting in a deadlock!

To prevent this, the library code should ensure continuation is not dispatched to the UI thread:

1
2
3
4
5
6
7
8
9

//library code (in the Downloader class)
public async Task 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();
} 

Beyond deadlock risks, dispatching to the UI thread when a thread pool thread suffices can impact performance, as the UI thread might already be busy. You can find further information on this topic in here.

Licensed under CC BY-NC-SA 4.0
Last updated on Nov 23, 2023 18:44 +0100