All you need to know about Task in C#

iamprovidence
9 min readOct 26, 2022

You may think multithreading is a complex topic. It was complex in age of dinosaurs, but not anymore. Modern languages provide us with variety of way how to handle threading. Nowadays, it becomes so simple, that a little few developers can explain a basic terminology and how exactly everything works.

If you want to be one of those, read to the very end. We will see what Task actually is. Why do we need it. How it works under the hood. And the most important, you after finishing this article you would be able to answer why Thread and Task are not the same thing.

So if you are ready, without any further ado, let’s jump into it.

Parallel vs Asynchronous

You can not tell the story about multithreading without mentioning the difference between parallel and asynchronous code.

Parallel execution means that you have multiple threads doing different job and not knowing about each other.

Asynchronous execution refers to having single thread switching between different jobs.

CPU bound vs I/O bound

When dealing with multithreading, you should understand there are two type of operations, CPU bound and I/O bound.

CPU bound — operation is limited by the speed of CPU. Can be simple calculation, adding two number, for loop etc. The thread is this case is blocked by doing calculation directly.

I/O bound — operation is limited by the speed of input/output systems. Examples of such operations can be reading a file, requesting data from another server or db etc. The thread is this case is blocked by waiting for a result.

For CPU bound operation you should use parallel execution, while for I/O bound asynchronous works best.

If you want to have performance gain for CPU bound operation, better use means of Parallel class. Boosting I/O bound operation can be performed with async/await. When you have multiple I/O operation in for loop, logically it makes no sense to wait each one successively, better group them together into an array and wait for all of them with Task.WhenAll().

Key concepts

What are available options in C# for multithreading programming? 🤔 There are actually quite a lot. But let just focus on those that we are interesting.

Thread — is an executable set of operation independent of others. If you want to calculate Fibonacci sequence and factorial simultaneously (or any other CPU bound operation), you would need to have two threads.

In C# the simplest example of usage would be followed:

ThreadPool — creating a Thread is a resource- and time-consuming operation. You don't want your program to freeze for 5 seconds every time you create a new Thread, allocate a stack for it, etc. The solution is simple: create a bunch of thread ahead and then reuse them. That what ThreadPool is, an array of reusable Threads.

It also provides simplified API with just single method:

Task — you may think of it as a simplified Thread, but it actually not. It just an attempt (quite successful) from Microsoft to simplify work with asynchronous code. It is a facade over Threads, but it behaves similarly, check it out:

Why you should care about Task?

Not many people really questioning why using Task matter. It is not even obvious, since it seems like in both cases thread is blocked until previous line finished:

While in reality, the difference is quite significant. Just compare how web server would work with synchronous approach and with Tasks.

Given a server with one thread available (no it is not JS, we're just running on quantum computer here 😁) and a database with billions of records:

All of a sudden, a new request come here to our server:

Our server has found an available thread to process that request.

So we can now query data from db. As we mentioned earlier we have tons of records, so it may take some time. Just to give you a number, let’s say we need to wait 5 minutes:

Nothing foretold troubles, but out of nowhere a new request appears to our server. And now we are ducked 😞 There is no available thread to handle that request, so it needs to wait until we finish querying the data.

Seems like a big issue. Let us see how it would be different if we just used Task in our code.

So here we go again. We have a request, our thread handling it, and we just started querying data.

But, since we are using Task it will bring the miracles of asynchronous approach here. Our thread has no reason to wait 5 minutes, it can go and do what ever it likes. Swim with other friends in a pool, I guess 😁

And what about a new request?

We all good. This time our thread is available and can do the job.

As you see, the benefits here that we can do more work with same amount of available resources.

That is exactly the case where Task shine the best. Requesting data from a database is an I/O bound operation, which better do asynchronously. If we were doing it in parallel, we would still have blocked thread.

Task != asynchronous

Having a Task does not always mean your code will be performed asynchronously.

Wrapping CPU bound operation into Task will not transform it into I/O bound. The code above will be executed in parallel, no matter how much you try.

This one is executed asynchronously. It is only possible because of the next reasons:

  • hard disk works asynchronously
  • developers of drivers implemented asynchronous support
  • developers of .NET environment used low-level code to implement asynchronous support
  • calling thread will go idle and operating system knows to switch it to another operation

Task under the hood

Now, it is time to unmask Task itself.

Task behave like a Thread, but it is closer to a DTO which contain information about execution, whenever it is completed or not, exception and so on. Of course, you can use Task on his own, you would likely do it in combination with async/await.

Having something simple as that will actually force compiler to do tons of work.

For that method, a new state machine class would be generated:

Yikes, looks grumpy 😰. And that is after I simplified it 😏. Do not believe me? Try yourself here. But don’t worry. You don't need to understand that. We can simplify it even more:

There are a few important points to mention:

  • Our method was split in half — before await and after.

Without async/await you would have to deal with callbacks hell and tons of ContinueWith() methods yourself. Now it is hidden. However, do not think async/await is a replacement of ContinueWith(). Those are completely different. The code above is just approximate example, it just happens so that callbacks easier to understand than state in state machine

  • SaveUserToDb() not necessarily would be executed asynchronously. If Task is completed, it will run synchronously in the same thread.

For example, such implementation will always run in the same thread, regardless of the fact it has Task in it.

Even if you have used SaveChangesAsync(), it is still possible to run Task in the same thread. Check the code below, the first time it will be executed asynchronously, and for the second time we just simply return the same result without doing any work.

  • If Task is not completed and executes asynchronously, at the end of its execution, the rest of the method will be running in SynchronizationContext

What the hell is SynchronizationContext?

Good question 😃

It is one of those topics that people used to overcomplicate. Which in reality it as plain as the nose on your face. It is so simple that I would not even bother to explain it, and will just show you the code:

So you are telling me it just delegates the rest of the method to ThreadPool? What even the point of that? Why not just using ThreadPool directly?

Another good question 😄

Look at our callback from example above. If we just post it to ThreadPool it will be executed in any available thread. It might be okay for you. However, we might do some operation that are only allowed in the same calling thread.

This is a valid point in some frameworks like WinForms and WPF. There, only the thread that creates the UI element has the right to update them. So if you want the code below to work, you need to replace implantation of SynchronizationContext to run the rest of the method after await in the same thread instead of TheadPool:

And that is exactly why we have a wrapper around static ThreadPool. To substitute it for our needs. WinForms define its own SynchronizationContext

And then somewhere hidden from human eyes it will set it on UI thread:

All other threads will likely use the default implementation of SynchronizationContext.

There is also one quite similar in WPF. You can also define your SynchronizationContext, but I wonder why would you do that 😅

Of course, if you know that the rest of the code is safe to run in ThreadPool and it will optimize your program, you can override default behavior with ConfigureAwait(false):

For technologies like WinForms and WPF this is important since they have a UI thread. You need to make sure the continuation task is taken by the UI thread. In these environments ConfigureAwait() is relevant, you could use ConfigureAwait(false) if you wish that any other thread than the UI thread can continue the task. You can imagine that very approximate implementation of ConfigureAwait() have the next look:

If the SynchronizationContext is not overriden, the ConfigureAwait() will alway put continuation of the callback in the ThreadPool. Seems like ConfigureAwait() is pointless, but is isn’t.

There are other context except SynchronizationContext nobody talking about like ExecutionContext, SecurityContext, CallContext, LogicalCallContext, CultureContext etc. If ConfigureAwait(true), those will be copied and passad as second argument of SynchronizationContext.Post(callback, state), so if your code rely on the Thread info, after continuation if will be the same, even though the thread is different.

Similarly, how you can prevent copying of thread context in Task with ConfigureAwait(), you can do the same in Thread by using ExecutionContext.SuppressFlow().

There are even more about multithreading, but if we're going to cover everything, it would be a whole book. So, let’s just finish here 😌 Now, let’s just quickly cover up everything, and call it a day.

Summary

So this, in a nutshell, everything you need to know about Task in C#. Of course, there are much more, but knowing the difference between parallel and asynchronous execution, CPU and I/O bound operation, howTask works will already lead you far.

Whenever you need to run code in multiple threads, you should always choose a Task. Thread is an ancient class that should never appear in your code. Even though it allows us to configure each meticulous detail, it is too easy to strike on concurrency issues. While ThreadPool remove all the complexity of Thread away, it does not give us some much freedom as you would get with Task. On the other hand, Task is relevantly newly born and frequently improving approach that is heavily used by everybody. The main reason why it got so popular, that it just looks identical to synchronous code, and you don't need to worry about threads at all.

That’s all for today, dear reader 😉

Don't forget to give this article a clap if you enjoyed it 👏

You can support me with link below ☕️

And follow me if you want to read more about Task

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence