Wednesday 25 September 2024

Should I use ConfigureAwait(true) or ConfigureAwait(false)?

 One of the most popular questions in the C# asynchronous programming world is when do I use 'ConfigureAwait(true)' and when do I use 'ConfigureAwait(false)'. Before moving deep in let me give you two simple rules which you should always keep in mind and should follow when writing async code in C#,

  • If you are writing code that updates the UI then set ConfigureAwait to true (ConfigureAwait(true))
  • If you are writing a library code that is shared and used by other people then set ConfigureAwait to false ( ConfigureAwait(false) )

Before getting further let me introduce a few basic concepts,

  • Code which follows await KeyWord is continuation task
  • Code above await keyword runs synchronously on the main thread (Eg: UI thread)
  • The async method runs on another thread, usually a worker thread (To unblock the main thread, for example, UI thread)
  • Task.Wait() will wait synchronously and block the main thread ( You should never use Task.Wait() )

To make this article super simple I am deliberately skipping the complexity of SynchronizationContext but it is worth reading, though it is not required to understand the topic of this article.

Now the question is which thread does the Continuation task run? Will it be the main thread (Eg: UI thread) or the same thread as the asynchronous method? This is where we set 'ConfigureAwait' to true or false to decide which thread the continuation task executes on. If we set 'ConfigureAwait(true)' then the continuation task runs on the same thread used before the 'await' statement. If we set 'ConfigureAwait(false)' then the continuation task runs on the available thread pool thread.

What is the purpose of running the continuation task using the main thread which was used before the await statement? Let me explain it with a simple UI application. Before getting your hands dirty one thing you should always keep in mind is UI controls can update only using the UI thread. With that let's jump straight into the code, which is a simple WPF application,

No alt text provided for this image

In the above lines of code when the control hits the 'await' statement (line number 32), it releases/unblocks the UI thread and calls "DoSomethingAsync" method, and waits asynchronously. Also, it instructs to run the continuation task (lines after await statement) on any available thread (Not forced to run on UI thread) by setting configure await to false ( ConfigureAwait(false) ). After waiting for 1 second (line number 39) the control returns and runs the continuation task and the continuation task tries to update the textbox text to a new value (line number 34) which is possible only using UI thread but as we instruct to use any available thread pool thread to run continuation task ( by setting ConfigureAwait(false) ) it fails with the error,

No alt text provided for this image

This error is obvious because no other thread other than the UI thread is allowed to update UI control but here it is some other thread trying to do update the UI hence it failed.

Now let's see what happens when we set 'ConfigureAwait(true)', which is the default,

No alt text provided for this image
No alt text provided for this image

and now the result is as expected, which updates the textbox with the new value. Here 'ConfigureAwait(true)' did the magic, which forced the continuation task to use the UI thread hence updated the UI properly. This is exactly why the rule is "If you are writing code on the UI then set ConfigureAwait to true".

Now let's see why it is a good idea to set ConfigureAwait to false if you are writing library code that is shared with other developers. Let's modify the same example we had before,

No alt text provided for this image

Imagine the method "DoSomethingAsync" is the part of a shared library and "ConfigureAwait" is not set to false in Task.Delay(1000) (Line number 38). As this is a shared library method you have no idea how the developers are going to use this method. If the developer asynchronously waits for the method ( await DoSomethingAsync() ) then everything is well and good as there won't be a blocking thread. But you cannot guarantee that all developers do the same.

What if somebody synchronously waits for "DoSomethingAsync" like DoSomethingAsync().Wait(), which you as a developer should never do. When you call "Wait" method of a "Task" it blocks the thread and waits synchronously.

Let's see line by line the sample code we have. On line number 32 it calls "DoSomethingAsync" and when it hit line number 38 ( on Task.Delay(1000) ) it asynchronously waits and returns "Task" to line number 32 and there it calls "Wait" method. What "Wait" method does is it synchronously waits and blocks the main thread. After one second of delay (Line number 38), it wants to do the continuation task which is line number 40. As we didn't set configure await to false (default is true) it is expecting the same thread which was used before running the task but that thread is blocked because of "DoSomethingAsync().Wait()". Can you guess what we call for this situation? Nothing but a deadlock. The only way to solve this problem is not to force the continuation task to use the same thread which used before, which can do by setting ConfigureAwait to false as below,

No alt text provided for this image

Now regardless of how people use your library the continuation task will never be blocked that is exactly why the rule is "If you are writing library code that is shared and used by other people the set ConfigureAwait to false ( ConfigureAwait(false) )"

No comments:

Post a Comment