Java: ExecutorService, що блокує подання після певного розміру черги


85

Я намагаюся кодувати рішення, в якому один потік виробляє завдання з інтенсивним введенням / виводом, які можна виконувати паралельно. Кожне завдання має значні дані в пам'яті. Тому я хочу мати можливість обмежити кількість завдань, які очікують на даний момент.

Якщо я створюю ThreadPoolExecutor так:

    ThreadPoolExecutor executor = new ThreadPoolExecutor(numWorkerThreads, numWorkerThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(maxQueue));

Потім executor.submit(callable)кидає, RejectedExecutionExceptionколи черга заповнюється і всі потоки вже зайняті.

Що я можу зробити, щоб executor.submit(callable)заблокувати, коли черга заповнена і всі потоки зайняті?

EDIT : Я спробував це :

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

І це дещо досягає ефекту, якого я хочу досягти, але неелегантно (в основному відхилені потоки запускаються у викличному потоці, тому це блокує викличний потік від подальшої подачі).

РЕДАКТУВАТИ: (через 5 років після запитання)

Будь-хто, хто читає це запитання та відповіді на нього, не приймає прийняту відповідь як одне з правильних рішень. Будь ласка, прочитайте всі відповіді та коментарі.



1
Я раніше використовував семафор, щоб зробити саме це, як і у відповіді на дуже подібне запитання, на яке посилається @axtavt.
Stephen Denne

2
@TomWolk З одного боку, ви отримуєте ще одне завдання, яке виконується паралельно, ніж numWorkerThreadsколи потік абонента також виконує завдання. Але важливішими проблемами є те, що якщо потік абонента отримує тривале запущене завдання, інші потоки можуть сидіти без роботи в очікуванні наступного завдання.
Тахір Ахтар

2
@TahirAkhtar, правда; Черга повинна бути достатньо довгою, щоб вона не пересихала, коли абонент повинен виконати завдання самостійно. Але я думаю, що це перевага, якщо ще один потік, що викликає, може бути використаний для виконання завдань. Якщо абонент просто блокує, потік абонента буде простоювати. Я використовую CallerRunsPolicy з чергою, яка в три рази перевищує можливості пулу потоків, і це працює приємно і плавно. Порівняно з цим рішенням, я б подумав про загартування з надмірним інжинірингом.
TomWolk

1
@TomWalk +1 Хороші бали. Здається, ще одна відмінність полягає в тому, що якщо завдання було відхилено з черги і запущене потоком, що викликає, тоді потік, що викликає, почне обробляти запит не в порядку, оскільки він не чекав своєї черги в черзі. Звичайно, якщо ви вже вибрали використання потоків, то ви повинні правильно обробляти будь-які залежності, але лише те, про що слід пам’ятати.
rimsky

Відповіді:


64

Я зробив те саме. Фокус полягає у створенні BlockingQueue, де метод offer () насправді є put (). (ви можете використовувати будь-який базовий імпорт BlockingQueue, який ви хочете).

public class LimitedQueue<E> extends LinkedBlockingQueue<E> 
{
    public LimitedQueue(int maxSize)
    {
        super(maxSize);
    }

    @Override
    public boolean offer(E e)
    {
        // turn offer() and add() into a blocking calls (unless interrupted)
        try {
            put(e);
            return true;
        } catch(InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
        return false;
    }

}

Зверніть увагу, що це працює лише для пулу потоків, де corePoolSize==maxPoolSizeбудьте там обережні (див. Коментарі).


2
в якості альтернативи ви можете розширити SynchronousQueue, щоб запобігти буферизації, дозволяючи лише прямі передачі обслуговування.
brendon

Елегантний і безпосередньо вирішує проблему. offer () стає put (), а put () означає "... чекаючи, якщо необхідно, щоб місце стало доступним"
Трентон

5
Я не думаю, що це хороша ідея, оскільки це змінює протокол методу пропозиції. Метод пропозиції повинен бути неблокуючим дзвінком.
Mingjiang Shi

6
Я не згоден - це змінює поведінку ThreadPoolExecutor.execute таким чином, що якщо у вас є corePoolSize <maxPoolSize, логіка ThreadPoolExecutor ніколи не додасть додаткових працівників за межі ядра.
Krease

5
Для уточнення - ваше рішення працює лише доти, доки ви підтримуєте обмеження де corePoolSize==maxPoolSize. Без цього він більше не дозволяє ThreadPoolExeecuter мати розроблену поведінку. Я шукав рішення цієї проблеми, яка брала б, не маючи цього обмеження; див. мою альтернативну відповідь нижче щодо підходу, який ми в підсумку застосували.
Krease

15

Ось як я це вирішив на своєму кінці:

(примітка: це рішення блокує потік, який подає Callable, тому запобігає викиданню RejectedExecutionException)

public class BoundedExecutor extends ThreadPoolExecutor{

    private final Semaphore semaphore;

    public BoundedExecutor(int bound) {
        super(bound, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        semaphore = new Semaphore(bound);
    }

    /**Submits task to execution pool, but blocks while number of running threads 
     * has reached the bound limit
     */
    public <T> Future<T> submitButBlockIfFull(final Callable<T> task) throws InterruptedException{

        semaphore.acquire();            
        return submit(task);                    
    }


    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);

        semaphore.release();
    }
}

1
Я припускаю, що це не працює добре у випадках, коли corePoolSize < maxPoolSize...: |
rogerdpack

1
Це працює для випадку, коли corePoolSize < maxPoolSize. У цих випадках семафор буде доступний, але не буде потоку, і SynchronousQueueповерне false. Потім ThreadPoolExecutorволя закрутить нову нитку. Проблема цього рішення полягає в тому, що воно має перегоновий стан . Після semaphore.release(), але до закінчення нитки execute, submit () отримає дозвіл на семафор. ЯКЩО super.submit () запустити до execute()закінчення, завдання буде відхилено.
Luís Guilherme

@ LuísGuilherme Але semaphore.release () ніколи не буде викликаний, поки потік не завершить виконання. Оскільки цей виклик виконується в методі after Execute (...). Мені щось не вистачає у сценарії, який ви описуєте?
cvacca

1
afterExecute викликається тим самим потоком, який запускає завдання, тому він ще не закінчений. Зробіть тест самостійно. Впроваджуйте це рішення і кидайте величезну кількість робіт на виконавця, кидаючи, якщо робота відхиляється. Ви помітите, що так, це має расовий стан, і відтворити його не важко.
Luís Guilherme

1
Перейдіть до ThreadPoolExecutor і перевірте метод runWorker (Worker w). Ви побачите, що речі трапляються після завершення afterExecute, включаючи розблокування працівника та збільшення кількості виконаних завдань. Отже, ви дозволили надходити завданням (випустивши семафор), не маючи пропускної спроможності їх обробити (викликаючи processWorkerExit).
Luís Guilherme

14

Наразі прийнята відповідь має потенційно значну проблему - вона змінює поведінку ThreadPoolExecutor.execute таким чином, що якщо у вас є corePoolSize < maxPoolSize, логіка ThreadPoolExecutor ніколи не додасть додаткових працівників за межі ядра.

Від ThreadPoolExecutor .Execute (Runnable):

    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);

Зокрема, цей останній блок "ще" ніколи не буде вражений.

Краща альтернатива - зробити щось подібне до того, що вже робить OP - використовуйте RejectedExecutionHandler, щоб зробити ту ж putлогіку:

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    try {
        if (!executor.isShutdown()) {
            executor.getQueue().put(r);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RejectedExecutionException("Executor was interrupted while the task was waiting to put on work queue", e);
    }
}

Є кілька речей, на які слід звернути увагу при такому підході, як зазначено в коментарях (посилаючись на цю відповідь ):

  1. Якщо corePoolSize==0, тоді існує умова перегонів, коли всі потоки в пулі можуть загинути, перш ніж завдання буде видно
  2. Використання реалізації, яка обгортає завдання черги (не застосовується до ThreadPoolExecutor), призведе до проблем, якщо обробник також не оберне її так само.

Пам'ятаючи про ці помилки, це рішення буде працювати для більшості типових виконавців ThreadPoolEx, і буде правильно обробляти справи, де corePoolSize < maxPoolSize.


Тим, хто голосував проти - чи можете ви дати деяку інформацію? Чи є в цій відповіді щось неправильне / оманливе / небезпечне? Я хотів би мати можливість вирішити ваші проблеми.
Krease

2
Я не голосував проти, але це, здається, дуже погана ідея
vanOekel

@vanOekel - дякую за посилання - ця відповідь піднімає кілька дійсних випадків, які слід знати, якщо використовувати такий підхід, але ІМО не робить це "дуже поганою ідеєю" - він все одно вирішує проблему, яка присутня у прийнятій на даний момент відповіді. Я оновив свою відповідь цими застереженнями.
Krease

Якщо розмір основного пулу дорівнює 0, і якщо завдання передається виконавцю, виконавець почне створювати потоки / потоки, якщо черга повна, щоб обробити завдання. Тоді чому вона схильна до глухого кута. Не зрозумів вашу думку. Не могли б ви детально розказати.?
Farhan Shirgill Ansari

@ShirgillFarhanAnsari - це випадок, піднятий у попередньому коментарі. Це може статися, оскільки додавання безпосередньо до черги не викликає створення потоків / запуску робочих рук. Це
крайній

4

Я знаю, що це старе запитання, але мала подібну проблему, що створення нових завдань було дуже швидким, і якщо було занадто багато OutOfMemoryError, оскільки існуюче завдання було виконано недостатньо швидко.

У моєму випадку Callablesподано, і мені потрібен результат, отже мені потрібно зберегти все Futuresповернене до executor.submit(). Моє рішення полягало в тому, щоб розмістити його Futuresв BlockingQueueмаксимальному розмірі. Коли ця черга заповнена, більше завдань не генерується, поки деякі не будуть завершені (елементи вилучені з черги). У псевдокоді:

final ExecutorService executor = Executors.newFixedThreadPool(numWorkerThreads);
final LinkedBlockingQueue<Future> futures = new LinkedBlockingQueue<>(maxQueueSize);
try {   
    Thread taskGenerator = new Thread() {
        @Override
        public void run() {
            while (reader.hasNext) {
                Callable task = generateTask(reader.next());
                Future future = executor.submit(task);
                try {
                    // if queue is full blocks until a task
                    // is completed and hence no future tasks are submitted.
                    futures.put(compoundFuture);
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();         
                }
            }
        executor.shutdown();
        }
    }
    taskGenerator.start();

    // read from queue as long as task are being generated
    // or while Queue has elements in it
    while (taskGenerator.isAlive()
                    || !futures.isEmpty()) {
        Future compoundFuture = futures.take();
        // do something
    }
} catch (InterruptedException ex) {
    Thread.currentThread().interrupt();     
} catch (ExecutionException ex) {
    throw new MyException(ex);
} finally {
    executor.shutdownNow();
}

2

У мене була подібна проблема, і я реалізував це, використовуючи beforeExecute/afterExecuteхуки від ThreadPoolExecutor:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Blocks current task execution if there is not enough resources for it.
 * Maximum task count usage controlled by maxTaskCount property.
 */
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {

    private final ReentrantLock taskLock = new ReentrantLock();
    private final Condition unpaused = taskLock.newCondition();
    private final int maxTaskCount;

    private volatile int currentTaskCount;

    public BlockingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
            long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue, int maxTaskCount) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.maxTaskCount = maxTaskCount;
    }

    /**
     * Executes task if there is enough system resources for it. Otherwise
     * waits.
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        taskLock.lock();
        try {
            // Spin while we will not have enough capacity for this job
            while (maxTaskCount < currentTaskCount) {
                try {
                    unpaused.await();
                } catch (InterruptedException e) {
                    t.interrupt();
                }
            }
            currentTaskCount++;
        } finally {
            taskLock.unlock();
        }
    }

    /**
     * Signalling that one more task is welcome
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        taskLock.lock();
        try {
            currentTaskCount--;
            unpaused.signalAll();
        } finally {
            taskLock.unlock();
        }
    }
}

Це має бути достатньо для вас. До речі, оригінальна реалізація була заснована на розмірі завдання, оскільки одне завдання могло бути більше в 100 разів, ніж інше, а подання двох величезних завдань вбивало коробку, але запустити одне велике та велике число малих було нормально. Якщо ваші завдання з інтенсивним введенням / виведенням приблизно однакові за розміром, ви можете використовувати цей клас, інакше просто повідомте мене, і я опублікую реалізацію на основі розміру.

PS Ви хотіли б перевірити ThreadPoolExecutorjavadoc. Це дуже приємний посібник користувача від Дуга Лі про те, як його можна легко налаштувати.


1
Мені цікаво, що станеться, коли Thread затримає замок у beforeExecute () і побачить це maxTaskCount < currentTaskCountі почне чекати за unpausedумови. Одночасно інший потік намагається отримати блокування в afterExecute (), щоб сигналізувати про завершення завдання. Хіба це не тупик?
Тахір Ахтар

1
Я також помітив, що це рішення не заблокує потік, який подає завдання, коли черга заповнюється. Так RejectedExecutionExceptionвсе ще можливо.
Тахір Ахтар

1
Семантика класів ReentrantLock / Condition схожа на те, що забезпечує синхронізація та очікування / сповіщення. Коли методи очікування стану називаються, блокування звільняється, тому не буде тупику.
Петро Семенюк

Правильно, ця програма ExecutorService блокує завдання під час надсилання, не блокуючи потік абонента. Завдання лише надсилається і буде оброблятися асинхронно, коли для нього буде достатньо системних ресурсів.
Петро Семенюк

2

Я реалізував рішення, дотримуючись шаблону декоратора та використовуючи семафор для контролю кількості виконаних завдань. Ви можете використовувати його з будь-якими Executorта:

  • Вкажіть максимум поточних завдань
  • Вкажіть максимальний тайм-аут для очікування дозволу на виконання завдання (якщо тайм-аут проходить і жодного дозволу не отримується, RejectedExecutionExceptionкидається a )
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;

import javax.annotation.Nonnull;

public class BlockingOnFullQueueExecutorDecorator implements Executor {

    private static final class PermitReleasingDecorator implements Runnable {

        @Nonnull
        private final Runnable delegate;

        @Nonnull
        private final Semaphore semaphore;

        private PermitReleasingDecorator(@Nonnull final Runnable task, @Nonnull final Semaphore semaphoreToRelease) {
            this.delegate = task;
            this.semaphore = semaphoreToRelease;
        }

        @Override
        public void run() {
            try {
                this.delegate.run();
            }
            finally {
                // however execution goes, release permit for next task
                this.semaphore.release();
            }
        }

        @Override
        public final String toString() {
            return String.format("%s[delegate='%s']", getClass().getSimpleName(), this.delegate);
        }
    }

    @Nonnull
    private final Semaphore taskLimit;

    @Nonnull
    private final Duration timeout;

    @Nonnull
    private final Executor delegate;

    public BlockingOnFullQueueExecutorDecorator(@Nonnull final Executor executor, final int maximumTaskNumber, @Nonnull final Duration maximumTimeout) {
        this.delegate = Objects.requireNonNull(executor, "'executor' must not be null");
        if (maximumTaskNumber < 1) {
            throw new IllegalArgumentException(String.format("At least one task must be permitted, not '%d'", maximumTaskNumber));
        }
        this.timeout = Objects.requireNonNull(maximumTimeout, "'maximumTimeout' must not be null");
        if (this.timeout.isNegative()) {
            throw new IllegalArgumentException("'maximumTimeout' must not be negative");
        }
        this.taskLimit = new Semaphore(maximumTaskNumber);
    }

    @Override
    public final void execute(final Runnable command) {
        Objects.requireNonNull(command, "'command' must not be null");
        try {
            // attempt to acquire permit for task execution
            if (!this.taskLimit.tryAcquire(this.timeout.toMillis(), MILLISECONDS)) {
                throw new RejectedExecutionException(String.format("Executor '%s' busy", this.delegate));
            }
        }
        catch (final InterruptedException e) {
            // restore interrupt status
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }

        this.delegate.execute(new PermitReleasingDecorator(command, this.taskLimit));
    }

    @Override
    public final String toString() {
        return String.format("%s[availablePermits='%s',timeout='%s',delegate='%s']", getClass().getSimpleName(), this.taskLimit.availablePermits(),
                this.timeout, this.delegate);
    }
}

1

Я думаю, це так просто, як використання ArrayBlockingQueueзамість aa LinkedBlockingQueue.

Ігноруй мене ... це абсолютно неправильно. ThreadPoolExecutorвиклики Queue#offerНЕ putякі матимуть ефект , який ви вимагаєте.

Ви можете розширити ThreadPoolExecutorта забезпечити реалізацію цих execute(Runnable)дзвінків putзамість offer.

Я боюся, це не цілком задовільна відповідь.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.