Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ConfigureAwait and mixing asynchronous with synchronous calls

I've been reading quite some articles on the async/await programming model, still there are some things which are not very clear, and I would like to share my perplexities around those.

Say we have the following configuration:

The main, async, method is public async Task<bool> DoSomethingBigAsync(){...} which internally has 3 other methods called as follows:

A) var a = await _someInstance.DoSomethingLittle_A_Async().ConfigureAwait(false);
B) var b = await _someInstance.DoSomethingLittle_B_Async();
C) var c = _someInstance.DoSomethingLittle_C();

The main method is called, say, from a UI thread. We know that the context will be captured and when the main method completes the context will be restored.

If I'm not wrong, when the first (A) async method is called, due to the ConfigureAwait(false) the UI context is not captured and the continuation is instead pushed to a thread from the pool. But this is not guaranteed to happen, because the call may complete immediately. For this reason, I suspect, that ConfigureAwait(false) should be called on all the inner async methods (A and B in this case). Is this correct, or the runtime knows what to do after it sees the first call to ConfigureAwait(false)?

My other concern is about two bad practices (or considered so) and their side effects.

One bad practice seems to be, as per Stephen Toub's article:

exposing asynchronous wrappers for synchronous methods in a library is bad

For this reason it wouldn't be a good idea to make an async version of the DoSomethingLittle_C() method.

The other bad practice seems to be:

Don’t mix blocking and async code.

Now, looking at the code above, if the first ConfigureAwait(false) would guarantee that continuation is pushed to the thread pool I would see no added value in using Task.StartNew(() => { c = _someInstance.DoSomethingLittle_C(); }). If, on the other side, ConfigureAwait(false) never guarantees that continuation is pushed to the thread pool, then we might have problem and we would have to make sure we use Task.StartNew(() => { c = _someInstance.DoSomethingLittle_C(); }).

WRONG(?):
A) var a = await _someInstance.DoSomethingLittle_A_Async().ConfigureAwait(false);
B) var b = await _someInstance.DoSomethingLittle_B_Async();
C) var c = _someInstance.DoSomethingLittle_C();

CORRECT(?):
A) var a = await _someInstance.DoSomethingLittle_A_Async().ConfigureAwait(false);
B) var b = await _someInstance.DoSomethingLittle_B_Async().ConfigureAwait(false);
C) var c = Task.StartNew(() => {return _someInstance.DoSomethingLittle_C();});

like image 697
Pinco Pallino Avatar asked Feb 09 '23 11:02

Pinco Pallino


1 Answers

"For this reason, I suspect, that ConfigureAwait(false) should be called on all the inner async methods (A and B in this case). Is this correct, or the runtime knows what to do after it sees the first call to ConfigureAwait(false)?"

Yes. It should. Because, as you said, the previous call may complete synchronously and also because the previous call may be changed in the future and you don't want to rely on it.

About the Task.Run (which is preferred to Task.Factory.StartNew), the issue is whether to use that in the API's implementation and the answer is almost always no.

Your case is different. If you are on the UI thread and you have extensive work (more than 50ms by Stephen Cleary's suggestion) that can be done in the background there's nothing wrong with offloading that work to a ThreadPool thread to keep the UI responsive. And just like ConfigureAwait I wouldn't rely on the previous call to move you to the ThreadPool as it can also complete synchronously.

So, correct:

var a = await _someInstance.DoSomethingLittle_A_Async().ConfigureAwait(false);
var b = await _someInstance.DoSomethingLittle_B_Async().ConfigureAwait(false);
var c = await Task.Run(() => _someInstance.DoSomethingLittle_C()).ConfigureAwait(false);
like image 173
i3arnon Avatar answered Feb 12 '23 01:02

i3arnon