Decompilation - async await problem hunt
The story
Internet never dies
Project that I love (because it's my baby) and work with, is written in very asynchronous manner.
It's great in most of the times - e.g. no blocking of user interface, faster background loading. But sometimes problems occurs...
We had one problem, strange things where happening. Whoever tested the case got different 'special' effects. I won't describe details but it looked like not all threads/tasks where synchronized on UI thread and refreshed data didn't appear before it was used again.
What could be the reason? Nice code and design, we are using generic method invoker to call other methods - it's a wrapper for all calls. It's of course asynchronous and calls asynchronous methods by lambda expression, something like this:
It's great in most of the times - e.g. no blocking of user interface, faster background loading. But sometimes problems occurs...
We had one problem, strange things where happening. Whoever tested the case got different 'special' effects. I won't describe details but it looked like not all threads/tasks where synchronized on UI thread and refreshed data didn't appear before it was used again.
What could be the reason? Nice code and design, we are using generic method invoker to call other methods - it's a wrapper for all calls. It's of course asynchronous and calls asynchronous methods by lambda expression, something like this:
return await CallSleepyComplicatedAwaitingWithFunc(() => CallSleepyComplicated());
Internet never dies
When something is posted once on internet it is there forever (or at leas a very long time). But world around changes and not all information are correct. Still all those information are available.
There is a MS blog post Potential pitfalls to avoid when passing around async lambdas that seams legit. And it fits our case.
So according to this article problem should be fixed with additional async await in lambda like this:
Why? because according to this blog post without async-await pair lambda would yield with first found yield, so to prevent there should always be async-await keywords pair.
This is strange for me, it just doesn't feel right.
There is a MS blog post Potential pitfalls to avoid when passing around async lambdas that seams legit. And it fits our case.
So according to this article problem should be fixed with additional async await in lambda like this:
return await CallSleepyComplicatedAwaitingWithFunc(async() => await CallSleepyComplicated());
Why? because according to this blog post without async-await pair lambda would yield with first found yield, so to prevent there should always be async-await keywords pair.
This is strange for me, it just doesn't feel right.
Hunt
Well I started to test different cases. It's not that easy to recreate exact environment but the code that is almost the same you can find in my GitHub in DecompilingSpying project (AsyncSleepMethods class).
So I have worker method that just sleeps for one second - Sleepy.
Sleepy5 is an async caller for Sleepy that additionally returns text that it's done.
Next I try to reproduce 'first yield' so the CallSleepyComplicated calls Sleepy5 twice.
And the last but not least CallSleepyComplicatedAwaiting with is the clue here - it calls CallSleepyComplicated as a lambda expresion with async-await or without it.
First things first - there is no difference in results whether I call CallSleepyComplicated with or without async-await in lambda expression. So the 'first yield' problem is not confirmed.
I even created two consumers - console application (for normal tests) and Xamarin.Android application just to confirm that PCL and Xamarin doesn't change anything.
yeah... but so ... what is happening here?
And should we use async-await in lambda expression?
Let's go deeper. Recently I wrote about decompiling tools. No is a time to use it.
Normal decompilation doesn't show anything interesting. But when we switch to compiler generated code we can see that there is a lot going on. Let's cut it into pieces:
This 3 methods seams easy enough but are decompiled into this code:
What can we see is that for every asynchronous call additional class is generated - the instance of AsyncStateMachine. To be honest in this moment I would like to go deeper about state machine but this story is long enough at this point. So stop at the thought that every time there is an async method Machine State class is generated.
Decompiling final version of my example code and comparing only the two ways of calling method in lambda:
The only difference in compiler generated code is the additional Machine State class.
Further more according to this great course on Pluralsight every lambda expression will generate the same Machine State class and delegate to call this class.
Because lambda expression generates delegate below code is exact equivalent in compiler generated code.
So I have worker method that just sleeps for one second - Sleepy.
Sleepy5 is an async caller for Sleepy that additionally returns text that it's done.
Next I try to reproduce 'first yield' so the CallSleepyComplicated calls Sleepy5 twice.
And the last but not least CallSleepyComplicatedAwaiting with is the clue here - it calls CallSleepyComplicated as a lambda expresion with async-await or without it.
First things first - there is no difference in results whether I call CallSleepyComplicated with or without async-await in lambda expression. So the 'first yield' problem is not confirmed.
I even created two consumers - console application (for normal tests) and Xamarin.Android application just to confirm that PCL and Xamarin doesn't change anything.
yeah... but so ... what is happening here?
And should we use async-await in lambda expression?
Let's go deeper. Recently I wrote about decompiling tools. No is a time to use it.
Normal decompilation doesn't show anything interesting. But when we switch to compiler generated code we can see that there is a lot going on. Let's cut it into pieces:
This 3 methods seams easy enough but are decompiled into this code:
What can we see is that for every asynchronous call additional class is generated - the instance of AsyncStateMachine. To be honest in this moment I would like to go deeper about state machine but this story is long enough at this point. So stop at the thought that every time there is an async method Machine State class is generated.
Decompiling final version of my example code and comparing only the two ways of calling method in lambda:
return await CallSleepyComplicatedAwaitingWithFunc(async () => await CallSleepyComplicated());
vs
return await CallSleepyComplicatedAwaitingWithFunc(() => CallSleepyComplicated());
The only difference in compiler generated code is the additional Machine State class.
Further more according to this great course on Pluralsight every lambda expression will generate the same Machine State class and delegate to call this class.
Because lambda expression generates delegate below code is exact equivalent in compiler generated code.
My conclusion
Well my conclusion is quite simple. If result is identical but with additional async () => await we have additional class - more memory, more things to be done, so we should not use it. I cannot see any advantages of using this additional async()=>await.
And the problem that we had at the beginning was at presentation side of project. Poorly written UI control just didn't refresh every time it should.
Below you can find links to all the code - written and decompiled to compare if you would like to.
Well my conclusion is quite simple. If result is identical but with additional async () => await we have additional class - more memory, more things to be done, so we should not use it. I cannot see any advantages of using this additional async()=>await.
And the problem that we had at the beginning was at presentation side of project. Poorly written UI control just didn't refresh every time it should.
Below you can find links to all the code - written and decompiled to compare if you would like to.
- Orginal file with all async methods
- AsyncSleepyMethods decompiled with lambda async-await
- AsyncSleepyMethods decompiled without async-await
Bibliography
Comments