Nasıl çatal / birleştirme çerçevesi bir iş parçacığı havuzundan daha iyidir?


134

Yeni çatal / birleştirme çerçevesini kullanmanın, yalnızca büyük görevi başlangıçta N alt göreve bölmek, bunları önbelleğe alınmış bir iş parçacığı havuzuna ( Yürütücülerden ) göndermek ve her görevin tamamlanmasını beklemek yerine kullanmanın avantajları nelerdir ? Çatal / birleştirme soyutlamasını kullanmanın sorunu nasıl basitleştirdiğini veya çözümü yıllardır sahip olduğumuzdan daha verimli hale getirdiğini göremiyorum.

Örneğin, eğitim örneğindeki paralelleştirilmiş bulanıklaştırma algoritması şu şekilde uygulanabilir:

public class Blur implements Runnable {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;

    private int mBlurWidth = 15; // Processing window size, should be odd.

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    public void run() {
        computeDirectly();
    }

    protected void computeDirectly() {
        // As in the example, omitted for brevity
    }
}

Başta bölün ve görevleri bir iş parçacığı havuzuna gönderin:

// source image pixels are in src
// destination image pixels are in dst
// threadPool is a (cached) thread pool

int maxSize = 100000; // analogous to F-J's "sThreshold"
List<Future> futures = new ArrayList<Future>();

// Send stuff to thread pool:
for (int i = 0; i < src.length; i+= maxSize) {
    int size = Math.min(maxSize, src.length - i);
    ForkBlur task = new ForkBlur(src, i, size, dst);
    Future f = threadPool.submit(task);
    futures.add(f);
}

// Wait for all sent tasks to complete:
for (Future future : futures) {
    future.get();
}

// Done!

Görevler, iş parçacığı havuzunun kuyruğuna gider ve buradan çalışan iş parçacıkları kullanılabilir hale geldikçe çalıştırılır. Bölme yeterince ayrıntılı olduğu (özellikle son görevi beklemek zorunda kalmamak için) ve iş parçacığı havuzunda yeterli (en az N işlemci) iş parçacığı olduğu sürece, tüm işlemciler tüm hesaplama tamamlanana kadar tam hızda çalışır.

Bir şey mi kaçırıyorum? Fork / join çerçevesini kullanmanın katma değeri nedir?

Yanıtlar:


136

Bence temel yanlış anlaşılma, Fork / Join örneklerinin iş hırsızlığı DEĞİL, sadece bir tür standart bölme ve yönetme göstermesidir.

İş hırsızlığı şöyle olur: İşçi B işini bitirdi. O nazik biridir, bu yüzden etrafına bakar ve İşçi A'nın hala çok çalıştığını görür. Yürüyerek soruyor: "Hey delikanlı, sana yardım edebilirim." Cevaplar. "Harika, 1000 birimlik bu görevim var. Şimdiye kadar 655'i bırakarak 345'i bitirdim. Lütfen 673'ten 1000'e kadar çalışabilir misiniz, 346'dan 672'ye yapacağım." B "Tamam, başlayalım böylece bara daha erken gidebiliriz."

Görüyorsunuz - işçiler gerçek işe başladıklarında bile birbirleriyle iletişim kurmalıdır. Örneklerdeki eksik kısım budur.

Öte yandan örnekler yalnızca "alt yüklenici kullanın" gibi bir şeyi gösterir:

İşçi A: "Dang, 1000 birim işim var. Benim için çok fazla. Kendim 500 yapacağım ve başkasına 500 taşeron vereceğim." Bu, büyük görev her biri 10 birimlik küçük paketlere bölünene kadar devam eder. Bunlar mevcut işçiler tarafından yürütülecektir. Ancak bir paket bir tür zehirli hapsa ve diğer paketlerden çok daha uzun sürerse - kötü şans, bölme aşaması sona erer.

Fork / Join ile görevi önceden bölmek arasında kalan tek fark şudur: Önden ayırırken, iş kuyruğunuz baştan doludur. Örnek: 1000 birim, eşik 10'dur, yani kuyrukta 100 giriş vardır. Bu paketler, iş parçacığı üyelerine dağıtılır.

Fork / Join daha karmaşıktır ve kuyruktaki paket sayısını daha az tutmaya çalışır:

  • Adım 1: (1 ... 1000) içeren bir paketi sıraya koyun
  • Adım 2: Bir işçi paketi (1 ... 1000) açar ve iki paketle değiştirir: (1 ... 500) ve (501 ... 1000).
  • Adım 3: Bir işçi paketi (500 ... 1000) açar ve (500 ... 750) ve (751 ... 1000) iter.
  • Adım n: Yığın şu paketleri içerir: (1..500), (500 ... 750), (750 ... 875) ... (991..1000)
  • Adım n + 1: Paket (991..1000) açılır ve yürütülür
  • Adım n + 2: Paket (981..990) açılır ve çalıştırılır
  • Adım n + 3: Paket (961..980) çıkarılır ve (961 ... 970) ve (971..980) 'e bölünür. ....

Görüyorsunuz: Çatal / Birleştir'de sıra daha küçüktür (örnekte 6) ve "bölme" ve "çalışma" aşamaları araya eklenir.

Birden çok işçi aynı anda patladığında ve ittiğinde, etkileşimler elbette o kadar net değildir.


Bence bu gerçekten cevap. Acaba herhangi bir yerde işini çalma yeteneklerini de gösterecek gerçek Fork / Join örnekleri var mı? Temel örneklerle, iş yükü miktarı, ünitenin boyutundan (örneğin dizi uzunluğu) oldukça mükemmel bir şekilde tahmin edilebilir olduğundan, önceden bölme kolaydır. Çalma, birim başına iş yükü miktarının birimin boyutundan iyi tahmin edilemediği sorunlarda kesinlikle fark yaratacaktır .
Joonas Pulakka

AH Cevabınız doğruysa nasıl olduğunu açıklamıyor. Oracle tarafından verilen örnek iş hırsızlığına neden olmaz. Burada tarif ettiğiniz örnekteki gibi fork ve join nasıl çalışır? Fork ve join çalmayı sizin tarif ettiğiniz şekilde çalıştıracak bir Java kodu gösterebilir misiniz? teşekkürler
Marc

@Marc: Üzgünüm ama elimde bir örnek yok.
AH

6
Oracle'ın IMO örneğindeki sorun, iş hırsızlığını göstermemesi (AH tarafından açıklandığı gibi yapıyor) değil, basit bir ThreadPool için bir algoritma kodlamanın kolay olması (Joonas'ın yaptığı gibi). FJ, çalışma yeterince bağımsız görevlere önceden bölünemediğinde, ancak kendi aralarında bağımsız görevlere yinelemeli olarak ayrılabildiğinde en yararlıdır. Bir örnek için
cevabıma

2
İş hırsızlığının işe yarayabileceği bazı örnekler: h-online.com/developer/features/…
voleybol

27

% 100 bağımsız olarak çalışan n meşgul iş parçacığınız varsa, bu bir Fork-Join (FJ) havuzundaki n iş parçacığından daha iyi olacaktır. Ama asla bu şekilde yürümez.

Problemi tam olarak n eşit parçaya bölmek mümkün olmayabilir. Yapsanız bile, iş parçacığı planlaması adil olmaktan bir yol. En yavaş iş parçacığını beklersiniz. Birden fazla göreviniz varsa, her biri n yönlü paralellikten daha az çalışabilir (genellikle daha verimli), ancak diğer görevler bittiğinde n-way'e kadar gidebilir.

Öyleyse neden sorunu FJ boyutunda parçalara ayırıp bunun üzerinde bir iş parçacığı havuzu oluşturmuyoruz. Tipik FJ kullanımı sorunu küçük parçalara ayırır. Bunları rastgele sırayla yapmak, donanım düzeyinde çok fazla koordinasyon gerektirir. Genel giderler katil olur. FJ'de görevler, iş parçacığının Son Giren İlk Çıkar sırasında (LIFO / yığın) okuduğu bir kuyruğa yerleştirilir ve iş hırsızlığı (genellikle temel çalışmada) İlk Giren İlk Çıkar (FIFO / "sıra") yapılır. Sonuç, uzun dizi işlemenin, küçük parçalara bölünmüş olsa bile, büyük ölçüde sırayla yapılabilmesidir. (Problemi tek bir büyük patlamada eşit büyüklükte küçük parçalara bölmek de önemsiz olmayabilir. Diyelim ki bir tür hiyerarşi ile dengelemeden uğraşın.)

Sonuç: FJ, birden fazla iş parçacığına sahipseniz her zaman olacak olan düzensiz durumlarda donanım iş parçacığının daha verimli kullanımına izin verir.


Ama neden FJ de en yavaş ipliği beklemek zorunda kalmasın? Önceden belirlenmiş sayıda alt görev vardır ve elbette bunlardan bazıları her zaman tamamlanan son görevler olacaktır. Örneğimdeki maxSizeparametrenin ayarlanması, FJ örneğindeki "ikili bölme" ile hemen hemen benzer alt görev bölümü üretecektir ( compute()yöntem içinde yapılır , ya bir şeyi hesaplar ya da alt görevleri gönderir invokeAll()).
Joonas Pulakka

Çünkü çok daha küçükler - cevabıma ekleyeceğim.
Tom Hawtin - tackline

Tamam, eğer alt görevlerin sayısı gerçekte paralel olarak işlenebilecek olandan daha büyükse (sonuncuyu beklemek zorunda kalmamak için mantıklıdır), o zaman koordinasyon sorunlarını görebilirim. FJ örneği , bölmenin bu kadar ayrıntılı olduğu varsayılırsa yanıltıcı olabilir: 1000x1000'lik bir görüntü için her biri 62500 öğe işleyen 16 gerçek alt görev üreten 100000'lik bir eşik kullanır. 10000x10000 bir görüntü için 1024 alt görev olacaktır ve bu zaten bir şeydir.
Joonas Pulakka

19

İş parçacığı havuzlarının ve Çatal / Birleştirmenin nihai amacı aynıdır: Her ikisi de mevcut CPU gücünü maksimum verim için ellerinden geldiğince kullanmak ister. Maksimum verim, mümkün olduğunca çok görevin uzun bir süre içinde tamamlanması gerektiği anlamına gelir. Bunu yapmak için ne gerekiyor? (Aşağıdakiler için hesaplama görevlerinde bir eksiklik olmadığını varsayacağız:% 100 CPU kullanımı için her zaman yeterli olacaktır. Ayrıca, hiper iş parçacığı durumunda çekirdekler veya sanal çekirdekler için eşdeğer olarak "CPU" kullanıyorum).

  1. En azından mevcut CPU'lar kadar çalışan çok sayıda iş parçacığı olması gerekir, çünkü daha az iş parçacığı çalıştırmak bir çekirdeği kullanılmadan bırakır.
  2. Maksimumda, mevcut CPU'lar kadar çalışan çok sayıda iş parçacığı olmalıdır, çünkü daha fazla iş parçacığı çalıştırmak, CPU'ları farklı iş parçacıklarına atayan Zamanlayıcı için ek yük yaratır ve bu da CPU zamanının hesaplama görevimiz yerine zamanlayıcıya gitmesine neden olur.

Böylece, maksimum verim için CPU'larla aynı sayıda iş parçacığına sahip olmamız gerektiğini anladık. Oracle'ın bulanıklaştırma örneğinde, hem kullanılabilir CPU sayısına eşit olan iş parçacığı sayısına sahip sabit boyutlu bir iş parçacığı havuzu alabilir veya bir iş parçacığı havuzu kullanabilirsiniz. Fark etmeyecek, haklısın!

Peki, iş parçacığı havuzlarıyla ne zaman başınız belaya girecek? Bu, bir iş parçacığının bloke olmasıdır , çünkü iş parçacığınız başka bir görevin tamamlanmasını bekler. Aşağıdaki örneği varsayalım:

class AbcAlgorithm implements Runnable {
    public void run() {
        Future<StepAResult> aFuture = threadPool.submit(new ATask());
        StepBResult bResult = stepB();
        StepAResult aResult = aFuture.get();
        stepC(aResult, bResult);
    }
}

Burada gördüğümüz, A, B ve C olmak üzere üç adımdan oluşan bir algoritmadır. A ve B birbirinden bağımsız olarak gerçekleştirilebilir, ancak C adımının A ve B adımının sonucuna ihtiyacı vardır. Bu algoritmanın yaptığı şey A görevini iş parçacığı havuzunu ve doğrudan b görevini gerçekleştirin. Bundan sonra, iş parçacığı A görevinin de yapılmasını bekleyecek ve C adımına devam edecektir. A ve B aynı anda tamamlanırsa, her şey yolundadır. Peki ya A, B'den daha uzun sürerse? Bunun nedeni, A görevinin doğası gereği onu dikte etmesi olabilir, ancak başlangıçta A görevi için iş parçacığı olmadığı ve A görevinin beklemesi gerektiği için de geçerli olabilir. (Yalnızca tek bir CPU mevcutsa ve bu nedenle iş parçacığınızın yalnızca tek bir iş parçacığı varsa, bu bir kilitlenmeye bile neden olur, ancak şimdilik noktanın dışında). Mesele şu ki, B görevini yerine getiren iş parçacığıtüm iş parçacığını engeller . CPU'larla aynı sayıda iş parçacığına sahip olduğumuzdan ve bir iş parçacığı engellendiğinden, bu, bir CPU'nun boşta olduğu anlamına gelir .

Fork / Join bu sorunu çözer: fork / join çerçevesinde aşağıdaki gibi aynı algoritmayı yazarsınız:

class AbcAlgorithm implements Runnable {
    public void run() {
        ATask aTask = new ATask());
        aTask.fork();
        StepBResult bResult = stepB();
        StepAResult aResult = aTask.join();
        stepC(aResult, bResult);
    }
}

Aynı görünüyor, değil mi? Ancak ipucu, bunun aTask.join engellemeyeceğidir . Bunun yerine iş hırsızlığının devreye girdiği yer burasıdır : İş parçacığı, geçmişte çatallanmış diğer görevleri arayacak ve bunlarla devam edecek. İlk olarak, çatallandığı görevlerin işlemeye başlayıp başlamadığını kontrol eder. Yani A henüz başka bir iş parçacığı tarafından başlatılmadıysa, sonra A yapacak, aksi takdirde diğer iş parçacıklarının kuyruğunu kontrol edecek ve çalışmalarını çalacaktır. Başka bir iş parçacığının bu diğer görevi tamamlandığında, A'nın şimdi tamamlanıp tamamlanmadığını kontrol edecektir. Yukarıdaki algoritma ise arayabilir stepC. Aksi takdirde çalacak başka bir görev daha arayacaktır. Böylece çatal / birleştirme havuzları, engelleme eylemleri karşısında bile% 100 CPU kullanımına ulaşabilir .

Ancak bir tuzak vardır: İş hırsızlığı yalnızca e-posta joinçağrıları için mümkündür ForkJoinTask. Başka bir iş parçacığı beklemek veya bir G / Ç eylemi beklemek gibi harici engelleme eylemleri için yapılamaz. Peki buna ne dersiniz, G / Ç'nin tamamlanmasını beklemek ortak bir görev mi? Bu durumda, Çatal / Birleştirme havuzuna ek bir iş parçacığı ekleyebilirsek, bu, engelleme eylemi tamamlanır tamamlanmaz durdurulacak en iyi ikinci şey olacaktır. Ve ForkJoinPooleğer biz ManagedBlockers kullanıyorsak aslında bunu yapabilir .

Fibonacci

Özyinelemeli Görev için JavaDoc'ta, Fibonacci sayılarının Fork / Join kullanılarak hesaplanması için bir örnek verilmiştir. Klasik bir özyinelemeli çözüm için bakınız:

public static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

JavaDocs'ta açıklandığı gibi, bu algoritma O (2 ^ n) karmaşıklığa sahipken daha basit yollar mümkün olduğundan, bu fibonacci sayılarını hesaplamak için oldukça dökümlü bir yoldur. Ancak bu algoritma çok basit ve anlaşılması kolay, bu yüzden ona bağlı kalıyoruz. Bunu Fork / Join ile hızlandırmak istediğimizi varsayalım. Saf bir uygulama şöyle görünür:

class Fibonacci extends RecursiveTask<Long> {
    private final long n;

    Fibonacci(long n) {
        this.n = n;
    }

    public Long compute() {
        if (n <= 1) {
            return n;
        }
        Fibonacci f1 = new Fibonacci(n - 1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n - 2);
        return f2.compute() + f1.join();
   }
}

Bu Görevin bölündüğü adımlar çok kısa ve bu nedenle bu korkunç bir performans sergileyecek, ancak çerçevenin genel olarak nasıl çok iyi çalıştığını görebilirsiniz: İki zirve bağımsız olarak hesaplanabilir, ancak daha sonra finali oluşturmak için ikisine de ihtiyacımız var sonuç. Yani bir yarısı başka bir iş parçacığında yapılır. Bir kilitlenme olmadan iş parçacığı havuzlarında aynı şeyi yaparak eğlenin (mümkün, ancak bu kadar basit değil).

Tamlık için: Bu yinelemeli yaklaşımı kullanarak gerçekten Fibonacci sayılarını hesaplamak istiyorsanız, burada optimize edilmiş bir sürüm var:

class FibonacciBigSubtasks extends RecursiveTask<Long> {
    private final long n;

    FibonacciBigSubtasks(long n) {
        this.n = n;
    }

    public Long compute() {
        return fib(n);
    }

    private long fib(long n) {
        if (n <= 1) {
            return 1;
        }
        if (n > 10 && getSurplusQueuedTaskCount() < 2) {
            final FibonacciBigSubtasks f1 = new FibonacciBigSubtasks(n - 1);
            final FibonacciBigSubtasks f2 = new FibonacciBigSubtasks(n - 2);
            f1.fork();
            return f2.compute() + f1.join();
        } else {
            return fib(n - 1) + fib(n - 2);
        }
    }
}

Bu, alt görevleri çok daha küçük tutar, çünkü bunlar yalnızca n > 10 && getSurplusQueuedTaskCount() < 2doğru olduğunda bölünürler , bu, yapılacak 100'den fazla yöntem çağrısı olduğu n > 10ve halihazırda bekleyen ( getSurplusQueuedTaskCount() < 2) çok sayıda insan görevi olmadığı anlamına gelir .

Bilgisayarımda (4 çekirdekli (Hyper-threading sayılırken 8), Intel (R) Core (TM) i7-2720QM CPU @ 2.20GHz) fib(50)klasik yaklaşımla 64 saniye ve Fork / Join yaklaşımı ile sadece 18 saniye sürer. teorik olarak mümkün olduğu kadar olmasa da oldukça dikkat çekici bir kazançtır.

özet

  • Evet, örneğinizde Fork / Join, klasik iş parçacığı havuzlarına göre avantajlı değildir.
  • Fork / Join, engelleme söz konusu olduğunda performansı önemli ölçüde artırabilir
  • Fork / Join bazı kilitlenme sorunlarını çözer

17

Çatal / birleştirme iş parçacığı havuzundan farklıdır çünkü iş çalmayı uygular. Gönderen Fork / Üyelik

Herhangi bir ExecutorService'de olduğu gibi, fork / join çerçevesi görevleri bir iş parçacığı havuzundaki çalışan iş parçacıklarına dağıtır. Çatal / birleştirme çerçevesi farklıdır çünkü bir iş çalma algoritması kullanır. Yapacakları kalmayan çalışan iş parçacıkları, hala meşgul olan diğer iş parçacıklarından görevleri çalabilir.

Diyelim ki iki iş parçacığınız ve sırasıyla 1, 1, 5 ve 6 saniye süren 4 görev a, b, c, d var. Başlangıçta, a ve b evre 1'e ve c ve d evre 2'ye atanır. Bir iş parçacığı havuzunda bu 11 saniye sürer. Çatal / birleştirme ile, diş 1 biter ve iş parçacığı 2'den işi çalabilir, böylece görev d iş parçacığı 1 tarafından yürütülür. İplik 1 a, b ve d'yi yürütür, iplik 2 sadece c. Toplam süre: 8 saniye, 11 değil.

DÜZENLEME: Joonas'ın belirttiği gibi, görevlerin bir iş parçacığına önceden tahsis edilmesi gerekmez. Çatal / birleştirme fikri, bir iş parçacığının bir görevi birden çok alt parçaya bölmeyi seçebilmesidir. Yani yukarıdakileri yeniden ifade etmek için:

Sırasıyla 2 ve 11 saniye süren iki görevimiz (ab) ve (cd) var. Konu 1, ab'yi yürütmeye başlar ve onu iki alt görev a ve b'ye ayırır. İplik 2 ile benzer şekilde, iki alt görev c & d'ye ayrılır. İplik 1 a & b'yi bitirdiğinde, iplik 2'den d çalabilir.


5
İş parçacığı havuzları tipik olarak ThreadPoolExecutor örnekleridir. Böyle bir durumda, görevler bir kuyruğa gider ( uygulamada BlockingQueue ), buradan çalışan iş parçacıkları önceki görevlerini bitirir bitirmez görevleri alır. Görevler edilir değil bildiğim kadarıyla anladığım kadarıyla, belirli iş parçacığı önceden atanmış. Her iş parçacığında bir seferde (en fazla) 1 görev bulunur.
Joonas Pulakka

4
AFAIK orada bir Sırası bir ThreadPoolExecutor dönüş kontrollerde birçok konu. Bu, bir yürütücüye görevler veya Runnables (İş Parçacığı değil!) Atamanın da belirli bir Konuya önceden tahsis edilmediği anlamına gelir. FJ'nin de yaptığı gibi. Şimdiye kadar FJ kullanmanın bir faydası yok.
AH

1
@AH Evet, ancak çatal / birleştirme mevcut görevi bölmenize izin verir. Görevi yürüten iş parçacığı, onu iki farklı göreve ayırabilir. Yani ThreadPoolExecutor ile sabit bir görev listesine sahip olursunuz. Çatal / birleştir ile, yürütme görevi kendi görevini ikiye bölebilir ve bu görev, işlerini bitirdiklerinde diğer iş parçacıkları tarafından alınabilir. Ya da sen ilk bitirirsen.
Matthew Farwell

1
@Matthew Farwell: FJ örneğinde , her görev içinde, compute()ya görevi hesaplar ya da iki alt göreve böler. Hangi seçeneği seçeceği yalnızca görevin boyutuna ( if (mLength < sThreshold)...) bağlıdır, bu nedenle bu, sabit sayıda görev oluşturmanın süslü bir yoludur. 1000x1000'lik bir görüntü için, aslında bir şeyi hesaplayan tam olarak 16 alt görev olacaktır. Ek olarak, yalnızca alt görevler oluşturan ve çağıran ve kendi başına hiçbir şey hesaplamayan 15 (= 16 - 1) "ara" görev olacaktır.
Joonas Pulakka

2
@Matthew Farwell: FJ'nin tamamını anlamıyor olabilirim, ancak bir alt görev kendi computeDirectly()yöntemini uygulamaya karar verdiyse , artık hiçbir şey çalmanın bir yolu yok. Tüm bölme , en azından örnekte, a priori yapılır .
Joonas Pulakka

14

Yukarıdaki herkes haklı, işin çalınmasıyla elde edilen faydalar ama bunun nedenini genişletmek için.

Birincil fayda, çalışan iş parçacıkları arasındaki verimli koordinasyondur. İşin bölünmesi ve yeniden birleştirilmesi gerekiyor, bu da koordinasyon gerektiriyor. AH'nin yukarıdaki cevabında görebileceğiniz gibi, her iş parçacığının kendi çalışma listesi vardır. Bu listenin önemli bir özelliği, sıralanmasıdır (üstte büyük görevler ve altta küçük görevler). Her iş parçacığı, listesinin altındaki görevleri yürütür ve diğer iş parçacığı listelerinin üstündeki görevleri çalar.

Bunun sonucu:

  • Görev listelerinin başı ve sonu bağımsız olarak senkronize edilebilir ve listedeki çekişmeyi azaltır.
  • Çalışmanın önemli alt ağaçları bölünür ve aynı iş parçacığı tarafından yeniden birleştirilir, bu nedenle bu alt ağaçlar için iş parçacıkları arası koordinasyona gerek yoktur.
  • Bir iş parçacığı çalışmayı çaldığında, daha sonra kendi listesine alt bölümlere ayırdığı büyük bir parça alır.
  • İş çeliği, dişlerin işlemin sonuna kadar neredeyse tamamen kullanıldığı anlamına gelir.

İş parçacığı havuzlarını kullanan diğer bölme ve yönetme şemalarının çoğu, iş parçacıkları arasında daha fazla iletişim ve koordinasyon gerektirir.


13

Bu örnekte, Fork / Join hiçbir değer katmaz çünkü çatallamaya gerek yoktur ve iş yükü çalışan iş parçacıkları arasında eşit olarak bölünür. Fork / Join yalnızca ek yük ekler.

İşte konuyla ilgili güzel bir makale . Alıntı:

Genel olarak, iş yükünün çalışan iş parçacıkları arasında eşit olarak bölündüğü durumlarda ThreadPoolExecutor tercih edilmelidir diyebiliriz. Bunu garanti edebilmek için, giriş verilerinin tam olarak neye benzediğini bilmeniz gerekir. Buna karşılık, ForkJoinPool, giriş verilerinden bağımsız olarak iyi performans sağlar ve bu nedenle önemli ölçüde daha sağlam bir çözümdür.


8

Bir diğer önemli fark, FJ ile çoklu, karmaşık "Birleştirme" aşamaları gerçekleştirebileceğiniz gibi görünüyor. Http://faculty.ycp.edu/~dhovemey/spring2011/cs365/lecture/lecture18.html adresindeki birleştirme sıralamasını düşünün, bu çalışmayı önceden bölmek için çok fazla düzenleme gerekecektir. örneğin, aşağıdakileri yapmanız gerekir:

  • ilk çeyreği sırala
  • ikinci çeyreği sırala
  • ilk 2 çeyreği birleştir
  • üçüncü çeyreği sırala
  • dördüncü çeyreği sırala
  • son 2 çeyreği birleştir
  • 2 yarıyı birleştir

Bunları ilgilendiren sıralamayı birleştirmeden önce yapmanız gerektiğini nasıl belirtirsiniz vb.

Bir öğe listesinin her biri için belirli bir şeyin en iyi nasıl yapılacağına bakıyorum. Sanırım listeyi önceden ayırıp standart bir ThreadPool kullanacağım. FJ, çalışma yeterince bağımsız görevlere önceden bölünemediğinde, ancak kendi aralarında bağımsız olan görevlere yinelemeli olarak ayrılabildiğinde (örneğin, yarıların sıralanması bağımsızdır, ancak 2 ayrılmış yarıyı sıralı bir bütün halinde birleştirmek değildir) en yararlıdır.


6

F / J, pahalı birleştirme işlemleriniz olduğunda da belirgin bir avantaja sahiptir. Bir ağaç yapısına bölündüğü için, doğrusal diş bölmeli n birleştirme yerine yalnızca log2 (n) birleştirme yaparsınız. (Bu, iş parçacıkları kadar çok işlemciye sahip olduğunuz teorik varsayımını yapar, ancak yine de bir avantajdır) Bir ev ödevi için, her dizindeki değerleri toplayarak birkaç bin 2D diziyi (hepsi aynı boyutlarda) birleştirmek zorunda kaldık. Çatallı birleştirme ve P işlemcilerde, P sonsuza yaklaştıkça zaman log2 (n) 'ye yaklaşır.

1 2 3 .. 7 3 1 .... 8 5 4
4 5 6 + 2 4 3 => 6 9 9
7 8 9 .. 1 1 0 .... 8 9 9


3

Tarayıcı gibi uygulamalarda ForkJoin performansına hayran kalacaksınız. İşte öğrenebileceğiniz en iyi öğretici .

Fork / Join mantığı çok basittir: (1) her büyük görevi daha küçük görevlere ayırın (çatal); (2) her görevi ayrı bir iş parçacığında işleyin (gerekirse bunları daha küçük görevlere ayırarak); (3) sonuçlara katılın.


3

Sorun, diğer iş parçacıklarının tamamlanmasını beklemek zorunda kalacağımız şekildeyse (dizi veya dizi toplamının sıralanması durumunda olduğu gibi), yürütücü (Executors.newFixedThreadPool (2)) sınırlı nedeniyle boğulacağından çatal birleştirme kullanılmalıdır. iş parçacığı sayısı. Forkjoin havuzu, bu durumda, aynı paralelliği korumak için engellenen iş parçacığını örtmek için daha fazla iş parçacığı oluşturacaktır.

Kaynak: http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

Yürütücülerle böl ve ele geçir algoritmalarını uygulamakla ilgili sorun, alt görevler oluşturmakla ilgili değildir, çünkü bir Çağrılabilir, uygulayıcısına yeni bir alt görev göndermekte ve sonucunu eşzamanlı veya eşzamansız bir şekilde beklemekte özgürdür. Sorun paralelliktir: Bir Çağrılabilir, başka bir Çağrılabilir'in sonucunu beklediğinde, bekleme durumuna alınır, böylece yürütme için kuyruğa alınmış başka bir Çağrılabilir'i işleme fırsatı boşa harcanır.

Doug Lea'nın çabalarıyla Java SE 7'de java.util.concurrent paketine eklenen çatal / birleştirme çerçevesi bu boşluğu dolduruyor

Kaynak: https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html

Havuz, bazı görevler diğerlerine katılmayı beklerken durdurulmuş olsa bile, iç çalışan iş parçacıklarını dinamik olarak ekleyerek, askıya alarak veya devam ettirerek yeterli sayıda etkin (veya kullanılabilir) iş parçacığı tutmaya çalışır. Bununla birlikte, engellenen GÇ veya diğer yönetilmeyen senkronizasyon karşısında bu tür ayarlamalar garanti edilmez

public int getPoolSize () Başlamış ancak henüz sonlandırılmamış çalışan iş parçacığı sayısını döndürür. Bu yöntem tarafından döndürülen sonuç, diğerleri birlikte engellendiğinde paralelliği korumak için iş parçacıkları oluşturulduğunda getParallelism () 'den farklı olabilir.


2

Uzun cevapları okumak için fazla zamanı olmayanlar için kısa bir cevap eklemek istiyorum. Karşılaştırma, Uygulamalı Akka Kalıpları kitabından alınmıştır:

Bir fork-join-executor veya bir thread-pool-executor kullanıp kullanmayacağınıza dair kararınız büyük ölçüde o dağıtım programındaki işlemlerin engelleyip engellemeyeceğine bağlıdır. Bir çatal-birleştirme yöneticisi size maksimum sayıda aktif iş parçacığı verirken, bir iş parçacığı havuzu yürüticisi size sabit sayıda iş parçacığı verir. Eğer evreler engellenirse, bir çatal-birleştirme-yürütücü daha fazlasını yaratacaktır, oysa bir evre havuzu-yürütücü bunu yapmayacaktır. Engelleme işlemleri için, genellikle bir iş parçacığı havuzu yürüticisi kullanmak daha iyidir çünkü iş parçacığı sayılarınızın patlamasını önler. Daha "reaktif" işlemler, bir çatal-birleştirme-yürütücüsünde daha iyidir.

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.