Целесообразно ли добавлять задачи в BlockingQueue ThreadPoolExecutor?

JavaDoc для ThreadPoolExecutor неясно, допустимо ли добавлять задачи непосредственно в BlockingQueue минусовки исполнителя. врачи говорят вызов executor.getQueue() "предназначен в первую очередь для отладки и мониторинга".

я строю ThreadPoolExecutor своими BlockingQueue. Я сохраняю ссылку на очередь, чтобы добавлять в нее задачи напрямую. Та же очередь возвращается getQueue() поэтому я предполагаю предостережение в getQueue() применяется к a ссылка на резервную очередь, полученную с помощью my means.

пример

общий шаблон кода:

int n = ...; // number of threads
queue = new ArrayBlockingQueue<Runnable>(queueSize);
executor = new ThreadPoolExecutor(n, n, 1, TimeUnit.HOURS, queue);
executor.prestartAllCoreThreads();
// ...
while (...) {
    Runnable job = ...;
    queue.offer(job, 1, TimeUnit.HOURS);
}
while (jobsOutstanding.get() != 0) {
    try {
        Thread.sleep(...);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
executor.shutdownNow();

queue.offer() vs executor.execute()

как я понимаю, типичным использованием является добавление задач через executor.execute(). Подход в моем примере выше имеет преимущество блокировки в очереди, тогда как execute() сбой немедленно, если очередь заполнена и отклоняет мою задачу. Мне также нравится, что отправка заданий взаимодействует с блокирующей очередью; это чувствует себя более "чистым" производителем-потребителем для меня.

подразумевается добавление задач непосредственно в очередь: я должен позвонить prestartAllCoreThreads() иначе нет рабочих потоков. Предполагая отсутствие других взаимодействий с исполнителем, ничто не будет контролировать очередь (проверка ThreadPoolExecutor источник это подтверждает). Это также подразумевает для прямого запроса, что ThreadPoolExecutor должен быть дополнительно настроен для > 0 основных потоков и не должен быть настроен для разрешения основных потоков перерыв.

tl; dr

дали ThreadPoolExecutor настроить следующим образом:

  • основные темы > 0
  • основные потоки не допускаются к таймауту
  • резьбы сердечника prestarted
  • держите ссылку на BlockingQueue минусовки исполнителя

допустимо ли добавлять задачи непосредственно в очередь вместо вызова executor.execute()?

по теме

этот вопрос ( очереди работы производителя/потребителя) аналогичен, но специально не охватывает добавление в очередь напрямую.

5 ответов


если бы это был я, я бы предпочел, используя Executor#execute() над Queue#offer(), просто потому, что я использую все остальное от java.util.concurrent уже.

ваш вопрос хороший, и он вызвал мой интерес, поэтому я взглянул на источник для ThreadPoolExecutor#execute():

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        if (runState == RUNNING && workQueue.offer(command)) {
            if (runState != RUNNING || poolSize == 0)
                ensureQueuedTaskHandled(command);
        }
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command); // is shutdown or saturated
    }
}

мы видим, что execute сам вызывает offer() на очереди работы, но не перед тем, как делать некоторые приятные, вкусные манипуляции с бассейном, если это необходимо. По этой причине, я думаю, что было бы целесообразно использовать execute(); не используя это может (хотя я не знаю наверняка) привести к тому, что пул будет работать неоптимальным образом. Однако, я не думаю, что использование offer() будет перерыв исполнитель-похоже, задачи вытаскиваются из очереди, используя следующее (также из ThreadPoolExecutor):

Runnable getTask() {
    for (;;) {
        try {
            int state = runState;
            if (state > SHUTDOWN)
                return null;
            Runnable r;
            if (state == SHUTDOWN)  // Help drain queue
                r = workQueue.poll();
            else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
                r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
            else
                r = workQueue.take();
            if (r != null)
                return r;
            if (workerCanExit()) {
                if (runState >= SHUTDOWN) // Wake up others
                    interruptIdleWorkers();
                return null;
            }
            // Else retry
        } catch (InterruptedException ie) {
            // On interruption, re-check runState
        }
    }
}

этой getTask() метод просто вызывается из цикла, поэтому, если исполнитель не завершает работу, он будет блокировать, пока новая задача не будет передана очереди (независимо от того, откуда она пришла от.)

Примечание: хотя я разместил фрагменты кода из исходного кода здесь, мы не можем полагаться на них для окончательного ответа - мы должны только кодировать API. Мы не знаем, как реализация execute() будет меняться с течением времени.


один трюк-реализовать пользовательский подкласс ArrayBlockingQueue и переопределить метод offer() для вызова вашей блокирующей версии, тогда вы все равно можете использовать обычный путь кода.

queue = new ArrayBlockingQueue<Runnable>(queueSize) {
  @Override public boolean offer(Runnable runnable) {
    try {
      return offer(runnable, 1, TimeUnit.HOURS);
    } catch(InterruptedException e) {
      // return interrupt status to caller
      Thread.currentThread().interrupt();
    }
    return false;
  }
};

(Как вы, вероятно, догадываетесь, я думаю, что вызов предложения непосредственно в очереди, поскольку ваш обычный путь кода, вероятно, плохая идея).


фактически можно настроить поведение пула, когда очередь заполнена, указав RejectedExecutionHandler при создании экземпляра. ThreadPoolExecutor определяет четыре политики как внутренние классы, включая AbortPolicy, DiscardOldestPolicy, DiscardPolicy, а также мой личный фаворит, CallerRunsPolicy, который запускает новое задание в управляющем потоке.

например:

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
        nproc, // core size
        nproc, // max size
        60, // idle timeout
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<Runnable>(4096, true), // Fairness = true guarantees FIFO
        new ThreadPoolExecutor.CallerRunsPolicy() ); // If we have to reject a task, run it in the calling thread.

поведение в вопрос можно получить, используя что-то вроде:

public class BlockingPolicy implements RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        executor.getQueue.put(r); // Self contained, no queue reference needed.
    }

в какой-то момент очереди должны быть доступны. Лучшее место для этого-автономный RejectedExecutionHandler, который сохраняет любое дублирование кода или потенциальные ошибки, возникающие при прямом манипулировании очередью в области объекта пула. Обратите внимание, что обработчики включены в ThreadPoolExecutor сами использовать getQueue().


это очень важный вопрос, если очередь, которую вы используете, полностью отличается от стандартной реализации в памяти LinkedBlockingQueue или ArrayBlockingQueue.

например, если вы реализуете шаблон производитель-потребитель, используя несколько производителей на разных машинах, и используете механизм очереди, основанный на отдельной подсистеме персистентности (например, Redis), тогда вопрос становится актуальным сам по себе, даже если вы не хотите блокировки offer() как OP.

так дан ответ, что prestartAllCoreThreads() должен быть вызван (или достаточно раз prestartCoreThread()) для рабочих потоков, которые будут доступны и запущены, достаточно важно подчеркнуть.


при необходимости мы также можем использовать парковку, которая отделяет основную обработку от отклоненных задач -

    final CountDownLatch taskCounter = new CountDownLatch(TASKCOUNT);
    final List<Runnable> taskParking = new LinkedList<Runnable>();
    BlockingQueue<Runnable> taskPool = new ArrayBlockingQueue<Runnable>(1);
    RejectedExecutionHandler rejectionHandler = new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.err.println(Thread.currentThread().getName() + " -->rejection reported - adding to parking lot " + r);
            taskCounter.countDown();
            taskParking.add(r);
        }
    };
    ThreadPoolExecutor threadPoolExecutor =  new ThreadPoolExecutor(5, 10, 1000, TimeUnit.SECONDS, taskPool, rejectionHandler);
    for(int i=0 ; i<TASKCOUNT; i++){
        //main 
        threadPoolExecutor.submit(getRandomTask());
    }
    taskCounter.await(TASKCOUNT * 5 , TimeUnit.SECONDS);
    System.out.println("Checking the parking lot..." + taskParking);
    while(taskParking.size() > 0){
        Runnable r = taskParking.remove(0);
        System.out.println("Running from parking lot..." + r);
        if(taskParking.size() > LIMIT){
          waitForSometime(...);
        }
        threadPoolExecutor.submit(r);
    }
    threadPoolExecutor.shutdown();