2 puan yazan GN⁺ 2025-03-01 | 1 yorum | WhatsApp'ta paylaş
  • Java 17 uygulamasında 32 çekirdeğin tamamı doyuma ulaştı ve CPU kullanımı %3.200’e kadar fırladı; thread dump’ı CPU time’a göre sıralayarak birden fazla thread’in TreeMap.put() içinde takıldığını doğruladı
  • İlk şüphelenilen döngü kodu verimsizdi, ancak O(N lg(M)) karmaşıklığı ve girdi boyutu doğrulaması tek başına arıza düzeyindeki çalışma süresini açıklayamıyordu
  • Asıl neden, birden fazla thread’in paylaşılan TreeMap’i koruma olmadan değiştirmesi ve bunun red-black tree içinde bir cycle oluşturmasıydı; sonuç olarak arama ve ekleme işlemleri sonsuz döngüye girdi
  • Basit yeniden üretim kodunda, ExecutorService içinde ve gRPC servisinde de aynı durum görüldü; thread pool, NPE’yi standart çıktıya yansıtmadığı için kök nedeni bulmak daha da zorlaştı
  • Düzeltme, yalnızca Collections.synchronizedMap veya ConcurrentHashMape geçmekle bitmiyor; executor istisna işleme, CPU·NPE uyarıları, statik analiz ve çok iş parçacıklı testlerin de birlikte bulunması gerekiyor

Arıza belirtileri ve ilk ipucu

  • Makine neredeyse ssh ile bağlanılamayacak kadar bozulmuştu ve CPU kullanımı %3.200’e kadar çıkmıştı
    • Host üzerindeki 32 çekirdeğin tamamı kullanılıyordu
    • Bu durum, daha önce yalnızca 1 çekirdeği %100 kullanan hatayla karşılaştırıldığında farklıydı
  • Java 17 runtime’ının thread dump’ında CPU time bilgisi vardı ve CPU time’a göre sıralama yapıldığında benzer thread’ler ortaya çıktı
    • Stack trace java.util.TreeMap.put() metodunu gösteriyordu
    • Uygulama kodundaki konum BusinessLogic.someFunction(BusinessLogic.java:29) idi

İlk şüphelenilen kod ve elenen hipotezler

  • Sorunlu noktadaki kod, unrelatedObjects üzerinde dolaşıyor ama döngü gövdesinde yalnızca relatedObject kullanıyordu
for (SomeOtherType unrelatedObject : unrelatedObjects) {
    treeMap.put(relatedObject.a(), relatedObject.b()); // line 29
}
  • Bu kod aşağıdaki gibi tek bir put çağrısına indirgenebilirdi
treeMap.put(relatedObject.a(), relatedObject.b());
  • unrelatedObjects fonksiyonun ilerleyen kısmında kullanıldığı için parametrenin kendisi kaldırılamıyordu
  • Refactoring sırasında unrelatedObject kullanımının ortadan kalkmış olabileceği düşünüldü
  • Unit testte treeMap ve unrelatedObjects ayrı ayrı 1.000.000 girdiye çıkarılsa da sorun yeniden üretilemedi
    • unrelatedObjects boyutuna N, treeMap boyutuna M denirse karmaşıklık O(N lg(M)) olur
    • 1 dakika düzeyinde çalışma süresi görmek için 100 milyon ila 1 milyar girdi gerektiği değerlendirildi
    • Bu da gerçek uygulamada iki veri yapısının 1.000’i geçmediği varsayımıyla uyuşmuyordu

Korunmayan TreeMap’in oluşturduğu sonsuz döngü

  • TreeMap alanı şu şekilde tanımlanmıştı
private final Map<K,V> treeMap = new TreeMap<>();
  • Birden fazla thread bu TreeMap’e erişiyordu, ancak senkronizasyon ya da başka bir koruma yoktu
  • Java’daki TreeMap, red-black tree olarak gerçeklenir; eşzamanlı değişiklikler sırasında iç düğüm bağlantıları bozulursa cycle oluşabilir
  • Arama yaparken ya da henüz var olmayan bir değeri eklerken bu cycle boyunca ilerlenip sonsuz döngüye girilebilir

Basit yeniden üretim deneyi

  • Paylaşılan TreeMapi birden fazla thread’in rastgele güncellediği bir deney yazıldı
for (int i = 0; i < numThreads; i++) {
    threads.add(new Thread(() -> {
        Random random = new Random();
        for(int j = 0; j < numUpdates; j++) {
            try {
                treeMap.put(random.nextInt(1000), random.nextInt(1000));
            } catch (NullPointerException e) {
                // let it keep going so we can reproduce the issue.
            }
        }
    }));
}
  • Tüm proje SimpleRepro.java içinde yer alıyor
  • Başta try/catch en kritik nokta gibi görünüyordu
    • try/catch olmazsa bazı thread’ler NullPointerException ile ölüyor ve program duruyordu
    • try/catch eklendikten sonra birkaç çalıştırmada %500 CPU kullanımı görüldü
  • Race condition yalnızca veri bozulmasına ya da deadlock’a yol açmaz; veri yapısını sonsuz döngü mümkün olacak şekilde bozup performans arızasına da neden olabilir

TreeMap içindeki cycle’ı doğrulama

  • Reflection ile TreeMapin root, left, right, key, color alanlarına erişip düğümleri ve renkleri yazdıran bir deney hazırlandı
  • Gezinme sırasında daha önce ziyaret edilmiş bir TreeMap.Entry ile tekrar karşılaşılırsa bunun cycle olduğu kabul edildi
private void print(
    Object treeMapEntry, String tabs, IdentityHashMap<Object, Object> visited
) throws Exception {
    if (treeMapEntry != null && !visited.containsKey(treeMapEntry)) {
        visited.put(treeMapEntry, treeMapEntry);
        print(treeMapEntryLeft.get(treeMapEntry), tabs + " ", visited);
        System.out.println(tabs + treeMapEntryKey.get(treeMapEntry) + ":"
            + (treeMapEntryColor.getBoolean(treeMapEntry) ? "BLACK" : "RED"));
        print(treeMapEntryRight.get(treeMapEntry), tabs + " ", visited);
    } else if (treeMapEntry != null && visited.containsKey(treeMapEntry)) {
        System.out.println(tabs + treeMapEntryKey.get(treeMapEntry) + ":"
            + (treeMapEntryColor.getBoolean(treeMapEntry) ? "BLACK" : "RED")
            + " CYCLE"
        );
    }
}
  • Java modül erişim kısıtlarını açmak için çalıştırma sırasında şu JVM argümanı gerekiyordu
--add-opens java.base/java.util=ALL-UNNAMED

Önceki benzer vakalar ve bu örneğin farkı

ExecutorService içinde daha gerçekçi bir yeniden üretim

  • Basitçe NPE’yi yok sayan kod gerçekçi görünmeyebilir, ancak ExecutorService içinde istisnalar kolayca görünmez hale gelebilir
final ExecutorService pool = Executors.newFixedThreadPool(numThreads);
final TreeMap<Integer,Integer> treeMap = new TreeMap<>();
Random random = new Random();

for (int i = 0; i < numThreads*numUpdatesPerThread; i++) {
    pool.submit(() -> {
        treeMap.put(random.nextInt(10000), random.nextInt(10000));
    });
}
  • Tüm kod ExecutorUncaughtRepro.java içinde yer alıyor
  • Çalıştırıldığında program takılıyor ve thread dump, TreeMap.put() içinde kalan thread’leri gösteriyor
  • Standart çıktıda hiçbir şey görünmüyordu
    • Thread pool NPE’yi yutuyor ve sorunun sinyali ortaya çıkmıyordu
    • Gerçek durum da buydu
  • Thread pool’u kendiniz yönetiyorsanız şu önlemler gerekir
    • thread factory üzerinden uncaught exception handler kaydetmek
    • Dönen Future nesnelerini işlemek ve future.get() ile ExecutionException içine sarılmış NPE’yi görmek

gRPC servisinde de aynı sorun

  • gRPC servisi gibi thread pool tabanlı bir serviste de korunmayan TreeMap aynı sorunu üretebilir
@Override
public void addReceipt(
    ReceiptProcessorServiceOuterClass.AddReceiptRequest req,
    StreamObserver<ReceiptProcessorServiceOuterClass.AddReceiptResponse> responseObserver
) {
    int timestamp = req.getTimestamp();
    int totalPrice = req.getTotalPrice();
    receipts.put(timestamp, totalPrice);
    ReceiptProcessorServiceOuterClass.AddReceiptResponse response =
        ReceiptProcessorServiceOuterClass.AddReceiptResponse.newBuilder().build();
    responseObserver.onNext(response);
    responseObserver.onCompleted();
}
  • Tüm kod GrpcRepro.java içinde yer alıyor
  • Protobuf tanımı ReceiptProcessorService.proto içinde bulunuyor
  • Thread dump, grpc-default-executor-* thread’lerinin TreeMap.put() içinde runnable durumda takıldığını gösteriyor

Nedenine dair tahminler ve dillere göre deneyler

  • Olası neden olarak, iki thread’in ağacı bağımsız şekilde ters yönlerde döndürmesi ya da çakışan döndürmelerin yazımlarının interleaving olarak cycle oluşturması ihtimali düşünüldü
  • Ancak hangi thread interleaving’inin cycle oluşturduğuna dair bir kanıt yok
  • Başta NPE’nin gerekli olduğundan şüphelenildi, ancak sonraki deneyler bu varsayımı çürüttü
  • Aynı sorunu birden fazla dilde yeniden üretme girişimleri yapıldı
    • Java, tüm vakanın temeli olduğu için yeniden üretildi
    • C++’taki std::map red-black tree kullanıyor; nadiren segfault yerine thread’lerin takıldığı ve yüksek CPU kullanımı görüldü
    • Go’da da beklentinin aksine sorun yeniden üretildi; kod Go deneyi içinde yer alıyor
    • Ruby, NPE benzeri hataları yakalayabilen bir dil olmasına rağmen yeniden üretilemedi; GIL’in soruna yol açan interleaving’i engellemiş olabileceği düşünülüyor
  • C++ deneyi, beklentinin aksine try/catch ya da null pointer istisnası olmadan da sonsuz döngünün yeniden üretilebildiğini gösterdi
    • Bazen segmentation fault ile sonlanıyordu
    • Çok nadir olarak 10 dakikadan uzun süre ilerleme duruyor ve top çıktısında SimpleRepro %170,8 CPU kullanıyordu
    • C++’ta null pointer referansı segfault oluşturduğundan, null üzerinden geçmeyen bir interleaving’in var olması gerekir
  • Bu sonuç görüldükten sonra Java deneyinde de NPE yakalanmadan 12 kez daha yeniden çalıştırıldı ve NPE catch olmadan da TreeMap.put() sonsuz döngüsü yeniden üretildi

Kolay düzeltme ve veri yapısı seviyesinde savunma

  • En kolay düzeltme, paylaşılan TreeMapi korumaya almaktır
    • Collections.synchronizedMap ile sarmalamak
    • ConcurrentHashMape geçip sıralamayı yalnızca gerektiğinde yapmak
  • Daha tartışmalı bir savunma, red-black tree dolaşımı sırasında ziyaret edilmiş düğümleri izlemektir
    • Daha önce ziyaret edilmiş bir düğüme tekrar gelinirse ConcurrentModificationException fırlatılır
    • Bu, veri bozulmasının kendisini engellemez
    • Ama sonsuz döngüden kaynaklanan %100 CPU kullanımını önleyebilir
    • Ek bellek kullanımı ağaç yüksekliğiyle sınırlıdır, yani O(lg(n))
    • Red-black tree yüksekliği için O(lg(n)) garantisi vardır
    • Standart kütüphanenin bu yaklaşımı benimsemesi pek olası görülmüyor
  • Düzeltme örneğinde getEntry ve put içinde IdentityHashMap ile ziyaret edilen düğümler kaydediliyor
IdentityHashMap<Entry<?,?>, Boolean> visited = new IdentityHashMap<>();

while (p != null) {
    visited.put(p, true);
    int cmp = k.compareTo(p.key);
    if (cmp < 0)
        p = p.left;
    else if (cmp > 0)
        p = p.right;
    else
        return p;

    if (visited.containsKey(p)) {
        throw new ConcurrentModificationException("TreeMap corrupted. Loop detected");
    }
}

Savunma katmanlarını çoğaltmak

  • Hatalar olur; bu yüzden tek bir savunma katmanı yeterli değildir

  • Bu vakada birden fazla hata üst üste geldi, ancak elde kalan bazı gözlem mekanizmaları sayesinde sorun fark edilebildi

  • NPE uyarıları

    • NPE oluşumuna dair doğrudan bir uyarı yoktu
    • Yalnızca hata oranı uyarısı vardı ve bu NPE, API handler worker thread başına yalnızca bir kez oluştuğu için hata oranı eşiğini aşmadı
    • Executor’ün davranışı nedeniyle NPE log’u da oluşmadı
  • CPU kullanım anomali uyarıları

    • CPU kullanımı izleniyor ve basit eşik tabanlı uyarılar kullanılıyordu
    • CPU kullanımı eşiği aşınca anormal davranış olarak değerlendirildi ve uyarı üretildi; sorun bu yolla fark edildi
  • Executor istisna işleme

    • Executor’e iş gönderiyorsanız uncaught exception handler mutlaka ayarlamalısınız
    thread.setUncaughtExceptionHandler(
        (dyingThread, throwable) -> {
            logger.error("uncaught exception!", throwable);
        }
    );
    
  • Kod incelemesi ve statik analiz

    • Kod incelemesinde thread ile TreeMap kombinasyonu fark edilebilir ya da sıralama gerekmiyorsa TreeMap kullanılmaması önerilebilirdi, ancak bu vakada olmadı
    • SpotBugs, JLint, Chord gibi statik analiz araçları bu tür eşzamanlılık sorunlarını build aşamasında yakalayabilir
    • Statik analizle ilgili kaynak olarak How Good is Static Analysis at Finding Concurrency Bugs? veriliyor
  • Testler

    • İlgili kod yolunda çok iş parçacıklı testler yoktu
    • Eşzamanlı erişime açık kodlar için testlerin de çok thread’li senaryoları ele alması gerekir

Sonuç

  • Korunmayan eşzamanlı değişiklikler yalnızca veri bozulmasıyla değil, sonsuz döngü ve yüksek CPU kullanımıyla da ortaya çıkabilir
  • TreeMap gibi iç işleyişinde pointer yapısı olan veri yapıları, birden fazla thread tarafından aynı anda değiştirildiğinde red-black tree yapısında cycle oluşturabilir
  • Senkronize veri yapısı seçimi, istisna işleme, uyarılar, statik analiz ve çok iş parçacıklı testler birlikte kullanılırsa sorunlar daha hızlı bulunabilir ya da tamamen önlenebilir

1 yorum

 
GN⁺ 2025-03-01
Hacker News görüşleri
  • Java’nın temel koleksiyonlarının tasarım gereği thread-safe olmadığı iyi bilinir; bu yüzden bunun yakalanmış olması gerekirdi.
    OP, kodun geri kalanına da göz atıp birden fazla thread’in koleksiyonları aynı anda değiştirme ihtimali olan başka yerler olup olmadığını kontrol etmeli.
    Collections.synchronizedMap ile sarmalamak ya da ConcurrentHashMap ile değiştirmek tek tek map işlemlerini thread-safe yapar; ancak işlemlerin ardışık bir bütününün de güvenli olup olmadığı ayrı bir konudur.
    TreeMap’i sahiplenen nesnenin kendisinin thread-safe olup olmadığı da şüpheli; ziyaret edilen düğümleri izlemek gibi düzeltmeler ise koleksiyon hâlâ güvensizken yalnızca daha incelikli biçimde arıza vermesine yol açar.
    Esas mesele TreeMap’in yan etkisi değil, koleksiyonun sözleşmesinin ihlal edilmesidir; HashMap’e geçirmek de kodu hâlâ hatalı bırakır.
    Birden fazla thread’in işlediği kodda, mümkün olan tüm nesneleri değişmez yapmak; değişmez yapılamayan nesneleri ise küçük, bağımsız ve sıkı denetlenen alanlarla sınırlamak en sağlam yol oldu.

    • Bu kısım gerçekten önemli: thread-safe bir dinamik dizi olsa bile size() ile uzunluğu kontrol edip ardından element(10)’a erişmekten oluşan iki işlemin birleşimi atomik değildir.
      İki çağrı arasında başka bir thread bir öğeyi kaldırırsa sınır dışı erişim oluşabilir.
      Çözüm, iki işi birlikte ele alan atomik bir metot, örneğin element_or_null() gibi bir şey kullanmak ya da en baştan normal bir dizi kullanıp iki işlemin tamamını bir mutex ile korumaktır.
    • Senkronize edilmemiş paylaşılan değiştirilebilir durumda veri yarışı oluşur.
      Üç koşulun da bulunması gerektiğinden çözümler de üç tanedir: lock vb. ile senkronizasyon eklemek, kanal kullanan tek sahipli modelde olduğu gibi değiştirilebilir erişimi paylaşmamak, saf fonksiyonel dillerden gelen içgörü gibi veriyi değişmez yapmak.
      Bildiğim kadarıyla Google da Guava’da bu modele epey yatırım yaptı.
      Rust, bu üçünden hangisinden vazgeçeceğinizi seçmenizi sağlar ve üç koşulun aynı anda gerçekleşmesini statik olarak engeller.
    • Benim de ilk düşüncem “bir dakika, bu veri yapısı thread-safe mi?” oldu; sonuçta amaca uygun olmayan bir veri yapısı seçimi gibi görünüyor.
      Mutex ile zorla uydurmaya çalışmak genellikle başka sorunlar ve darboğazlar doğurur.
      Değişmez nesneler kullanacaksanız, bellek kullanımındaki patlamayı önlemek ya da azaltmak için yapısal paylaşımı kullanan değişmez veri yapıları kullanmanız gerekir.
      Saf fonksiyonel veri yapıları kullanınca sorunun bir tarafı ortadan kalkar; ancak thread’ler birbirlerinin oluşturduğu ara sürümlere bağımlıysa bu kez başka can sıkıcı sorunlar ortaya çıkabilir ve farklı bir veri yapısı gerekebilir.
      Zaten kullanılan değiştirilebilir veri yapısını zorla yaşatmak istiyorsanız, erişim girişimlerini veri yapısına ulaşmadan önce serileştirip erişimi transaction’lar hâlinde gruplayarak yalnızca eksiksiz transaction’ların çalışmasını sağlayabilirsiniz; ama bu noktada neredeyse veritabanı yazıyormuşsunuz gibi olur.
    • Paylaşımlı bellek ortamında bağımsız çalıştırma yapmaya çalışan threading modelinin kendisinin temelden yanlış olduğunu düşünüyorum.
      Böyle bir şeyi çalışır hâle getirmeye harcanan çabanın daha iyi bir process modeline harcanması gerekirdi diye düşünüyorum.
  • Asıl metinde kodu basitleştiren örnek doğru değil.
    Orijinal kod, unrelatedObjects boş olmadığında yalnızca treeMap.put çalıştırıyor; bu fark bug olabilir de olmayabilir de.
    Ayrıca a ve b’nin her seferinde aynı değeri döndürüp döndürmediği, treeMap’in gerçekten yalnızca bir map gibi davranıp davranmadığı da kontrol edilmeli.
    Örneğin güncellemeleri loglayan bir map ise, bunu yalnızca bir kez loglayacak şekilde değiştirmek uygun mu diye değerlendirmek gerekir.

    • Doğru bir nokta. Boş olmadığını kontrol eden bir if ile değiştirmek gerekir.
  • Comparator ya da Comparable implementasyonu tutarlı bir toplam sıralama sağlamasa da sonsuz döngü oluşabilir: https://stackoverflow.com/questions/62994606/concurrentskips...
    Bunun eşzamanlılıkla ilgisi yok.
    Ortaya çıkıp çıkmaması, işlenen somut veriye ve işleme sırasına bağlı olabileceği için uzun süre normal görünürken production ortamında patlayabilir.

    • Bunu gerçekten bizzat görmüş olan var mı merak ediyorum. İyi bir blog yazısı konusu olur.
      Toplam sıralaması olmayan hatalı bir karşılaştırıcıyla ben şahsen henüz karşılaşmadım.
  • Yarış koşullarının yalnızca veri bozulmasına ya da kilitlenmeye yol açtığını sanıyordum; performans sorunları da yaratabileceği aklıma gelmemişti.
    Ama veriyi bozup sonsuz döngü yaratabiliyorsa bu mantıklı.
    Bu yüzden bir projedeki hataların, anormal davranışların ve uyarıların ilke olarak düzeltilmesi gerektiğini sık sık düşünürüm. Çünkü görünüşte ilgisiz sorunlara yol açabilirler.
    Ne var ki ne yapılacağına karar veren tarafta bu ilkenin kabul gördüğü pek nadirdir.

    • İyi bir pratik kural, ama pratiklik de gerekiyor.
      Bazı projelerde tüm hataları, anormal davranışları ve uyarıları ortadan kaldırmanın maliyeti, ara sıra ortaya çıkan görünüşte ilgisiz sorunlara katlanmanın maliyetinden çok daha yüksek olabilir.
      Belirli bir hatanın gelecekteki bir soruna katkıda bulunma olasılığını ve önceden düzeltmenin daha ucuz olup olmayacağını tahmin etmek neredeyse imkânsızdır; bu yüzden iş sonunda bilimden çok zanaata yaklaşır.
      “Hiçbir şeyi düzeltmemek” korkunçtur, “her şeyi düzeltmek” ise çoğu zaman gerçekçi değildir; bu yüzden bir karar verme çerçevesine, deneyimden gelen sezgiye ve paydaşların güvenine ihtiyaç vardır.
    • İş yerinde tüm uyarıları hata olarak ele alıyoruz ve otomatik çalışan tüm CI pipeline’ları geçmeden pull request birleştirilemiyor.
      Disiplin gerektiriyor, ama bir kez o duruma getirdikten sonra sürdürmek çok daha kolay.
    • İstisnaları yakalayıp yok saymamak da önemli.
      Programın çalışmaya devam etmesinin güvenli olduğunu somut olarak bilmiyorsanız “yakalayıp yalnızca loglamak” da kötü bir fikir.
      İstisnanın yayılmasına izin verip 500 döndürmek, hata iletişim kutusu göstermek gibi faydalı bir işlem yapılabilecek yere kadar çıkmasına bırakmak daha iyi.
    • Sonuçta hangi savaşları vereceğinizi seçmeniz gerekiyor.
      Bakımını yaptığım bir Rails projesinde logların “unsupported parameters” ile dolduğu bir sorun vardı; controller’ları dikkatle kontrol edip izin vermemize rağmen görünmeye devam ediyordu.
      Muhtemelen zararsızdı ama loglarda çok fazla gürültü oluşturuyordu.
      Birkaç kişi çözmeye çalıştı, fakat her zaman daha yüksek öncelikli işler vardı ve işlevsel etkisi olmayan bir şeye çok zaman harcamayı gerekçelendirmek de zordu.
      Hemoroit gibi can sıkıcı bir sorun. Ameliyat olup birkaç hafta ciddi acı çekmekle, katlanıp yaşamak arasında bir seçim; çoğunlukla iyi huylu ama kötüleşip büyük bir soruna dönüşme ihtimali de var.
      Bu tür olgulara dijital hemoroit demek uygun olabilir.
    • Bozulma olmasa bile yarış koşulları aynı işi birden çok kez yaptırıp yalnızca tek bir sonucu bırakarak büyük performans sorunlarına yol açabilir.
      Bir uyarının ilgisiz olduğuna karar verdiyseniz en azından bunu yorumla açıklamak ve mümkünse uyarıyı en dar kapsamda kapatan bir pragma eklemek iyi olur.
      Anormal davranışları ortadan kaldırmayı tercih ederim. “Neden çalıştığını bilmiyorum ama” diye işaretlenmiş kodun daha sonra artık çalışmadığı ve önceden temizlense dikkatlice düzeltmeye zaman varken aceleyle yeniden yazmak zorunda kaldığımız durumlar oldu.
  • “ssh ile zar zor bağlandım” kısmını görünce yüksek lisans dönemimdeki bir durum aklıma geldi.
    Laboratuvarımızda paralel/dağıtık işlemeyle ilgili küçük bir gruptan yaklaşık 8 kişi, sanırım Sun UltraSparc 170 olan bir makineyi paylaşıyordu; sabit disk 1GB, RAM de 128 ya da 256MB civarındaydı.
    Sun makinelerin neredeyse hiç yeniden başlatılmadığını da hesaba katmak gerekir.
    Yeni bir öğrenci büyük bir metin dosyasını satır numarasına göre 32 ya da 64 aralığa bölmüş, sonra ayrı dosyalar oluşturmadan perl’ün N kopyasını paralel çalıştırıp her birine kendi satır aralığını işlettirmiş gibiydi.
    O dönemin ölçülerine göre çok sayıda RAM’i N adet perl yorumlayıcısı tüketti; swap başlayınca da aynı dosyanın farklı bölümlerine çılgınca seek yaparak her süreç birkaç satır daha okumaya çalıştı.
    Üstelik J’inci sürecin kendi aralığına ulaşmak için dosyanın J/N kadarını okuması gerekmiş olma ihtimali yüksek.
    Konsolda login prompt’u bile alamıyorduk; neyse ki zaten açık bir oturum vardı ve su 20-30 dakika sonra parola prompt’unu gösterdi.
    5-10 dakika daha geçince root oturumu elde edip top ile nedeni tespit ettik; kullanıcıyla iletişim kurmaya çalışıp sorunlu süreçleri öldürünce sistem normale döndü.
    Fikrin kendisi doğruydu ama sistem sınırlarının hiç anlaşılmadığı bir örnekti; sabit disk ve düşük RAM yüzünden I/O darboğazı aşırıydı, basitçe doğrusal işlemek muhtemelen çok daha iyi olurdu.

  • Java olsun başka bir dil olsun, thread-safe olmayan nesneler üzerinde eşzamanlı işlemler yaparsanız dünyanın en ilginç bug’ları ortaya çıkar.

    • Bu yüzden thread-safe olmayan nesneleri birden fazla thread’de kullanırken atomik erişimi kendiniz yönetmeli ya da thread-safe bir sürüm kullanmalısınız.
      Çok iş parçacıklı çalışma hatalarını debug etmek berbattır.
      Bu durumda tasarım aşamasında tespit etmek çok kolaydı; çok iş parçacıklı bir ortamda normal bir container kullanmaya çalışıldığı anda uyarı ışıkları yanmalıydı.
    • Eşzamanlılık bug’larını epey yaşadım ama Java’daki değiştirilebilir alanlardan volatile niteleyicisini kaldırınca oluşan tutarsızlığı kasıtlı olarak yeniden ürettiğim olmadı.
      O dönemde kullandığım JVM fazla iyi yapılmış olabilir.
    • Senkronize edilmemiş bir şeyi paylaşmanın sonuçlarına katlanabileceğime emin olduğumda bile, gerçek sonuçlar her zaman beklenmedikti.
    • Biraz uç bir düşünce ama bu yüzden C/C++’ta yarış koşulu debug etmeyi daha çok tercih ediyorum.
      Dil, yarış koşulu oluştuğunda pratikte hiçbir anlamı olmayan çılgın semantikler tanımlasa da, gerçekte bellek bozulması ya da gürültülü sorunlar sık sık görünür hale gelir.
      Yarış koşullarının çoğu yasadışı olduğundan, anlamı belirtmek için kaynak kodu değiştirmeden thread checker gibi araçlar yapılabilir.
      Java’da ise tanımsız davranış yoktur ama iki alanın ince biçimde birbirinden sapması kolaydır ve bunları izlemek çok daha zordur.
    • Java Collections’daki bazı, belki de çoğu işlem, bu tür sorunlara karşı uyarmak için bütünlük kontrolleri yapar.
      Örneğin bir map’in ConcurrentModificationException fırlatması gibi.
  • Çalışan bir C# sisteminde de aynı olguyu görmüştüm.
    Belirtiler aynıydı ve process dump’a bakınca bozulmuş bir dictionary vardı.
    Her yerde ConcurrentDictionary kullandığımızı sanıyorduk ama kütüphaneden gelen bir tanesi sorun çıkarmıştı.
    O zaman .NET Framework kullanıyorduk; hatırladığım kadarıyla .NET Core’da eşzamanlı değişikliği algılayan kod var.
    Uygulamayı tam bilmiyorum ama sadece bir sürüm sayacı olsa bile mümkün olurdu.
    NPE’nin temel bileşenmiş gibi takıntı haline getirilmesi tuhaf. Asıl ortaya çıkış biçiminde bu yok gibi görünüyor ve sırf istisna yok diye C’de böyle bir bug olmadığı sonucuna varmak için de bir neden yok.
    Esas mesele sınıf invariant’ları. Genel olarak bir değiştirici çalışırken invariant sağlanmaz, ancak sonunda yeniden kurulur.
    Invariant geri yüklenmeden başka bir değiştirici çalıştırırsanız veri yapısı bozulur. Geçerli bir durumdan başlamıyorsanız, bittiğinde geçerli bir durumda olma olasılığı düşüktür.

    • Sonuçta sebep hatalı mantıktı.
      Yakalanmamış NPE ile yeniden üretememe gibi bir talihsizlik yaşayıp, yakalanmamış NPE’nin gerekli koşul olduğu sonucuna yanlışlıkla vardık.
  • Yeterli senkronizasyon kullanılmayan java.util.HashMap içinde de aynı şeyi gördüm
    2009 civarıydı ama bildiğim kadarıyla bugün de mümkün
    Hatırladığım kadarıyla HashMap, çakışma çözümü için chaining kullanıyor; sanırım zincirin içinde bir döngü oluşmuştu
    Ancak doğrulamak için derine inmek yerine sorunlu kodu tamamen kaldırmaya odaklandım
    Eşzamanlılık bilgisi mülakatlarda sıkça soruluyor; veri yarışlarını önemsiz bir sorun gibi görürseniz iyi bir izlenim bırakmak zor, bu örnek de bunun nedeni

    • HashMap içinde de böyle bir şey olabileceğini bilmiyordum
      Çakışma işleme için kullanılan bağlı listeyle mi ilgili?
  • Döngüleri bulmak için artan bir sayaç tutup, ağaç derinliğini veya koleksiyon boyutunu aşarsa istisna fırlatmak nasıl olur diye düşünüyorum
    Yazarın önerdiği hash kümesi yönteminden farklı olarak bellek ya da CPU ek yükü neredeyse yok ve kabul edilme olasılığı da daha yüksek görünüyor
    Yine de C#’ı 10 yılı aşkın süredir kullanırken, eşzamanlı durumlarda veri yapısına eşzamanlı erişimi hesaba katmadığım hiç olmadı

    • Kötü bir fikir değil ama veri yarışı olan bir durumda işe yarar ön koşullar eklemek genelde epey zordur
      Ağacın bozulma biçimleri bununla sınırlı değil
    • İyi fikir. Bunu daha önce ikili arama ve ağaç yapılarında yapmıştım
      Mümkünse sonsuz döngüden kaçınırsınız; kaçınılamayan durumlar da oldukça nadirdir
      Bir düzeltme değil ama iyi bir hafifletme stratejisi
      Sonsuz döngü en berbat hatalardan biridir. Debugger’da bulması kolaydır ama OP’nin “ssh bile zar zor çalışıyor” durumunda olduğu gibi sonuçları çok kullanıcı düşmanı olabilir
      Özellikle kütüphane kodundaki sonsuz döngüler daha da can sıkıcıdır
    • Bu çok daha iyi. Yalnızca sabit bellek kullanmak yeterli
      Düğüm sayısının ağaç yüksekliğinden fazla olmayacağı garantisi var
  • Thread içindeki istisnalar gerçekten ölümcül
    C++, select() ve istisnaları etrafa savuran bir thread’in başrolde olduğu korkunç bir hata izleme hikâyesi var: https://news.ycombinator.com/item?id=42532979

    • O yazıyı okuduğumu hatırlıyorum ama o alandaki bilgim yetersiz olduğu için anlayamamıştım
      Tekrar okumam gerek