What the async keyword actually does
作者:互联网
What the async keyword actually does
When it comes to curiousity about inner workings of C# keywords or construct, async and await are at the top of my list by a mile. The amount of complicated intrinsics packed into two simple keywords which magically improve performance by a big margin is absolutely astounding.
However, one of the things I picked up over the years was a saying along the lines of “You should always know what’s going on one or two layers below the abstraction you’re working on.
Therefore, in this post, I’d like to dissect a bit what the async keyword actually does to your code, and potentially also teach you some pitfalls and tricks to use.
The async keyword
In short, the async method allows your method to make use of the async/await paradigm. When using this keyword, your method is required to either return void, Task, Task<T>, a task-like type, or one of the new kids on the block, like ValueTask, IAsyncEnumerable<T> etc.
Also, the await keyword can only be used within async methods.
However, the most significant piece of work connected to the async keyword is the state machine the compiler makes out of it.
The async state machine
Let’s start with the following code:
async Task Main()
{
var httpClient = new HttpClient();
var result = await httpClient.GetStringAsync("https://medium.com");
Console.WriteLine(result);
}
This is a super basic async method. It creates an HttpClient, performs an asynchronous GET HTTP request, and writes the result to the Console.
However, let’s now take a look what the compiler creates out of this method:
Now, that is arguably quite a different beast. This is the state machine the compiler generates for every method / lambda with the async keyword. It essentially turned the method into a class.
Let’s go through the interesting parts outside of the boilerplate attributes and explicit interface implementations.
The transformed Main() method
The transformed Main() method is not too exciting, and fairly straightforward:
Here we create an instance of the state machine class. In addition to that it
- Creates an AsyncTaskMethodBuilder. This will later be used to glue callbacks to the state machine
- The this context is set.
- The initial state of the state machine is set
- The AsyncTaskMethodBuilder is started. This will cause execution and context captures so they can later be restored and kicks off the state machine.
Also, we return the inner Task here.
The <Main>d__0 class
Let’s take a quick look at the generated class itself. It implements the IAsyncStateMachine interface, and this is exactly what this class represents - a state machine.
This causes various things to happen:
- A state field is introduced to keep track of the current state
- A way to advance the state machine (In this case, the MoveNext() method)
- All variables which were used in the async method are “lifted” to fields. This is required, as we need to store the state across several exits and reentries.
The MoveNext() method
This is the heart of the state machine, and this is also where the magic happens:
If you take a closer look at this method, you will see that this essentially represents the initial async method, but in a very split up fashion. Note that this split also happens exactly around the places the await keyword is placed.
Let’s step through:
- On the first execution, the state is set to -1, as it was initialized in the Main() method. This means, execution will step into the first if clause, and initialize the HttpClient.
- Now we reached the point where the first await happens. In this case, we grab the TaskAwaiter<string> of the GetStringAsync() method. Essentially, we now have captured the awaitable value of the call.
- Naively, one could assume that we will just continue now. However, with every awaitable item, there is also a fair chance that the awaitable already returned. Imagine an asynchronous method responsible for filling a cache. The cache access is synchronous, but the method might contain a condition if the cache is not filled, which would query the data to be cached from a remote source in an async fashion. This code will probably be synchronous 99% of the time, but still provides the regular asynchronous call signature.
Now, in case this does occur, the code would now simply skip to the next step. - In case the awaitable did indeed not complete yet, the state tracking variable will be moved accordingly, and AwaitUnsafeOnCompleted will be called, passing both the current awaiter and the state machine. Now, the code will return. For now, the execution of your code is essentially in free float, while the asynchronous operation happens on disk, on a remote server, or whatever your operation is concerned with. However, there is no Thread which “waits” for your operation to complete. This is very important to understand.
- Once the operation is done, the state machine will be restored, and another call to MoveNext() will occur. Note that this is why the variables are also lifted to local fields, as we now need them at a disjointed place in time. This time, as we moved the state in step 3, this entry will now run through, access the previously stored awaitable, and synchronously access it’s result. This is fine, since the operation completed and the result is present. Now that we are past the await step, the only thing left to do is to perform the Console write, and we are done.
- As our state machine is now done, as a final piece of cleanup, we will now clean up some fields, as well as set the result of the Task of the original method, marking it as done.
If at any point in the state machine’s execution an Exception would have been raised, you can see that everything takes places in a try/catch block, and potential Exceptions are stored on the Task of our method. This exception will be raised, as soon as an attempt is made to grab the Result of the Task, which, as we just saw, happens regularily within state machines. Note that this is also the reason why using async void is considered dangerous. If there is no Task to stick Exceptions on, then it would just bubble up and potentially crash your application. However, even with a Task you are not guaranteed to be safe. If no one ever tries to access the result of your Task, then the CLR will throw an UnobservedTaskException when attempting to collect the memory at some point in time.
The state machine can grow increasingly complex, with more await’s, the compiler will change the basic if usages to more complex switch statements accordingly as well. However, even in a very complex operation, the basic principle will always stay the same, and you can stick together layers on layers of these state machines, the mechanism will always be the same.
Anyways, that’s it! With a bit of reading through the code, it is not too hard to roughly understand what is going on. Of course, I skipped through some of the internals even deeper, but as I mentioned, it’s always worth it to know the abstraction one or two layer below your code, but that doesn’t mean you need to know everything in detail. Knowledge of how it roughly works is very valuable already.
标签:machine,What,Task,will,state,does,async,method 来源: https://www.cnblogs.com/chucklu/p/16441921.html