Tarihçe
Phaser, Java 7'de CyclicBarrier ve CountDownLatch'ın katılımcı sınırlamaları ve sabit yapı kısıtlarını aşmak için tanıtıldı. Bu yapılar, önceden belirlenmiş sayıda iş parçacığı gerektiriyor ve yüzlerce iş parçacığı aynı anda tek bir atomic sayaçta yoğun bir şekilde işlem yaparken büyük veri yolu tutarsızlık trafiklerinden muzdarip oluyordu. Tanıtımından önce, geniş ölçekli fork-join boru hatları veya sahnelenmiş hesaplama grafiklerinin, her gelen iş parçacığı merkezi 64-bit durum kelimesini güncellemek için tüm işlemci soketleri arasında bir önbellek satırını geçersiz kılmak zorunda kaldığı için CAS deneme fırtınaları altında çökmesi söz konusuydu.
Problem
Düz bir engelleyici bir bellek sıcak noktası oluşturur; yüzlerce iş parçacığı aynı anda arriveAndAwaitAdvance()'i çağırdığında, faz, katılımcı ve varmamış sayıların yerleştirildiği tek bir atomic değişken üzerinden seri hale gelir, bu da NUMA makinelerini tekrar deneme döngüleriyle bağlantılarını doygun hale getirir. Bu rekabet, CPU'ların yararlı iş yapmaktansa önbellekleri gözetleyerek ve CMPXCHG talimatları üzerinde döngü yaparak daha fazla döngü harcamasına neden olur, bu da mevcut çekirdek sayısına bakılmaksızın verimliliği tek iş parçacıklı bir yürütücü seviyesine düşürür.
Çözüm
Phaser, bir kök Phaser'ın çocuk phaseleri yönettiği, hiyerarşili, ağaç yapısında bir topoloji uygularak varış sayacını donanım sınırlarına hizalanmış farklı bellek konumlarına dağıtır. Varışlar yalnızca bir çocuk fazı tamamlandığında yukarıya doğru yayılır, bu da rekabeti logaritmik olarak amorti eder; kökün atomic durum kelimesi her çocuk tamamlandığında yalnızca bir kez değiştirilir ve iş parçacığı başına değil, bekleyenleri serbest bırakırken gürültülü sürülerden kaçınmak için QNode nesnelerinin Treiber yığını kullanılır.
Problem Tanımı
Yüksek frekanslı bir ticaret platformu, dört NUMA soketi arasında sekiz yüz iş parçacığını senkronize eden üç aşamalı bir boru hattı gerektirdi—piyasa verisi alımı, risk hesaplaması ve sipariş gönderimi. Mevcut CyclicBarrier uygulaması, volatilite açılışında p99 gecikme zirvelerine seksen milisaniyeyi aşan bir artış neden oldu, çünkü sekiz yüz iş parçacığı tek bir 64-bit durum değişkeni üzerinde yarıştılar ve büyük veri yolu kilitlenmelerine neden olan ve çekirdekleri %100 kullanıma sıkıştıran CAS tekrar denemelerini tetiklediler.
Çözüm Değerlendirmesi
Dağıtılmış Sayaclarla Şeritli Engelleyici
Engelleyiciyi otuz iki daha küçük CyclicBarrier örneğine manuel olarak bölmeyi düşündük ve iş parçacıklarını yuvarlak robinde parçalara atadık. Bu yaklaşım, engelleyici başına rekabeti otuz iki kat azaltacak, ancak küresel faz tutarlılığını sağlamak için yarış koşullarına eğilimli ek bir koordinasyon katmanı ekleyerek felaket bir karmaşıklık getirecekti ve dinamik iş parçacığı kaydı, çalışma zamanındaki zirveler sırasında parçalar arasında iş parçacıklarını yeniden dengeleme zorluğu nedeniyle neredeyse imkansız hale geldi.
Düz Phaser Yapılandırması
Tek bir kök Phaser'a geçiş yapmak, dinamik kayıt faydaları sağladı ve sabit katılımcı kısıtlamasını ortadan kaldırdı, ancak profil oluşturma, sekiz yüz iş parçacığının aynı anda arriveAndDeregister'i çağırmasının hâlâ tek atomic durum kelimesinde bir önbellek satırı fırtınası yarattığını gösterdi. Phaser'ın Treiber yığını, Object.wait() ile karşılaştırıldığında park etme maliyetini azalttı, ancak kök sayacı hâlâ bir bellek sıcak noktası olarak kalmış ve bu katılımcı ölçeğinde CyclicBarrier'dan sadece marjinal bir iyileştirme sunmuştur.
Hiyerarşik Phaser Ağaç
Her bir fiziksel CPU soketini bir dal ve bireysel çekirdekleri yapraklara eşleyen dengeli bir dörtlü ağaç Phaser nesneleri uyguladık, bu durumda yerel varışların soket yerel önbellek hatlarıyla sınırlı kalmasını sağladık. Bu logaritmik yayılım, yalnızca dört soket düzeyinde phaselerin kökte rekabet etmesini sağladı ve soketler arası önbellek tutarsızlık trafiğini iki sıralı büyüklükte azalttı, ticaretçi iş parçacıklarının oturum ortasında katılacak şekilde dinamik kayıt gerekliliklerini koruyarak.
Seçilen Çözüm ve Gerekçe
Hiyerarşik ağaç, üretim donanımının NUMA mimarisini doğrudan ele aldığı için seçildi ve O(N) önbellek geçersiz kılmalarını O(log N) soket düzeyindeki güncellemeler haline dönüştürdü. Şeritli engelleyicinin aksine, ağaç Phaser API'sinin basitliğini koruyarak, iş parçacıklarının yapıyı bilmeden yaprak düğümlere kayıt olmasına izin verdi ve içsel ebeveyn-çocuk referansları, arriveAndAwaitAdvance rekürsiyonu aracılığıyla yayılımı otomatik olarak yönetti.
Uygulama Kodu Parçası
// 2 katmanlı ağaç inşa etme: Kök -> Soket -> Çekirdek Phaser root = new Phaser() { protected boolean onAdvance(int phase, int parties) { return phase >= MAX_PHASES || parties == 0; // Sonlanma mantığı } }; Phaser[] socketPhasers = new Phaser[SOCKET_COUNT]; for (int s = 0; s < SOCKET_COUNT; s++) { socketPhasers[s] = new Phaser(root); for (int c = 0; c < CORES_PER_SOCKET; c++) { Phaser corePhaser = new Phaser(socketPhasers[s]); corePhaser.register(); // İşçi iş parçacıklarını ön kayıt etme corePhasers.add(corePhaser); } }
Sonuç
Üretim dağıtımı, faz geçiş gecikmesini p99'u seksen milisaniyeden bir milisaniyenin altına düşürdü, volatilite zirveleri sırasında çekirdek pinlemeyi ortadan kaldırdı ve boru hattı yeniden başlatmaları olmadan yük yanıtında iş parçacığı havuzlarının dinamik olarak ölçeklenmesini sağladı ve nihayetinde ek kırk bin işlemi saniyede işledi.
Aktif bir faz sırasında bir iş parçacığının arriveAndDeregister() çağırması ile diğer bir iş parçacığının register() aracılığıyla aynı anda kaydolması arasında nasıl yarış koşullarını önlüyor?
register(), atomic olarak her iki parties ve unarrived sayısını CAS kullanarak 64-bit durum kelimesine gömülü olarak artırırken, arriveAndDeregister() bunları atomic olarak azaltmak zorundadır ve varmamış sayı sıfıra ulaştığında faz geçişini tetikleyebilir. Uygulama, faz numarası işlemin ortasında ilerlerse kaydın bir sonraki faza ertelenmesini sağlamak için durum kelimesi üzerindeki CAS işlemini tekrar denemekle çözüme ulaşır; bu nedenle, yeni partiler asla kısmi faz geçişleri veya bozulmuş iç sayılar gözlemleyemez.
Neden Phaser, iş parçacıklarını engellemek için Object.wait()/notify() yerine LockSupport.parkNanos() kullanıyor ve bu, "katmanlı varış" protokolü sırasında hangi özel tehlikeyi engelliyor?
Object.monitor mekanizmaları, iş parçacıklarının beklemeden önce bir denetleyici kilidi edinmelerini gerektirir ve bu, merkezi kilit nesnesinde ek bir rekabet noktası yaratır ve varışlar için bekleme özgürlüğü ilerleme garantisini ihlal eder. Phaser, bunun yerine her bekleyen iş parçacığının kısa bir süre dönerken sonra LockSupport.parkNanos()'i aramasını sağlamak üzere bir Treiber yığını QNode nesnelerinden oluşan bir yapı kullanır; bu, gelen iş parçacığının belirli halefleri LockSupport.unpark() aracılığıyla serbest bırakmasına izin verir ve bu, herhangi bir kilidi tutmadan gerçekleşir; bu, bir bildirim iş parçacığının öncelikle bekleyenin denetleyiciye girmeden önce sinyal vermesi durumunda "kaybolmuş uyanma" tehlikesini önler ve özellikle belirli çocuk-phaser bekleyicilerinin ilerlemesini sağlamada hiyerarşik ağaçlarda seçici serbest bırakma sağlar.
Faz tam sayısının Integer.MAX_VALUE'dan sıfıra sarılmasının cebirsel önemi nedir ve bu tam sayının taşması, olur-önce ilişkilerinde zamansal sıralamayı nasıl garanti eder?
Faz sayacı, kasten 2^32 modülünde taşan bir işaretsiz 32-bit tam sayıdır; çünkü Phaser, faz p'nin faz p+1'den önce olduğunu, durum kelimesinde güçlü yazma-okuma çiftleri aracılığıyla garanti eder; taşma, 4 milyar fazdan sonra sıfırlanan bir olur-önce döngüsü yaratır. Adaylar, (phase - targetPhase) < 0 karşılaştırmasının, taşma sınırı üzerinde bile zamansal sıralamayı doğru bir şekilde belirlediğini, iki'nin tamamlayıcısı aritmetiği nedeniyle garantilediğini gözden kaçırır ve Integer.MAX_VALUE fazındaki katılımcıların yaptığı tüm yazmaları doğru bir şekilde algılamasını sağlar; bu da faz alanını görünürlük kenarlarının bir halka tamponu olarak değerlendirmesine olanak tanır.