Tasks Are NOT Threads
Tags: csharp, dotnet, concurrency
Published: 2023-09-05 23:09:34

What do you expect from the following code?
```csharp
Console.WriteLine($"Before Delay: {Environment.CurrentManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"After Delay: {Environment.CurrentManagedThreadId}");
```

Yes, you're right, it is something like:

> thead numbers may vary on your run
```
Before Delay: 1
After Delay: 4
```

Now let's do some refactoring and move the `Task.Delay()` part to another method like `CalcAsync()`:

```csharp
Console.WriteLine($"Before Calc: {Environment.CurrentManagedThreadId}");
await CalcAsync();
Console.WriteLine($"After Calc: {Environment.CurrentManagedThreadId}");

async Task CalcAsync()
{
    Console.WriteLine($"In Calc -> Before Delay: {Environment.CurrentManagedThreadId}");
    await Task.Delay(1000);
    Console.WriteLine($"In Calc -> After Delay: {Environment.CurrentManagedThreadId}");
}
```

This is the output:
```
Before Calc: 1
In Calc -> Before Delay: 1
In Calc -> After Delay: 4
After Calc: 4
```
But why!?
 - Why are we still in `Thread 1` when we are in the body of `CalcAsync` method?
 - Why the thread is switched after `Task.Delay()`?
 - Any why are we still in `Thread 4`, although we returned back to the calling method?

Let's make it even more weird by replacting the `Task.Delay()` with a loop:

```csharp
Console.WriteLine($"Before Calc: {Environment.CurrentManagedThreadId}");
await CalcAsync();
Console.WriteLine($"After Calc: {Environment.CurrentManagedThreadId}");

async Task CalcAsync()
{
    Console.WriteLine($"In Calc -> Before Loop: {Environment.CurrentManagedThreadId}");
    var counter = 0;
    for (var i = 0; i < 10000; i++) { counter++; }
    Console.WriteLine($"In Calc -> After Loop: {Environment.CurrentManagedThreadId}");
}
```
Can you guess the output?
```
Before Calc: 1
In Calc -> Before Loop: 1
In Calc -> After Loop: 1
After Calc: 1
```

Although we are working with different tasks, all the work is being done using just one thread: `Tread 1`!!! This is due to the magic of .NET, which allocates threads to tasks as few as possible. If the work is CPU-bound, no thread revoking will occur even if a task is created. If the work is I/O-bound, .NET is smart enough to revoke the thread and wait for the result. In your example, `Task.Delay()` simulates an I/O-bound work, allowing .NET to revoke the thread. This approach can improve performance and reduce resource usage.

But you should be careful if you had expected the `CalcAsync` method to run in another thread. For example, if you were in a UI application (WinForm or Blazor), the main thread will be busy by the loop, and you may have an unresponsive UI. In this case you should create your task explicitly like:

```csharp
await Task.Run(CalcAsync);
```
This will get you an output like this:
```
Before Calc: 1
In Calc -> Before Loop: 4
In Calc -> After Loop: 4
After Calc: 4
```

## Conclusion
As you can see, .NET is an excellent framework that manages threads and tasks efficiently. It is smart enough to decide when to continue using the current thread and when to revoke it. When you work with Tasks, you let .NET manage how to accomplish those tasks using and allocating threads.