Semaphore thread throttling with async/await

async-await, c#, multithreading

Solution

It's not much leaner, just a little bit. Usually, I avoid `ContinueWith` in `async` code because `await` is cleaner and has more `async`-friendly default semantics. Optimize for developer time first, then optimize for other considerations.

Your code does change the semantics slightly: in the original code, `LongRunningTask` was executed from a thread pool context, and in your code it's executed from whatever the `CallThrottledTasks` context is. Also, your code won't propagate exceptions from `LongRunningTask` cleanly; `Task<T>.Result` will wrap exceptions in `AggregateException`, while `await` will not do any wrapping.

Problem

I recently came across an example of throttling threads for async/await calls. After analyzing and playing with the code on my machine, I came up with a slightly different way of doing the same thing. What I'm uncertain about is wether what is happening under the hood is pretty much the same or if there are any subtle differences worth noting? Here's the code based on the original example: ``` private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5); public async Task CallThrottledTasks() { var tasks = new List<Task>(); for (int count = 1; count <= 20; count++) { await _semaphore.WaitAsync(); tasks.Add(Task.Run(async () => { try { int result = await LongRunningTask(); Debug.Print(result.ToString()); } finally { _semaphore.Release(); } })); } await Task.WhenAll(tasks); Debug.Print("Finished CallThrottledTasks"); } ``` And here's my take on the same code: ``` private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5); public async Task CallThrottledTasks() { var tasks = new List<Task>(); for (int count = 1; count <= 20; count++) { await _semaphore.WaitAsync(); tasks.Add(LongRunningTask().ContinueWith(t => { try { int result = t.Result; Debug.Print(result.ToString()); } finally { _semaphore.Release(); } })); } await Task.WhenAll(tasks); Debug.Print("Finished CallThrottledTasks"); } ``` I'm probably way off, but It seems like the Task.Run approach is creating a task to run LongRunningTask() and then adds a continuation to print the result whereas my approach bypasses the task created by Task.Run and is a bit leaner as a result. Is this accurate or am I way off base here?

Original source