Je voudrais exécuter un tas de tâches asynchrones, avec une limite sur le nombre de tâches pouvant être en attente d'achèvement à un moment donné.
Supposons que vous ayez 1 000 URL et que vous ne souhaitiez ouvrir que 50 demandes à la fois; mais dès qu'une demande est terminée, vous ouvrez une connexion à l'URL suivante dans la liste. De cette façon, il y a toujours exactement 50 connexions ouvertes à la fois, jusqu'à ce que la liste d'URL soit épuisée.
Je souhaite également utiliser un nombre donné de threads si possible.
J'ai trouvé une méthode d'extension, ThrottleTasksAsync
qui fait ce que je veux. Existe-t-il déjà une solution plus simple? Je suppose que c'est un scénario courant.
Usage:
class Program
{
static void Main(string[] args)
{
Enumerable.Range(1, 10).ThrottleTasksAsync(5, 2, async i => { Console.WriteLine(i); return i; }).Wait();
Console.WriteLine("Press a key to exit...");
Console.ReadKey(true);
}
}
Voici le code:
static class IEnumerableExtensions
{
public static async Task<Result_T[]> ThrottleTasksAsync<Enumerable_T, Result_T>(this IEnumerable<Enumerable_T> enumerable, int maxConcurrentTasks, int maxDegreeOfParallelism, Func<Enumerable_T, Task<Result_T>> taskToRun)
{
var blockingQueue = new BlockingCollection<Enumerable_T>(new ConcurrentBag<Enumerable_T>());
var semaphore = new SemaphoreSlim(maxConcurrentTasks);
// Run the throttler on a separate thread.
var t = Task.Run(() =>
{
foreach (var item in enumerable)
{
// Wait for the semaphore
semaphore.Wait();
blockingQueue.Add(item);
}
blockingQueue.CompleteAdding();
});
var taskList = new List<Task<Result_T>>();
Parallel.ForEach(IterateUntilTrue(() => blockingQueue.IsCompleted), new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism },
_ =>
{
Enumerable_T item;
if (blockingQueue.TryTake(out item, 100))
{
taskList.Add(
// Run the task
taskToRun(item)
.ContinueWith(tsk =>
{
// For effect
Thread.Sleep(2000);
// Release the semaphore
semaphore.Release();
return tsk.Result;
}
)
);
}
});
// Await all the tasks.
return await Task.WhenAll(taskList);
}
static IEnumerable<bool> IterateUntilTrue(Func<bool> condition)
{
while (!condition()) yield return true;
}
}
La méthode utilise BlockingCollection
et SemaphoreSlim
pour la faire fonctionner. Le régulateur est exécuté sur un thread et toutes les tâches asynchrones sont exécutées sur l'autre thread. Pour réaliser le parallélisme, j'ai ajouté un paramètre maxDegreeOfParallelism qui est passé à une boucle Parallel.ForEach
Re-Purpose en une boucle while
.
L'ancienne version était:
foreach (var master = ...)
{
var details = ...;
Parallel.ForEach(details, detail => {
// Process each detail record here
}, new ParallelOptions { MaxDegreeOfParallelism = 15 });
// Perform the final batch updates here
}
Mais, le pool de threads s'épuise rapidement et vous ne pouvez pas faire async
/await
.
Prime: Pour contourner le problème dans BlockingCollection
où une exception est levée dans Take()
lorsque CompleteAdding()
est appelée, j'utilise la surcharge TryTake
avec un temps libre. Si je n'utilisais pas le délai d'attente dans TryTake
, cela irait à l'encontre du but d'utiliser un BlockingCollection
puisque TryTake
ne bloquerait pas. Y a-t-il une meilleure façon? Idéalement, il y aurait une méthode TakeAsync
.
Comme suggéré, utilisez TPL Dataflow.
UNE TransformBlock<TInput, TOutput>
est peut-être ce que vous recherchez.
Vous définissez un MaxDegreeOfParallelism
pour limiter le nombre de chaînes pouvant être transformées (c'est-à-dire le nombre d'URL pouvant être téléchargées) en parallèle. Vous postez ensuite des URL dans le bloc, et lorsque vous avez terminé, vous dites au bloc que vous avez terminé d'ajouter des éléments et vous récupérez les réponses.
var downloader = new TransformBlock<string, HttpResponse>(
url => Download(url),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 50 }
);
var buffer = new BufferBlock<HttpResponse>();
downloader.LinkTo(buffer);
foreach(var url in urls)
downloader.Post(url);
//or await downloader.SendAsync(url);
downloader.Complete();
await downloader.Completion;
IList<HttpResponse> responses;
if (buffer.TryReceiveAll(out responses))
{
//process responses
}
Remarque: TransformBlock
met en mémoire tampon son entrée et sa sortie. Pourquoi, alors, devons-nous le lier à un BufferBlock
?
Parce que TransformBlock
ne se terminera pas tant que tous les éléments (HttpResponse
) n'auront pas été consommés et await downloader.Completion
se bloquerait. Au lieu de cela, nous laissons le downloader
transmettre toute sa sortie à un bloc tampon dédié - puis nous attendons que le downloader
se termine et inspectons le bloc tampon.
Comme demandé, voici le code avec lequel j'ai fini par aller.
Le travail est configuré dans une configuration maître-détail et chaque maître est traité comme un lot. Chaque unité d'oeuvre est mise en file d'attente de cette façon:
var success = true;
// Start processing all the master records.
Master master;
while (null != (master = await StoredProcedures.ClaimRecordsAsync(...)))
{
await masterBuffer.SendAsync(master);
}
// Finished sending master records
masterBuffer.Complete();
// Now, wait for all the batches to complete.
await batchAction.Completion;
return success;
Les maîtres sont tamponnés un à la fois pour économiser du travail pour d'autres processus externes. Les détails de chaque maître sont envoyés pour le travail via le masterTransform
TransformManyBlock
. Un BatchedJoinBlock
est également créé pour collecter les détails en un seul lot.
Le travail réel est effectué dans le detailTransform
TransformBlock
, de manière asynchrone, 150 à la fois. BoundedCapacity
est défini sur 300 pour garantir que trop de Masters ne soient pas mis en mémoire tampon au début de la chaîne, tout en laissant suffisamment de place pour que suffisamment d'enregistrements détaillés soient mis en file d'attente pour permettre le traitement de 150 enregistrements en même temps. Le bloc génère un object
vers ses cibles, car il est filtré sur les liens selon qu'il s'agit d'un Detail
ou Exception
.
batchAction
ActionBlock
collecte la sortie de tous les lots et effectue des mises à jour en masse de la base de données, la journalisation des erreurs, etc. pour chaque lot.
Il y aura plusieurs BatchedJoinBlock
, un pour chaque maître. Étant donné que chaque ISourceBlock
est sorti séquentiellement et que chaque lot accepte uniquement le nombre d'enregistrements de détail associés à un maître, les lots seront traités dans l'ordre. Chaque bloc ne produit qu'un seul groupe et n'est plus lié à la fin. Seul le dernier bloc batch propage son achèvement au ActionBlock
final.
Le réseau de flux de données:
// The dataflow network
BufferBlock<Master> masterBuffer = null;
TransformManyBlock<Master, Detail> masterTransform = null;
TransformBlock<Detail, object> detailTransform = null;
ActionBlock<Tuple<IList<object>, IList<object>>> batchAction = null;
// Buffer master records to enable efficient throttling.
masterBuffer = new BufferBlock<Master>(new DataflowBlockOptions { BoundedCapacity = 1 });
// Sequentially transform master records into a stream of detail records.
masterTransform = new TransformManyBlock<Master, Detail>(async masterRecord =>
{
var records = await StoredProcedures.GetObjectsAsync(masterRecord);
// Filter the master records based on some criteria here
var filteredRecords = records;
// Only propagate completion to the last batch
var propagateCompletion = masterBuffer.Completion.IsCompleted && masterTransform.InputCount == 0;
// Create a batch join block to encapsulate the results of the master record.
var batchjoinblock = new BatchedJoinBlock<object, object>(records.Count(), new GroupingDataflowBlockOptions { MaxNumberOfGroups = 1 });
// Add the batch block to the detail transform pipeline's link queue, and link the batch block to the the batch action block.
var detailLink1 = detailTransform.LinkTo(batchjoinblock.Target1, detailResult => detailResult is Detail);
var detailLink2 = detailTransform.LinkTo(batchjoinblock.Target2, detailResult => detailResult is Exception);
var batchLink = batchjoinblock.LinkTo(batchAction, new DataflowLinkOptions { PropagateCompletion = propagateCompletion });
// Unlink batchjoinblock upon completion.
// (the returned task does not need to be awaited, despite the warning.)
batchjoinblock.Completion.ContinueWith(task =>
{
detailLink1.Dispose();
detailLink2.Dispose();
batchLink.Dispose();
});
return filteredRecords;
}, new ExecutionDataflowBlockOptions { BoundedCapacity = 1 });
// Process each detail record asynchronously, 150 at a time.
detailTransform = new TransformBlock<Detail, object>(async detail => {
try
{
// Perform the action for each detail here asynchronously
await DoSomethingAsync();
return detail;
}
catch (Exception e)
{
success = false;
return e;
}
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 150, BoundedCapacity = 300 });
// Perform the proper action for each batch
batchAction = new ActionBlock<Tuple<IList<object>, IList<object>>>(async batch =>
{
var details = batch.Item1.Cast<Detail>();
var errors = batch.Item2.Cast<Exception>();
// Do something with the batch here
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });
masterBuffer.LinkTo(masterTransform, new DataflowLinkOptions { PropagateCompletion = true });
masterTransform.LinkTo(detailTransform, new DataflowLinkOptions { PropagateCompletion = true });