Introduction
Let’s say you have an old code base, that doesn’t hold pointer to Tasks, and you need to wait all of them to be done on caller side, and you have nested calls so it’s hard to refactor, so you can’t do Task.WhenAll(tasks).Wait()
, so we need a wait all tasks to finish before executing code without keeping track of which Tasks are executed.
Coding
C# Task.WhenAll
I will start by show casing how it’s done usually with Task.WhenAll
, we will start with a code that does not wait tasks and refactor to wait tasks.
static void Main(string[] args)
{
for(int i = 0; i < 5; i++)
Work(i);
Console.WriteLine("Done");
}
public static void Work(int i)
{
Task.Run(() => {
Console.WriteLine($"Entry index: {i}");
Thread.Sleep(1000);
Console.WriteLine($"Out index: {i}");
});
}
As you can see in output down below it doesn’t wait to tasks to finish as we didn’t get any Out index
message
Done
Entry index: 1
Entry index: 0
Entry index: 2
Entry index: 3
Entry index: 4
C:\Users\User\Documents\code\ConsoleApp3\bin\Debug\net6.0\ConsoleApp3.exe (process 21140) exited with code 0.
to fix this issue with Task.WhenAll
we need to hold pointers of those tasks somewhere, I will store them in List<Task>
static void Main(string[] args)
{
List<Task> tasks = new();
for (int i = 0; i < 5; i++)
tasks.Add(Work(i));
Task.WhenAll(tasks).Wait();
Console.WriteLine("Done");
}
public static Task Work(int i)
{
return Task.Run(() =>
{
Console.WriteLine($"Entry index: {i}");
Thread.Sleep(1000);
Console.WriteLine($"Out index: {i}");
});
}
As you can see in output down below it now waits the tasks to finish then print Done
Entry index: 0
Entry index: 1
Entry index: 2
Entry index: 3
Entry index: 4
Out index: 3
Out index: 2
Out index: 0
Out index: 4
Out index: 1
Done
C:\Users\User\Documents\code\ConsoleApp3\bin\Debug\net6.0\ConsoleApp3.exe (process 27356) exited with code 0.
C# CountdownEvent
Okay so method above works but the refactoring was so small,
What if you run into situation where there is deeply nested function calls? That will take time and huge refactoring.
So I wanted something very similar to sync.WaitGroup
in Go
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
worker(i)
}(i)
}
wg.Wait()
The pattern is as follows: Increment counter by number of jobs that are going to be executed, then after job completes it will decrement counter by one.
In C#
there is equivalent class to sync.WaitGroup
and it’s CountdownEvent
class
I will reuse code from above but this time will fix it with CountdownEvent
instead of Task.WhenAll
1private static CountdownEvent _countdownEvent = new(1);
2
3static void Main(string[] args)
4{
5
6 for (int i = 0; i < 5; i++)
7 Work(i);
8
9 _countdownEvent.Signal();
10 _countdownEvent.Wait();
11 Console.WriteLine("Done");
12}
13
14public static void Work(int i)
15{
16 _countdownEvent.AddCount(1);
17
18 Task.Run(() => {
19 Console.WriteLine($"Entry index: {i}");
20 Thread.Sleep(1000);
21 Console.WriteLine($"Out index: {i}");
22 }).ContinueWith(t => _countdownEvent.Signal());
23}
At line 1
I initialized CountdownEvent to 1
because setting it to 0 will throw an exception if we try to add counter.
then on line 9
I called _countdownEvent.Signal()
which will decrement counter by 1
. So the one that I added is decremented later.
then on line 16
each time I am running new task I am adding counter by 1.
then on line 22
after task is completed I am decrementing this one, to run a code after Task
execution I am using ContinueWith
which will execute code right after completion of task.
Then on line 10
I call wait _countdownEvent.Wait()
which will wait the counter to become 0
again.
As we can see we were able to wait all tasks to finish without holding a pointer to any task.
Entry index: 0
Entry index: 3
Entry index: 2
Entry index: 4
Entry index: 1
Out index: 2
Out index: 4
Out index: 0
Out index: 3
Out index: 1
Done
C:\Users\User\Documents\code\ConsoleApp3\bin\Debug\net6.0\ConsoleApp3.exe (process 22704) exited with code 0.