Bir Konu oluşturmanın neden pahalı olduğu söyleniyor?


180

Java dersleri bir iş parçacığı oluşturmanın pahalı olduğunu söylüyor. Ama neden tam olarak pahalı? Bir Java İş Parçacığı oluşturulduğunda, oluşturulmasını pahalı hale getiren tam olarak neler oluyor? İfadeyi doğru olarak alıyorum, ama sadece JVM'de Thread oluşturma mekaniği ile ilgileniyorum.

İplik yaşam döngüsü yükü. İplik oluşturma ve sökme ücretsiz değildir. Gerçek ek yük, platformlar arasında değişiklik gösterir, ancak iş parçacığı oluşturma işlemi zaman alır ve istek işlemeye gecikme getirir ve JVM ve işletim sistemi tarafından bir miktar işlem etkinliği gerektirir. İstekler, çoğu sunucu uygulamasında olduğu gibi sık ve hafifse, her istek için yeni bir iş parçacığı oluşturmak önemli bilgi işlem kaynaklarını tüketebilir.

Gönderen Uygulama Java eşzamanlılık
tarafından Brian Goetz, Tim Peierls Joshua Bloch, Joseph Bowbeer David Holmes, Doug Lea
ISBN-10 yazdır: 0-321-34960-1


Okuduğunuz öğreticilerin bunu söylediği bağlamı bilmiyorum: yaratımın kendisinin pahalı olduğunu veya "bir iş parçacığı oluşturmak" pahalı olduğunu ima ediyorlar mı? Göstermeye çalıştığım fark, iş parçacığının saf eylemi (onu örnekleme ya da bir şey diyelim) ya da bir iş parçacığına sahip olmanız (yani bir iş parçacığı kullanarak: açıkça ek yüke sahip olmak) arasındadır. Hangisi talep edildi // hangisine sormak istersiniz?
Nanne

9
@typoknig - Yeni bir iş parçacığı oluşturmamaya kıyasla pahalı :)
willcodejavaforfood


1
kazanmak için threadpools. görevler için her zaman yeni evreler oluşturmaya gerek yoktur.
Alexander Mills

Yanıtlar:


149

Java iş parçacığı oluşturma pahalıdır, çünkü işin oldukça az bir kısmı vardır:

  • İplik yığını için büyük bir bellek bloğu tahsis edilmeli ve başlatılmalıdır.
  • Yerel iş parçacığını ana bilgisayar işletim sistemine oluşturmak / kaydetmek için sistem çağrıları yapılmalıdır.
  • Tanımlayıcıların oluşturulması, başlatılması ve JVM iç veri yapılarına eklenmesi gerekir.

İpliğin canlı olduğu sürece kaynakları bağlaması açısından da pahalıdır; örneğin iplik yığını, yığından erişilebilen nesneler, JVM iplik tanımlayıcıları, işletim sistemi yerel iplik tanımlayıcıları.

Tüm bunların maliyeti platforma özgüdür, ancak şimdiye kadar karşılaştığım herhangi bir Java platformunda ucuz değildir.


Bir Google araması, 2002 vintage Linux çalıştıran 2002 vintage çift işlemcili Xeon'da Sun Java 1.4.1'de saniyede ~ 4000 iş parçacığı oluşturma oranı rapor eden eski bir ölçüt buldu . Daha modern bir platform daha iyi rakamlar verecektir ... ve metodoloji hakkında yorum yapamam ... ama en azından iplik yaratmanın ne kadar pahalı olabileceğine dair bir basketbol sahası veriyor .

Peter Lawrey'in kıyaslaması, iş parçacığı oluşturmanın bu günlerde mutlak olarak önemli ölçüde daha hızlı olduğunu gösteriyor, ancak bunun ne kadarının Java ve / veya OS ... veya daha yüksek işlemci hızlarındaki iyileştirmelerden kaynaklandığı belli değil. Ancak, her seferinde yeni bir iş parçacığı oluşturma / başlatmaya karşı bir iş parçacığı havuzu kullanırsanız , sayıları hala 150'den fazla bir iyileşme olduğunu gösterir. (Ve bunların hepsinin göreceli olduğuna dikkat çekiyor ...)


(Yukarıda "yeşil iplikler" yerine "yerel iplikler" olduğu varsayılır, ancak modern JVM'lerin tümü performans nedenleriyle yerel iplikleri kullanır. Yeşil iplikler oluşturmak için daha ucuzdur, ancak diğer alanlarda ödeme yaparsınız.)


Bir Java iş parçacığı yığını gerçekten tahsis nasıl görmek için biraz kazma yaptım. Linux'ta OpenJDK 6 durumunda, iş parçacığı yığını pthread_createyerel iş parçacığını oluşturan çağrı tarafından ayrılır . (JVM önceden yerleştirilmiş pthread_createbir yığını geçmez .)

Daha sonra, pthread_createyığın içinde mmapaşağıdaki gibi bir çağrı ile ayrılır :

mmap(0, attr.__stacksize, 
     PROT_READ|PROT_WRITE|PROT_EXEC, 
     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

Buna göre man mmap, MAP_ANONYMOUSbayrak belleğin sıfırlanmasına neden olur.

Bu nedenle, yeni Java iş parçacığı yığınlarının (JVM spesifikasyonuna göre) sıfırlanması gerekli olmasa da, pratikte (en azından Linux'ta OpenJDK 6 ile) sıfırlanırlar.


2
@Raedwald - pahalı olan başlatma kısmıdır. Bir yerlerde, blok bir iş parçacığı yığınına dönüştürülmeden önce bir şey (örn. GC veya OS) baytları sıfırlar. Bu, tipik donanımda fiziksel bellek döngülerini alır.
Stephen C

2
"Bir yerlerde, bir şey (örn. GC veya OS) baytları sıfırlayacaktır". Olacak? İşletim sistemi, güvenlik nedeniyle yeni bir bellek sayfasının tahsisini gerektiriyorsa çalışacaktır. Ancak bu olağandışı olacaktır. Ve işletim sistemi zaten sıfırlanmış sayfaların önbelleğini tutabilir (IIRC, Linux bunu yapar). JVM'nin herhangi bir Java programının içeriğini okumasını önleyeceği göz önüne alındığında GC neden rahatsız olsun? malloc()JVM'nin iyi kullanabileceği standart C işlevinin, tahsis edilen belleğin sıfır olduğunu garanti etmediğini unutmayın (muhtemelen bu tür performans sorunlarından kaçınmak için).
Raedwald

1
stackoverflow.com/questions/2117072/... "Bir ana faktör, her bir iş parçacığına atanan yığın belleğidir".
Raedwald

2
@Raedwald - yığının gerçekte nasıl tahsis edildiği hakkında bilgi için güncellenmiş cevaba bakınız.
Stephen C

2
mmap()Çağrı tarafından ayrılan bellek sayfalarının sıfır sayfa ile yazma üzerine eşlenmiş olması mümkündür ( bu nedenle başlatmalar kendi içinde değil mmap(), sayfalar ilk kez yazıldığında ve sonra yalnızca bir sayfa bir zaman. Yani, iş parçacığı yürütmeye başladığında, oluşturucu iş parçacığı yerine oluşturulan iş parçacığı tarafından üretilen maliyetle.
Raedwald

76

Diğerleri, diş çekme maliyetlerinin nereden geldiğini tartıştılar. Bir iş parçacığı oluşturma birçok operasyonlara oranla pahalı değil, Bu yüzden cevap kapakları nispeten olan görev yürütme alternatifleri ile kıyaslandığında pahalı nispeten daha az pahalı.

Görevi başka bir iş parçacığında çalıştırmanın en belirgin alternatifi, görevi aynı iş parçacığında çalıştırmaktır. Daha fazla iş parçacığının her zaman daha iyi olduğunu varsayanlar için bunu anlamak zordur. Mantık, görevi başka bir iş parçacığına ekleme yükü kaydettiğiniz zamandan daha büyükse, görevi geçerli iş parçacığında gerçekleştirmenin daha hızlı olabilmesidir.

Başka bir alternatif de bir iş parçacığı havuzu kullanmaktır. Bir iş parçacığı havuzu iki nedenden dolayı daha verimli olabilir. 1) önceden oluşturulmuş konuları yeniden kullanır. 2) optimum performansa sahip olmak için iplik sayısını ayarlayabilir / kontrol edebilirsiniz.

Aşağıdaki program yazdırılır ....

Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us

Bu, her bir diş çekme seçeneğinin ek yükünü ortaya çıkaran önemsiz bir görev için yapılan bir testtir. (Bu sınama görevi, geçerli iş parçacığında en iyi performans gösteren görev türüdür.)

final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
    @Override
    public void run() {
        queue.add(1);
    }
};

for (int t = 0; t < 3; t++) {
    {
        long start = System.nanoTime();
        int runs = 20000;
        for (int i = 0; i < runs; i++)
            new Thread(task).start();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
    }
    {
        int threads = Runtime.getRuntime().availableProcessors();
        ExecutorService es = Executors.newFixedThreadPool(threads);
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            es.execute(task);
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
        es.shutdown();
    }
    {
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            task.run();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
    }
}
}

Gördüğünüz gibi, yeni bir iplik oluşturmak sadece ~ 70 µs. Bu, çoğu olmasa da birçok kullanım durumunda önemsiz olarak değerlendirilebilir. Nispeten konuşmak gerekirse, alternatiflerden daha pahalıdır ve bazı durumlarda bir iplik havuzu veya hiç iplik kullanmamak daha iyi bir çözümdür.


8
Bu harika bir kod parçası. Özlü, noktaya ve açıkça kendi jist görüntüler.
Nicholas

Son blokta, sonucun çarpık olduğuna inanıyorum, çünkü ilk iki blokta ana iplik, işçi ipliklerinin koyduğu gibi paralel olarak çıkarılıyor. Ancak son blokta, alma eylemi seri olarak gerçekleştirilir, bu nedenle değeri genişletir. Muhtemelen iş parçacıklarının tamamlanmasını beklemek için queue.clear () ve bir CountDownLatch kullanabilirsiniz.
Victor Grazi

@VictorGrazi Sonuçları merkezi olarak toplamak istediğinizi düşünüyorum. Her durumda aynı miktarda kuyruk işi yapıyor. Geri sayım mandalı biraz daha hızlı olurdu.
Peter Lawrey

Aslında, neden bir sayacı artırmak gibi sürekli hızlı bir şey yapmıyorsunuz; tüm BlockingQueue şeyi bırakın. Derleyicinin artış işlemini optimize etmesini önlemek için sondaki sayacı kontrol edin
Victor Grazi

@grazi bunu bu durumda yapabilirsiniz, ancak en gerçekçi durumlarda bir tezgahta beklemek verimsiz olabileceği için yapamazsınız. Bunu yaparsanız örnekler arasındaki fark daha da büyük olurdu.
Peter Lawrey

31

Teorik olarak, bu JVM'ye bağlıdır. Pratikte, her iş parçacığı nispeten büyük miktarda yığın bellek (varsayılan 256 KB, sanırım) vardır. Ayrıca, iş parçacıkları OS iş parçacıkları olarak uygulanır, bu nedenle bunları oluşturmak bir işletim sistemi çağrısı, yani bir bağlam anahtarı içerir.

Bilgi işlemdeki “pahalı” nın her zaman çok göreceli olduğunu fark edin. İş parçacığı oluşturma, çoğu nesnenin oluşturulmasına göre çok pahalıdır, ancak rastgele bir sabit disk aramasına göre çok pahalı değildir. Her ne pahasına olursa olsun iş parçacığı oluşturmaktan kaçınmak zorunda değilsiniz, ancak saniyede yüzlerce tane oluşturmak akıllı bir hareket değildir. Çoğu durumda, tasarımınız çok sayıda iş parçacığı gerektiriyorsa, sınırlı boyutlu bir iş parçacığı havuzu kullanmalısınız.


9
Btw kb = kilo bit, kB = kilo bayt. Gb = giga biti, GB = giga bayt.
Peter Lawrey

@PeterLawrey 'kb' ve 'kB' içindeki 'k' harfini büyük yapar mıyız, yani 'Gb' ve 'GB' için simetri var mı? Bunlar beni rahatsız ediyor.
Jack

3
@Jack bir bulunmaktadır K= 1024 ve k) = 1000 en.wikipedia.org/wiki/Kibibyte
Peter Lawrey

9

İki çeşit iplik vardır:

  1. Uygun dişler : Bunlar, temel işletim sisteminin diş açma tesislerinin etrafındaki soyutlamalardır. Bu nedenle iplik oluşturma, sisteminki kadar pahalıdır - her zaman bir ek yük vardır.

  2. "Yeşil" iş parçacıkları : JVM tarafından yaratıldı ve planlandı, bunlar daha ucuz, ancak uygun bir paralellik oluşmuyor. Bunlar iş parçacıkları gibi davranır, ancak işletim sistemindeki JVM iş parçacığında yürütülür. Onlar benim bilgime pek alışkın değiller.

İplik oluşturma yükünde düşünebildiğim en büyük faktör, dişleriniz için tanımladığınız yığın boyutudur . VM çalıştırılırken iş parçacığı yığın boyutu parametre olarak geçirilebilir.

Bunun dışında, iş parçacığı oluşturma çoğunlukla işletim sistemine ve hatta VM uygulamasına bağlıdır.

Şimdi, bir şeye dikkat çekeyim: Saniyede 2000 iş parçacığı çalıştırmayı planlıyorsanız iş parçacığı oluşturmak pahalıdır . JVM bunun üstesinden gelmek için tasarlanmamıştır . Eğer ateşlenip öldürülmeyecek birkaç istikrarlı işçiniz varsa, rahatlayın.


19
“... işten atılmayacak ve öldürülemeyecek birkaç istikrarlı işçi ...” Neden işyeri koşullarını düşünmeye başladım? :-)
Stephen C

6

Oluşturmak Threads, bir değil iki yeni yığın (biri java kodu için, biri yerel kod için) yapmak zorunda olduğundan adil miktarda bellek ayırmayı gerektirir. Kullanımı executors / Konu Havuzlar için birden görevler için konuları tekrar kullanarak, yükü önleyebilirsiniz Executor .


@Raedwald, ayrı yığınlar kullanan jvm nedir?
bestsss

1
Philip JP 2 yığın diyor.
Raedwald

Bildiğim kadarıyla, tüm JVM'ler iş parçacığı başına iki yığın tahsis eder. Çöp toplamanın Java kodunu (JIT'de bile) serbest dökümden farklı şekilde işlemesi yararlıdır c.
Philip JF

@Philip JF Lütfen detaylandırır mısınız? Java kodu ve diğeri yerel kod için 2 yığın ile ne demek istersiniz? Bu ne işe yarıyor?
Gurinder

"Bildiğim kadarıyla, tüm JVM'ler iş parçacığı başına iki yığın ayırıyor." - Bunu destekleyecek hiçbir kanıt görmedim. Belki de JVM spesifikasyonundaki opstack'ın gerçek doğasını yanlış anlıyorsunuzdur. (Bu, bayt kodlarının davranışını modellemenin bir yoludur, bunları yürütmek için çalışma zamanında kullanılması gereken bir şey değildir.)
Stephen C

1

Açıkçası sorunun temel noktası 'pahalı' ne anlama geliyor.

Bir iş parçacığının bir yığın oluşturması ve çalışma yöntemine göre yığını başlatması gerekir.

Kontrol durumu yapılarını, yani hangi durumda çalıştırılabileceğini, beklediğini vb.

Muhtemelen bu şeyleri ayarlamak için iyi bir senkronizasyon var.

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.