Multi-threading in C#: Task-Based Asynchronous Pattern (TAP)
DEMO CODE |
Since .NET Framework 4, Task-Based Asynchronous Pattern (TAP) is Microsoft’s recommended way of developing multi-threaded applications in C#. Before that, it was Threads. I’ve briefly covered the basics of threads and provided links to them in my 2017 post Multi-threading in C#: A must have in your programming arsenal (IMO). In fact all of the links there are still valid and still serve as a good refresher. Tasks are part of Microsoft’s Task Parallel Libray (TPL) which also covers parallel programming. For a complete guide to .NET’s parallel programming, visit Parallel programming in .NET.
I created a demo code on my .NET Fiddle library, Threads vs Tasks, illustrating how to program both threads and tasks in C#. Tasks in C# is akin to Promises in JavaScript. It is much easier to code with tasks than with threads but with threads of course we have more control. We do get code running faster with threads over tasks but threads are less scalable in terms of memory. With tasks, thread pooling is automatic thereby reducing chance of running out of memory when scaling to a large number of running tasks. We can implement thread pooling in threads but that complicates our code and thus much harder to understand.
Check out the sample output below from running the demo code on my .NET Fiddle library:
Running Threads...
Worker 01 Thread Id 04: Running...
Worker 02 Thread Id 05: Running...
Worker 03 Thread Id 06: Running...
Worker 04 Thread Id 07: Running...
Worker 05 Thread Id 08: Running...
Worker 06 Thread Id 09: Running...
Worker 07 Thread Id 10: Running...
Worker 08 Thread Id 11: Running...
Worker 09 Thread Id 12: Running...
Worker 10 Thread Id 13: Running...
Worker 03 Thread Id 06: Done 100ms
Worker 01 Thread Id 04: Done 100ms
Worker 04 Thread Id 07: Done 100ms
Worker 02 Thread Id 05: Done 100ms
Worker 05 Thread Id 08: Done 100ms
Worker 06 Thread Id 09: Done 100ms
Worker 07 Thread Id 10: Done 100ms
Worker 08 Thread Id 11: Done 100ms
Worker 09 Thread Id 12: Done 100ms
Worker 10 Thread Id 13: Done 100ms
All Threads Completed 105ms
Running Tasks Using New, Start, and Wait...
Worker 02 Thread Id 16: Running...
Worker 01 Thread Id 14: Running...
Worker 03 Thread Id 18: Running...
Worker 04 Thread Id 17: Running...
Worker 02 Thread Id 16: Done 100ms
Worker 01 Thread Id 14: Done 100ms
Worker 03 Thread Id 18: Done 100ms
Worker 05 Thread Id 14: Running...
Worker 06 Thread Id 18: Running...
Worker 07 Thread Id 16: Running...
Worker 04 Thread Id 17: Done 100ms
Worker 08 Thread Id 17: Running...
Worker 05 Thread Id 14: Done 100ms
Worker 06 Thread Id 18: Done 100ms
Worker 10 Thread Id 18: Running...
Worker 09 Thread Id 14: Running...
Worker 07 Thread Id 16: Done 100ms
Worker 08 Thread Id 17: Done 100ms
Worker 10 Thread Id 18: Done 100ms
Worker 09 Thread Id 14: Done 100ms
All Tasks Completed 304ms
You will notice we spawned 10 threads
that run the same block of code that takes 100ms
to finish. And all 10 threads
completed in a span of just 105ms
.
With tasks however, when we spawned 10 tasks
, only 4 threads
were created and were reused to complete all our tasks. And thus it took a span of 304ms
for all 10 tasks
to complete.
This goes to show that with tasks, threads and thread pooling are being handled for you. See The managed thread pool to learn more.
Tasks or not, we still need to be aware though of the synchronization primitives and how to use them properly for when our tasks or threads need to share a resource or coordinate interaction. In my next post or maybe my next demo code I will illustrate the use of these synchronization primitives. For the meantime you can check Overview of synchronization primitives for a list of these synchronization primitives in C#.