Java: Belirli bir kuyruk boyutundan sonra gönderimi engelleyen ExecutorService


85

Tek bir iş parçacığının paralel olarak gerçekleştirilebilen G / Ç yoğun görevler ürettiği bir çözümü kodlamaya çalışıyorum. Her görevin önemli bellek içi verileri vardır. Bu yüzden, bir anda bekleyen görevlerin sayısını sınırlayabilmek istiyorum.

ThreadPoolExecutor'ı şu şekilde oluşturursam:

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

Ardından, sıra dolduğunda ve tüm iş parçacıkları meşgul olduğunda executor.submit(callable)atar RejectedExecutionException.

executor.submit(callable)Kuyruk dolu ve tüm iş parçacıkları meşgul olduğunda blok yapmak için ne yapabilirim ?

DÜZENLEME : Denedim bu :

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

Ve benim elde etmek istediğim etkiyi bir şekilde elde ediyor, ancak uygun olmayan bir şekilde (temelde reddedilen iş parçacıkları çağıran iş parçacığında çalıştırılır, bu nedenle bu, çağıran iş parçacığının daha fazlasını göndermesini engeller).

DÜZENLEME: (soruyu sorduktan 5 yıl sonra)

Bu soruyu ve cevaplarını okuyan herkese, lütfen kabul edilen yanıtı tek bir doğru çözüm olarak almayın. Lütfen tüm cevapları ve yorumları okuyun.



1
Tam olarak bunu yapmak için daha önce bir Semafor kullandım, tıpkı @axtavt ile bağlantılı çok benzer sorunun cevabında olduğu gibi.
Stephen Denne

2
@TomWolk Öncelikle, numWorkerThreadsarayan iş parçacığının aynı zamanda bir görevi yürüttüğünden daha paralel olarak yürütülen bir görev daha elde edersiniz . Ancak, daha önemli olan husus, arayan iş parçacığı uzun süre çalışan bir görev alırsa, diğer iş parçacıkları bir sonraki görevi beklerken boşta kalabilir.
Tahir Akhtar

2
@TahirAkhtar, doğru; Kuyruk yeterince uzun olmalıdır, böylece arayan kişi kendi görevini yerine getirmek zorunda kaldığında kuru çalışmaz. Ancak, görevleri yürütmek için bir iş parçacığı daha, arayan iş parçacığı kullanılabilirse bunun bir avantaj olduğunu düşünüyorum. Arayan kişi bloke ederse, arayanın ileti dizisi boşta kalır. CallerRunsPolicy'yi threadpool'un kapasitesinin üç katı bir kuyrukla kullanıyorum ve güzel ve sorunsuz çalışıyor. Bu çözüme kıyasla, çerçevenin aşırı mühendisliğine uymayı düşünürdüm.
TomWolk

1
@TomWalk +1 İyi puan. Görünüşe göre başka bir fark, görev kuyruktan reddedilmişse ve arayan iş parçacığı tarafından çalıştırılmışsa, arayan iş parçacığı, kuyruktaki sırasını beklemediği için bir isteği sıra dışı işlemeye başlayacaktır. Elbette, zaten konuları kullanmayı seçtiyseniz, tüm bağımlılıkları doğru bir şekilde ele almalısınız, ancak akılda tutulması gereken bir şey.
rimsky

Yanıtlar:


64

Ben de aynı şeyi yaptım. İşin püf noktası, offer () yönteminin gerçekten bir put () olduğu bir BlockingQueue oluşturmaktır. (İstediğiniz temel BlockingQueue uygulamasını kullanabilirsiniz).

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;
    }

}

Bunun yalnızca iş parçacığı havuzu için işe yaradığını unutmayın, burada corePoolSize==maxPoolSizeçok dikkatli olun (yorumlara bakın).


2
alternatif olarak, arabelleğe almayı önlemek için SynchronousQueue'yu genişletebilir ve yalnızca doğrudan el değiştirmelere izin verebilirsiniz.
brendon

Zarif ve doğrudan soruna hitap eder. offer (), put () olur ve put (), "... gerekirse alanın kullanılabilir olmasını beklemek" anlamına gelir
Trenton

5
Teklif yönteminin protokolünü değiştirdiği için bunun iyi bir fikir olduğunu düşünmüyorum. Teklif yöntemi, engelleyici olmayan bir çağrı olmalıdır.
Mingjiang Shi

6
Katılmıyorum - bu, ThreadPoolExecutor.execute'un davranışını, eğer bir corePoolSize <maxPoolSize değerine sahipseniz, ThreadPoolExecutor mantığı hiçbir zaman çekirdeğin ötesine ek işçi eklemeyecek şekilde değiştirir.
Krease

5
Açıklığa kavuşturmak gerekirse - çözümünüz ancak kısıtlamayı koruduğunuz sürece çalışır corePoolSize==maxPoolSize. Bu olmadan, artık ThreadPoolExecutor'un tasarlanmış davranışa sahip olmasına izin vermez. Bu kısıtlamaya sahip olmayan bu soruna bir çözüm arıyordum; Aldığımız yaklaşım için aşağıdaki alternatif cevabıma bakın.
Krease

15

İşte sonunda bunu nasıl çözdüm:

(not: bu çözüm, Çağrılabilir'i gönderen iş parçacığını engeller, bu nedenle RejectExecutionException'ın atılmasını engeller)

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
Bunun corePoolSize < maxPoolSize...: |
rogerdpack

1
Durum için çalışıyor corePoolSize < maxPoolSize. Bu durumlarda, semafor kullanılabilir olacak, ancak bir iş parçacığı SynchronousQueueolmayacak ve yanlış döndürecektir. Daha ThreadPoolExecutorsonra yeni bir iplik döndürür. Bu çözümün sorunu, bir yarış durumuna sahip olmasıdır . Sonrasında semaphore.release(), ancak iş parçacığı bitmeden önce execute, submit () semafor iznini alacaktır. EĞER super.submit () önce çalıştırılan execute()bittikten iş reddedilecektir.
Luís Guilherme

@ LuísGuil Hernández Ancak semaphore.release () , iş parçacığı yürütmeyi bitirmeden önce asla çağrılmaz . Çünkü bu çağrı after Execute (...) yönteminde yapılır. Açıkladığınız senaryoda bir şey mi eksik?
cvacca

1
afterExecute, görevi çalıştıran aynı iş parçacığı tarafından çağrıldığından, henüz tamamlanmamıştır. Testi kendiniz yapın. Bu çözümü uygulayın ve uygulayıcıya büyük miktarda iş verin, iş reddedilirse fırlatın. Evet, bunun bir yarış durumu olduğunu ve onu yeniden üretmenin zor olmadığını fark edeceksiniz.
Luís Guilherme

1
ThreadPoolExecutor'a gidin ve runWorker (Worker w) yöntemini kontrol edin. Çalışanın kilidinin açılması ve tamamlanan görevlerin sayısının artması dahil olmak üzere, her şeyin Yürütme bittikten sonra gerçekleştiğini göreceksiniz. Böylece, görevlerin (semaforu serbest bırakarak) onları işlemek için bant genişliği olmadan (processWorkerExit'i çağırarak) gelmesine izin verdiniz.
Luís Guilherme

14

Şu anda kabul edilen cevabın potansiyel olarak önemli bir sorunu vardır - bu, ThreadPoolExecutor.execute'un davranışını değiştirir, öyle ki bir a sahipseniz corePoolSize < maxPoolSize, ThreadPoolExecutor mantığı çekirdeğin ötesine asla ek işçi eklemeyecektir.

Gönderen 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);

Spesifik olarak, bu son 'else' bloğu asla vurulmayacaktır.

Daha iyi bir alternatif, OP'nin halihazırda yaptığı şeye benzer bir şey yapmaktır - aynı mantığı yapmak için bir RedExecutionHandler kullanın 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);
    }
}

Yorumlarda belirtildiği gibi ( bu yanıta atıfta bulunularak) bu yaklaşımda dikkat edilmesi gereken bazı şeyler vardır :

  1. Eğer corePoolSize==0, o zaman havuzdaki tüm iş parçacıklarının görev görünür olmadan önce ölebileceği bir yarış koşulu varsa
  2. Kuyruk görevlerini saran (uygulanamaz ThreadPoolExecutor) bir uygulama kullanmak , işleyici de aynı şekilde sarmadığı sürece sorunlara neden olacaktır.

Bu kavramları akılda tutarak, bu çözüm çoğu tipik ThreadPoolExecutors için işe yarayacak ve durumu düzgün bir şekilde ele alacaktır corePoolSize < maxPoolSize.


Kime olumsuz oy verdiyse - biraz fikir verebilir misiniz? Bu cevapta yanlış / yanıltıcı / tehlikeli bir şey var mı? Endişelerinizi gidermek için bir fırsat istiyorum.
Krease

2
Olumsuz oy vermedim, ancak bu çok kötü bir fikir
vanOekel

@vanOekel - bağlantı için teşekkürler - bu yanıt, bu yaklaşımı kullanıyorsanız bilinmesi gereken bazı geçerli durumları ortaya çıkarır, ancak IMO onu "çok kötü bir fikir" yapmaz - halen kabul edilen yanıtta bulunan bir sorunu çözer. Cevabımı bu uyarılarla güncelledim.
Krease

Çekirdek havuz boyutu 0 ise ve görev yürütücüye gönderilmişse, yürütme görevini yerine getirmek için kuyruk doluysa yürütme iş parçacıkları oluşturmaya başlayacaktır. Öyleyse neden kilitlenmeye meyillidir? Ne demek istediğini anlamadım. Biraz detaylandırır mısınız?
Farhan Shirgill Ansari

@ShirgillFarhanAnsari - önceki yorumda dile getirilen durum. Bu, doğrudan kuyruğa ekleme işlemlerinin oluşturulmasını / çalışanların başlatılmasını tetiklemediği için olabilir. Sıfır olmayan bir çekirdek havuz boyutuna sahip olarak hafifletilebilecek bir uç durum / yarış durumu
Krease

4

Bunun eski bir soru olduğunu biliyorum, ancak benzer bir sorunla yeni görevler oluşturmanın çok hızlı olması ve çok fazla sayıda OutOfMemoryError oluşmasının nedeni mevcut görevin yeterince hızlı tamamlanmamasıydı.

Benim durumumda Callablesgönderilir ve sonuca ihtiyacım var, bu nedenle tüm Futuresiade edilenleri saklamam gerekiyor executor.submit(). Benim çözümüm, maksimum boyutta bir Futuresiçine koymaktı BlockingQueue. Bu sıra dolduktan sonra, bazıları tamamlanana kadar başka görev oluşturulmaz (öğeler kuyruktan kaldırılır). Sözde kodda:

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

Benzer bir sorunu yaşadım ve bunu beforeExecute/afterExecutekancaları kullanarak uyguladım 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();
        }
    }
}

Bu senin için yeterince iyi olmalı. Btw, orijinal uygulama görev boyutuna dayanıyordu çünkü bir görev diğerinden 100 kez daha büyük olabilirdi ve iki büyük görev göndermek kutuyu öldürüyordu, ancak birini büyük ve çok sayıda küçük çalıştırmak tamamdı. G / Ç yoğun görevleriniz kabaca aynı boyuttaysa, bu sınıfı kullanabilirsiniz, aksi takdirde bana bildirin ve boyuta dayalı uygulamaları yayınlayacağım.

PS Kontrol etmek istersiniz ThreadPoolExecutor javadoc'u . Doug Lea'nın nasıl kolayca özelleştirilebileceğiyle ilgili çok güzel bir kullanım kılavuzu.


1
Bir İş parçacığı, Execute () öncesinde kilidi tuttuğunda ve bunu görüp durumu maxTaskCount < currentTaskCountbeklemeye başladığında ne olacağını merak ediyorum unpaused. Aynı zamanda, başka bir iş parçacığı, bir görevin tamamlandığını belirtmek için afterExecute () 'deki kilidi edinmeye çalışır. Bir çıkmaz olmayacak mı?
Tahir Akhtar

1
Ayrıca bu çözümün, kuyruk dolduğunda görevleri gönderen iş parçacığını engellemeyeceğini de fark ettim. Yani RejectedExecutionExceptionyine de mümkündür.
Tahir Akhtar

1
ReentrantLock / Condition sınıflarının anlamsallığı, senkronize edilmiş ve bekle / bildirimin sağladığına benzer. Durum bekleme yöntemleri çağrıldığında kilit serbest bırakılır, bu nedenle kilitlenme olmaz.
Petro Semeniuk

Doğru, bu ExecutorService, arayan iş parçacığını engellemeden gönderimdeki görevleri engeller. İş henüz gönderiliyor ve bunun için yeterli sistem kaynağı olduğunda zaman uyumsuz olarak işlenecek.
Petro Semeniuk

2

Dekoratör modelini izleyen ve yürütülen görevlerin sayısını kontrol etmek için bir semafor kullanan bir çözüm uyguladım. Herhangi biriyle kullanabilirsiniz Executorve:

  • Maksimum devam eden görevi belirtin
  • Bir görev yürütme izni için beklenecek maksimum zaman aşımını belirtin (zaman aşımı geçerse ve herhangi bir izin alınmazsa, a RejectedExecutionExceptionatılır)
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

ArrayBlockingQueueAa yerine a kullanmak kadar basit olduğunu düşünüyorum LinkedBlockingQueue.

Beni görmezden gel ... bu tamamen yanlış. ihtiyaç duyduğunuz etkiye sahip olmayan ThreadPoolExecutoraramalar .Queue#offerput

Sen uzanabilir ThreadPoolExecutorve bir uygulamasını sağlamak execute(Runnable)olduğunu aramaların putyerine offer.

Korkarım bu tamamen tatmin edici bir cevap gibi görünmüyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.