web-dev-qa-db-fra.com

File d'attente de blocage et consommateur multi-thread, comment savoir quand s'arrêter

J'ai un seul producteur de thread qui crée des objets de tâche qui sont ensuite ajoutés dans un ArrayBlockingQueue (qui est de taille fixe).

Je lance également un consommateur multi-thread. Il s'agit d'un pool de threads fixe (Executors.newFixedThreadPool(threadCount);). Je soumets ensuite certaines instances ConsumerWorker à ce threadPool, chaque ConsumerWorker ayant une référence à l'instance ArrayBlockingQueue mentionnée ci-dessus.

Chacun de ces travailleurs effectuera une take() dans la file d'attente et traitera la tâche.

Mon problème est de savoir quelle est la meilleure façon de faire savoir à un travailleur quand il n'y aura plus de travail à faire. En d'autres termes, comment puis-je dire aux ouvriers que le producteur a fini d'ajouter à la file d'attente, et à partir de ce moment, chaque ouvrier doit s'arrêter quand il voit que la file d'attente est vide.

Ce que j'ai maintenant, c'est une configuration où mon producteur est initialisé avec un rappel qui est déclenché lorsqu'il a terminé son travail (d'ajouter des choses à la file d'attente). Je garde également une liste de tous les ConsumerWorkers que j'ai créés et soumis au ThreadPool. Lorsque le rappel au producteur me dit que le producteur a terminé, je peux le dire à chacun des travailleurs. À ce stade, ils doivent simplement continuer à vérifier si la file d'attente n'est pas vide, et lorsqu'elle devient vide, ils doivent s'arrêter, ce qui me permet de fermer gracieusement le pool de threads ExecutorService. C'est quelque chose comme ça

public class ConsumerWorker implements Runnable{

private BlockingQueue<Produced> inputQueue;
private volatile boolean isRunning = true;

public ConsumerWorker(BlockingQueue<Produced> inputQueue) {
    this.inputQueue = inputQueue;
}

@Override
public void run() {
    //worker loop keeps taking en element from the queue as long as the producer is still running or as 
    //long as the queue is not empty:
    while(isRunning || !inputQueue.isEmpty()) {
        System.out.println("Consumer "+Thread.currentThread().getName()+" START");
        try {
            Object queueElement = inputQueue.take();
            //process queueElement
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//this is used to signal from the main thread that he producer has finished adding stuff to the queue
public void setRunning(boolean isRunning) {
    this.isRunning = isRunning;
}

}

Le problème ici est que j'ai une condition de concurrence évidente où parfois le producteur finit, le signale et les ConsumerWorkers s'arrêtent AVANT de tout consommer dans la file d'attente.

Ma question est quelle est la meilleure façon de synchroniser cela pour que tout fonctionne bien? Dois-je synchroniser toute la partie où il vérifie si le producteur est en cours d'exécution plus si la file d'attente est vide plus prendre quelque chose de la file d'attente dans un bloc (sur l'objet de file d'attente)? Dois-je simplement synchroniser la mise à jour du booléen isRunning sur l'instance ConsumerWorker? Une autre suggestion?

MISE À JOUR, VOICI LA MISE EN ŒUVRE DE TRAVAIL QUE J'AI FINI D'UTILISER:

public class ConsumerWorker implements Runnable{

private BlockingQueue<Produced> inputQueue;

private final static Produced POISON = new Produced(-1); 

public ConsumerWorker(BlockingQueue<Produced> inputQueue) {
    this.inputQueue = inputQueue;
}

@Override
public void run() {
    //worker loop keeps taking en element from the queue as long as the producer is still running or as 
    //long as the queue is not empty:
    while(true) {
        System.out.println("Consumer "+Thread.currentThread().getName()+" START");
        try {
            Produced queueElement = inputQueue.take();
            Thread.sleep(new Random().nextInt(100));
            if(queueElement==POISON) {
                break;
            }
            //process queueElement
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Consumer "+Thread.currentThread().getName()+" END");
    }
}

//this is used to signal from the main thread that he producer has finished adding stuff to the queue
public void stopRunning() {
    try {
        inputQueue.put(POISON);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

}

Cela a été fortement inspiré par la réponse de JohnVint ci-dessous, avec seulement quelques modifications mineures.

=== Mise à jour due au commentaire de @ vendhan.

Merci pour votre obeservation. Vous avez raison, le premier extrait de code de cette question a (entre autres problèmes) celui où la while(isRunning || !inputQueue.isEmpty()) n'a pas vraiment de sens.

Dans ma mise en œuvre finale réelle de cela, je fais quelque chose qui est plus proche de votre suggestion de remplacer "||" (ou) avec "&&" (et), dans le sens où chaque travailleur (consommateur) ne vérifie désormais que si l'élément qu'il a dans la liste est une pilule empoisonnée, et si c'est le cas (on peut donc théoriquement dire que le travailleur a être en cours d'exécution ET la file d'attente ne doit pas être vide).

65
Shivan Dragon

Vous devez continuer à take() depuis la file d'attente. Vous pouvez utiliser une pilule empoisonnée pour dire au travailleur d'arrêter. Par exemple:

private final Object POISON_PILL = new Object();

@Override
public void run() {
    //worker loop keeps taking en element from the queue as long as the producer is still running or as 
    //long as the queue is not empty:
    while(isRunning) {
        System.out.println("Consumer "+Thread.currentThread().getName()+" START");
        try {
            Object queueElement = inputQueue.take();
            if(queueElement == POISON_PILL) {
                 inputQueue.add(POISON_PILL);//notify other threads to stop
                 return;
            }
            //process queueElement
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//this is used to signal from the main thread that he producer has finished adding stuff to the queue
public void finish() {
    //you can also clear here if you wanted
    isRunning = false;
    inputQueue.add(POISON_PILL);
}
85
John Vint

J'enverrais aux travailleurs un paquet de travail spécial pour signaler qu'ils devraient fermer:

public class ConsumerWorker implements Runnable{

private static final Produced DONE = new Produced();

private BlockingQueue<Produced> inputQueue;

public ConsumerWorker(BlockingQueue<Produced> inputQueue) {
    this.inputQueue = inputQueue;
}

@Override
public void run() {
    for (;;) {
        try {
            Produced item = inputQueue.take();
            if (item == DONE) {
                inputQueue.add(item); // keep in the queue so all workers stop
                break;
            }
            // process `item`
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

}

Pour arrêter les travailleurs, ajoutez simplement ConsumerWorker.DONE à la file d'attente.

14
NPE

Ne pouvons-nous le faire en utilisant un CountDownLatch, où la taille est le nombre d'enregistrements dans le producteur. Et chaque consommateur va countDown après avoir traité un enregistrement. Et son traverse la méthode awaits() lorsque toutes les tâches sont terminées. Arrêtez ensuite tous vos consommateurs. Comme tous les enregistrements sont traités.

1
Arpan Das

Dans votre bloc de code où vous tentez de récupérer l'élément de la file d'attente, utilisez poll(time,unit) au lieu de take().

try { 
    Object queueElement = inputQueue.poll(timeout,unit);
     //process queueElement        
 } catch (InterruptedException e) {
        if(!isRunning && queue.isEmpty())
         return ; 
 } 

En spécifiant les valeurs appropriées de délai d'expiration, vous vous assurez que les threads ne continueront pas à bloquer en cas de séquence malheureuse de

  1. isRunning est vrai
  2. La file d'attente devient vide, donc les threads entrent en attente bloquée (si vous utilisez take()
  3. isRunning est défini sur false
1
Bhaskar

Il existe un certain nombre de stratégies que vous pouvez utiliser, mais une simple consiste à avoir une sous-classe de tâches qui signale la fin du travail. Le producteur n'envoie pas directement ce signal. Au lieu de cela, il met en file d'attente une instance de cette sous-classe de tâches. Lorsque l'un de vos consommateurs arrête cette tâche et l'exécute, cela provoque l'envoi du signal.

0
Kaelin Colclasure

J'ai dû utiliser un producteur multi-thread et un consommateur multi-thread. Je me suis retrouvé avec un Scheduler -- N Producers -- M Consumers schéma, chacun communique via une file d'attente (deux files d'attente au total). Le planificateur remplit la première file d'attente avec des demandes de production de données, puis la remplit avec N "pilules empoisonnées". Il y a un compteur de producteurs actifs (atomic int), et le dernier producteur qui reçoit la dernière pilule empoisonnée envoie M pilules empoisonnées à la file d'attente des consommateurs.

0
18446744073709551615