Boyut sınırı olan bir önbelleğe alınmış iş parçacığı havuzu yapmak imkansız mı?


127

Önbelleğe alınmış bir iş parçacığı havuzunun oluşturabileceği iş parçacığı sayısı sınırıyla yapmak imkansız gibi görünüyor.

Statik Executors.newCachedThreadPool, standart Java kitaplığında şu şekilde uygulanır:

 public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Dolayısıyla, bu şablonu kullanarak sabit boyutlu bir önbelleğe alınmış iş parçacığı havuzu oluşturmaya devam edin:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronusQueue<Runable>());

Şimdi bunu kullanır ve 3 görev gönderirseniz, her şey yoluna girecek. Başka görevlerin gönderilmesi, yürütme istisnalarının reddedilmesine neden olacaktır.

Bunu denemek:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runable>());

Tüm iş parçacıklarının sırayla yürütülmesine neden olur. Yani, iş parçacığı havuzu görevlerinizi yerine getirmek için asla birden fazla iş parçacığı oluşturmaz.

Bu, ThreadPoolExecutor yürütme yönteminde bir hata mı? Ya da belki bu kasıtlı mı? Yoksa başka bir yolu var mı?

Düzenleme: Tam olarak önbelleğe alınmış iş parçacığı havuzu gibi bir şey istiyorum (istek üzerine iş parçacıkları oluşturur ve daha sonra bir zaman aşımından sonra onları öldürür), ancak oluşturabileceği iş parçacığı sayısında bir sınır ve bir kez ek görevleri sıraya koymaya devam etme yeteneği ile iş parçacığı sınırına ulaştı. Sjlee'nin cevabına göre bu imkansız. ThreadPoolExecutor'ın execute () yöntemine bakıldığında bu gerçekten imkansızdır. ThreadPoolExecutor alt sınıfına girmem ve bir şekilde SwingWorker'ın yaptığı gibi execute () 'yi geçersiz kılmam gerekir, ancak SwingWorker'ın execute ()' de yaptığı tam bir hack'tir.


1
Sorun nedir? 2. kod örneğiniz başlığınızın cevabı değil mi?
rsp

4
Görev sayısı arttıkça talep üzerine iş parçacığı ekleyecek, ancak hiçbir zaman maksimum iş parçacığı sayısından fazlasını eklemeyecek bir iş parçacığı havuzu istiyorum. CachedThreadPool bunu zaten yapıyor, ancak sınırsız sayıda iş parçacığı ekleyecek ve önceden tanımlanmış bir boyutta durmayacak. Örneklerde tanımladığım boyut 3'tür. İkinci örnek 1 iş parçacığı ekler, ancak diğer görevler henüz tamamlanmamışken yeni görevler geldiğinde iki tane daha eklemez.
Matt Crinklaw-Vogt

Bunu kontrol edin, çözer, hata
ayıklayarakisfun.blogspot.com/2012/05/…

Yanıtlar:


235

ThreadPoolExecutor aşağıdaki birkaç temel davranışa sahiptir ve sorunlarınız bu davranışlarla açıklanabilir.

Görevler gönderildiğinde,

  1. İş parçacığı havuzu çekirdek boyutuna ulaşmadıysa, yeni iş parçacıkları oluşturur.
  2. Çekirdek boyutuna ulaşılmışsa ve boşta kalan iş parçacığı yoksa, görevleri sıraya koyar.
  3. Çekirdek boyutuna ulaşılmışsa, boşta kalan iş parçacığı yoktur ve kuyruk dolarsa, yeni iş parçacıkları oluşturur (maksimum boyuta ulaşana kadar).
  4. Maksimum boyuta ulaşılmışsa, boşta kalan iş parçacığı yoktur ve kuyruk dolarsa, reddetme ilkesi devreye girer.

İlk örnekte, SynchronousQueue'nun esasen 0 boyutuna sahip olduğuna dikkat edin. Bu nedenle, maksimum boyuta (3) ulaştığınız anda, reddetme politikası devreye girer (# 4).

İkinci örnekte, seçilen kuyruk, sınırsız boyuta sahip bir LinkedBlockingQueue'dur. Bu nedenle, 2. davranışta sıkışıp kalıyorsunuz.

Davranışları neredeyse tamamen belirlendiğinden, önbelleğe alınmış türle veya sabit türle gerçekten fazla bir şey yapamazsınız.

Sınırlı ve dinamik bir iş parçacığı havuzuna sahip olmak istiyorsanız, pozitif bir çekirdek boyutu ve sonlu boyutlu bir kuyrukla birlikte maksimum boyut kullanmanız gerekir. Örneğin,

new ThreadPoolExecutor(10, // core size
    50, // max size
    10*60, // idle timeout
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<Runnable>(20)); // queue with a size

Ek : Bu oldukça eski bir cevaptır ve çekirdek boyutu 0 olduğunda JDK'nın davranışını değiştirdiği görülmektedir. JDK 1.6'dan beri, çekirdek boyutu 0 ise ve havuzda herhangi bir iş parçacığı yoksa, ThreadPoolExecutor bir bu görevi yürütmek için iş parçacığı. Bu nedenle, 0 çekirdek boyutu yukarıdaki kuralın bir istisnasıdır. Teşekkürler Steve için getiren dikkatimi söyledi.


4
allowCoreThreadTimeOutBu cevabı mükemmel hale getirmek için yöntem hakkında birkaç kelime yazmalısınız . @ User1046052'nin cevabına bakın
hsestupin

1
Mükemmel cevap! Eklemek için sadece bir nokta: Diğer ret politikalarından da bahsetmeye değer. @Brianegge
Jeff

1
Davranış 2 ' maxThread boyutuna ulaşılmışsa ve boşta iş parçacığı yoksa, görevleri sıraya koyar ' dememelidir . ?
Zoltán

1
Sıranın büyüklüğünün ne anlama geldiğini ayrıntılarıyla açıklayabilir misiniz? Bu, reddedilmeden önce yalnızca 20 görevin sıraya alınabileceği anlamına mı geliyor?
Zoltán

1
@ Zoltán Bunu bir süre önce yazdım, bu yüzden o zamandan beri bazı davranışların değişme ihtimali var (en son etkinlikleri çok yakından takip etmedim), ancak bu davranışların değişmediğini varsayarsak, # 2 belirtildiği gibi doğrudur ve bu belki de bunun en önemli (ve biraz da şaşırtıcı) noktası. Çekirdek boyutuna ulaşıldığında, TPE yeni iş parçacığı oluşturmak yerine sıraya girmeyi tercih eder. Kuyruk boyutu, kelimenin tam anlamıyla TPE'ye iletilen kuyruğun boyutudur. Kuyruk dolarsa ancak maksimum boyuta ulaşmadıysa, yeni bir iş parçacığı oluşturur (görevleri reddetmez). 3 numaralı maddeye bakın. Umarım yardımcı olur.
sjlee

60

Bir şeyi kaçırmadıysam, asıl sorunun çözümü basittir. Aşağıdaki kod, orijinal gönderen tarafından açıklanan istenen davranışı uygular. Sınırsız bir kuyrukta çalışmak için 5 adede kadar iş parçacığı oluşturacak ve boştaki iş parçacıkları 60 saniye sonra sona erecektir.

tp = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>());
tp.allowCoreThreadTimeOut(true);

1
Haklısın. Bu yöntem jdk 1.6'ya eklendi, bu yüzden pek çok insanın bildiği kadar değil. ayrıca talihsiz bir "min" çekirdek havuz boyutuna sahip olamazsınız.
jtahlborn

4
Bu konudaki tek endişem (JDK 8 belgelerinden): "Yöntem çalıştırmada (Çalıştırılabilir) yeni bir görev gönderildiğinde ve corePoolSize iş parçacıklarından daha azı çalıştığında, diğer çalışan olsa bile isteği işlemek için yeni bir iş parçacığı oluşturulur. ileti dizileri boşta. "
veegee

Bunun gerçekten işe yaramadığından oldukça eminim. Yukarıdakileri en son yaptığımda, 5'i oluşturmanıza rağmen, işinizi sadece tek bir iş parçacığında çalıştırdığımda, yine, birkaç yıl oldu, ancak ThreadPoolExecutor uygulamasına girdiğimde, sıranız dolduğunda yeni iş parçacıklarına gönderiliyordu. Sınırsız bir kuyruk kullanmak bunun asla olmamasına neden olur. Çalışmayı göndererek ve iş parçacığı adını girdikten sonra uyuyarak test edebilirsiniz. Her çalıştırılabilir aynı adı yazdıracak / başka herhangi bir iş parçacığında çalıştırılmayacaktır.
Matt Crinklaw-Vogt

2
Bu işe yarıyor Matt. Çekirdek boyutunu 0 olarak ayarladınız, bu yüzden yalnızca 1 dişiniz vardı. Buradaki hile, çekirdek boyutunu maksimum boyuta ayarlamaktır.
T-Gergely

1
@vegee haklı - Bu aslında pek iyi çalışmıyor - ThreadPoolExecutor yalnızca corePoolSize üzerindeyken konuları yeniden kullanacak. Bu nedenle, corePoolSize, maxPoolSize'a eşit olduğunda, yalnızca havuzunuz dolu olduğunda iş parçacığı önbelleğe alma işleminden yararlanacaksınız (Yani, bunu kullanmayı amaçlıyorsanız ancak genellikle maksimum havuz boyutunuzun altında kalırsanız, iş parçacığı zaman aşımını da düşük bir değere düşürebilirsiniz. değer; ve önbelleğe alma olmadığının farkında olun - her zaman yeni konular)
Chris Riddell

7

Aynı sorun vardı. Başka hiçbir yanıt tüm sorunları bir araya getirmediğinden benimkini ekliyorum:

Artık dokümanlarda açıkça yazılmıştır : LinkedBlockingQueueMaksimum iş parçacığı ayarını engellemeyen ( ) bir kuyruk kullanırsanız , yalnızca çekirdek iş parçacıkları kullanılır.

yani:

public class MyExecutor extends ThreadPoolExecutor {

    public MyExecutor() {
        super(4, 4, 5,TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        allowCoreThreadTimeOut(true);
    }

    public void setThreads(int n){
        setMaximumPoolSize(Math.max(1, n));
        setCorePoolSize(Math.max(1, n));
    }

}

Bu uygulayıcıda:

  1. Sınırsız bir kuyruk kullandığımız için maksimum iş parçacığı kavramı yok. Bu iyi bir şey çünkü böyle bir kuyruk, yürütücünün olağan ilkesini izlerse çok sayıda çekirdek olmayan, ekstra iş parçacığı oluşturmasına neden olabilir.

  2. Maksimum boyutta bir sıra Integer.MAX_VALUE. bekleyen görevlerin sayısı aşarsa Submit()atar . Önce hafızamızın dolacağından emin değilim, yoksa bu olacak.RejectedExecutionExceptionInteger.MAX_VALUE

  3. Mümkün olan 4 çekirdek dişe sahiptir. Boşta çekirdek iş parçacıkları 5 saniye boşta kalırsa otomatik olarak çıkar.Yani, evet, kesinlikle isteğe bağlı iş parçacıkları için Sayı setThreads()yöntem kullanılarak değiştirilebilir .

  4. Minimum çekirdek iş parçacığı sayısının asla birden az olmadığından emin olur, aksi submit()takdirde her görevi reddeder. Çekirdek iş parçacığı> = max iş parçacığı olması gerektiğinden, yöntem setThreads()aynı zamanda maksimum iş parçacığı da ayarlar, ancak maksimum iş parçacığı ayarı sınırsız bir kuyruk için işe yaramaz.


Ayrıca 'allowCoreThreadTimeOut' öğesini de 'true' olarak ayarlamanız gerektiğini düşünüyorum, aksi takdirde, iş parçacıkları oluşturulduktan sonra onları sonsuza kadar saklayacaksınız: gist.github.com/ericdcobb/46b817b384f5ca9d5f5d
eric

oops bunu kaçırdım, özür dilerim, cevabınız o zaman mükemmel!
eric

6

İlk örneğinizde, AbortPolicyvarsayılan görevler olduğu için sonraki görevler reddedilir RejectedExecutionHandler. ThreadPoolExecutor, setRejectedExecutionHandleryöntem aracılığıyla değiştirebileceğiniz aşağıdaki politikaları içerir :

CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy

CallerRunsPolicy ile önbelleğe alınmış iş parçacığı havuzu istiyormuşsunuz gibi geliyor.


5

Buradaki yanıtların hiçbiri, Apache'nin HTTP istemcisini (3.x sürümü) kullanarak sınırlı sayıda HTTP bağlantısı oluşturmakla ilgili sorunumu çözmedi. İyi bir kurulum bulmam birkaç saat sürdüğü için paylaşacağım:

private ExecutorService executor = new ThreadPoolExecutor(5, 10, 60L,
  TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
  Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

Bu, ThreadPoolExecutorbeş ile başlayan ve CallerRunsPolicyyürütme için kullanılan en fazla on eşzamanlı çalışan iş parçacığı tutan bir oluşturur .


Bu çözümle ilgili sorun, sayısını veya üreticileri artırırsanız, arka plan iş parçacıklarını çalıştıran iş parçacığı sayısını artıracağınızdır. Çoğu durumda istediğiniz bu değildir.
Gri

3

ThreadPoolExecutor için Javadoc'a göre:

CorePoolSize değerinden fazla ancak maksimumPoolSize iş parçacığından daha az çalışıyorsa, yeni bir iş parçacığı yalnızca kuyruk doluysa oluşturulur . CorePoolSize ve maximumPoolSize değerlerini aynı ayarlayarak, sabit boyutlu bir iş parçacığı havuzu oluşturursunuz.

(Vurgu benim.)

Jitter'in cevabı sizin istediğiniz şeydir, ancak benimki diğer sorunuza cevap verir. :)


2

bir seçenek daha var. Yeni SynchronousQueue kullanmak yerine başka herhangi bir kuyruğu da kullanabilirsiniz, ancak boyutunun 1 olduğundan emin olmanız gerekir, böylece executorservice'i yeni iş parçacığı oluşturmaya zorlar.


Sanırım 0 boyutunu kastettiğinizi (varsayılan olarak), böylece sıraya alınan hiçbir görev olmayacak ve yürütme hizmetini her seferinde yeni iş parçacığı oluşturmaya gerçekten zorlayacaksınız.
Leonmax

2

Yanıtlardan herhangi biri soruyu gerçekten yanıtlamış gibi görünmüyor - aslında bunu yapmanın bir yolunu göremiyorum - PooledExecutorService'den alt sınıflar oluştursanız bile, yöntemlerin / özelliklerin çoğu özeldir, örneğin addIfUnderMaximumPoolSize'ın korumalı olması gibi aşağıdakileri yapın:

class MyThreadPoolService extends ThreadPoolService {
    public void execute(Runnable run) {
        if (poolSize() == 0) {
            if (addIfUnderMaximumPoolSize(run) != null)
                return;
        }
        super.execute(run);
    }
}

En yakın bulduğum şey şuydu - ama bu bile pek iyi bir çözüm değil

new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()) {
    public void execute(Runnable command) {
        if (getPoolSize() == 0 && getActiveCount() < getMaximumPoolSize()) {        
            super.setCorePoolSize(super.getCorePoolSize() + 1);
        }
        super.execute(command);
    }

    protected void afterExecute(Runnable r, Throwable t) {
         // nothing in the queue
         if (getQueue().isEmpty() && getPoolSize() > min) {
             setCorePoolSize(getCorePoolSize() - 1);
         }
    };
 };

ps yukarıda test edilmedi


2

İşte başka bir çözüm. Bu çözümün istediğiniz gibi davranacağını düşünüyorum (bu çözümle gurur duymasa da):

final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    public boolean offer(Runnable o) {
        if (size() > 1)
            return false;
        return super.offer(o);
    };

    public boolean add(Runnable o) {
        if (super.offer(o))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
};

RejectedExecutionHandler handler = new RejectedExecutionHandler() {         
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        queue.add(r);
    }
};

dbThreadExecutor =
        new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, queue, handler);

2

İstediğin bu (en azından sanırım öyle). Açıklama için Jonathan Feinberg cevabını kontrol edin

Executors.newFixedThreadPool(int n)

Paylaşılan sınırsız bir kuyrukta çalışan sabit sayıda iş parçacığını yeniden kullanan bir iş parçacığı havuzu oluşturur. Herhangi bir noktada, en fazla nThreads iş parçacığı aktif işleme görevleri olacaktır. Tüm iş parçacıkları etkinken ek görevler gönderilirse, iş parçacığı kullanılabilir olana kadar kuyrukta beklerler. Kapanmadan önce yürütme sırasında bir hata nedeniyle herhangi bir iş parçacığı sona ererse, sonraki görevleri yürütmek için gerekirse yeni bir iş parçacığı yerini alacaktır. Havuzdaki iş parçacıkları, açıkça kapatılana kadar var olacaktır.


4
Elbette, sabit bir iş parçacığı havuzu kullanabilirdim, ancak bu sonsuza kadar veya kapatmayı çağırana kadar n iş parçacığı bırakacaktı. Tam olarak önbelleğe alınmış iş parçacığı havuzu gibi bir şey istiyorum (istek üzerine iş parçacıkları oluşturur ve bir zaman aşımından sonra onları öldürür), ancak oluşturabileceği iş parçacığı sayısında bir sınırla.
Matt Crinklaw-Vogt

0
  1. @SjleeThreadPoolExecutor tarafından önerildiği gibi kullanabilirsiniz

    Havuzun boyutunu dinamik olarak kontrol edebilirsiniz. Daha fazla ayrıntı için bu soruya bir göz atın:

    Dinamik İş Parçacığı Havuzu

    VEYA

  2. Java 8 ile tanıtılan newWorkStealingPool API'yi kullanabilirsiniz .

    public static ExecutorService newWorkStealingPool()

    Hedef paralellik düzeyi olarak tüm kullanılabilir işlemcileri kullanarak bir iş çalma iş parçacığı havuzu oluşturur.

Varsayılan olarak paralellik düzeyi, sunucunuzdaki CPU çekirdeği sayısına ayarlanır. 4 çekirdekli CPU sunucunuz varsa, iş parçacığı havuzu boyutu 4 olacaktır. Bu API , ForkJoinPool'daki meşgul iş parçacıklarından görevleri çalarak boş iş parçacığı ForkJoinPooltürünü döndürür ExecutorServiceve işin çalınmasına izin verir.


0

Sorun şu şekilde özetlendi:

Tam olarak önbelleğe alınmış iş parçacığı havuzu gibi bir şey istiyorum (istek üzerine iş parçacıkları oluşturur ve ardından bir zaman aşımından sonra onları öldürür), ancak oluşturabileceği iş parçacığı sayısında bir sınır ve ona ulaştığında ek görevleri sıraya koyma yeteneği ile iş parçacığı sınırı.

Çözüme işaret etmeden önce, aşağıdaki çözümlerin neden işe yaramadığını açıklayacağım:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());

Bu, 3 sınırına ulaşıldığında herhangi bir görevi sıraya koymaz çünkü SynchronousQueue tanımı gereği herhangi bir öğeyi tutamaz.

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

Bu, tek bir iş parçacığından fazlasını oluşturmayacaktır çünkü ThreadPoolExecutor, kuyruk doluysa yalnızca corePoolSize değerini aşan evreler oluşturur. Ancak LinkedBlockingQueue asla dolu değildir.

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.allowCoreThreadTimeOut(true);

Bu, corePoolSize ulaşılana kadar iş parçacıklarını yeniden kullanmayacaktır çünkü ThreadPoolExecutor, mevcut iş parçacıkları boşta olsa bile corePoolSize ulaşılana kadar iş parçacığı sayısını arttırır. Eğer bu dezavantajla yaşayabilirseniz, o zaman sorunun en kolay çözümü budur. Aynı zamanda "Uygulamada Java Eş Zamanlılığı" (p172'deki dipnot) bölümünde açıklanan çözümdür.

Açıklanan sorunun tek tam çözümü, sıranın offeryöntemini geçersiz kılmak ve RejectedExecutionHandlerbu sorunun yanıtlarında açıklandığı gibi bir a yazmak gibi görünüyor : ThreadPoolExecutor, sıraya girmeden önce iş parçacıklarını maksimuma çıkarmak için nasıl elde edilir?


0

Bu Java8 + (ve diğerleri için şimdilik ..)

     Executor executor = new ThreadPoolExecutor(3, 3, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()){{allowCoreThreadTimeOut(true);}};

burada 3, iş parçacığı sayısı sınırı ve 5 boş iş parçacıkları için zaman aşımıdır.

Kendi kendinize çalışıp çalışmadığını kontrol etmek istiyorsanız , işte işi yapmak için kod:

public static void main(String[] args) throws InterruptedException {
    final int DESIRED_NUMBER_OF_THREADS=3; // limit of number of Threads for the task at a time
    final int DESIRED_THREAD_IDLE_DEATH_TIMEOUT=5; //any idle Thread ends if it remains idle for X seconds

    System.out.println( java.lang.Thread.activeCount() + " threads");
    Executor executor = new ThreadPoolExecutor(DESIRED_NUMBER_OF_THREADS, DESIRED_NUMBER_OF_THREADS, DESIRED_THREAD_IDLE_DEATH_TIMEOUT, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()) {{allowCoreThreadTimeOut(true);}};

    System.out.println(java.lang.Thread.activeCount() + " threads");

    for (int i = 0; i < 5; i++) {
        final int fi = i;
        executor.execute(() -> waitsout("starting hard thread computation " + fi, "hard thread computation done " + fi,2000));
    }
    System.out.println("If this is UP, it works");

    while (true) {
        System.out.println(
                java.lang.Thread.activeCount() + " threads");
        Thread.sleep(700);
    }

}

static void waitsout(String pre, String post, int timeout) {
    try {
        System.out.println(pre);
        Thread.sleep(timeout);
        System.out.println(post);
    } catch (Exception e) {
    }
}

yukarıdaki kodun benim için çıktısı

1 threads
1 threads
If this is UP, it works
starting hard thread computation 0
4 threads
starting hard thread computation 2
starting hard thread computation 1
4 threads
4 threads
hard thread computation done 2
hard thread computation done 0
hard thread computation done 1
starting hard thread computation 3
starting hard thread computation 4
4 threads
4 threads
4 threads
hard thread computation done 3
hard thread computation done 4
4 threads
4 threads
4 threads
4 threads
3 threads
3 threads
3 threads
1 threads
1 threads
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.