web-dev-qa-db-fra.com

Assurer l'ordre d'exécution des tâches dans le pool de threads

J'ai lu sur le modèle de pool de threads et je n'arrive pas à trouver la solution habituelle au problème suivant.

Je souhaite parfois que les tâches soient exécutées en série. Par exemple, je lis des morceaux de texte dans un fichier et, pour une raison quelconque, j'ai besoin que les morceaux soient traités dans cet ordre. Donc, fondamentalement, je veux éliminer la concurrence pour certaines tâches .

Considérez ce scénario dans lequel les tâches avec * doivent être traitées dans l'ordre dans lequel elles ont été insérées. Les autres tâches peuvent être traitées dans n'importe quel ordre.

Push task1
Push task2
Push task3   *
Push task4   *
Push task5
Push task6   *
....
and so on

Dans le contexte d'un pool de threads, sans cette contrainte, une seule file d'attente de tâches en attente fonctionne bien, mais c'est clairement le cas ici.

Je pensais avoir certains des threads opéré sur une file d'attente spécifique à un thread et les autres sur la file "globale". Ensuite, pour exécuter certaines tâches en série, je dois simplement les placer dans une file d'attente où un seul thread a l'air. Ça fait ça sonne un peu maladroit.

Alors, la vraie question dans cette longue histoire: comment résoudriez-vous cela? Comment feriez-vous pour que ces tâches soient ordonnées ?

MODIFIER

En tant que problème plus général, supposons que le scénario ci-dessus devienne

Push task1
Push task2   **
Push task3   *
Push task4   *
Push task5
Push task6   *
Push task7   **
Push task8   *
Push task9
....
and so on

Ce que je veux dire, c'est que les tâches d'un groupe doivent être exécutées de manière séquentielle, mais les groupes eux-mêmes peuvent se mélanger. Donc, vous pouvez avoir 3-2-5-4-7 par exemple.

Une autre chose à noter est que je n’ai pas accès à toutes les tâches d’un groupe à l’avance (et j’ai hâte de toutes les recevoir avant de commencer le groupe).

Merci pour votre temps.

46
nc3b

Quelque chose comme ce qui suit va permettre aux tâches série et parallèles d'être mises en file d'attente, où les tâches série seront exécutées les unes après les autres, et les tâches parallèles seront exécutées dans n'importe quel ordre, mais en parallèle. Cela vous donne la possibilité de sérialiser les tâches si nécessaire, de faire des tâches parallèles, mais cela se produit lorsque les tâches sont reçues, c'est-à-dire que vous n'avez pas besoin de connaître la séquence complète à l'avance, l'ordre d'exécution est maintenu de manière dynamique.

internal class TaskQueue
{
    private readonly object _syncObj = new object();
    private readonly Queue<QTask> _tasks = new Queue<QTask>();
    private int _runningTaskCount;

    public void Queue(bool isParallel, Action task)
    {
        lock (_syncObj)
        {
            _tasks.Enqueue(new QTask { IsParallel = isParallel, Task = task });
        }

        ProcessTaskQueue();
    }

    public int Count
    {
        get{lock (_syncObj){return _tasks.Count;}}
    }

    private void ProcessTaskQueue()
    {
        lock (_syncObj)
        {
            if (_runningTaskCount != 0) return;

            while (_tasks.Count > 0 && _tasks.Peek().IsParallel)
            {
                QTask parallelTask = _tasks.Dequeue();

                QueueUserWorkItem(parallelTask);
            }

            if (_tasks.Count > 0 && _runningTaskCount == 0)
            {
                QTask serialTask = _tasks.Dequeue();

                QueueUserWorkItem(serialTask);
            }
        }
    }

    private void QueueUserWorkItem(QTask qTask)
    {
        Action completionTask = () =>
        {
            qTask.Task();

            OnTaskCompleted();
        };

        _runningTaskCount++;

        ThreadPool.QueueUserWorkItem(_ => completionTask());
    }

    private void OnTaskCompleted()
    {
        lock (_syncObj)
        {
            if (--_runningTaskCount == 0)
            {
                ProcessTaskQueue();
            }
        }
    }

    private class QTask
    {
        public Action Task { get; set; }
        public bool IsParallel { get; set; }
    }
}

Mettre à jour

Pour gérer les groupes de tâches avec des mélanges de tâches en série et en parallèle, une GroupedTaskQueue peut gérer une TaskQueue pour chaque groupe. Encore une fois, vous n'avez pas besoin de connaître les groupes à l'avance, tout est géré de manière dynamique au fur et à mesure de la réception des tâches. 

internal class GroupedTaskQueue
{
    private readonly object _syncObj = new object();
    private readonly Dictionary<string, TaskQueue> _queues = new Dictionary<string, TaskQueue>();
    private readonly string _defaultGroup = Guid.NewGuid().ToString();

    public void Queue(bool isParallel, Action task)
    {
        Queue(_defaultGroup, isParallel, task);
    }

    public void Queue(string group, bool isParallel, Action task)
    {
        TaskQueue queue;

        lock (_syncObj)
        {
            if (!_queues.TryGetValue(group, out queue))
            {
                queue = new TaskQueue();

                _queues.Add(group, queue);
            }
        }

        Action completionTask = () =>
        {
            task();

            OnTaskCompleted(group, queue);
        };

        queue.Queue(isParallel, completionTask);
    }

    private void OnTaskCompleted(string group, TaskQueue queue)
    {
        lock (_syncObj)
        {
            if (queue.Count == 0)
            {
                _queues.Remove(group);
            }
        }
    }
}
17
Tim Lloyd

Les pools de threads conviennent aux cas où l'ordre relatif des tâches n'a pas d'importance, à condition qu'ils soient tous exécutés. En particulier, il faut que tout soit fait en parallèle.

Si vos tâches doivent être effectuées dans un ordre spécifique, elles ne sont pas adaptées au parallélisme. Par conséquent, un pool de threads n'est pas approprié.

Si vous souhaitez déplacer ces tâches série du thread principal, un seul thread en arrière-plan avec une file d'attente de tâches conviendrait pour ces tâches. Vous pouvez continuer à utiliser un pool de threads pour les tâches restantes adaptées au parallélisme.

Oui, cela signifie que vous devez décider où soumettre la tâche, qu'il s'agisse d'une tâche en ordre ou d'une tâche "peut être mise en parallèle", mais ce n'est pas grave.

Si vous avez des groupes qui doivent être sérialisés, mais qui peuvent s'exécuter en parallèle avec d'autres tâches, vous avez plusieurs choix:

  1. Créez une tâche unique pour chaque groupe, qui effectue les tâches de groupe appropriées dans l'ordre, puis publiez cette tâche dans le pool de threads.
  2. Demandez à chaque tâche d'un groupe d'attendre explicitement la tâche précédente du groupe et envoyez-les au pool de threads. Cela nécessite que votre pool de threads puisse gérer le cas où un thread attend une tâche non encore planifiée sans interblocage.
  3. Ayez un thread dédié pour chaque groupe et publiez des tâches de groupe sur la file de messages appropriée.
14
Anthony Williams

Fondamentalement, plusieurs tâches sont en attente. Certaines tâches ne peuvent être exécutées que lorsqu'une ou plusieurs autres tâches en attente ont été exécutées.

Les tâches en attente peuvent être modélisées dans un graphe de dépendance:

  • "tâche 1 -> tâche2" signifie "la tâche 2 ne peut être exécutée qu'une fois la tâche 1 terminée". les flèches indiquent la direction de l'ordre d'exécution.
  • l'indegree d'une tâche (le nombre de tâches pointant vers elle) détermine si la tâche est prête à être exécutée. Si l'indegree est 0, il peut être exécuté.
  • parfois une tâche doit attendre que plusieurs tâches soient terminées, l'indegree est alors> 1.
  • si une tâche ne doit plus attendre que d'autres tâches se terminent (son indice est zéro), elle peut être soumise au pool de threads avec les threads de travail ou à la file d'attente avec des tâches en attente d'être récupérées par un thread de travail. Vous savez que la tâche soumise ne causera pas de blocage, car la tâche n'attend rien. En tant qu'optimisation, vous pouvez utiliser une file d'attente prioritaire, par exemple Les tâches sur lesquelles dépendent le plus de tâches du graphe de dépendance seront exécutées en premier. Cela ne peut pas non plus provoquer un blocage, car toutes les tâches du pool de threads peuvent être exécutées. Cela peut cependant provoquer la famine.
  • Si une tâche termine son exécution, elle peut être supprimée du graphe de dépendance, en réduisant éventuellement l’indegree des autres tâches, qui peuvent à leur tour être soumises au pool de threads de travail.

Donc, il y a (au moins) un thread utilisé pour ajouter/supprimer des tâches en attente, et il existe un pool de threads de travail.

Lorsqu'une tâche est ajoutée au graphique de dépendance, vous devez vérifier:

  • comment la tâche est connectée dans le graphe de dépendance: quelles tâches doit-elle attendre pour se terminer et quelles tâches doivent attendre pour se terminer? Établissez des connexions de et vers la nouvelle tâche en conséquence.
  • une fois les connexions établies: les nouvelles connexions ont-elles provoqué des cycles dans le graphe de dépendance? Si c'est le cas, il y a une impasse.

Performance :

  • ce modèle est plus lent que l'exécution séquentielle si l'exécution parallèle est en fait rarement possible, car vous avez de toute façon besoin d'une administration supplémentaire pour tout faire de manière séquentielle.
  • ce schéma est rapide si de nombreuses tâches peuvent être effectuées simultanément dans la pratique.

Hypothèses :

Comme vous l'avez peut-être lu entre les lignes, vous devez concevoir les tâches de manière à ce qu'elles n'interfèrent pas avec d'autres tâches. En outre, il doit y avoir un moyen de déterminer la priorité des tâches. La priorité de la tâche doit inclure les données traitées par chaque tâche. Deux tâches ne peuvent pas modifier le même objet simultanément; l'une des tâches doit plutôt avoir la priorité sur l'autre, ou les opérations effectuées sur l'objet doivent être thread-safe.

6
pvoosten

Pour faire ce que vous voulez faire avec un pool de threads, vous devrez peut-être créer une sorte de planificateur.

Quelque chose comme ca:

TaskQueue -> Planificateur -> File d'attente -> ThreadPool

Le planificateur s'exécute dans son propre thread, en gardant une trace des dépendances entre les travaux. Lorsqu'un travail est prêt à être effectué, le planificateur l'insère simplement dans la file d'attente du pool de threads. 

Le ThreadPool devra peut-être envoyer des signaux au planificateur pour indiquer quand un travail est terminé afin que le planificateur puisse placer des travaux en fonction de ce travail dans la file d'attente.

Dans votre cas, les dépendances pourraient probablement être stockées dans une liste chaînée.

Disons que vous avez les dépendances suivantes: 3 -> 4 -> 6 -> 8

Le travail 3 est en cours d'exécution sur le pool de threads, vous ne savez toujours pas que le travail 8 existe.

Job 3 se termine. Vous supprimez le 3 de la liste liée, vous mettez le travail 4 dans la file d'attente sur le pool de threads.

Job 8 arrive. Vous le mettez à la fin de la liste liée.

Les seules constructions devant être entièrement synchronisées sont les files d'attente avant et après le planificateur.

6
Martin

Si je comprends bien le problème, les exécuteurs jdk n’ont pas cette capacité, mais il est facile de lancer la vôtre. Vous avez essentiellement besoin 

  • un pool de threads de travail, chacun ayant une file d'attente dédiée
  • une abstraction sur les files d'attente pour lesquelles vous proposez du travail (cf. la ExecutorService)
  • un algorithme qui sélectionne de manière déterministe une file d'attente spécifique pour chaque travail
  • chaque travail reçoit ensuite des offres dans la bonne file d'attente et est donc traité dans le bon ordre

La différence par rapport aux exécuteurs jdk réside dans le fait qu’ils ont une file d’attente avec n threads mais que vous souhaitez n files d’attente et m threads (où n peut correspondre ou non à m).

* modifier après avoir lu que chaque tâche a une clé *

Un peu plus en détail

  • écrire du code qui transforme une clé en un index (un int) dans une plage donnée (0-n où n est le nombre de threads que vous souhaitez), ceci peut être aussi simple que key.hashCode() % n ou il peut s'agir d'un mappage statique de clé connue les valeurs aux fils ou ce que vous voulez
  • au démarrage
    • créer n files d'attente, les placer dans une structure indexée (tableau, liste quelconque)
    • démarrer n threads, chaque thread effectue simplement un blocage de la file d'attente
    • quand il reçoit du travail, il sait comment exécuter un travail spécifique à cette tâche/cet événement (vous pouvez évidemment associer des tâches à des actions si vous avez des événements hétérogènes).
  • rangez-le derrière une façade acceptant les éléments de travail
  • quand une tâche arrive, remettez-la à la façade
    • la façade trouve la bonne file d'attente pour la tâche en fonction de la clé, la propose à cette file

il est assez facile d'ajouter des threads de travail à redémarrage automatique à ce schéma; vous avez alors besoin que le thread de travail s'enregistre auprès d'un gestionnaire pour indiquer "Je possède cette file d'attente", puis de la gestion interne autour de cela + détection des erreurs dans le thread (ce qui annule l'enregistrement de la propriété de cette file d'attente, ce qui ramène la file d'attente à un groupe de files d'attente libres, ce qui est un déclencheur pour le démarrage d'un nouveau thread)

4
Matt

Je pense que le pool de threads peut être utilisé efficacement dans cette situation. L'idée est d'utiliser un objet strand distinct pour chaque groupe de tâches dépendantes. Vous ajoutez des tâches à votre file d'attente avec ou sans objet strand. Vous utilisez le même objet strand avec des tâches dépendantes. Votre planificateur vérifie si la tâche suivante a une strand et si cette strand est verrouillée. Sinon, verrouillez cette strand et exécutez cette tâche. Si strand est déjà verrouillé, laissez cette tâche en file d'attente jusqu'au prochain événement de planification. Lorsque la tâche est terminée, déverrouillez sa strand.

En conséquence, vous avez besoin d'une seule file d'attente, vous n'avez besoin d'aucun thread supplémentaire, d'aucun groupe compliqué, etc. L'objet strand peut être très simple avec deux méthodes lock et unlock.

Je rencontre souvent le même problème de conception, par exemple pour un serveur de réseau asynchrone qui gère plusieurs sessions simultanées. Les sessions sont indépendantes (cela les mappe à vos tâches indépendantes et aux groupes de tâches dépendantes) lorsque les tâches à l'intérieur des sessions sont dépendantes (cela mappe les tâches internes d'une session à vos tâches dépendantes d'un groupe). En utilisant l'approche décrite, j'évite complètement la synchronisation explicite dans la session. Chaque session a son propre objet strand.

Et qui plus est, j'utilise la (grande) implémentation existante de cette idée: Bibliothèque Boost Asio (C++). Je viens d'utiliser leur terme strand. L'implémentation est élégante: je wrap mes tâches asynchrones dans l'objet strand correspondant avant leur planification.

4
Andriy Tylychko

Les réponses suggérant de ne pas utiliser de pool de threads sont comme coder en dur la connaissance des dépendances de tâches/ordre d'exécution. Au lieu de cela, je créerais un CompositeTask qui gérera la dépendance début/fin entre deux tâches. En encapsulant la dépendance derrière l'interface de tâche, toutes les tâches peuvent être traitées de manière uniforme et ajoutées au pool. Cela masque les détails de l'exécution et permet aux dépendances de la tâche de changer sans affecter l'utilisation ou non d'un pool de threads. 

La question ne spécifie pas de langue - je vais utiliser Java, qui, j'espère, est lisible pour la plupart.

class CompositeTask implements Task
{
    Task firstTask;
    Task secondTask;

    public void run() {
         firstTask.run();
         secondTask.run();
    }
}

Cela exécute les tâches séquentiellement et sur le même thread. Vous pouvez chaîner plusieurs CompositeTask pour créer une séquence de autant de tâches séquentielles que nécessaire.

L'inconvénient est que cela bloque le fil pour la durée de toutes les tâches exécutées de manière séquentielle. Vous voudrez peut-être exécuter d'autres tâches entre la première et la deuxième tâche. Ainsi, plutôt que d'exécuter directement la seconde tâche, demandez à la tâche composite de planifier l'exécution de la seconde tâche:

class CompositeTask implements Runnable
{
    Task firstTask;
    Task secondTask;
    ExecutorService executor;

    public void run() {
         firstTask.run();
         executor.submit(secondTask);
    }
}

Cela garantit que la deuxième tâche ne s'exécutera pas après la fin de la première tâche et permettra également au pool d'exécuter d'autres tâches (éventuellement plus urgentes). Notez que les première et deuxième tâches peuvent s'exécuter sur des threads distincts. Ainsi, bien qu'elles ne s'exécutent pas simultanément, toutes les données partagées utilisées par les tâches doivent être rendues visibles par les autres threads (par exemple, en rendant les variables volatile.).

Il s’agit d’une approche simple, à la fois puissante et flexible, qui permet aux tâches de définir elles-mêmes les contraintes d’exécution, plutôt que de le faire en utilisant différents pools de threads.

3
mdma

Option 1 - Le complexe

Comme vous avez des tâches séquentielles, vous pouvez les regrouper dans une chaîne et laisser les tâches elles-mêmes être soumises à nouveau au pool de threads une fois qu'elles sont terminées. Supposons que nous ayons une liste d'emplois:

 [Task1, ..., Task6]

comme dans votre exemple. Nous avons une dépendance séquentielle, telle que [Task3, Task4, Task6] est une chaîne de dépendance. Nous faisons maintenant un travail (pseudo-code Erlang):

 Task4Job = fun() ->
               Task4(), % Exec the Task4 job
               Push_job(Task6Job)
            end.
 Task3Job = fun() ->
               Task3(), % Execute the Task3 Job
               Push_job(Task4Job)
            end.
 Push_job(Task3Job).

En d’autres termes, nous modifions le travail Task3 en l’enveloppant dans un travail qui en tant que continuation pousse le travail suivant de la file d’attente vers le pool de threads. Il existe de fortes similitudes avec un style de passage général continuation également présent dans des systèmes comme Node.js ou Pythons Twisted framework.

En généralisant, vous créez un système dans lequel vous pouvez définir des chaînes de tâches pouvant defer poursuivre le travail et le soumettre à nouveau.

Option 2 - Le simple

Pourquoi avons-nous même la peine de séparer les emplois? Je veux dire, puisqu'ils sont séquentiellement dépendants, les exécuter tous sur le même Thread ne sera pas plus rapide ni plus lent que de prendre cette chaîne et de l'étaler sur plusieurs threads. En supposant que la charge de travail soit "suffisante", tous les threads auront toujours du travail, alors il est probablement plus simple de regrouper les travaux:

  Task = fun() ->
            Task3(),
            Task4(), 
            Task6()  % Just build a new job, executing them in the order desired
         end,
  Push_job(Task).

C'est assez facile de faire des choses comme celle-ci si vous avez des fonctions de citoyens de première classe afin que vous puissiez les construire à votre guise, comme vous pouvez le faire dans n'importe quel langage de programmation fonctionnel, Python, Ruby-Block, etc. .

Je n'aime pas particulièrement l'idée de créer une file d'attente, ou une pile de continuation, comme dans "Option 1", et j'opterais certainement pour la deuxième option. À Erlang, nous avons même un programme appelé jobs écrit par Erlang Solutions et publié en Open Source. jobs est conçu pour exécuter et charger les exécutions de tâches régulées comme celles-ci. Je combinerais probablement l'option 2 avec des emplois si je devais résoudre ce problème.

3
I GIVE CRAP ANSWERS

Utilisez deux Objets actifs . En deux mots: le modèle d'objet actif consiste en une file d'attente prioritaire et en un ou plusieurs threads de travail pouvant extraire des tâches de la file d'attente et la traiter.

Utilisez donc un objet actif avec un thread de travail: toutes les tâches qui seraient des lieux à mettre en file d'attente seraient traitées de manière séquentielle. Utilisez le deuxième objet actif avec le nombre de threads de travail supérieur à 1. Dans ce cas, les threads de travail obtiennent et traitent les tâches de la file d'attente dans n'importe quel ordre.

La chance. 

3
garik

Ceci est réalisable, pour autant que je comprenne votre scénario. Fondamentalement, vous avez besoin de faire quelque chose d'intelligent pour coordonner vos tâches dans le fil principal. Les API Java dont vous avez besoin sont ExecutorCompletionService et Callable

Commencez par implémenter votre tâche appelable:

public interface MyAsyncTask extends Callable<MyAsyncTask> {
  // tells if I am a normal or dependent task
  private boolean isDependent;

  public MyAsyncTask call() {
    // do your job here.
    return this;
  }
}

Ensuite, dans votre thread principal, utilisez CompletionService pour coordonner l’exécution de la tâche dépendante (c’est-à-dire un mécanisme d’attente):

ExecutorCompletionService<MyAsyncTask> completionExecutor = new 
  ExecutorCompletionService<MyAsyncTask>(Executors.newFixedThreadPool(5));
Future<MyAsyncTask> dependentFutureTask = null;
for (MyAsyncTask task : tasks) {
  if (task.isNormal()) {
    // if it is a normal task, submit it immediately.
    completionExecutor.submit(task);
  } else {
    if (dependentFutureTask == null) {
      // submit the first dependent task, get a reference 
      // of this dependent task for later use.
      dependentFutureTask = completionExecutor.submit(task);
    } else {
      // wait for last one completed, before submit a new one.
      dependentFutureTask.get();
      dependentFutureTask = completionExecutor.submit(task);
    }
  }
}

En faisant cela, vous utilisez un seul exécutant (taille de pool de threads 5) pour exécuter les tâches normales et dépendantes. La tâche normale est exécutée immédiatement dès qu'elle est soumise. () sur Future avant de soumettre une nouvelle tâche dépendante). Ainsi, à tout moment, vous avez toujours plusieurs tâches normales et une seule tâche dépendante (le cas échéant) exécutée dans un seul pool de threads. 

Ceci est juste une longueur d'avance. En utilisant ExecutorCompletionService, FutureTask et Semaphore, vous pouvez implémenter un scénario de coordination de threads plus complexe.

2
yorkw

Je pense que vous mélangez des concepts. Threadpool est acceptable lorsque vous souhaitez distribuer du travail entre des threads, mais si vous commencez à mélanger des dépendances entre threads, alors ce n'est pas une si bonne idée.

Mon conseil, simplement n'utilisez pas le threadpool pour ces tâches. Créez simplement un thread dédié et conservez une file d'attente simple d'éléments séquentiels devant être traités par ce thread uniquement. Vous pouvez ensuite continuer à envoyer des tâches au pool de threads lorsque vous n'avez pas d'exigence séquentielle et utiliser le thread dédié lorsque vous en avez.

Une précision: selon le bon sens, une file d'attente de tâches série doit être exécutée par un seul thread qui traite chaque tâche l'une après l'autre :)

2
Jorge Córdoba

Etant donné qu'il suffit d'attendre la fin d'une tâche pour lancer la tâche dépendante, vous pouvez le faire facilement si vous pouvez planifier la tâche dépendante dans la première tâche. Ainsi, dans votre deuxième exemple: À la fin de la tâche 2, planifiez la tâche 7età la fin de la tâche 3, planifiez la tâche 4, etc. pour 4-> 6 et 6-> 8.

Au début, planifiez simplement les tâches 1, 2, 5, 9 ... et le reste devrait suivre.

Un problème encore plus général se pose lorsque vous devez attendre plusieurs tâches avant qu'une tâche dépendante puisse démarrer. Le gérer efficacement est un exercice non trivial.

1
ritesh

Comment feriez-vous pour que ces tâches soient ordonnées?

Push task1
Push task2
Push task346
Push task5

En réponse à la modification:

Push task1
Push task27   **
Push task3468   *
Push task5
Push task9
1
Amy B

Vous avez deux types de tâches différentes. Les mélanger dans une seule file d'attente est plutôt étrange. Au lieu d'avoir une file d'attente en avoir deux. Par souci de simplicité, vous pouvez même utiliser un ThreadPoolExecutor pour les deux. Pour les tâches en série, donnez-lui simplement une taille fixe de 1, pour les tâches pouvant être exécutées simultanément, donnez-en plus. Je ne vois pas pourquoi cela serait maladroit du tout. Restez simple et stupide. Vous avez deux tâches différentes, alors traitez-les en conséquence.

1
tcurdt

Il existe à cet effet un framework Java spécifiquement appelé dexecutor (disclaimer: je suis le propriétaire)

DefaultDependentTasksExecutor<String, String> executor = newTaskExecutor();

    executor.addDependency("task1", "task2");
    executor.addDependency("task4", "task6");
    executor.addDependency("task6", "task8");

    executor.addIndependent("task3");
    executor.addIndependent("task5");
    executor.addIndependent("task7");

    executor.execute(ExecutionBehavior.RETRY_ONCE_TERMINATING);

task1, task3, task5, task7 s'exécute en parallèle (selon la taille du pool de threads), une fois que tâche1 est terminée, tâche2 est exécutée, une fois tâche2 terminée, tâche4 exécutée, tâche6 exécutée et enfin tâche8 exécutée.

0
craftsmannadeem

Il y a eu beaucoup de réponses, et évidemment une a été acceptée. Mais pourquoi ne pas utiliser les continuations?

Si vous avez une condition "série" connue, maintenez la tâche lorsque vous mettez la première tâche en file d'attente avec cette condition. et pour d'autres tâches, appelez Task.ContinueWith ().

public class PoolsTasks
{
    private readonly object syncLock = new object();
    private Task serialTask = Task.CompletedTask;


    private bool isSerialTask(Action task) {
        // However you determine what is serial ...
        return true;
    }

    public void RunMyTask(Action myTask) {
        if (isSerialTask(myTask)) {
            lock (syncLock)
                serialTask = serialTask.ContinueWith(_ => myTask());
        } else
            Task.Run(myTask);
    }
}
0
Steven Coco

Pool de threads avec les méthodes d'exécution ordonnées et non ordonnées:

import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;

public class OrderedExecutor {
    private ExecutorService multiThreadExecutor;
    // for single Thread Executor
    private ThreadLocal<ExecutorService> threadLocal = new ThreadLocal<>();

    public OrderedExecutor(int nThreads) {
        this.multiThreadExecutor = Executors.newFixedThreadPool(nThreads);
    }

    public void executeUnordered(Runnable task) {
        multiThreadExecutor.submit(task);
    }

    public void executeOrdered(Runnable task) {
        multiThreadExecutor.submit(() -> {
            ExecutorService singleThreadExecutor = threadLocal.get();
            if (singleThreadExecutor == null) {
                singleThreadExecutor = Executors.newSingleThreadExecutor();
                threadLocal.set(singleThreadExecutor);
            }
            singleThreadExecutor.submit(task);
        });
    }

    public void clearThreadLocal() {
        threadLocal.remove();
    }

}

Après avoir rempli toutes les files d'attente, threadLocal doit être effacé . Le seul inconvénient est que singleThreadExecutor sera créé chaque fois que la méthode 

executeOrdered (tâche exécutable)

invoqué dans un thread séparé

0
Alexander Yakovlev