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 ?
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.
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);
}
}
}
}
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:
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:
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:
Performance :
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.
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.
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
ExecutorService
)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
key.hashCode() % n
ou il peut s'agir d'un mappage statique de clé connue les valeurs aux fils ou ce que vous voulezil 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)
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.
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.
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.
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.
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.
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.
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 :)
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.
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
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.
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.
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);
}
}
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é