What's in a Task?
Introduction
Tasks in .NET have become ubiquitous: by now we all know that in order to make our apps and services more scalable (not necessarily performant, they are different concepts), we need to use them. A task is an operation that is designed to run in the background and which may or may not return a value. Tasks allow complex operations (I/O or CPU-bound) to be executed without blocking the main thread and are implemented by the Task and Task<T> classes. These are actually monads in mathematical and functional terminology, which means they have certain properties which I won't cover here.
There are two ways by which we can call a method that is meant to be processed by another thread (one that returns Task or Task<T>, which inherits from Task but wraps a result):
- We use await to ask for the callee to be called by another thread and wait for its completion
- We omit await and have the callee processing in the background without waiting (sometimes called fire-and-forget)
Waiting for the Completion
When we call await on a Task-returning method, the calling thread will wait for the called method to finish. It will not block, but it will appear as such: the thread on the caller will be free to do other work if needed. If all went well, we can then access its result. We generally want this, but, there is an alternative.
Not Waiting for Completion
If we assign a Task or Task<T> variable to the return of the method we want to call asynchronously but omit the await, it will run in the background and will eventually finish (the fire-and-forget pattern). The thing here is: we should manually check for its status, which means, we aren't really forgetting about it.
We need to inspect our Task or Task<T> variable to see if:
- If the processing is finished (Task.IsCompleted property)
- If it has been cancelled (Task.IsCanceled)
- Any exceptions were thrown during its processing (Task.Exception)
If it has completed, we can then get its result (in the case of Task<T> it will be stored in Task<T>.Result). Any exceptions (one AggregateException will wrap many exceptions) thrown while executing will be in Task.Exception.
When we omit the await keyword we get a compiler warning: CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the await operator to the result of the call. It is OK to ignore or suppress it, as long as we know what we are doing! The main problems with this are:
- Forgetting to watch IsCompleted and IsCanceled before accessing Result, which may result in a blocking call or an exception being thrown
- Calling GetAwaiter().GetResult(), which will block (this time, for real) the calling thread, if the task has not yet finished
- Failing to have a look at Exception, resulting in an unobserved exception
Read more here.
Comments
Post a Comment