İplik havuzlarının ve Çatal/Birleşmenin nihai hedefi aynıdır: Her ikisi de mevcut CPU gücünü maksimum verim için ellerinden gelenin en iyisini kullanmak ister. Maksimum verim, mümkün olduğunca çok sayıda görevin uzun bir sürede tamamlanması gerektiği anlamına gelir. Bunu yapmak için ne gerekiyor? (Aşağıdakiler için hesaplama görevlerinde herhangi bir eksiklik olmadığı varsayılacaktır:% 100 CPU kullanımı için her zaman yeterli olacaktır.Ayrıca "CPU" hiper-iş parçacığı durumunda çekirdekler veya sanal çekirdekler için eşdeğer olarak kullanıyorum).
- En azından, CPU'lar mevcut olduğu için çalışan çok sayıda iş parçacığına ihtiyaç vardır, çünkü daha az iş parçacığının çalıştırılması çekirdek kullanılmadan bırakılır.
- En yüksek düzeyde, CPU'lar mevcut olduğu kadar çok sayıda iş parçacığı olmalıdır çünkü daha fazla iş parçacığının çalıştırılması, CPU'ları farklı iş parçacıklarına atayan Zamanlayıcı için ek yük oluşturacaktır; bu, bazı CPU zamanlarının, bizim hesaplamamıza değil, zamanlayıcıya gitmesine neden olur. görev.
Böylece, maksimum verim için, CPU'lardan tam olarak aynı sayıda ipliğe sahip olmamız gerektiğini anladık. Oracle'ın bulanıklaştırma örneğinde, hem mevcut CPU'ların sayısına eşit sayıda iş parçacığı sayısına sahip bir sabit boyutlu iş parçacığı havuzu alabilir veya bir iş parçacığı havuzu kullanabilirsiniz. Fark etmez, haklısın!
So when will you get into trouble with a thread pools? That is if a thread blocks, because your thread is waiting for another task to complete. Assume the following example:
class AbcAlgorithm implements Runnable {
public void run() {
Future aFuture = threadPool.submit(new ATask());
StepBResult bResult = stepB();
StepAResult aResult = aFuture.get();
stepC(aResult, bResult);
}
}
Burada gördüğümüz, üç adım A, B ve C'den oluşan bir algoritmadır. A ve B birbirinden bağımsız olarak gerçekleştirilebilir, ancak C aşaması, A ve B adımının sonucuna ihtiyaç duyar. Bu algoritmanın yaptığı şey, A görevine gönderilir. threadpool ve doğrudan görev b gerçekleştirin. Bundan sonra iş parçacığı A işinin yapılması için bekleyecek ve adım C ile devam edecektir. A ve B aynı anda tamamlanırsa, her şey yolunda. Ama ya B'nin B'den daha uzun sürmesi? Bunun sebebi A görevinin doğasını gerektirdiğinden, ancak durum böyle olmayabilir çünkü
Görev için iş parçacığı Başlangıçta mevcut A ve görev A'nın beklemesi gerekiyor. (Eğer sadece tek bir CPU mevcutsa ve bu yüzden threadpool'unuz sadece tek bir iş parçacığına sahipse, bu bile bir kilitlenmeye neden olacaktır, ancak şimdilik bu noktaya ek olarak). Bunun anlamı, sadece B görevini yürüten iş parçacığının tüm iş parçacığını engellemesidir . CPU'lar ile aynı sayıda iş parçacığına sahip olduğumuzdan ve bir iş parçacığı engellendiğinden, bir CPU boşta kalıyor demektir .
Çatal/Birleştir bu sorunu çözer: Çatal/birleştirme ç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, aTask.join
'un engellenmeyeceğidir . Bunun yerine çalınan çalınan burada devreye girer: İş parçacığı geçmişte çatallanmış olan diğer işler için etrafa bakacak ve bunlarla devam edecektir. Öncelikle, kendisine attığı görevlerin işlenmeye başladığını kontrol eder. Bu yüzden eğer A henüz başka bir iş parçacığı tarafından başlatılmamışsa, bir sonraki görevini yapar, aksi takdirde diğer iş parçacıklarının sırasını kontrol eder ve işlerini çalacaktır. Başka bir iş parçacığının bu görevi tamamlandığında, A'nın şimdi tamamlanıp tamamlanmadığını kontrol eder. Yukarıdaki algoritma ise stepC
'yi arayabilir. Aksi takdirde çalmak için başka bir görev arar. Böylece çatal/birleştirme havuzları, engelleme eylemleri karşısında bile% 100 CPU kullanımı sağlayabilir .
Ancak bir tuzak vardır: İşten çalma sadece ForkJoinTask
'un join
çağrısı için mümkündür. Başka bir iş parçacığı beklemek veya bir G/Ç eylemi beklemek gibi harici engelleme eylemleri için yapılamaz. Peki ya, I/O'nun tamamlanmasını beklemek ortak bir görev midir? Bu durumda, engelleme eylemi tamamlanır tamamlanmaz durdurulacak olan Çatal/Birleştir havuzuna ek bir iş parçacığı ekleyebilirsek, yapılacak en iyi ikinci şey olacaktır. Ve ForkJoinPool
aslında ManagedBlocker
kullanıyorsak bunu yapabilir.
Fibonacci
RecursiveTask için JavaDoc bir Çatal/Birleştir kullanarak Fibonacci sayılarını hesaplamak için örnek. Klasik özyinelemeli bir çözüm için bkz.
public static int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
Açıklandığı gibi JavaDocs, fibonacci sayılarını hesaplamak için oldukça basit bir yoldur, çünkü bu algoritmanın O (2 ^ n) karmaşıklığı vardır, ancak daha basit yollar mümkündür. Ancak bu algoritma çok basit ve anlaşılması kolay, bu yüzden ona bağlıyız. Bunu Çatal/Katıl ile hızlandırmak istediğimizi varsayalım. Saf bir uygulama şöyle görünecekti:
class Fibonacci extends RecursiveTask {
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örev'in ayrıldığı adımlar çok kısadır ve bu yüzden de korkunç bir şekilde çalışacaktır, ancak çerçevenin genel olarak nasıl çok iyi çalıştığını görebilirsiniz: İki summand bağımsız olarak hesaplanabilir, ancak her ikisine de finali oluşturmak için ihtiyaç duyarız. sonuç. Yani bir yarısı başka bir iş parçacığında yapılır. Çarpışmaya girmeden iplik havuzlarıyla aynı işi yapmak (mümkün, ama neredeyse basit değil).
Sadece tamlık için: Bu özyinelemeli yaklaşımı kullanarak Fibonacci sayılarını gerçekten hesaplamak istiyorsanız, optimize edilmiş bir versiyon:
class FibonacciBigSubtasks extends RecursiveTask {
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);
}
}
}
This keeps the subtasks much smaller because they are only split when n > 10 && getSurplusQueuedTaskCount() < 2
is true, which means that there are significantly more than 100 method calls to do (n > 10
) and there are not very man tasks already waiting (getSurplusQueuedTaskCount() < 2
).
Bilgisayarımda (Hyper-threading sayılırken 8 çekirdek), Intel (R) Core (TM) i7-2720QM CPU @ 2.20GHz) fib (50)
klasik yaklaşımla 64 saniye sürüyor ve teorik olarak mümkün olmasa da oldukça dikkat çekici bir kazanç olan Çatal/Birleştirme yaklaşımı ile sadece 18 saniye.
özet
- Evet, Örneğinizde Çatal/Birleştir'in klasik iplik havuzlarına göre avantajı yoktur.
- Engel/Katılma, engelleme söz konusu olduğunda performansı önemli ölçüde artırabilir
- Çatal/Katılma bazı kilitlenme sorunlarını giderir