web-dev-qa-db-fra.com

Bloc ThreadPoolExecutor lorsque la file d'attente est pleine?

J'essaye d'exécuter beaucoup de tâches en utilisant un ThreadPoolExecutor. Voici un exemple hypothétique:

def workQueue = new ArrayBlockingQueue<Runnable>(3, false)
def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue)
for(int i = 0; i < 100000; i++)
    threadPoolExecutor.execute(runnable)

Le problème est que je reçois rapidement une exception Java.util.concurrent.RejectedExecutionException car le nombre de tâches dépasse la taille de la file d'attente de travail. Cependant, le comportement souhaité que je recherche est d'avoir le bloc de thread principal jusqu'à ce qu'il y ait de la place dans la file d'attente. Quelle est la meilleure façon d'y parvenir?

47
ghempton

Dans certaines circonstances très limitées, vous pouvez implémenter un Java.util.concurrent.RejectedExecutionHandler qui fait ce dont vous avez besoin. 

RejectedExecutionHandler block = new RejectedExecutionHandler() {
  rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
     executor.getQueue().put( r );
  }
};

ThreadPoolExecutor pool = new ...
pool.setRejectedExecutionHandler(block);

À présent. C'est une très mauvaise idée pour les raisons suivantes

  • Il est sujet au blocage, car tous les threads du pool risquent de mourir avant que la chose que vous avez mise dans la file ne soit visible. Atténuez ceci en définissant un temps raisonnable de maintien en vie.
  • La tâche n'est pas emballée comme l'exige votre exécuteur. De nombreuses implémentations d'exécuteurs exécutent leurs tâches dans une sorte d'objet de suivi avant exécution. Regardez la source de la vôtre.
  • L'ajout via getQueue () est fortement déconseillé par l'API et peut être interdit à un moment donné.

Une stratégie presque toujours meilleure consiste à installer ThreadPoolExecutor.CallerRunsPolicy qui étranglera votre application en exécutant la tâche sur le thread qui appelle execute ().

Cependant, il est parfois utile de choisir une stratégie de blocage, avec tous ses risques inhérents. Je dirais dans ces conditions

  • Vous n'avez qu'un seul thread appelant execute ()
  • Vous devez (ou voulez) avoir une très petite longueur de file d'attente
  • Vous devez absolument limiter le nombre de threads exécutant ce travail (généralement pour des raisons externes), et une stratégie d'exécution de l'appelant empêcherait cela.
  • Vos tâches ont une taille imprévisible. Par conséquent, les exécutions d’appelants peuvent créer de la famine si le pool est momentanément occupé par 4 tâches courtes et que votre appel d’un seul fil d’exécution est bloqué par un gros.

Donc, comme je le dis. C'est rarement nécessaire et peut être dangereux, mais voilà.

Bonne chance.

54
Darren Gilroy

Vous pouvez utiliser une variable semaphore pour empêcher les threads d'entrer dans le pool.

ExecutorService service = new ThreadPoolExecutor(
    3, 
    3, 
    1, 
    TimeUnit.HOURS, 
    new ArrayBlockingQueue<>(6, false)
);

Semaphore lock = new Semaphore(6); // equal to queue capacity

for (int i = 0; i < 100000; i++ ) {
    try {
        lock.acquire();
        service.submit(() -> {
            try {
              task.run();
            } finally {
              lock.release();
            }
        });
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

Quelques pièges:

  • Utilisez ce modèle uniquement avec un pool de threads fixe. Il est peu probable que la file d’attente soit souvent pleine. Par conséquent, les nouveaux threads ne seront pas créés. Consultez la documentation Java sur ThreadPoolExecutor pour plus de détails: https://docs.Oracle.com/javase/8/docs/api/Java/util/concurrent/ThreadPoolExecutor.html Il existe un moyen de contourner ce problème, mais c'est hors de portée de cette réponse.
  • La taille de la file d'attente doit être supérieure au nombre de threads principaux. Si nous devions faire la taille de la file d'attente 3, ce qui arriverait finalement serait:

    • T0: les trois threads travaillent, la file d’attente est vide, aucun permis n’est disponible.
    • T1: le fil 1 se termine, libère un permis.
    • T2: le fil 1 interroge la file d'attente pour un nouveau travail, n'en trouve aucun et attend.
    • T3: Le thread principal soumet le travail dans le pool, le thread 1 commence le travail.

    L'exemple ci-dessus traduit le thread principal bloquant du thread 1. Cela peut sembler une petite période, mais multipliez maintenant la fréquence par des jours et des mois. Tout à coup, de courtes périodes de temps représentent une perte de temps considérable.

5
devkaoru

Ce que vous devez faire est d’envelopper votre ThreadPoolExecutor dans Executor, ce qui limite explicitement le nombre d’opérations exécutées simultanément:

 private static class BlockingExecutor implements Executor {

    final Semaphore semaphore;
    final Executor delegate;

    private BlockingExecutor(final int concurrentTasksLimit, final Executor delegate) {
        semaphore = new Semaphore(concurrentTasksLimit);
        this.delegate = delegate;
    }

    @Override
    public void execute(final Runnable command) {
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
            return;
        }

        final Runnable wrapped = () -> {
            try {
                command.run();
            } finally {
                semaphore.release();
            }
        };

        delegate.execute(wrapped);

    }
}

Vous pouvez ajuster concurrentTasksLimit à la threadPoolSize + queueSize de votre exécuteur délégué et cela résoudra votre problème

5

Voici mon extrait de code dans ce cas:

public void executeBlocking( Runnable command ) {
    if ( threadPool == null ) {
        logger.error( "Thread pool '{}' not initialized.", threadPoolName );
        return;
    }
    ThreadPool threadPoolMonitor = this;
    boolean accepted = false;
    do {
        try {
            threadPool.execute( new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    }
                    // to make sure that the monitor is freed on exit
                    finally {
                        // Notify all the threads waiting for the resource, if any.
                        synchronized ( threadPoolMonitor ) {
                            threadPoolMonitor.notifyAll();
                        }
                    }
                }
            } );
            accepted = true;
        }
        catch ( RejectedExecutionException e ) {
            // Thread pool is full
            try {
                // Block until one of the threads finishes its job and exits.
                synchronized ( threadPoolMonitor ) {
                    threadPoolMonitor.wait();
                }
            }
            catch ( InterruptedException ignored ) {
                // return immediately
                break;
            }
        }
    } while ( !accepted );
}

threadPool est une instance locale de Java.util.concurrent.ExecutorService déjà initialisée.

1
MoeHoss

C'est ce que j'ai fini par faire:

int NUM_THREADS = 6;
Semaphore lock = new Semaphore(NUM_THREADS);
ExecutorService pool = Executors.newCachedThreadPool();

for (int i = 0; i < 100000; i++) {
    try {
        lock.acquire();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    pool.execute(() -> {
        try {
            // Task logic
        } finally {
            lock.release();
        }
    });
}
1
disrupt

J'ai résolu ce problème en utilisant un RejectedExecutionHandler personnalisé, qui bloque simplement le fil d'appel pendant un moment, puis essaie de soumettre à nouveau la tâche:

public class BlockWhenQueueFull implements RejectedExecutionHandler {

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        // The pool is full. Wait, then try again.
        try {
            long waitMs = 250;
            Thread.sleep(waitMs);
        } catch (InterruptedException interruptedException) {}

        executor.execute(r);
    }
}

Cette classe peut simplement être utilisée dans l'exécuteur du pool de threads en tant que RejectedExecutionHandler comme un autre. Dans cet exemple:

executorPool = new def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue, new BlockWhenQueueFull())

Le seul inconvénient que je vois, c’est que le fil d’appel peut être verrouillé un peu plus longtemps que ce qui est strictement nécessaire (jusqu’à 250 ms). Pour de nombreuses tâches de courte durée, réduisez peut-être le temps d'attente à 10 ms environ. De plus, étant donné que cet exécuteur est effectivement appelé de manière récursive, de très longues attentes avant qu'un thread devienne disponible (en heures) peuvent entraîner un débordement de pile.

Néanmoins, j'aime personnellement cette méthode. Il est compact, facile à comprendre et fonctionne bien. Est-ce que je manque quelque chose d'important?

0
TinkerTank