33
C# Async Await, Eventually: Thread Pools
In my previous post in the series I covered what Asynchronous Programming is and introduced threads (https://dev.to/glsolaria/c-async-await-eventually-asynchronous-programming-5h46). Now I am going to eventually get to an explanation of what the Synchronization Context (now don't be scared) is but to do that I need to answer a different question.
Trust me I will bring this together and get to Async/Await eventually and hopefully you will be in a better position to understand what is happening under the hood.
Threads are resource intensive critters. It takes a relatively long time to create them. By creating a pool of them, the Thread Pool has a ready set of threads to use to execute your code.
You asked for it so here I go! Imagine you have pool of secretaries that can do work for you: photocopy, file, answer phones etc. Getting a new secretary takes time to advertise, interview, then hire so you decide to have a set already ready to go. When the work comes in, you allocate a secretary to do the work and then the secretary returns to the pool waiting for the next job. (The pool could be a swimming pool or a pool table but basically it's where the secretaries hang out waiting for new work). In this analogy you are running a Secretary Pool. Just replace secretary with Thread and hopefully you can see where I am going with this.
Threads are resource intensive critters. It takes a while to start them because it involves allocating resources and may involve communication between the kernel space and the runtime. I have included the following code for your reference but I really want you to focus on the bar plot this code produces ...
[RPlotExporter]
[SimpleJob(RunStrategy.ColdStart, RuntimeMoniker.Net50, baseline: true)]
public class ThreadPoolBaseliner
{
private Thread[] simpleThreadPool;
[Params(1, 2, 4, 8, 16, 32, 64, 128)] public int N;
[IterationSetup]
public void Setup() => simpleThreadPool = new Thread[N];
[Benchmark]
public void ThreadCreation() => CreateThreads(N);
[Benchmark]
public void ForLoopAddition() => LoopAdditionFor(N);
public void CreateThreads(int numberOfThreads)
{
for (int ii = 0; ii < numberOfThreads; ++ii)
{
simpleThreadPool[ii] = new Thread(() =>
{ Thread.Sleep(0); });
}
}
public void LoopAdditionFor(int numberOfIterations)
{
int total = 0;
for (int ii = 0; ii < numberOfIterations; ++ii) total += 42;
}
private class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<ThreadPoolBaseliner>();
}
}
}
I used https://github.com/dotnet/BenchmarkDotNet to produce the following bar plot ...
The time is shown in microseconds and the plot compares the benchmark results between a simple for loop and thread creation where the number of iterations starts at 1 then doubles until we get to 128. Hopefully you can see that starting threads can be a costly exercise.
So in conclusion, a Thread Pool manages thread creation, execution, and destruction in such a way to balance the resource costs against the benefits of asynchronous execution.
33