Penggunaan CPU 3.200%
(josephmate.github.io)- Pada aplikasi Java 17, seluruh 32 core tersaturasi, membuat penggunaan CPU melonjak hingga 3.200%; pengurutan thread dump berdasarkan CPU time menunjukkan beberapa thread tertahan di
TreeMap.put() - Kode loop yang awalnya dicurigai memang tidak efisien, tetapi kompleksitas
O(N lg(M))dan validasi ukuran input saja tidak cukup menjelaskan waktu eksekusi pada level gangguan layanan - Penyebab sebenarnya adalah beberapa thread memodifikasi TreeMap bersama tanpa proteksi, sehingga muncul cycle di dalam red-black tree dan operasi pencarian/penyisipan masuk ke loop tak berujung
- Fenomena yang sama muncul pada kode reproduksi sederhana,
ExecutorService, dan layanan gRPC; thread pool tidak menampilkan NPE ke standard output, sehingga akar masalah makin sulit ditemukan - Perbaikannya tidak cukup hanya dengan beralih ke
Collections.synchronizedMapatauConcurrentHashMap; perlu juga penanganan exception di executor, alert CPU/NPE, analisis statis, dan pengujian multithread
Gejala gangguan dan petunjuk pertama
- Mesin rusak parah sampai hampir sulit diakses lewat
ssh, dan penggunaan CPU naik hingga 3.200%- Seluruh 32 core pada host sedang digunakan
- Ini kontras dengan bug sebelumnya yang hanya menggunakan 1 core pada 100%
- Thread dump dari runtime Java 17 menyertakan CPU time, dan pengurutan berdasarkan CPU time memperlihatkan thread-thread yang mirip
- Stack mengarah ke
java.util.TreeMap.put() - Lokasi kode aplikasi adalah
BusinessLogic.someFunction(BusinessLogic.java:29)
- Stack mengarah ke
Kode yang awalnya dicurigai dan hipotesis yang dieliminasi
- Kode di titik masalah melakukan iterasi atas
unrelatedObjects, tetapi di dalam body loop hanya menggunakanrelatedObject
for (SomeOtherType unrelatedObject : unrelatedObjects) {
treeMap.put(relatedObject.a(), relatedObject.b()); // line 29
}
- Kode ini bisa disederhanakan menjadi satu kali
putseperti berikut
treeMap.put(relatedObject.a(), relatedObject.b());
- Karena
unrelatedObjectsdigunakan di bagian akhir fungsi, parameter itu sendiri tidak bisa dihapus - Ada kemungkinan penggunaan
unrelatedObjecthilang selama proses refactoring - Pada unit test, masalah tidak dapat direproduksi meski
treeMapdanunrelatedObjectsmasing-masing dibesarkan hingga 1.000.000 entri- Jika ukuran
unrelatedObjectsadalahNdan ukurantreeMapadalahM, kompleksitasnyaO(N lg(M)) - Untuk melihat waktu eksekusi sekitar 1 menit, diperkirakan perlu ukuran 100 juta hingga 1 miliar entri
- Ini juga tidak cocok dengan asumsi bahwa di aplikasi nyata kedua struktur data tersebut tidak melebihi 1.000 entri
- Jika ukuran
Loop tak berujung yang dibuat TreeMap tanpa proteksi
- Definisi field
TreeMapadalah sebagai berikut
private final Map<K,V> treeMap = new TreeMap<>();
- Beberapa thread mengakses TreeMap ini, tetapi tidak ada sinkronisasi atau mekanisme proteksi
TreeMapdi Java diimplementasikan sebagai red-black tree, dan jika koneksi node internal rusak akibat modifikasi serentak, cycle dapat terbentuk- Saat mencari atau memasukkan nilai yang belum ada, traversal dapat mengikuti cycle tersebut dan masuk ke loop tak berujung
Eksperimen reproduksi sederhana
- Ditulis eksperimen di mana beberapa thread memperbarui
TreeMapbersama secara acak
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.
}
}
}));
}
- Proyek lengkapnya ada di SimpleRepro.java
- Awalnya
try/catchtampak seperti bagian kunci- Tanpa
try/catch, sebagian thread mati karenaNullPointerExceptiondan program berhenti - Setelah menambahkan
try/catchdan menjalankannya beberapa kali, terlihat penggunaan CPU 500%
- Tanpa
- Race condition tidak hanya menyebabkan kerusakan data atau deadlock, tetapi juga dapat merusak struktur data menjadi bentuk yang memungkinkan loop tak berujung, sehingga berujung pada gangguan performa
Memastikan cycle internal TreeMap
- Ditulis eksperimen yang memakai reflection untuk mengakses field
root,left,right,key, dancolorpadaTreeMap, lalu mencetak node dan warnanya - Jika saat traversal bertemu lagi dengan
TreeMap.Entryyang sudah dikunjungi, itu dianggap sebagai cycle
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"
);
}
}
- Untuk membuka pembatasan akses modul Java, argumen JVM berikut diperlukan saat menjalankan
--add-opens java.base/java.util=ALL-UNNAMED
Kasus terkait yang sudah ada dan perbedaannya
- Masalah dengan tipe yang sama sudah pernah diketahui sebelumnya
- Kasus ini ikut meninjau kode reproduksi nyata, upaya reproduksi lintas bahasa, serta ide modifikasi
TreeMapdan red-black tree
Reproduksi yang lebih realistis dengan ExecutorService
- Kode yang sekadar mengabaikan NPE mungkin terlihat tidak realistis, tetapi pada
ExecutorService, exception bisa tidak mudah terlihat
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));
});
}
- Kode lengkapnya ada di ExecutorUncaughtRepro.java
- Saat dijalankan, program berhenti, dan thread dump menunjukkan thread yang tertahan di
TreeMap.put() - Tidak ada apa pun di standard output
- Thread pool menelan NPE sehingga sinyal masalah tidak terlihat
- Situasi nyata juga seperti ini
- Jika mengelola thread pool sendiri, langkah berikut diperlukan
- Mendaftarkan uncaught exception handler melalui thread factory
- Memproses
Futureyang dikembalikan dan memeriksa NPE yang dibungkus dalamExecutionExceptionlewatfuture.get()
Masalah yang sama pada layanan gRPC
- Pada layanan berbasis thread pool seperti layanan gRPC,
TreeMaptanpa proteksi juga dapat menimbulkan masalah yang sama
@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();
}
- Kode lengkapnya ada di GrpcRepro.java
- Definisi protobuf ada di ReceiptProcessorService.proto
- Thread dump menunjukkan thread
grpc-default-executor-*berhenti dalam status runnable diTreeMap.put()
Dugaan penyebab dan eksperimen lintas bahasa
- Kemungkinan penyebabnya diduga berupa dua thread yang secara independen memutar tree ke arah berlawanan, atau penulisan dari rotasi yang tumpang tindih saling ter-interleave hingga membentuk cycle
- Namun tidak ada pembuktian tentang interleaving thread mana yang membentuk cycle
- Awalnya NPE diduga diperlukan, tetapi eksperimen berikutnya mematahkan asumsi ini
- Upaya reproduksi masalah yang sama dilakukan di beberapa bahasa
- Java menjadi basis seluruh kasus, sehingga dapat direproduksi
std::mappada C++ menggunakan red-black tree, dan sesekali thread berhenti dengan penggunaan CPU tinggi, bukan segfault- Go juga, berbeda dari perkiraan, dapat direproduksi; kodenya ada di eksperimen Go
- Ruby tidak berhasil direproduksi meski bahasanya dapat menangkap NPE, dan ada kemungkinan GIL mencegah interleaving yang memicu masalah
- Eksperimen C++, di luar dugaan, mereproduksi loop tak berujung bahkan tanpa
try/catchatau exception null pointer- Kadang berakhir dengan
segmentation fault - Sangat jarang, proses berhenti lebih dari 10 menit dan
topmenunjukkanSimpleRepromenggunakan 170,8% CPU - Karena dereference null pointer di C++ adalah segfault, harus ada interleaving yang tidak melewati null
- Kadang berakhir dengan
- Setelah melihat hasil ini, eksperimen Java dijalankan ulang 12 kali tanpa menangkap NPE, dan loop tak berujung di
TreeMap.put()tetap berhasil direproduksi tanpa catch NPE
Perbaikan mudah dan pertahanan di level struktur data
- Perbaikan paling mudah adalah melindungi
TreeMapbersama- Membungkusnya dengan
Collections.synchronizedMap - Menggantinya dengan
ConcurrentHashMapdan hanya melakukan pengurutan saat diperlukan
- Membungkusnya dengan
- Pertahanan yang lebih kontroversial adalah melacak node yang sudah dikunjungi saat traversal red-black tree
- Jika bertemu kembali dengan node yang sudah dikunjungi, lempar
ConcurrentModificationException - Ini tidak mencegah kerusakan data itu sendiri
- Tetapi dapat mencegah penggunaan CPU 100% akibat loop tak berujung
- Memori tambahan dibatasi oleh tinggi tree, yaitu
O(lg(n)) - Tinggi red-black tree dijamin
O(lg(n)) - Kemungkinan kecil standard library akan mengadopsi cara ini
- Jika bertemu kembali dengan node yang sudah dikunjungi, lempar
- Contoh perbaikan mencatat node yang sudah dikunjungi dengan
IdentityHashMapdigetEntrydanput
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");
}
}
- Kode perbaikan lengkapnya ada di ProtectedTreeMap.java
Membangun beberapa lapis pertahanan
-
Kesalahan bisa terjadi, jadi satu lapis pertahanan saja tidak cukup
-
Dalam kasus ini, beberapa kesalahan terjadi bersamaan, tetapi sebagian pemantauan masih tersisa sehingga masalah dapat ditemukan
-
Alert NPE
- Tidak ada alert untuk terjadinya NPE itu sendiri
- Hanya ada alert tingkat error, dan NPE ini hanya terjadi sekali per API handler worker thread sehingga tidak melewati ambang tingkat error
- Karena cara executor menanganinya, log NPE juga tidak tersisa
-
Alert anomali penggunaan CPU
- Penggunaan CPU dipantau dan alert berbasis ambang sederhana digunakan
- Saat penggunaan CPU melewati ambang, itu dinilai sebagai perilaku abnormal dan alert muncul; lewat jalur inilah masalah ditemukan
-
Penanganan exception executor
- Jika memasukkan pekerjaan ke executor, uncaught exception handler harus selalu disetel
thread.setUncaughtExceptionHandler( (dyingThread, throwable) -> { logger.error("uncaught exception!", throwable); } );- Alih-alih
ThreadFactorybuatan sendiri, dapat digunakan Apache Commons BasicThreadFactory atau Guava ThreadFactoryBuilder - Tanpa handler, exception tertelan dan monitoring log juga tidak dapat mendeteksi masalah
-
Code review dan analisis statis
- Pada code review, kombinasi thread dan
TreeMapbisa saja ditemukan, atau mungkin muncul saran untuk tidak memakaiTreeMapjika pengurutan tidak diperlukan, tetapi ini tidak terjadi dalam kasus ini - Alat analisis statis seperti SpotBugs, JLint, dan Chord dapat menemukan masalah concurrency semacam ini pada saat build
- Sebagai materi terkait analisis statis, disebutkan How Good is Static Analysis at Finding Concurrency Bugs?
- Pada code review, kombinasi thread dan
-
Pengujian
- Tidak ada pengujian multithread untuk jalur kode tersebut
- Jika kode dapat diakses secara bersamaan, pengujiannya juga perlu mencakup situasi multithread
Kesimpulan
- Modifikasi serentak tanpa proteksi tidak hanya menyebabkan kerusakan data, tetapi juga dapat muncul sebagai loop tak berujung dan penggunaan CPU tinggi
- Jika struktur data dengan pointer internal seperti
TreeMapdimodifikasi oleh beberapa thread secara bersamaan, struktur red-black tree dapat membentuk cycle - Memilih struktur data tersinkronisasi, penanganan exception, alert, analisis statis, dan pengujian multithread secara bersamaan dapat membantu menemukan atau mencegah masalah lebih cepat
1 komentar
Pendapat di Hacker News
Sudah diketahui luas bahwa koleksi inti Java secara desain tidak thread-safe, jadi ini seharusnya terdeteksi
OP perlu menelusuri sisa kodenya juga untuk memastikan apakah ada tempat lain yang memungkinkan beberapa thread memanipulasi koleksi secara bersamaan
Membungkusnya dengan
Collections.synchronizedMapatau menggantinya denganConcurrentHashMapakan membuat operasi map individual menjadi thread-safe, tetapi apakah rangkaian beberapa operasi juga aman adalah persoalan terpisahObjek yang memiliki
TreeMapitu sendiri juga patut dicurigai apakah thread-safe, dan modifikasi seperti melacak node yang sudah dikunjungi hanya membuat koleksinya tetap tidak aman sambil gagal dengan cara yang lebih subtilIntinya bukan efek samping
TreeMap, melainkan pelanggaran kontrak koleksi; menggantinya denganHashMappun tetap kode yang salahDalam kode yang ditangani banyak thread, pendekatan yang paling pasti adalah membuat semua objek yang mungkin menjadi immutable, dan membatasi objek yang tidak bisa dibuat immutable hanya pada area kecil, independen, dan dikendalikan ketat
size()lalu mengakseselement(10)—tidak atomikJika thread lain menghapus elemen di antara dua pemanggilan itu, akses di luar batas bisa terjadi
Solusinya adalah memakai metode atomik yang menangani keduanya sekaligus, misalnya
element_or_null(), atau sejak awal memakai array biasa dan melindungi keseluruhan dua operasi tersebut dengan mutexKarena ketiga syarat itu semuanya diperlukan, solusinya juga ada tiga: menambahkan sinkronisasi seperti lock, tidak membagikan akses mutable seperti pada model single owner yang memakai channel, atau membuat data menjadi immutable seperti insight dari bahasa fungsional murni
Sejauh yang saya tahu, Google juga cukup banyak berinvestasi pada model ini di Guava
Rust memungkinkan kita memilih mana dari tiga hal ini yang akan dilepas, dan secara statis mencegah ketiga syarat itu berlaku bersamaan
Jika dipaksakan dengan mutex, biasanya akan muncul masalah lain dan bottleneck
Jika ingin memakai objek immutable, perlu memakai struktur data immutable yang memanfaatkan structural sharing agar lonjakan penggunaan memori bisa dihindari atau dikurangi
Dengan struktur data fungsional murni, satu sisi masalahnya hilang, tetapi jika thread-thread saling bergantung pada versi perantara yang dibuat satu sama lain, akan muncul masalah lain yang merepotkan dan mungkin perlu struktur data lain
Jika tetap memaksakan struktur data mutable yang sudah dipakai, upaya akses bisa diserialisasi sebelum mencapai struktur data, lalu akses dibungkus sebagai transaksi sehingga hanya transaksi lengkap yang dijalankan; lama-lama rasanya seperti hampir mengimplementasikan database
Upaya untuk membuat hal semacam itu bekerja seharusnya dicurahkan pada model proses yang lebih baik
Contoh penyederhanaan kode di artikel asli tidak akurat
Kode aslinya hanya menjalankan
treeMap.putketikaunrelatedObjectstidak kosong, dan perbedaan ini bisa saja merupakan bug, bisa juga bukanSelain itu, perlu dipastikan apakah
adanbmengembalikan nilai yang sama setiap kali, dan apakahtreeMapbenar-benar berperilaku hanya seperti mapMisalnya, jika itu map yang mencatat update ke log, perlu dipertimbangkan apakah mengubahnya agar hanya mencatat log sekali saja memang tidak masalah
ifyang memeriksa apakah tidak kosongLoop tak berhingga juga bisa terjadi jika implementasi Comparator atau
Comparabletidak menyediakan total order yang konsisten: https://stackoverflow.com/questions/62994606/concurrentskips...Ini tidak terkait dengan concurrency
Terjadi atau tidaknya bisa bergantung pada data konkret yang diproses dan urutan pemrosesannya, sehingga dapat terlihat normal dalam waktu lama lalu meledak di lingkungan produksi
Secara pribadi saya belum pernah menemui comparator buggy yang tidak memiliki total order
Saya dulu mengira race condition hanya menyebabkan kerusakan data atau deadlock, tidak terpikir bahwa ia juga bisa menimbulkan masalah performa
Namun kalau race condition bisa merusak data hingga membuat infinite loop, itu masuk akal
Karena itu saya sering berpikir bahwa error, perilaku aneh, dan warning dalam proyek pada prinsipnya harus diperbaiki. Sebab hal-hal itu bisa menimbulkan masalah yang tampaknya tidak berkaitan
Namun jarang sekali prinsip seperti ini diterima oleh pihak yang menentukan apa yang harus dikerjakan
Dalam beberapa proyek, biaya untuk menghilangkan semua error, perilaku aneh, dan warning bisa jauh lebih besar daripada biaya menanggung masalah yang sesekali muncul dan tampaknya tidak berkaitan
Hampir mustahil memprediksi apakah error tertentu akan terlibat dalam masalah di masa depan dan apakah biaya memperbaikinya lebih awal akan lebih murah, jadi pada akhirnya ini lebih dekat ke keterampilan daripada sains
“Tidak memperbaiki apa pun” itu buruk, dan “memperbaiki semuanya” biasanya tidak realistis, jadi dibutuhkan kerangka pengambilan keputusan, intuisi dari pengalaman, serta kepercayaan para pemangku kepentingan
Ini membutuhkan disiplin, tetapi setelah sudah dibuat seperti itu, mempertahankannya jauh lebih mudah
Kecuali kita tahu secara spesifik bahwa aman untuk melanjutkan eksekusi program, “menangkap lalu hanya mencatat ke log” juga ide yang buruk
Lebih baik membiarkan exception dipropagasikan sampai ke tempat yang bisa menanganinya secara berguna, misalnya mengembalikan
500atau menampilkan dialog errorAda masalah di proyek Rails yang saya rawat: log penuh dengan “unsupported parameters”. Saya sudah memeriksa controller dengan cermat dan mengizinkannya, tetapi pesan itu tetap muncul
Mungkin tidak berbahaya, tetapi membuat log sangat berisik
Beberapa orang mencoba menyelesaikannya, tetapi selalu ada prioritas yang lebih tinggi, dan sulit membenarkan menghabiskan banyak waktu untuk sesuatu yang tidak berdampak pada fitur
Ini seperti wasir yang menjengkelkan. Pilihannya antara operasi lalu menderita berat selama beberapa minggu, atau menahannya saja; biasanya jinak, tetapi ada kemungkinan memburuk dan menjadi masalah besar
Rasanya fenomena seperti ini boleh disebut wasir digital
Jika sebuah warning dinilai tidak relevan, setidaknya sebaiknya dijelaskan dengan komentar, dan kalau memungkinkan tambahkan
pragmayang mematikan warning dalam cakupan sesempit mungkinSaya lebih suka menghilangkan perilaku aneh. Saya pernah melihat kode yang ditandai “tidak tahu kenapa ini bekerja” kemudian tidak lagi bekerja, dan seandainya dirapikan lebih awal, kami akan punya waktu untuk memperbaikinya dengan hati-hati, bukannya harus menulis ulang dengan tergesa-gesa
Bagian “nyaris berhasil masuk lewat ssh” mengingatkan saya pada masa pascasarjana
Lab kami adalah kelompok kecil sekitar 8 orang yang meneliti pemrosesan paralel dan terdistribusi, berbagi sebuah mesin yang sepertinya Sun UltraSparc 170, dengan hard disk 1GB dan RAM sekitar 128 atau 256MB
Perlu juga diingat bahwa mesin Sun hampir tidak pernah di-reboot
Seorang mahasiswa baru tampaknya membagi file teks besar menjadi 32 atau 64 bagian berdasarkan nomor baris, lalu tanpa membuat file terpisah menjalankan N salinan
perlsecara paralel agar masing-masing memproses rentang barisnya sendiriUntuk ukuran saat itu, N interpreter
perlmemakan banyak RAM, dan ketika swapping terjadi, masing-masing proses mencoba membaca beberapa baris lagi sambil melakukan seek secara gila-gilaan ke bagian berbeda dari file yang samaSelain itu, kemungkinan besar proses ke-J harus membaca J/N dari file untuk sampai ke bagiannya sendiri
Dari konsol, bahkan prompt login pun tidak muncul, tetapi untungnya sudah ada sesi yang sedang login, dan
sumenampilkan prompt kata sandi setelah 20–30 menitSetelah 5–10 menit lagi saya mendapatkan sesi root, memeriksa penyebabnya dengan
top, lalu mencoba menghubungi pengguna dan membunuh proses bermasalah, sehingga sistem kembali normalIdenya sendiri benar, tetapi itu kasus yang sama sekali tidak memahami batas sistem; karena hard disk dan RAM yang rendah, bottleneck I/O sangat parah, sehingga pemrosesan linear sederhana kemungkinan jauh lebih baik
Baik di Java maupun bahasa lain, melakukan operasi bersamaan pada objek yang tidak thread-safe akan menghasilkan bug-bug paling menarik di dunia
Error multithreading adalah yang paling buruk untuk di-debug
Dalam kasus ini, seharusnya sangat mudah dikenali pada tahap desain, dan lampu peringatan semestinya menyala begitu hendak memakai container biasa di lingkungan multithread
volatiledihapus dari field mutable JavaMungkin JVM yang dipakai saat itu dibuat terlalu baik
Bahasa itu memang menetapkan semantik gila yang pada praktiknya hampir tidak bermakna ketika race terjadi, tetapi dalam kenyataannya kerusakan memori atau masalah yang berisik sering muncul
Karena sebagian besar race bersifat ilegal, kita bisa membuat alat seperti thread checker tanpa harus mengubah source code untuk menandai maknanya
Sebaliknya, Java tidak memiliki undefined behavior, tetapi mudah terjadi dua field melenceng secara halus, dan hal semacam ini jauh lebih sulit dilacak
Misalnya map melempar
ConcurrentModificationExceptionSaya juga pernah melihat fenomena yang sama di C# yang sedang berjalan di production
Gejalanya sama, dan saat melihat process dump ada dictionary yang rusak
Saya kira semuanya memakai
ConcurrentDictionary, tetapi ternyata satu yang berasal dari library menjadi masalahSaat itu kami memakai .NET Framework, dan seingat saya .NET Core memiliki kode untuk mendeteksi modifikasi bersamaan
Saya tidak tahu implementasinya, tetapi penghitung versi saja seharusnya cukup
Aneh kalau terpaku seolah-olah NPE adalah bahan utama. Pada pola kejadian aslinya, tampaknya itu tidak ada, dan hanya karena tidak ada exception bukan berarti bug seperti ini tidak ada di C
Intinya adalah invarian kelas. Secara umum, selama mutator berjalan, invarian tidak berlaku dan baru dipulihkan di akhir
Jika mutator lain dijalankan sebelum invarian dipulihkan, struktur data akan rusak. Kalau tidak dimulai dari keadaan yang valid, kecil kemungkinan akhirnya menjadi keadaan yang valid
Karena mengalami nasib sial berupa tidak bisa mereproduksinya dengan NPE yang tidak tertangkap, saya keliru menyimpulkan bahwa NPE yang tidak tertangkap adalah syarat yang diperlukan
Saya pernah melihat hal yang sama pada
java.util.HashMapyang kurang sinkronisasiItu sekitar tahun 2009, tetapi setahu saya masih mungkin terjadi sampai sekarang
Sepingat saya
HashMapmemakai chaining untuk menyelesaikan collision, dan sepertinya ada siklus yang terbentuk di dalam chainNamun alih-alih menggali untuk memverifikasinya, kami fokus menghapus seluruh kode yang bermasalah
Pengetahuan tentang concurrency sering ditanyakan dalam wawancara, dan kalau menganggap data race sebagai masalah sepele, sulit memberi kesan yang baik; kasus ini menunjukkan alasannya
HashMapApakah ini berkaitan dengan linked list yang dipakai untuk menangani collision?
Saya pikir bisa saja menaruh counter yang bertambah untuk mencari siklus, lalu melempar exception jika nilainya lebih besar daripada kedalaman tree atau ukuran collection
Berbeda dari pendekatan hash set yang diusulkan penulis, cara ini nyaris tidak punya overhead memori atau CPU dan tampaknya lebih mungkin diterima
Namun selama memakai C# lebih dari 10 tahun, saya belum pernah gagal mempertimbangkan akses bersamaan ke struktur data dalam situasi concurrency
Ada banyak cara lain tree bisa rusak selain yang ini
Jika memungkinkan, hindari infinite loop, dan kasus yang tidak bisa dihindari cukup jarang
Ini bukan perbaikan, tetapi strategi mitigasi yang bagus
Infinite loop adalah salah satu bug paling menyebalkan. Mudah ditemukan di debugger, tetapi hasilnya bisa sangat tidak ramah seperti situasi OP yang “ssh saja nyaris tidak bisa”
Infinite loop di kode library terasa lebih menjengkelkan lagi
Ada jaminan bahwa jumlah node tidak lebih dari tinggi tree
Exception di dalam thread benar-benar fatal
Ada kisah pelacakan bug mengerikan yang tokoh utamanya adalah C++,
select(), dan thread yang melempar-lempar exception: https://news.ycombinator.com/item?id=42532979Saya harus membacanya lagi