Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Semaphore thread throttling with async/await

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?

like image 526
AFM Avatar asked Jul 12 '13 17:07

AFM


1 Answers

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.

like image 156
Stephen Cleary Avatar answered Sep 28 '22 10:09

Stephen Cleary