When you have multiple threads trying to do work at the same time, and you want to throttle how many of them are actually executing (such as when you’re sending concurrent requests with HttpClient), you can use SemaphoreSlim.
//Step 1 - create the semaphore
//Specifying how many threads
//to execute concurrently
var semaphore = new SemaphoreSlim(numThreadsToRunConcurrently);
//Step 2 - In the code where you're executing the work
//await the semaphore
await semaphore.WaitAsync();
//Step 3 - release when finished
semaphore.Release();
Code language: C# (cs)
Example – a busy grocery store
Grocery stores have a limited number of checkout lanes open.
Let’s say the grocery store has two lanes open, and there are currently 10 shoppers who all want to checkout at the same time.
Because only two lanes are open, only two shoppers can checkout. The remaining eight have to wait.
The manager sees there is a big backlog forming, so they open up a new lane, which can immediately start checking out one of the waiting shoppers.
Note: This example is updating the UI from different threads in a WinForms app.
Code
Initialize SemaphoreSlim
In our example, the grocery store initially has two lanes open.
checkoutLanes = new SemaphoreSlim(2);
Code language: C# (cs)
Send shoppers to checkout
Every time we click “Send to checkout,” for every shopper we are calling Checkout(Shopper) in a new Task.
private void SendShoppersToCheckout(int numberOfShoppers)
{
for (int i = 1; i <= numberOfShoppers; i++)
{
var shopper = new Shopper()
{
Items = random.Next(5, 15),
Number = totalShoppers++
};
Task.Run(async () =>
{
await Checkout(shopper);
});
}
}
Code language: C# (cs)
Throttled checkout
When a shopper goes to checkout, they must first check if a checkout lane is open by calling await checkoutLanes.WaitAsync().
If there are no lanes open, the shopper thread will await here (non-blocking).
If lanes are open, they continue forward with their checkout.
Once a shopper is finished, they exit the lane by calling Release(), which allows another awaiting shopper to continue forward.
private async Task Checkout(Shopper s)
{
Log($"Shopper {s.Number} is waiting to checkout with {s.Items} item(s)");
await checkoutLanes.WaitAsync();
var timeToCheckout = TimeSpan.FromSeconds(1 * s.Items);
Log($"Shopper {s.Number} is now checking out. This will take {timeToCheckout.TotalSeconds} seconds");
await Task.Delay(timeToCheckout);
Log($"Shopper {s.Number} finished checking out");
checkoutLanes.Release();
}
Code language: C# (cs)
Read more about unit testing async methods.
Opening more lanes
To open a new lane, the manager would call Release(1).
We started with two open lanes, and now have three. Going forward, three shoppers will be able to checkout at the same time.
private void btnOpenNewLane_Click(object sender, EventArgs e)
{
Log("Opening a new lane!");
checkoutLanes.Release(1);
numLanesOpen.Value++;
}
Code language: C# (cs)
GitHub repository
You can find the source code shown in this article in the following GitHub repository: https://github.com/makolyte/semaphoreslim-thread-throttling
Comments are closed.