Mức sử dụng CPU 3.200%
(josephmate.github.io)- Trong một ứng dụng Java 17, toàn bộ 32 lõi đều bị bão hòa, khiến mức sử dụng CPU tăng vọt lên 3.200%; khi sắp xếp thread dump theo CPU time, phát hiện nhiều thread bị kẹt ở
TreeMap.put() - Đoạn code vòng lặp bị nghi ngờ ban đầu đúng là kém hiệu quả, nhưng chỉ dựa vào độ phức tạp
O(N lg(M))và kiểm tra kích thước đầu vào thì không thể giải thích được thời gian chạy ở mức gây sự cố - Nguyên nhân thực sự là nhiều thread cùng sửa đổi TreeMap dùng chung mà không có cơ chế bảo vệ, làm phát sinh cycle bên trong red-black tree, khiến thao tác tìm kiếm/chèn rơi vào vòng lặp vô hạn
- Hiện tượng tương tự xuất hiện cả trong code tái hiện đơn giản,
ExecutorServicevà dịch vụ gRPC; thread pool không để lộ NPE ra stdout, khiến việc xác định nguyên nhân càng khó hơn - Cách sửa không chỉ dừng ở việc chuyển sang
Collections.synchronizedMaphayConcurrentHashMap; cần có thêm xử lý ngoại lệ trong executor, cảnh báo CPU/NPE, phân tích tĩnh và kiểm thử đa luồng
Triệu chứng sự cố và manh mối đầu tiên
- Máy gần như hỏng đến mức khó cả việc kết nối
ssh, và mức sử dụng CPU tăng lên tới 3.200%- Trạng thái lúc đó là toàn bộ 32 lõi của host đều đang được dùng
- Đây là tình huống trái ngược với một bug trước đó chỉ dùng 100% trên 1 lõi
- Thread dump của runtime Java 17 có bao gồm CPU time, và khi sắp xếp theo CPU time thì xuất hiện các thread tương tự nhau
- Stack trỏ tới
java.util.TreeMap.put() - Vị trí trong code ứng dụng là
BusinessLogic.someFunction(BusinessLogic.java:29)
- Stack trỏ tới
Đoạn code bị nghi ngờ ban đầu và giả thuyết bị loại trừ
- Code tại điểm có vấn đề đang duyệt qua
unrelatedObjectsnhưng trong thân vòng lặp lại chỉ dùngrelatedObject
for (SomeOtherType unrelatedObject : unrelatedObjects) {
treeMap.put(relatedObject.a(), relatedObject.b()); // line 29
}
- Code này có thể rút gọn thành một lần
putnhư sau
treeMap.put(relatedObject.a(), relatedObject.b());
- Vì
unrelatedObjectsđược dùng ở phần sau của hàm, nên không thể loại bỏ chính tham số này - Có khả năng việc sử dụng
unrelatedObjectđã biến mất trong quá trình refactor - Trong unit test, dù tăng
treeMapvàunrelatedObjectslên 1.000.000 entry mỗi bên, vẫn không tái hiện được vấn đề- Nếu gọi kích thước của
unrelatedObjectslàNvà kích thước củatreeMaplàM, độ phức tạp làO(N lg(M)) - Để thấy thời gian chạy ở mức khoảng 1 phút, cần tới cỡ 100 triệu đến 1 tỷ entry
- Điều này cũng không khớp với giả định rằng trong ứng dụng thực tế, cả hai cấu trúc dữ liệu đều không vượt quá 1.000 phần tử
- Nếu gọi kích thước của
Vòng lặp vô hạn do TreeMap không được bảo vệ tạo ra
- Định nghĩa field
TreeMapnhư sau
private final Map<K,V> treeMap = new TreeMap<>();
- Nhiều thread truy cập TreeMap này nhưng không có đồng bộ hóa hay cơ chế bảo vệ nào
TreeMapcủa Java được triển khai bằng red-black tree, và nếu các liên kết node bên trong bị hỏng do sửa đổi đồng thời, cycle có thể xuất hiện- Trong quá trình tìm kiếm hoặc chèn một giá trị chưa tồn tại, chương trình có thể đi theo cycle và rơi vào vòng lặp vô hạn
Thử nghiệm tái hiện đơn giản
- Tác giả viết một thử nghiệm trong đó nhiều thread cập nhật ngẫu nhiên một
TreeMapdùng chung
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.
}
}
}));
}
- Toàn bộ project nằm ở SimpleRepro.java
- Ban đầu,
try/catchtrông có vẻ là điểm then chốt- Nếu không có
try/catch, một số thread chết vìNullPointerExceptionvà chương trình dừng lại - Sau khi thêm
try/catchvà chạy vài lần, tác giả ghi nhận mức sử dụng CPU 500%
- Nếu không có
- Race condition không chỉ gây hỏng dữ liệu hoặc deadlock; nó còn có thể làm hỏng cấu trúc dữ liệu thành dạng có thể lặp vô hạn, dẫn tới sự cố hiệu năng
Xác nhận cycle bên trong TreeMap
- Tác giả viết thử nghiệm dùng reflection để truy cập các field
root,left,right,key,colorcủaTreeMapvà in ra node cùng màu của chúng - Nếu trong lúc duyệt gặp lại một
TreeMap.Entryđã từng được thăm, nó được xem là 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"
);
}
}
- Để mở giới hạn truy cập module của Java, khi chạy cần đối số JVM sau
--add-opens java.base/java.util=ALL-UNNAMED
Các trường hợp liên quan đã có và điểm khác biệt
- Vấn đề cùng loại đã được biết đến từ trước
- Trường hợp này xem xét cùng lúc code tái hiện thực tế, nỗ lực tái hiện theo từng ngôn ngữ, cũng như ý tưởng sửa đổi
TreeMapvà red-black tree
Tái hiện thực tế hơn với ExecutorService
- Code đơn giản bỏ qua NPE có thể trông không thực tế, nhưng trong
ExecutorService, ngoại lệ có thể không dễ lộ ra
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));
});
}
- Toàn bộ code nằm ở ExecutorUncaughtRepro.java
- Khi chạy, chương trình bị treo và thread dump cho thấy các thread bị kẹt ở
TreeMap.put() - Không có gì xuất hiện trên stdout
- Thread pool đã nuốt NPE nên tín hiệu vấn đề không lộ ra
- Tình huống thực tế cũng như vậy
- Nếu tự quản lý thread pool, cần các biện pháp sau
- Đăng ký uncaught exception handler thông qua thread factory
- Xử lý
Futuređược trả về và dùngfuture.get()để kiểm tra NPE được bọc trongExecutionException
Vấn đề tương tự trong dịch vụ gRPC
- Trong các dịch vụ dựa trên thread pool như dịch vụ gRPC, một
TreeMapkhông được bảo vệ cũng có thể tạo ra cùng vấn đề
@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();
}
- Toàn bộ code nằm ở GrpcRepro.java
- Định nghĩa protobuf nằm ở ReceiptProcessorService.proto
- Thread dump cho thấy các thread
grpc-default-executor-*dừng ở trạng thái runnable trongTreeMap.put()
Suy đoán về nguyên nhân và thử nghiệm theo từng ngôn ngữ
- Một nguyên nhân khả dĩ được nghi ngờ là hai thread độc lập xoay cây theo hai hướng ngược nhau, hoặc các thao tác ghi của những lần xoay chồng lấn bị interleaving và tạo ra cycle
- Tuy nhiên, không có chứng minh về interleaving cụ thể nào giữa các thread tạo ra cycle
- Ban đầu tác giả nghi ngờ cần có NPE, nhưng các thử nghiệm sau đó đã phá vỡ giả định này
- Tác giả đã thử tái hiện cùng vấn đề trong nhiều ngôn ngữ
- Java là nền tảng của toàn bộ trường hợp nên tái hiện được
std::mapcủa C++ dùng red-black tree, và hiếm khi thay vì segfault, thread bị kẹt và mức sử dụng CPU cao- Go cũng tái hiện được ngoài dự đoán; code nằm ở thử nghiệm Go
- Ruby, dù là ngôn ngữ có thể bắt NPE, lại không tái hiện được; có thể GIL đã ngăn interleaving tạo ra vấn đề
- Trái với dự đoán, thử nghiệm C++ tái hiện vòng lặp vô hạn ngay cả khi không có
try/catchhay ngoại lệ null pointer- Đôi khi chương trình kết thúc bằng
segmentation fault - Rất hiếm khi tiến trình bị kẹt hơn 10 phút và trên
top,SimpleReprodùng 170,8% CPU - Trong C++, dereference null pointer là segfault, nên phải tồn tại một interleaving không đi qua null
- Đôi khi chương trình kết thúc bằng
- Sau khi thấy kết quả này, tác giả chạy lại thử nghiệm Java 12 lần mà không bắt NPE, và vẫn tái hiện được vòng lặp vô hạn trong
TreeMap.put()mà không cần catch NPE
Cách sửa dễ nhất và phòng vệ ở cấp cấu trúc dữ liệu
- Cách sửa dễ nhất là bảo vệ
TreeMapdùng chung- Bọc bằng
Collections.synchronizedMap - Chuyển sang
ConcurrentHashMapvà chỉ sắp xếp khi cần
- Bọc bằng
- Một biện pháp phòng vệ gây tranh luận hơn là theo dõi các node đã thăm trong lúc duyệt red-black tree
- Nếu gặp lại node đã thăm, ném
ConcurrentModificationException - Cách này không ngăn được bản thân việc hỏng dữ liệu
- Nhưng có thể ngăn mức sử dụng CPU 100% do vòng lặp vô hạn
- Bộ nhớ bổ sung bị giới hạn bởi chiều cao cây, tức
O(lg(n)) - Chiều cao red-black tree được đảm bảo là
O(lg(n)) - Tác giả cho rằng khả năng thư viện chuẩn áp dụng cách này là thấp
- Nếu gặp lại node đã thăm, ném
- Ví dụ sửa đổi ghi lại các node đã thăm bằng
IdentityHashMaptronggetEntryvàput
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");
}
}
- Toàn bộ code sửa đổi nằm ở ProtectedTreeMap.java
Xây nhiều lớp phòng vệ
-
Sai sót vẫn xảy ra, nên một lớp phòng vệ là chưa đủ
-
Trong trường hợp này, nhiều sai sót chồng lên nhau, nhưng vẫn còn một số cơ chế giám sát nên có thể phát hiện vấn đề
-
Cảnh báo NPE
- Không có cảnh báo về việc NPE phát sinh
- Chỉ có cảnh báo tỷ lệ lỗi, và NPE này chỉ xảy ra một lần trên mỗi API handler worker thread nên không vượt ngưỡng tỷ lệ lỗi
- Do cách executor xử lý, cũng không có log NPE nào được ghi lại
-
Cảnh báo bất thường về mức sử dụng CPU
- Mức sử dụng CPU được giám sát và có cảnh báo dựa trên ngưỡng đơn giản
- Khi mức sử dụng CPU vượt ngưỡng, hệ thống bị đánh giá là hoạt động bất thường và phát cảnh báo; nhờ đường này mà vấn đề được phát hiện
-
Xử lý ngoại lệ trong Executor
- Nếu đưa task vào executor, nhất định phải thiết lập uncaught exception handler
thread.setUncaughtExceptionHandler( (dyingThread, throwable) -> { logger.error("uncaught exception!", throwable); } );- Thay vì
ThreadFactorytự viết, có thể dùng Apache Commons BasicThreadFactory hoặc Guava ThreadFactoryBuilder - Nếu không có handler, ngoại lệ bị nuốt và việc giám sát log cũng không phát hiện được vấn đề
-
Code review và phân tích tĩnh
- Code review có thể đã phát hiện tổ hợp thread và
TreeMap, hoặc đưa ra đề xuất không dùngTreeMapnếu không cần sắp xếp, nhưng trong trường hợp này điều đó đã không xảy ra - Các công cụ phân tích tĩnh như SpotBugs, JLint, Chord có thể tìm ra loại vấn đề đồng thời này tại thời điểm build
- Tài liệu liên quan đến phân tích tĩnh được nêu là How Good is Static Analysis at Finding Concurrency Bugs?
- Code review có thể đã phát hiện tổ hợp thread và
-
Kiểm thử
- Không có kiểm thử đa luồng cho đường code này
- Nếu code có khả năng bị truy cập đồng thời, kiểm thử cũng phải bao phủ tình huống đa luồng
Kết luận
- Sửa đổi đồng thời mà không có bảo vệ không chỉ gây hỏng dữ liệu, mà còn có thể biểu hiện thành vòng lặp vô hạn và mức sử dụng CPU cao
- Khi nhiều thread đồng thời sửa đổi một cấu trúc dữ liệu có cấu trúc con trỏ bên trong như
TreeMap, cấu trúc red-black tree có thể tạo ra cycle - Nếu kết hợp lựa chọn cấu trúc dữ liệu có đồng bộ hóa, xử lý ngoại lệ, cảnh báo, phân tích tĩnh và kiểm thử đa luồng, vấn đề có thể được phát hiện hoặc phòng tránh sớm hơn
1 bình luận
Các ý kiến trên Hacker News
Ai cũng biết các collection cốt lõi của Java về mặt thiết kế không an toàn luồng, nên lỗi này lẽ ra phải được phát hiện
OP nên rà soát cả phần mã còn lại để kiểm tra xem còn chỗ nào có khả năng nhiều luồng cùng lúc thao tác trên collection hay không
Bọc bằng
Collections.synchronizedMaphoặc đổi sangConcurrentHashMapsẽ làm cho từng thao tác riêng lẻ trên map trở nên an toàn luồng, nhưng việc một chuỗi các thao tác liên tiếp có an toàn hay không lại là chuyện khácBản thân đối tượng sở hữu
TreeMapcũng đáng nghi là có an toàn luồng hay không, và các sửa đổi kiểu theo dõi những nút đã thăm chỉ khiến collection vẫn không an toàn nhưng thất bại theo cách tinh vi hơn mà thôiĐiểm cốt lõi không phải là tác dụng phụ của
TreeMap, mà là đã vi phạm hợp đồng của collection; dù đổi sangHashMapthì mã vẫn saiTrong mã được nhiều luồng xử lý, cách chắc chắn nhất là biến mọi đối tượng có thể thành bất biến; còn những đối tượng không thể làm bất biến thì giới hạn chúng trong các vùng nhỏ, độc lập và được kiểm soát chặt chẽ
size()rồi truy cậpelement(10)không có tính nguyên tửNếu giữa hai lần gọi có luồng khác xóa phần tử, có thể xảy ra truy cập ngoài phạm vi
Cách giải quyết là dùng một phương thức nguyên tử xử lý cả hai việc cùng lúc, chẳng hạn
element_or_null(), hoặc ngay từ đầu dùng mảng thông thường và bảo vệ toàn bộ hai thao tác bằng mutexVì cần cả ba điều kiện, cũng có ba hướng giải quyết: thêm đồng bộ hóa bằng lock v.v., không chia sẻ quyền truy cập khả biến như mô hình một chủ sở hữu dùng channel, hoặc biến dữ liệu thành bất biến như góc nhìn từ các ngôn ngữ hàm thuần túy
Theo tôi biết, Google cũng đã đầu tư khá nhiều vào mô hình này trong Guava
Rust cho phép bạn chọn sẽ từ bỏ điều nào trong ba điều này, và ngăn chặn tĩnh việc cả ba điều kiện cùng đồng thời đúng
Nếu cố nhét mutex vào cho vừa thì thường sẽ sinh ra vấn đề khác và nút thắt cổ chai
Muốn dùng đối tượng bất biến thì phải dùng cấu trúc dữ liệu bất biến tận dụng structural sharing để tránh hoặc giảm bùng nổ mức dùng bộ nhớ
Dùng cấu trúc dữ liệu hàm thuần túy thì một phía của vấn đề biến mất, nhưng nếu các luồng phụ thuộc vào những phiên bản trung gian do nhau tạo ra thì lại nảy sinh một vấn đề đau đầu khác và có thể cần cấu trúc dữ liệu khác
Nếu cố cứu cấu trúc dữ liệu khả biến đang dùng, bạn có thể tuần tự hóa các nỗ lực truy cập trước khi chúng chạm tới cấu trúc dữ liệu, rồi gom truy cập thành transaction để chỉ cho chạy các transaction hoàn chỉnh; làm vậy dần dần có cảm giác như đang triển khai gần như cả một cơ sở dữ liệu
Tôi nghĩ công sức để làm cho thứ đó hoạt động lẽ ra nên được đổ vào một mô hình process tốt hơn
Ví dụ đơn giản hóa mã trong bài gốc không chính xác
Mã ban đầu chỉ thực hiện
treeMap.putkhiunrelatedObjectskhông rỗng, và khác biệt này có thể là bug hoặc khôngNgoài ra cũng cần kiểm tra xem
avàbcó trả về cùng một giá trị mỗi lần hay không, vàtreeMapcó thực sự chỉ hoạt động như một map hay khôngVí dụ nếu đó là một map ghi log các cập nhật, thì cần cân nhắc xem đổi thành chỉ ghi log một lần có ổn không
ifđể kiểm tra không rỗngCũng có thể xảy ra vòng lặp vô hạn nếu phần triển khai Comparator hoặc
Comparablekhông cung cấp một thứ tự toàn phần nhất quán: https://stackoverflow.com/questions/62994606/concurrentskips...Điều này không liên quan đến concurrency
Việc có xảy ra hay không có thể phụ thuộc vào dữ liệu cụ thể được xử lý và thứ tự xử lý, nên nó có thể trông như chạy bình thường trong thời gian dài rồi mới nổ ở môi trường production
Cá nhân tôi thì chưa gặp comparator lỗi không có thứ tự toàn phần
Tôi từng nghĩ điều kiện tranh chấp chỉ gây hỏng dữ liệu hoặc deadlock, chứ không nghĩ nó có thể gây ra vấn đề hiệu năng
Nhưng nếu nó có thể làm hỏng dữ liệu và tạo ra vòng lặp vô hạn thì cũng hợp lý
Vì vậy tôi thường nghĩ rằng, về nguyên tắc, lỗi, hành vi bất thường và cảnh báo trong dự án đều nên được sửa. Bởi chúng có thể gây ra những vấn đề nhìn bề ngoài tưởng như không liên quan
Tuy nhiên, hiếm khi nguyên tắc này được chấp nhận ở phía quyết định sẽ làm gì
Trong một số dự án, chi phí loại bỏ mọi lỗi, hành vi bất thường và cảnh báo có thể lớn hơn rất nhiều so với chi phí chấp nhận những vấn đề thỉnh thoảng phát sinh và nhìn bề ngoài có vẻ không liên quan
Gần như không thể dự đoán một lỗi cụ thể có khả năng góp phần gây ra vấn đề trong tương lai hay không, và liệu sửa trước có rẻ hơn hay không; nên cuối cùng việc này gần với kỹ nghệ hơn là khoa học
“Không sửa gì cả” thì tệ hại, còn “sửa hết mọi thứ” thì thường không thực tế, nên cần một khung ra quyết định, trực giác từ kinh nghiệm, và sự tin tưởng của các bên liên quan
Cần kỷ luật, nhưng một khi đã đưa được về trạng thái đó thì việc duy trì dễ hơn nhiều
Trừ khi bạn biết cụ thể rằng tiếp tục chạy chương trình là an toàn, “bắt rồi chỉ ghi log” cũng là một ý tưởng tệ
Tốt hơn là cứ để exception lan lên tới nơi có thể xử lý hữu ích, chẳng hạn trả về
500hoặc hiển thị hộp thoại lỗiTrong một dự án Rails tôi bảo trì, từng có vấn đề log đầy các dòng “unsupported parameters”; dù đã kiểm tra kỹ các controller và cho phép tham số, nó vẫn tiếp tục xuất hiện
Có lẽ là vô hại, nhưng tạo ra rất nhiều nhiễu trong log
Nhiều người đã cố giải quyết, nhưng lúc nào cũng có ưu tiên cao hơn, và cũng khó biện minh cho việc dành nhiều thời gian vào thứ không ảnh hưởng đến tính năng
Đó là một vấn đề phiền toái như trĩ. Bạn phải chọn giữa phẫu thuật rồi chịu đau dữ dội trong vài tuần, hoặc cứ chịu đựng; đa phần là lành tính nhưng cũng có khả năng nặng lên thành vấn đề lớn
Có lẽ có thể gọi hiện tượng này là trĩ kỹ thuật số
Nếu đã phán đoán rằng cảnh báo không liên quan, ít nhất nên giải thích bằng chú thích, và nếu có thể thì nên thêm
pragmađể tắt cảnh báo trong phạm vi hẹp nhất có thểTôi thiên về việc loại bỏ các hành vi bất thường. Tôi từng gặp đoạn mã được đánh dấu “không biết vì sao nó chạy được”, rồi về sau nó không còn chạy nữa; nếu dọn dẹp từ trước thì đã có thời gian sửa cẩn thận, thay vì phải viết lại trong vội vã
Đoạn “chật vật lắm mới kết nối được bằng ssh” làm tôi nhớ đến một tình huống thời học cao học
Phòng lab của chúng tôi là một nhóm nhỏ khoảng 8 người làm về xử lý song song/phân tán, dùng chung một máy có lẽ là Sun UltraSparc 170, với ổ cứng 1GB và RAM khoảng 128 hoặc 256MB
Cũng cần tính đến việc máy Sun hầu như không được reboot
Một sinh viên mới dường như đã chia một file văn bản lớn thành 32 hoặc 64 đoạn theo số dòng, rồi thay vì tạo file riêng, chạy song song N bản sao
perlđể mỗi bản xử lý đoạn dòng của mìnhTheo tiêu chuẩn thời đó, N trình thông dịch
perlđã ngốn rất nhiều RAM, và khi swap xảy ra, chúng seek điên cuồng tới các đoạn khác nhau của cùng một file trong khi mỗi process cố đọc thêm vài dòngHơn nữa, nhiều khả năng process thứ J phải đọc J/N của file để tới được đoạn của nó
Trên console thậm chí không nhận được prompt đăng nhập, nhưng may là đã có một session đăng nhập sẵn, và
suhiện prompt nhập mật khẩu sau 20–30 phútSau thêm 5–10 phút nữa, tôi có được session root, dùng
topxác định nguyên nhân, rồi cố liên hệ người dùng và kill các process gây vấn đề, hệ thống mới trở lại bình thườngÝ tưởng tự nó thì đúng, nhưng đó là trường hợp hoàn toàn không hiểu giới hạn hệ thống; vì ổ cứng và RAM thấp tạo ra nút thắt I/O nghiêm trọng, xử lý tuyến tính đơn giản có lẽ còn tốt hơn nhiều
Dù là Java hay ngôn ngữ khác, nếu thực hiện các thao tác đồng thời trên đối tượng không thread-safe thì sẽ tạo ra những bug thú vị nhất trên đời
Lỗi đa luồng là thứ tệ nhất để debug
Trong trường hợp này, đáng ra rất dễ nhận ra ngay từ giai đoạn thiết kế, và khoảnh khắc định dùng container thông thường trong môi trường đa luồng thì đèn cảnh báo phải bật lên rồi
volatilekhỏi một field mutable trong JavaCũng có thể JVM tôi dùng khi đó được làm quá tốt
Ngôn ngữ đúng là quy định những ngữ nghĩa điên rồ, gần như vô nghĩa khi race xảy ra, nhưng trên thực tế thường lộ ra hỏng bộ nhớ hoặc các vấn đề ồn ào
Vì phần lớn race là bất hợp pháp, có thể tạo các công cụ như thread checker mà không cần sửa mã nguồn để đánh dấu ý nghĩa
Ngược lại, Java không có undefined behavior, nhưng rất dễ xảy ra chuyện hai field lệch nhau một cách tinh vi, và những thứ như vậy khó truy vết hơn nhiều
Ví dụ như map ném ra
ConcurrentModificationExceptionTôi cũng từng thấy hiện tượng tương tự trong C# đang chạy production
Triệu chứng giống nhau, và khi xem process dump thì thấy có một dictionary bị hỏng
Tôi tưởng tất cả đều dùng
ConcurrentDictionary, nhưng một cái đến từ thư viện lại là nguyên nhânKhi đó chúng tôi dùng .NET Framework, và theo tôi nhớ thì .NET Core có mã phát hiện sửa đổi đồng thời
Tôi không rõ phần triển khai, nhưng chỉ cần version counter cũng có thể làm được
Việc bám vào NPE như thể đó là thành phần cốt lõi thì hơi lạ. Dạng phát sinh ban đầu dường như không có nó, và cũng không có lý do gì để cho rằng C không có bug kiểu này chỉ vì không có exception
Cốt lõi là bất biến của lớp. Thông thường, trong lúc mutator đang chạy thì bất biến không còn được bảo toàn, và chỉ được khôi phục ở cuối
Nếu chạy một mutator khác trước khi bất biến được khôi phục, cấu trúc dữ liệu sẽ bị hỏng. Nếu không bắt đầu từ một trạng thái hợp lệ, khả năng kết thúc ở trạng thái hợp lệ là thấp
Vì gặp vận rủi là không thể tái hiện bằng NPE không được bắt, nên đã kết luận nhầm rằng NPE không được bắt là điều kiện cần
Tôi từng thấy điều tương tự trong
java.util.HashMapkhi thiếu đồng bộ hóaDù đó là khoảng năm 2009, nhưng theo tôi biết thì đến nay vẫn có thể xảy ra
Theo tôi nhớ,
HashMapdùng chaining để xử lý va chạm, và có lẽ đã có một vòng lặp hình thành trong chainTuy nhiên, thay vì đào sâu để xác minh, tôi tập trung vào việc loại bỏ toàn bộ đoạn mã có vấn đề
Kiến thức về concurrency thường được hỏi trong phỏng vấn, và nếu xem data race chỉ là chuyện nhỏ thì khó tạo ấn tượng tốt; trường hợp này chính là lý do
HashMapCó liên quan đến linked list dùng để xử lý va chạm không?
Tôi nghĩ có thể đặt một bộ đếm tăng dần để phát hiện vòng lặp, rồi ném ngoại lệ nếu nó lớn hơn độ sâu của cây hoặc kích thước collection
Khác với cách dùng hash set mà tác giả đề xuất, cách này gần như không có overhead về bộ nhớ hay CPU và có vẻ cũng dễ được chấp nhận hơn
Tuy nhiên, trong hơn 10 năm dùng C#, tôi chưa từng không tính đến việc truy cập đồng thời vào cấu trúc dữ liệu trong các tình huống concurrent
Cây còn có nhiều cách hỏng khác ngoài cách này
Nếu có thể thì tránh vòng lặp vô hạn; những trường hợp không tránh được là khá hiếm
Đây không phải bản sửa lỗi, nhưng là một chiến lược giảm thiểu tốt
Vòng lặp vô hạn là một trong những bug tệ hại nhất. Trong debugger thì dễ tìm, nhưng kết quả có thể rất khó chịu như tình huống “đến ssh cũng chật vật mới dùng được” của OP
Đặc biệt vòng lặp vô hạn trong mã thư viện còn khó chịu hơn
Có đảm bảo rằng số node không vượt quá chiều cao của cây
Ngoại lệ trong thread thực sự rất chí mạng
Có một câu chuyện truy vết bug kinh khủng với C++,
select(), và một thread ném ngoại lệ làm nhân vật chính: https://news.ycombinator.com/item?id=42532979Tôi sẽ phải đọc lại