Java 8 paralel akışında özel iş parçacığı havuzu


398

Java 8 paralel akışı için özel bir iş parçacığı havuzu belirtmek mümkün mü ? Hiçbir yerde bulamadım.

Bir sunucu uygulamam olduğunu ve paralel akışlar kullanmak istediğimizi düşünün. Ancak uygulama büyük ve çok iş parçacıklı olduğundan, bölümlere ayırmak istiyorum. Başka bir modülün applicationblock görevlerinin bir modülünde yavaş çalışan bir görev istemiyorum.

Farklı modüller için farklı iş parçacığı havuzları kullanamıyorsam, gerçek dünyadaki durumların çoğunda paralel akışları güvenle kullanamayacağım anlamına gelir.

Aşağıdaki örneği deneyin. Ayrı iş parçacıklarında yürütülen bazı CPU yoğun görevleri vardır. Görevler paralel akışlardan yararlanır. İlk görev bozulur, bu nedenle her adım 1 saniye sürer (iplik uyku ile simüle edilir). Sorun, diğer iş parçacıklarının takılması ve kırık görevin bitmesini beklemesidir. Bu bir örnek, ancak bir sunucu uygulaması ve paylaşılan çatal birleştirme havuzuna uzun süredir çalışan bir görev gönderen birini hayal edin.

public class ParallelTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        es.execute(() -> runTask(1000)); //incorrect task
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));


        es.shutdown();
        es.awaitTermination(60, TimeUnit.SECONDS);
    }

    private static void runTask(int delay) {
        range(1, 1_000_000).parallel().filter(ParallelTest::isPrime).peek(i -> Utils.sleep(delay)).max()
                .ifPresent(max -> System.out.println(Thread.currentThread() + " " + max));
    }

    public static boolean isPrime(long n) {
        return n > 1 && rangeClosed(2, (long) sqrt(n)).noneMatch(divisor -> n % divisor == 0);
    }
}

3
Özel iş parçacığı havuzu ile ne demek istiyorsun? Tek bir ortak ForkJoinPool var, ancak her zaman kendi ForkJoinPool'unuzu oluşturabilir ve ona istek gönderebilirsiniz.
edharned

7
İpucu: Java Şampiyonu Heinz Kabutz aynı sorunu daha da kötü bir şekilde inceliyor: Ortak çatal birleştirme havuzunun çıkmaz dişleri. Bkz. Javaspecialists.eu/archive/Issue223.html
Peti

Yanıtlar:


395

Aslında, belirli bir çatal birleştirme havuzunda paralel bir işlemin nasıl gerçekleştirileceği hakkında bir hile vardır. Bir çatal birleştirme havuzunda görev olarak yürütürseniz, orada kalır ve ortak olanı kullanmaz.

final int parallelism = 4;
ForkJoinPool forkJoinPool = null;
try {
    forkJoinPool = new ForkJoinPool(parallelism);
    final List<Integer> primes = forkJoinPool.submit(() ->
        // Parallel task here, for example
        IntStream.range(1, 1_000_000).parallel()
                .filter(PrimesPrint::isPrime)
                .boxed().collect(Collectors.toList())
    ).get();
    System.out.println(primes);
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
} finally {
    if (forkJoinPool != null) {
        forkJoinPool.shutdown();
    }
}

Hile, aşağıdakileri belirten ForkJoinTask.fork'u temel alır : "Geçerli görevin çalıştığı havuzda bu görevi eşzamansız olarak yürütmesi veya varsaForkJoinPool () kullanılması durumunda düzenleme"



3
Ancak aynı zamanda akışların kullandığı belirtilir ForkJoinPoolmi yoksa bu bir uygulama ayrıntısı mıdır? Belgelere bir bağlantı iyi olurdu.
Nicolai

6
@Lukas Snippet için teşekkürler. Ben bir iplik sızıntısını önlemek için artık gerekmediğinde ForkJoinPoolörnek olması gerektiğini ekleyeceğim shutdown(). (örnek)
jck

5
Java 8'de görevler özel bir havuz örneğinde çalışsa bile, yine de paylaşılan havuza bağlı olduklarını unutmayın: hesaplamanın boyutu özel havuzla değil, ortak havuzla orantılı kalır. Java 10'da düzeltildi: JDK-8190974
Terran

3
@terran Bu sorun, Java 8 için de giderilmiştir bugs.openjdk.java.net/browse/JDK-8224620
Cutberto Ocampo

192

Paralel akışlar, varsayılan olarak, işlemcileriniz tarafından döndürüldüğü gibi bir tane daha az iş parçacığına sahipForkJoinPool.commonPool olan varsayılanı kullanır (Bu, paralel akışların ana işlemciyi de kullandıkları için tüm işlemcilerinizi kullandıkları anlamına gelir):Runtime.getRuntime().availableProcessors()

Ayrı veya özel havuzlar gerektiren uygulamalar için, belirli bir hedef paralellik düzeyiyle bir ForkJoinPool oluşturulabilir; varsayılan olarak, kullanılabilir işlemci sayısına eşittir.

Bu, aynı zamanda iç içe paralel akışlar veya birden çok paralel akış başladıysanız, bunların tümü aynı havuzu paylaşacakları anlamına gelir . Avantajı: asla varsayılandan daha fazlasını kullanamazsınız (kullanılabilir işlemci sayısı). Dezavantajı: Başlattığınız her paralel akışa "tüm işlemcileri" atamayabilirsiniz (birden fazla varsa). (Görünüşe göre bunu atlatmak için bir ManagedBlocker kullanabilirsiniz .)

Paralel akışların yürütülme şeklini değiştirmek için,

  • paralel akış yürütmesini kendi ForkJoinPool'unuza gönderin: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();veya
  • sistem özelliklerini kullanarak ortak havuzun boyutunu değiştirebilirsiniz: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20")20 iş parçacığının hedef paralelliği için. Ancak, bu desteklenen yamadan sonra artık çalışmaz https://bugs.openjdk.java.net/browse/JDK-8190974 .

Makinemde 8 işlemci bulunan ikincisi örneği. Aşağıdaki programı çalıştırırsam:

long start = System.currentTimeMillis();
IntStream s = IntStream.range(0, 20);
//System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
s.parallel().forEach(i -> {
    try { Thread.sleep(100); } catch (Exception ignore) {}
    System.out.print((System.currentTimeMillis() - start) + " ");
});

Çıktı:

215216216216216216216216 316 316 316 316 3163163164154154 416 416

Böylece paralel akışın bir seferde 8 öğeyi işlediğini görebilirsiniz, yani 8 iş parçacığı kullanır. Ancak, yorum satırını uncomment, çıktı:

2152152152152152162162162162162162162162162162162162162162

Bu sefer, paralel akış 20 iş parçacığı kullandı ve akıştaki 20 öğenin hepsi aynı anda işlendi.


30
commonPoolBir az aslında sahip availableProcessorstoplam paralellik sonuçlanan için eşit availableProcessorsolarak çağıran iş parçacığı sayımı nedeniyle.
Marko Topolnik

2
iade gönderin ForkJoinTask. Taklit etmek parallel() get()için gerekli:stream.parallel().forEach(soSomething)).get();
Grigory Kislin

5
ForkJoinPool.submit(() -> stream.forEach(...))Stream eylemlerimi verilenlerle çalıştıracağından emin değilim ForkJoinPool. Tüm Stream-Action'ın ForJoinPool'da ONE eylemi olarak yürütülmesini bekliyorum, ancak dahili olarak hala varsayılan / ortak ForkJoinPool kullanıyor. ForkJoinPool.submit () 'in söylediklerini yapacağını nereden gördün?
Frederic Leitenberger

@FredericLeitenberger Muhtemelen yorumunuzu Lukas'ın cevabının altına yerleştirmek istediniz.
Assylias

2
Şimdi görebiliyorum stackoverflow.com/a/34930831/1520422 aslında duyurulduğu gibi çalıştığını güzel gösteriyor. Yine de nasıl çalıştığını hala anlamıyorum. Ama ben "işe yarıyor" ile iyiyim. Teşekkürler!
Frederic Leitenberger

39

Alternatif olarak kendi forkJoinPool'unuzdaki paralel hesaplamayı tetikleme hilesine alternatif olarak, bu havuzu CompletableFuture.supplyAsync yöntemine aşağıdaki gibi de iletebilirsiniz:

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
CompletableFuture<List<Integer>> primes = CompletableFuture.supplyAsync(() ->
    //parallel task here, for example
    range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList()), 
    forkJoinPool
);

22

Orijinal çözüm (ForkJoinPool ortak paralellik özelliğini ayarlamak) artık çalışmıyor. Orijinal yanıttaki bağlantılara bakıldığında, bunu kıran bir güncelleme Java 8'e geri taşındı. Bağlantılı iş parçacıklarında belirtildiği gibi, bu çözümün sonsuza dek çalışacağı garanti edilmedi. Buna dayanarak, çözüm forkjoinpool.submit kabul edilen cevapta tartışılan .get çözümü ile. Ben backport bu çözümün güvenilmezliğini giderir düşünüyorum.

ForkJoinPool fjpool = new ForkJoinPool(10);
System.out.println("stream.parallel");
IntStream range = IntStream.range(0, 20);
fjpool.submit(() -> range.parallel()
        .forEach((int theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();
System.out.println("list.parallelStream");
int [] array = IntStream.range(0, 20).toArray();
List<Integer> list = new ArrayList<>();
for (int theInt: array)
{
    list.add(theInt);
}
fjpool.submit(() -> list.parallelStream()
        .forEach((theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();

ForkJoinPool.commonPool().getParallelism()Hata ayıklama modunda yaptığımda paralellikteki değişikliği görmüyorum .
d-coder

Teşekkürler. Biraz test / araştırma yaptım ve cevabı güncelledim. Eski sürümlerde çalıştığı için bir güncelleme değiştirilmiş gibi görünüyor.
Tod Casasent

Bunu neden alıyorum: döngüdeki unreported exception InterruptedException; must be caught or declared to be throwntüm catchistisnalar olsa bile .
Rocky Li

Rocky, hiç hata görmüyorum. Java sürümünü ve tam satırını bilmek yardımcı olacaktır. "InterruptedException" sürümü, uyku etrafında denemek / yakalamak sürümünüzde düzgün kapalı olmadığını gösterir.
Tod Casasent

13

Aşağıdaki özelliği kullanarak varsayılan paralelliği değiştirebiliriz:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

Bu da daha fazla paralellik kullanmak için ayarlanabilir.


Her ne kadar küresel bir ortam olsa da, paralel
akışı

Bu benim için openjdk "1.8.0_222" sürümünde çalıştı
abbas

Yukarıdaki ile aynı kişi, bu benim için openjdk "11.0.6" üzerinde çalışmıyor
abbas

8

Kullanılan iş parçacıklarının gerçek sayısını ölçmek için şunları kontrol edebilirsiniz Thread.activeCount():

    Runnable r = () -> IntStream
            .range(-42, +42)
            .parallel()
            .map(i -> Thread.activeCount())
            .max()
            .ifPresent(System.out::println);

    ForkJoinPool.commonPool().submit(r).join();
    new ForkJoinPool(42).submit(r).join();

Bu, 4 çekirdekli bir CPU'da şöyle bir çıkış üretebilir:

5 // common pool
23 // custom pool

O olmadan .parallel()verir:

3 // common pool
4 // custom pool

6
Thread.activeCount () size akışınızın hangi evreleri işlediğini söylemez. Bunun yerine Thread.currentThread (). GetName () öğesini ve ardından farklı () öğesini eşleyin. Daha sonra havuzdaki her iş parçacığının kullanılmayacağını anlayacaksınız ... İşleminize gecikme ekleyin ve havuzdaki tüm iş parçacıkları kullanılacaktır.
keyoxy

7

Şimdiye kadar, bu sorunun cevaplarında açıklanan çözümleri kullandım. Şimdi bunun için Paralel Akış Desteği adlı küçük bir kütüphane buldum :

ForkJoinPool pool = new ForkJoinPool(NR_OF_THREADS);
ParallelIntStreamSupport.range(1, 1_000_000, pool)
    .filter(PrimesPrint::isPrime)
    .collect(toList())

Ancak @PabloMatiasGomez'in yorumlarda belirttiği gibi, büyük ölçüde ortak havuzun büyüklüğüne bağlı olan paralel akışların ayrılma mekanizması ile ilgili dezavantajlar vardır. Bkz. HashSet'ten gelen paralel akış paralel çalışmaz .

Bu çözümü yalnızca farklı çalışma türleri için ayrı havuzlara sahip olmak için kullanıyorum, ancak kullanmasam bile ortak havuzun boyutunu 1 olarak ayarlayamıyorum.



1

Havuz boyutunu ayarlamak için aşağıdaki gibi özel ForkJoinPool denedim :

private static Set<String> ThreadNameSet = new HashSet<>();
private static Callable<Long> getSum() {
    List<Long> aList = LongStream.rangeClosed(0, 10_000_000).boxed().collect(Collectors.toList());
    return () -> aList.parallelStream()
            .peek((i) -> {
                String threadName = Thread.currentThread().getName();
                ThreadNameSet.add(threadName);
            })
            .reduce(0L, Long::sum);
}

private static void testForkJoinPool() {
    final int parallelism = 10;

    ForkJoinPool forkJoinPool = null;
    Long result = 0L;
    try {
        forkJoinPool = new ForkJoinPool(parallelism);
        result = forkJoinPool.submit(getSum()).get(); //this makes it an overall blocking call

    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        if (forkJoinPool != null) {
            forkJoinPool.shutdown(); //always remember to shutdown the pool
        }
    }
    out.println(result);
    out.println(ThreadNameSet);
}

Havuzun varsayılan 4'ten daha fazla iş parçacığı kullandığını söyleyen çıktı .

50000005000000
[ForkJoinPool-1-worker-8, ForkJoinPool-1-worker-9, ForkJoinPool-1-worker-6, ForkJoinPool-1-worker-11, ForkJoinPool-1-worker-10, ForkJoinPool-1-worker-1, ForkJoinPool-1-worker-15, ForkJoinPool-1-worker-13, ForkJoinPool-1-worker-4, ForkJoinPool-1-worker-2]

Ama aynı sonucu aşağıdaki gibi kullanarak elde etmeye çalıştığımda bir garip var ThreadPoolExecutor:

BlockingDeque blockingDeque = new LinkedBlockingDeque(1000);
ThreadPoolExecutor fixedSizePool = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, blockingDeque, new MyThreadFactory("my-thread"));

ama başaramadım.

Sadece başlayacak parallelStream yeni bir iş parçacığı ve daha sonra her şey sadece, hangi aynıdır tekrar kanıtlıyor parallelStreamkullanacağı ForkJoinPool onun alt konuları başlatın.


Diğer uygulayıcılara izin vermemenin arkasındaki olası neden ne olabilir?
omjego

@omjego Bu belki de yeni bir soru başlatabilir ve fikirlerinizi hazırlamak için daha fazla ayrıntı verebilirsiniz;)
Hearen

1

AbaküsÜt'ü almak için git . İş parçacığı numarası paralel akış için belirtilebilir. İşte örnek kod:

LongStream.range(4, 1_000_000).parallel(threadNum)...

Açıklama : AbacusUtil'in geliştiricisiyim.


1

Uygulama korsanlıklarına güvenmek istemiyorsanız, birleştirecek mapve collectsemantiği birleştirecek özel koleksiyoncular uygulayarak her zaman bunu başarmanın bir yolu vardır ... ve ForkJoinPool ile sınırlı kalmazsınız:

list.stream()
  .collect(parallelToList(i -> fetchFromDb(i), executor))
  .join()

Neyse ki, burada zaten yapıldı ve Maven Central'da mevcut: http://github.com/pivovarit/parallel-collectors

Feragatname: Yazdım ve onun sorumluluğunu üstlendim.


0

Üçüncü taraf bir kitaplık kullanmanın sakıncası yoksa, cyclops-tepki ile aynı ardışık ve paralel Akışları aynı boru hattı içinde karıştırabilir ve özel ForkJoinPools sağlayabilirsiniz. Örneğin

 ReactiveSeq.range(1, 1_000_000)
            .foldParallel(new ForkJoinPool(10),
                          s->s.filter(i->true)
                              .peek(i->System.out.println("Thread " + Thread.currentThread().getId()))
                              .max(Comparator.naturalOrder()));

Veya sıralı bir Akış içinde işlemeye devam etmek istiyorsak

 ReactiveSeq.range(1, 1_000_000)
            .parallel(new ForkJoinPool(10),
                      s->s.filter(i->true)
                          .peek(i->System.out.println("Thread " + Thread.currentThread().getId())))
            .map(this::processSequentially)
            .forEach(System.out::println);

[Açıklama Ben cyclops-tepki reaksiyonu geliştiricisiyim]


0

Özel bir ThreadPool'a ihtiyacınız yoksa ancak eşzamanlı görevlerin sayısını sınırlamak istiyorsanız, aşağıdakileri kullanabilirsiniz:

List<Path> paths = List.of("/path/file1.csv", "/path/file2.csv", "/path/file3.csv").stream().map(e -> Paths.get(e)).collect(toList());
List<List<Path>> partitions = Lists.partition(paths, 4); // Guava method

partitions.forEach(group -> group.parallelStream().forEach(csvFilePath -> {
       // do your processing   
}));

(Bunun için sorulan yinelenen soru kilitli, bu yüzden lütfen beni buraya taşıyın)


-2

Bu ForkJoinWorkerThreadFactory uygulamasını uygulamayı deneyebilir ve Fork-Join sınıfına enjekte edebilirsiniz.

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

bunu yapmak için Fork-Join havuzunun bu yapıcısını kullanabilirsiniz.

notlar: - 1. Bunu kullanırsanız, yeni iş parçacıklarını uygulamanıza bağlı olarak, genel olarak çatal birleştirme iş parçacıklarını farklı çekirdeklere (hesaplama iş parçacığı olarak kabul edilir) zamanlayan JVM'den zamanlamanın etkileneceğini göz önünde bulundurun. 2. iş parçacığı çatal-birleştirme ile görev zamanlama etkilenmez. 3. Paralel akışın çatal-birleştirmeden dişleri nasıl aldığını gerçekten anlayamadım (üzerinde uygun belgeleri bulamadık), bu yüzden paralel akıştaki dişlerin seçildiğinden emin olmak için farklı bir iplik takma fabrikası kullanmayı deneyin. Sağladığınız customThreadFactory'den. 4. commonThreadPool bu customThreadFactory öğesini kullanmayacaktır.


Belirttiğiniz şeyin nasıl kullanılacağını gösteren kullanışlı bir örnek verebilir misiniz?
J. Murray
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.