web-dev-qa-db-fra.com

Meilleur moyen de limiter le nombre de tâches actives exécutées via la bibliothèque de tâches parallèles

Considérons une file d'attente contenant un lot de travaux à traiter. La limitation de la file d'attente ne permet d'obtenir qu'un seul travail à la fois et aucun moyen de savoir combien il y a de travaux. Les travaux prennent 10 secondes et impliquent beaucoup d'attente pour les réponses des services Web et ne sont donc pas liés au processeur.

Si j'utilise quelque chose comme ça

while (true)
{
   var job = Queue.PopJob();
   if (job == null)
      break;
   Task.Factory.StartNew(job.Execute); 
}

Ensuite, il fera furieusement sortir les travaux de la file d'attente beaucoup plus rapidement qu'il ne peut les terminer, manquera de mémoire et tombera sur ses fesses. >. <

Je ne peux pas utiliser (je ne pense pas) ParallelOptions.MaxDegreeOfParallelism parce que je ne peux pas utiliser Parallel.Invoke ou Parallel.ForEach

3 alternatives que j'ai trouvées

  1. Remplacez Task.Factory.StartNew par

    Task task = new Task(job.Execute,TaskCreationOptions.LongRunning)
    task.Start();
    

    Ce qui semble résoudre quelque peu le problème mais je ne suis pas clairement ce que cela fait et si c'est la meilleure méthode.

  2. Créer un planificateur de tâches personnalisé qui limite le degré de simultanéité

  3. Utilisez quelque chose comme BlockingCollection pour ajouter des travaux à la collection une fois démarré et supprimer une fois terminé pour limiter le nombre qui peut être en cours d'exécution.

Avec # 1, je dois avoir confiance que la bonne décision est prise automatiquement, # 2/# 3, je dois déterminer le nombre maximum de tâches qui peuvent être exécutées moi-même.

Ai-je bien compris - quelle est la meilleure façon, ou y a-t-il une autre façon?

[~ # ~] modifier [~ # ~] - Voici ce que j'ai trouvé à partir des réponses ci-dessous, modèle producteur-consommateur.

De plus, l'objectif de débit global n'était pas de retirer les tâches plus rapidement que ce qui pouvait être traité et de ne pas avoir de file d'attente d'interrogation à plusieurs threads (non illustré ici, mais c'est une opération non bloquante et entraînera d'énormes coûts de transaction s'il est interrogé à haute fréquence à partir de plusieurs endroits) .

// BlockingCollection<>(1) will block if try to add more than 1 job to queue (no
// point in being greedy!), or is empty on take.
var BlockingCollection<Job> jobs = new BlockingCollection<Job>(1);

// Setup a number of consumer threads.
// Determine MAX_CONSUMER_THREADS empirically, if 4 core CPU and 50% of time
// in job is blocked waiting IO then likely be 8.
for(int numConsumers = 0; numConsumers < MAX_CONSUMER_THREADS; numConsumers++)
{
   Thread consumer = new Thread(() =>
   {
      while (!jobs.IsCompleted)
      {
         var job = jobs.Take();
         job.Execute();
      }
   }
   consumer.Start();
}

// Producer to take items of queue and put in blocking collection ready for processing
while (true)
{
    var job = Queue.PopJob();
    if (job != null)
       jobs.Add(job);
    else
    {
       jobs.CompletedAdding()
       // May need to wait for running jobs to finish
       break;
    }
}
36
Ryan

Je viens de donner un réponse qui est très applicable à cette question.

Fondamentalement, la classe TPL Task est conçue pour planifier le travail lié au processeur. Il n'est pas fait pour bloquer le travail.

Vous travaillez avec une ressource qui n'est pas CPU: en attente de réponses de service. Cela signifie que le TPL va mal mélanger votre ressource car il suppose la limite du CPU dans une certaine mesure.

Gérez les ressources vous-même: démarrez un nombre fixe de threads ou de tâches LongRunning (qui sont essentiellement les mêmes). Décidez empiriquement du nombre de fils.

Vous ne pouvez pas mettre en production des systèmes non fiables. Pour cette raison, je recommande # 1 mais étranglé. Ne créez pas autant de threads qu'il y a d'éléments de travail. Créez autant de threads nécessaires pour saturer le service distant. Écrivez-vous une fonction d'aide qui génère N threads et les utilise pour traiter M éléments de travail. Vous obtenez ainsi des résultats totalement prévisibles et fiables.

22
usr

Les fractionnements et les continuations de flux potentiels causés par await, plus tard dans votre code ou dans une bibliothèque tierce, ne fonctionneront pas bien avec les tâches longues (ou les threads), alors ne vous embêtez pas à utiliser des tâches longues. Dans le async/await monde, ils sont inutiles. Plus de détails ici .

Tu peux appeler ThreadPool.SetMaxThreads mais avant de passer cet appel, assurez-vous de définir le nombre minimum de threads avec ThreadPool.SetMinThreads, en utilisant des valeurs inférieures ou égales aux valeurs maximales. Et au fait, la documentation MSDN est fausse. Vous POUVEZ descendre en dessous du nombre de cœurs sur votre machine avec ces appels de méthode, au moins dans .NET 4.5 et 4.6 où j'ai utilisé cette technique pour réduire la puissance de traitement d'un service 32 bits à mémoire limitée.

Si toutefois vous ne souhaitez pas restreindre l'ensemble de l'application, mais seulement sa partie de traitement, un planificateur de tâches personnalisé fera le travail. Il y a longtemps, MS a publié échantillons avec plusieurs planificateurs de tâches personnalisés, dont un LimitedConcurrencyLevelTaskScheduler. Générez manuellement la tâche de traitement principale avec Task.Factory.StartNew, fournissant le planificateur de tâches personnalisé, et toutes les autres tâches générées par celui-ci l'utiliseront, y compris async/await et même Task.Yield, utilisé pour réaliser l'asynchronisme très tôt dans une méthode async.

Mais pour votre cas particulier, les deux solutions n'arrêteront pas d'épuiser votre file d'attente de travaux avant de les terminer. Cela pourrait ne pas être souhaitable, selon la mise en œuvre et le but de votre file d'attente. Ils ressemblent plutôt à "lancer un tas de tâches et à laisser le planificateur trouver le temps de les exécuter". Donc peut-être quelque chose d'un peu plus approprié ici pourrait être une méthode plus stricte de contrôle sur l'exécution des travaux via semaphores. Le code ressemblerait à ceci:

semaphore = new SemaphoreSlim(max_concurrent_jobs);

while(...){
 job = Queue.PopJob();
 semaphore.Wait();
 ProcessJobAsync(job);
}

async Task ProcessJobAsync(Job job){
 await Task.Yield();
 ... Process the job here...
 semaphore.Release();
}

Il y a plus d'une façon d'écorcher un chat. Utilisez ce que vous jugez approprié.

12
MoonStom

Microsoft a une bibliothèque très cool appelée DataFlow qui fait exactement ce que vous voulez (et bien plus). Détails ici .

Vous devez utiliser la classe ActionBlock et définir le MaxDegreeOfParallelism de l'objet ExecutionDataflowBlockOptions. ActionBlock fonctionne bien avec async/wait, donc même lorsque vos appels externes sont attendus, aucun nouveau travail ne commencera à être traité.

ExecutionDataflowBlockOptions actionBlockOptions = new ExecutionDataflowBlockOptions
{
     MaxDegreeOfParallelism = 10
};

this.sendToAzureActionBlock = new ActionBlock<List<Item>>(async items => await ProcessItems(items),
            actionBlockOptions);
...
this.sendToAzureActionBlock.Post(itemsToProcess)
8
Alon Catz

Le problème ici ne semble pas être trop en cours d'exécution Tasks, c'est trop programmé Tasks. Votre code essaiera de planifier autant de Task que possible, quelle que soit leur vitesse d'exécution. Et si vous avez trop d'emplois, cela signifie que vous obtiendrez OOM.

Pour cette raison, aucune des solutions que vous proposez ne résoudra réellement votre problème. S'il semble que la simple spécification de LongRunning résout votre problème, c'est probablement parce que la création d'un nouveau Thread (ce que fait LongRunning) prend un certain temps, ce qui réduit efficacement la Nouveau travail. Ainsi, cette solution ne fonctionne que par accident et entraînera très probablement d'autres problèmes plus tard.

En ce qui concerne la solution, je suis principalement d'accord avec usr: la solution la plus simple qui fonctionne raisonnablement bien est de créer un nombre fixe de tâches LongRunning et d'avoir une boucle qui appelle Queue.PopJob() (protégée par un lock si cette méthode n'est pas adaptée aux threads) et Execute() est le travail.

MISE À JOUR: Après un peu de réflexion, j'ai réalisé que la tentative suivante se comportera très probablement très mal. Utilisez-le uniquement si vous êtes vraiment sûr que cela fonctionnera bien pour vous.


Mais le TPL essaie de trouver le meilleur degré de parallélisme, même pour les Tasks liés aux IO. Donc, vous pourriez essayer de l'utiliser à votre avantage. Les longs Tasks ne fonctionneront pas ici, car du point de vue de TPL, il semble qu'aucun travail ne soit fait et il démarrera de nouveaux Tasks encore et encore. Ce que vous pouvez faire à la place est de démarrer un nouveau Task à la fin de chaque Task. De cette façon, TPL saura ce qui se passe et son algorithme pourrait bien fonctionner. De plus, pour laisser le TPL décider du degré de parallélisme, au début d'un Task qui est le premier dans sa ligne, commencez une autre ligne de Tasks.

Cet algorithme peut bien fonctionner. Mais il est également possible que le TPL prenne une mauvaise décision concernant le degré de parallélisme, je n'ai en fait rien essayé de tel.

En code, cela ressemblerait à ceci:

void ProcessJobs(bool isFirst)
{
    var job = Queue.PopJob(); // assumes PopJob() is thread-safe
    if (job == null)
        return;

    if (isFirst)
        Task.Factory.StartNew(() => ProcessJobs(true));

    job.Execute();

    Task.Factory.StartNew(() => ProcessJob(false));
}

Et commencez avec

Task.Factory.StartNew(() => ProcessJobs(true));
7
svick

TaskCreationOptions.LongRunning est utile pour bloquer des tâches et l'utiliser ici est légitime. Ce qu'il fait, c'est qu'il suggère au planificateur de consacrer un thread à la tâche. Le planificateur lui-même essaie de maintenir le nombre de threads au même niveau que le nombre de cœurs de processeur pour éviter un changement de contexte excessif.

Il est bien décrit dans Threading in C # by Joseph Albahari

1
Maciej

J'utilise un mécanisme de file d'attente de messages/boîte aux lettres pour y parvenir. Cela ressemble au modèle d'acteur. J'ai une classe qui a une MailBox. J'appelle cette classe mon "travailleur". Il peut recevoir des messages. Ces messages sont mis en file d'attente et définissent essentiellement les tâches que je souhaite que le travailleur exécute. Le travailleur utilisera Task.Wait () pour que sa tâche se termine avant de retirer la file d'attente du message suivant et de démarrer la tâche suivante.

En limitant le nombre de travailleurs dont je dispose, je suis en mesure de limiter le nombre de threads/tâches simultanés en cours d'exécution.

Ceci est décrit, avec le code source, dans mon article de blog sur un moteur de calcul distribué. Si vous regardez le code pour IActor et WorkerNode, j'espère que cela a du sens.

https://long2know.com/2016/08/creating-a-distributed-computing-engine-with-the-actor-model-and-net-core/

1
long2know