What actually is Task<T>?
02 Nov 2023If you see such source code, do you immediately think of multi-threading?
What if I would tell you that Task<T>
is nothing but a fancy delegate which is not coupled
to threads at all?
Let’s start with some very simple component. It provides a single API which accepts a request object, does some computation and returns a response object.
Such an API design obviously forces the caller to “actively” wait for the response to be available in order to be able to continue the processing. This design couples the control flow of processing a request and the control flow of handling its response.
Now let’s assume you want to decouple these two parts of the control flow e.g. because the processing of the request is delayed because of some queue or it runs in a different thread or process or even on a different machine or in the cloud.
One common way to achieve this in .NET is using events.
(Hint: This sample code is kept as simple as possible, event deregistration is skipped for this reason.)
An alternative to .NET events would be passing a simple delegate which we could also call continuation.
In order to improve the readability of this design, let’s get it closer to the initial request-response
semantic by inventing a small class called Promise
which is returned from the Execute
API
and which is then used to hook up the continuation.
Let’s also add error handling to this design.
The implementation of the IComponent
interface would use the Promise
class like this:
Now this design is already pretty close to the one of Task<T>
which shows us that Task<T>
is actually
nothing but an implementation of a promise provided by .NET.
But what about async/await
? Isn’t that what makes Task<T>
so powerful?
Well, actually async/await
is an compiler feature which is completely independent from Task<T>
and
the Task Parallel Library (TPL). To prove this, let’s enable it for our custom Promise
implementation as well.
Therefore, we just need to provide an API (e.g. an extension method) called GetAwaiter
which returns a type
or an interface which has the following properties:
- it implements
INotifyCompletion
- it provides an API called
IsCompleted
- it provides an API called
GetResult
And with this the compiler allows us to use async/await
for the Execute
API as usual.
In essence, that’s exactly how Task<T>
works!
So let’s use it instead of our custom Promise
implementation.
Quod erat demonstrandum.
Task<T>
and async/await
are quite convenient and powerful concepts when dealing with asynchronous APIs
and concurrency BUT neither of these concepts is causing (!) threads or concurrency.
Full source code: https://github.com/plainionist/AboutCleanCode/tree/main/Promise