web-dev-qa-db-fra.com

Limiter les tâches asynchrones

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.

51
Josh Wyant

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.

52
dcastro

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 masterTransformTransformManyBlock. Un BatchedJoinBlock est également créé pour collecter les détails en un seul lot.

Le travail réel est effectué dans le detailTransformTransformBlock, 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.

batchActionActionBlock 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 });
3
Josh Wyant