I've had an odd attitude to the async/await magic added in C# 5.0 some years ago. On one side I was marvelled by all the compiler magic making it possible, but being a unique C# feature I felt more inclined to use ContinueWith, as using Tasks these way felt basically like using Promises in javascript. ES7 will include async/await magic, and Python already includes it, so I felt it was about time to fully jump into the async/await wagon. There are tons of impressive articles and tutorial at any level of detail, but I'll put here my own basic notes, the ones that I hope will be enough to put me up and running again if I spend time without doing any async stuff.
As everything in async/await revolves around Task (and Task<T>) it's important to clear up some concepts. As I said before, we can think of Tasks as javascript Promises, but there is a difference. In javascript we have no control over thread creation as javascript engines are single threaded (at least in the browser, with the exception of Web Workers). I assume under the covers a javascript instruction could do some native code run in separate threads (for example you call a native node module that does some encryption running native code in multiple threads), but you won't have javascript instructions running in parallel. So Promises are just that, a promise of one operation that at some point will finish and will tell you about it, you don't know if maybe under the covers that Promise is blocked waiting for I/O or it's running native threads in parallel. With Tasks the main idea is the same, when the operation is done it will tell you, but you can explicitly create a Task to run code in a separate Thread. That's what happens when you call Task.Run. Indeed you should no longer use "new Thread()", it's the TPL who will do that for you (well, by default it'll use a ThreadPool thread). This reading is pretty good.
A Task represents a promise of work to be completed some time in the future.
There are a couple of options to how a Task gets created:
- Using Task.Run or Task.Factory.StartNew without any flags - This will queue work on the .NET ThreadPool, as stated.
- Using Task.Factory.StartNew and specifying the TaskCreationOptions.LongRunning - This will signal the TaskScheduler to use a new thread instead of a ThreadPool thread.
- When using async-await feature of C#-5, also known as Promise Tasks. When you await on a method returning a Task or a Task
, they may not use any threading mechanism, either ThreadPool or new Thread to do their work as they may use a purely asynchronous API, meaning they will continue to execute on the thread which executed them, and will yield control back to calling method once hitting the firstawait keyword. - Thanks to Servy for this one. Another options for execution is creating a custom TaskScheduler and passing it to Task.Factory.StartNew. A custom TaskScheduler will give you fine-grained control over the execution of your Task
Of course await does not create new threads on its own or magically turns into asynchronous the method you are calling. That method that you are invoking has to return a Task, and async/await just helps you handle those tasks. As someone nicely put here, The new Async and await keywords allow you to orchestrate concurrency in your applications. They don't actually introduce any concurrency in to your application.
The basis
A method with the await keyword has to be marked as
Error CS4032 The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task<Task<string>>'.
You can call a method marked as async without using await (you'll use then: Task.Wait/Task.Result) but you have to be careful with what you do, as that can be a recipe for a deadlock, unless we are using ConfigureAwait(false) (or we do a ContinueWith in the caller rather than a Wait). You can check here
The basic point to understand how await works is to think of it as if we were calling a ContinueWith on the Task, and passing a lambda to that ContinueWith with the code that comes after the await. If we have several await calls in one method, think of it as chaining continuations. Check this question if you don't believe me. In the end, based on what I've managed to understand from this chapter in that very in depth series, I think it's a bit more complex, you push the lambda, and then the ContinueWith is called later by the Awaiter, but the basic idea is the same.
We can draw a comparison between yield and await. Both of them involve a lot of compiler magic than basically creates a State Machine. We can say that a yield or await keyword suspend the execution of the current method (but they do not suspend the thread). The big difference is that a method suspended via yield is restarted by you (when you call Next in the generated enumerator), while a method suspended by await is "magically" restarted when the Task it's waiting for is complete, you don't have to do anything.
The similarities between yield and await go one step further. Same as we have an IEnumerable and IEnumerator pair, we also have an Awaitable and Awaiter pair. The Awaiter is a bit hidden, most of the time you don't care about it, you just care about the Awaitable (that for the most part is just a Task). The Awaiter manages the Awaitable (checks if it's fully done or there are continuations waiting). In the end is similar to when you use a foreach loop to iterate an IEnumerable, you don't care about the IEnumerator, though it's him who really does the iteration. I like this mention done here about the "duck typing" nature of both patterns
I used to do something like this:
await Task.Run(()=>Thread.Sleep(5000));
you better use await Task.Delay(5000), it's more "academic", and I read somewhere that it uses a Timer internally rather than wasting a ThreadPool thread in this Run-Sleep.
In some later post I intend to talk about Synchronization Contexts
No comments:
Post a Comment