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?
Task
API of TPL and TaskScheduler
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With