Java ThreadPoolExecutor: Çekirdek havuz boyutunu güncellemek, gelen görevleri aralıklı olarak dinamik olarak reddeder


13

ThreadPoolExecutorHavuz oluşturulduktan sonra bir çekirdek havuz boyutunu farklı bir sayıya yeniden boyutlandırmaya çalışırsam , o zaman aralıklı olarak, bazı görevler RejectedExecutionExceptionhiçbir zaman birden fazla queueSize + maxPoolSizegörev göndermeme rağmen reddedildiği bir sorunla karşılaşıyorum .

Çözmeye çalıştığım sorun ThreadPoolExecutor, iş parçacığı havuzu kuyruğunda oturan bekleyen yürütmelere dayalı olarak çekirdek iş parçacıklarını yeniden boyutlandırmak genişletmektir . Varsayılan olarak bir sadece sıra dolu olduğunda ThreadPoolExecutoryeni bir yaratacaktır çünkü buna ihtiyacım var Thread.

İşte sorunu gösteren küçük bir bağımsız Pure Java 8 programı.

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolResizeTest {

    public static void main(String[] args) throws Exception {
        // increase the number of iterations if unable to reproduce
        // for me 100 iterations have been enough
        int numberOfExecutions = 100;

        for (int i = 1; i <= numberOfExecutions; i++) {
            executeOnce();
        }
    }

    private static void executeOnce() throws Exception {
        int minThreads = 1;
        int maxThreads = 5;
        int queueCapacity = 10;

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                minThreads, maxThreads,
                0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(queueCapacity),
                new ThreadPoolExecutor.AbortPolicy()
        );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(() -> resizeThreadPool(pool, minThreads, maxThreads),
                0, 10, TimeUnit.MILLISECONDS);
        CompletableFuture<Void> taskBlocker = new CompletableFuture<>();

        try {
            int totalTasksToSubmit = queueCapacity + maxThreads;

            for (int i = 1; i <= totalTasksToSubmit; i++) {
                // following line sometimes throws a RejectedExecutionException
                pool.submit(() -> {
                    // block the thread and prevent it from completing the task
                    taskBlocker.join();
                });
                // Thread.sleep(10); //enabling even a small sleep makes the problem go away
            }
        } finally {
            taskBlocker.complete(null);
            scheduler.shutdown();
            pool.shutdown();
        }
    }

    /**
     * Resize the thread pool if the number of pending tasks are non-zero.
     */
    private static void resizeThreadPool(ThreadPoolExecutor pool, int minThreads, int maxThreads) {
        int pendingExecutions = pool.getQueue().size();
        int approximateRunningExecutions = pool.getActiveCount();

        /*
         * New core thread count should be the sum of pending and currently executing tasks
         * with an upper bound of maxThreads and a lower bound of minThreads.
         */
        int newThreadCount = min(maxThreads, max(minThreads, pendingExecutions + approximateRunningExecutions));

        pool.setCorePoolSize(newThreadCount);
        pool.prestartAllCoreThreads();
    }
}

QueueCapacity + maxThreads öğesinden daha fazlasını hiç göndermezsem neden havuz hiç RejectedExecutionException özel durumu oluşturmalıdır. Ben max iş parçacığı asla ThreadPoolExecutor'ın tanımı ile değiştirmiyorum, ya bir iş parçacığı ya da kuyruk görev uygun olmalıdır.

Tabii ki, ben asla havuzu yeniden boyutlandırmak, o zaman iş parçacığı havuzu herhangi bir başvuru reddetmez. Bu ayrıca hata ayıklamak zordur çünkü gönderilere herhangi bir gecikme eklemek sorunu ortadan kaldırır.

RejectedExecutionException düzeltmek için herhangi bir işaretçiler?


Neden ExecutorServiceyeniden boyutlandırma nedeniyle gönderilemeyen görevleri yeniden gönderen mevcut bir paketi sararak kendi uygulamanızı sağlamıyorsunuz ?
daniu

@daniu bu geçici bir çözümdür. Soruların amacı, hiç queueCapacity + maxThreads daha fazla gönderirseniz havuz neden bir RejectedExecutionException atmak gerekir. Ben max iş parçacığı asla ThreadPoolExecutor'ın tanımı ile değiştirmiyorum, ya bir iş parçacığı ya da kuyruk görev uygun olmalıdır.
Swaranga Sarma

Tamam, sorunuzun yanlış anlaşıldığı anlaşılıyor. Bu ne? Davranışın neden gerçekleştiğini veya bu sorunu nasıl çözdüğünüzü sizin için sorunlara neden olduğunu bilmek ister misiniz?
daniu

Evet, kodumun çoğu ThreadPoolExecutor'a başvurduğundan uygulamamı bir yönetici hizmetine değiştirmek mümkün değildir. Yine de yeniden boyutlandırılabilir bir ThreadPoolExecutor olmasını istedim, nasıl düzeltebilirim bilmek gerekir. Bunun gibi bir şey yapmanın doğru yolu, ThreadPoolExecutor'u genişletmek ve korumalı değişkenlerinden bazılarına erişmek ve süper sınıf tarafından paylaşılan bir kilit üzerindeki senkronize blok içindeki havuz boyutunu güncellemektir.
Swaranga Sarma

Genişletme ThreadPoolExecutormuhtemelen kötü bir fikirdir ve bu durumda da mevcut kodu değiştirmeniz gerekmez mi? Gerçek kodunuzun yürütücüye nasıl eriştiğine dair bir örnek vermeniz iyi olur. Eğer özel ThreadPoolExecutor(yani değil ExecutorService) için birçok yöntem kullandıysanız şaşırırdım .
daniu

Yanıtlar:


5

İşte bunun neden olduğu bir senaryo:

Örneğimde, minThreads = 0, maxThreads = 2 ve queueCapacity = 2'yi kısaltmak için kullanıyorum. İlk komut gönderilir, bu yürütme yönteminde yapılır:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    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);
}

Bu komut için addWorker'dan (null, false) workQueue.offer (command) yürütülür. İşçi iş parçacığı önce bu komutu iş parçacığı çalıştırma yöntemindeki kuyruktan çıkarır, bu nedenle kuyrukta hala bir komut vardır,

İkinci komut workQueue.offer (komut) yürütüldüğünde gönderilir. Şimdi sıra dolu

Şimdi ScheduledExecutorService, maxThreads ile setCorePoolSize öğesini çağıran resizeThreadPool yöntemini yürütür. SetCorePoolSize yöntemi şöyledir:

 public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

Bu yöntem addWorker (null, true) kullanarak bir işçi ekler. Hayır çalışan 2 işçi kuyruğu var, maksimum ve kuyruk dolu.

Üçüncü komut gönderilir ve başarısız olur, çünkü workQueue.offer (command) ve addWorker (command, false) başarısız olur, bu da İstisnaya yol açar:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@24c22fe rejected from java.util.concurrent.ThreadPoolExecutor@cd1e646[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at ThreadPoolResizeTest.executeOnce(ThreadPoolResizeTest.java:60)
at ThreadPoolResizeTest.runTest(ThreadPoolResizeTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:69)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:48)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:292)
at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:365)

Ben bu sorunu çözmek için kuyruk kapasitesini yürütmek istediğiniz komutların maksimum olarak ayarlamanız gerektiğini düşünüyorum.


Doğru. Kodu kendi sınıfıma kopyalayarak ve kaydediciler ekleyerek çoğaltabildim. Temel olarak, kuyruk dolduğunda ve yeni bir görev gönderdiğimde, yeni bir Çalışan oluşturmaya çalışacaktır. Bu arada, yeniden boyutlandırıcım da setCorePoolSize öğesini 2 olarak çağırırsa, bu da yeni bir İşçi yaratır. Bu noktada, iki İşçi eklenmek için yarışıyor, ancak her ikisi de, maksimum havuz boyutu kısıtlamasını ihlal edeceği için yeni görev gönderimi reddedildiği için olamaz. Bence bu bir yarış koşulu ve OpenJDK'ya bir hata raporu verdim. Bakalım. Ama soruma cevap verdin, böylece ödül kazan. Teşekkür ederim.
Swaranga Sarma

2

Bunun hata olarak nitelenip nitelenmediğinden emin değilim. Bu, sıra dolduktan sonra ek işçi iş parçacığı oluşturulduğunda, ancak bu, arayanın reddedilen görevlerle uğraşması gereken java belgelerinde belirtildiği bir davranıştır.

Java belgeleri

Yeni dişler için fabrika. Tüm iş parçacıkları bu fabrika kullanılarak oluşturulur (addWorker yöntemi ile). Tüm arayanlar, bir sistemi veya kullanıcının iş parçacığı sayısını sınırlayan politikasını yansıtabilecek şekilde addWorker'ın başarısız olmasına hazırlıklı olmalıdır. Hata olarak ele alınmasa da, iş parçacığı oluşturulamaması yeni görevlerin reddedilmesine veya mevcut görevlerin kuyrukta kalmasına neden olabilir.

Çekirdek havuz boyutunu yeniden boyutlandırdığınızda, diyelim ki artış, ek çalışanlar oluşturulur ( addWorkeryöntem içinde setCorePoolSize) ve yeterli ek çalışan zaten olduğu gibi yanlış ( son kod snippet'i) döndürdüğünde , ek iş ( addWorkeryöntemden execute) oluşturma çağrısı reddedilir tarafından oluşturulan ancak henüz sıradaki güncellemeyi yansıtmayacak şekilde çalıştırılmadı .addWorkeradd WorkersetCorePoolSize

İlgili parçalar

Karşılaştırmak

public void setCorePoolSize(int corePoolSize) {
    ....
    int k = Math.min(delta, workQueue.size());
    while (k-- > 0 && addWorker(null, true)) {
        if (workQueue.isEmpty())
             break;
    }
}

public void execute(Runnable command) {
    ...
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    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);
}

private boolean addWorker(Runnable firstTask, boolean core) {
....
   if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
     return false;             
}

Özel yeniden deneme reddetme yürütme işleyicisi kullanın (Maksimum havuz boyutu olarak üst sınırınız olduğu için durumunuz için çalışmalıdır). Lütfen gerektiği şekilde ayarlayın.

public static class RetryRejectionPolicy implements RejectedExecutionHandler {
    public RetryRejectionPolicy () {}

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
           while(true)
            if(e.getQueue().offer(r)) break;
        }
    }
}

ThreadPoolExecutor pool = new ThreadPoolExecutor(
      minThreads, maxThreads,
      0, TimeUnit.SECONDS,
      new LinkedBlockingQueue<Runnable>(queueCapacity),
      new ThreadPoolResizeTest.RetryRejectionPolicy()
 );

Ayrıca, kapatma görevinizin doğru olmadığına dikkat edin, çünkü bu, gönderilen görevin yürütülmesini tamamlamasını beklemez, bununla birlikte awaitTerminationkullanılır.


JavaDoc'a göre shutdown zaten gönderilen görevleri beklediğini düşünüyorum: shutdown () Daha önce gönderilen görevlerin yürütüldüğü düzenli bir kapatma başlatır, ancak yeni görevler kabul edilmez.
Thomas Krieger

@ThomasKrieger - Zaten gönderilen görevleri yürütecek ancak bitirmelerini beklemeyecek - docs.oracle.com/javase/7/docs/api/java/util/concurrent/… adresinden - Bu yöntem daha önce gönderilenleri beklemez yürütmeyi tamamlamak için görevler. Bunu yapmak için awaitTermination kullanın.
Sagar Veeram
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.