Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Limit parallelism of an Async method and not block a Thread-Pool thread

I have an asynchronous method RequestInternalAsync() which makes requests to an external resource, and want to write a wrapper method which limits a number of concurrent asynchronous requests to the method by reducing parallelism.

First option, that comes to mind is a TaskScheduler with limited concurrency (LimitedConcurrencyLevelTaskScheduler, ConcurrentExclusiveSchedulerPair etc.).

But to run a task with a custom scheduler, I have to start the task using a TaskFactory which accepts only Action<>, i.e. I cannot do it by not blocking an extra thread for just waiting for execution of inner method.

Second option is SemaphoreSlim, it does its job, but in this case I'm implementing throttling myself, instead of using a TaskScheduler.

static void Main(string[] args)
{
    // TESTING 1

    var task1 = Task.WhenAll(Enumerable.Range(1, 10).Select(i => RequestAsyncBad()));

    task1.Wait();

    // TESTING 2

    var task2 = Task.WhenAll(Enumerable.Range(1, 10).Select(i => RequestAsyncBetter()));

    task2.Wait();
}

private static Task RequestInternalAsync()
{
    return Task.Delay(500);
}

Solution #1:

private static readonly ConcurrentExclusiveSchedulerPair _concurrentPair
    = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 2);

public static Task RequestAsyncBad()
{
    // Dumb: Because TaskFactory doesn't provide an overload which accepts another task, only action.
    // As result, we blocking a thread to just wait until the inner task finishes.

    return Task.Factory.StartNew(() => RequestInternalAsync().Wait(),
        CancellationToken.None, TaskCreationOptions.DenyChildAttach, _concurrentPair.ConcurrentScheduler);
}

Solution #2 (better):

private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(2);

public static async Task RequestAsyncBetter()
{
    // Here we don't waste thread-pool thread on waiting for a completion of inner task,
    // but instead of using TaskScheduler, implementing a hand-made stuff with semaphore. 

    await _semaphore.WaitAsync().ConfigureAwait(false);

    try
    {
        await RequestInternalAsync();
    }
    finally
    {
        _semaphore.Release();
    }
}

What is the more elegant way to do this?

  • to reuse standard Task API of TPL and TaskScheduler
  • and not block an extra thread
like image 205
Sergey Kostrukov Avatar asked May 08 '14 19:05

Sergey Kostrukov


1 Answers

TaskScheduler is only useful for CPU-bound work. Your work is not using threads. It uses IO completion ports which means that your network call does not hold any threads at all. There is no way to involve a TaskScheduler for an IO operation.

If you're not yet convinced: Async IO in .NET is based on using TaskCompletionSource which is not bound to threads or schedulers in the slightest way.

SemaphoreSlim is the right way to do it. Or, create a ServicePoint and set its maximum concurrency. Only applicable for HTTP requests.

Note, that if you find yourself using Wait you should hesitate and think about what you're doing. Often, this is a mistake.

like image 66
usr Avatar answered Oct 05 '22 00:10

usr