2 điểm bởi GN⁺ 2025-03-01 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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, ExecutorService và 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.synchronizedMap hay ConcurrentHashMap; 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)

Đ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 unrelatedObjects nhưng trong thân vòng lặp lại chỉ dùng relatedObject
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 put như sau
treeMap.put(relatedObject.a(), relatedObject.b());
  • 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 treeMapunrelatedObjects lê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 unrelatedObjectsN và kích thước của treeMapM, độ 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ử

Vòng lặp vô hạn do TreeMap không được bảo vệ tạo ra

  • Định nghĩa field TreeMap như 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
  • TreeMap củ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 TreeMap dù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/catch trông có vẻ là điểm then chốt
    • Nếu không có try/catch, một số thread chết vì NullPointerException và chương trình dừng lại
    • Sau khi thêm try/catch và chạy vài lần, tác giả ghi nhận mức sử dụng CPU 500%
  • 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, color của TreeMap và 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

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ùng future.get() để kiểm tra NPE được bọc trong ExecutionException

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 TreeMap khô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 trong TreeMap.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::map củ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/catch hay 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, SimpleRepro dù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
  • 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ệ TreeMap dùng chung
    • Bọc bằng Collections.synchronizedMap
    • Chuyển sang ConcurrentHashMap và chỉ sắp xếp khi cần
  • 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
  • Ví dụ sửa đổi ghi lại các node đã thăm bằng IdentityHashMap trong getEntryput
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");
    }
}

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);
        }
    );
    
  • 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ùng TreeMap nế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?
  • 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

 
GN⁺ 2025-03-01
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.synchronizedMap hoặc đổi sang ConcurrentHashMap sẽ 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ác
    Bản thân đối tượng sở hữu TreeMap cũ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 sang HashMap thì mã vẫn sai
    Trong 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ẽ

    • Phần này thực sự quan trọng: ngay cả khi có một dynamic array an toàn luồng, tổ hợp hai thao tác kiểm tra độ dài bằng size() rồi truy cập element(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 mutex
    • Data race phát sinh từ trạng thái khả biến được chia sẻ không được đồng bộ hóa
      Vì 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
    • Ý nghĩ đầu tiên của tôi cũng là “khoan đã, cấu trúc dữ liệu đó có an toàn luồng không?”, và rốt cuộc trông có vẻ là lựa chọn cấu trúc dữ liệu không phù hợp với mục đích
      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 cho rằng bản thân mô hình threading muốn chạy độc lập trong môi trường bộ nhớ chia sẻ về căn bản là sai
      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.put khi unrelatedObjects không rỗng, và khác biệt này có thể là bug hoặc không
    Ngoài ra cũng cần kiểm tra xem ab có trả về cùng một giá trị mỗi lần hay không, và treeMap có thực sự chỉ hoạt động như một map hay không
    Ví 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

    • Nhận xét đúng. Nên đổi thành if để kiểm tra không rỗng
  • Cũng có thể xảy ra vòng lặp vô hạn nếu phần triển khai Comparator hoặc Comparable khô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

    • Tôi tò mò không biết có ai thực sự từng thấy tận mắt chưa. Có vẻ là chất liệu hay cho một bài blog
      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ì

    • Đây là một quy tắc kinh nghiệm ổn, nhưng cần có tính thực dụng
      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
    • Ở chỗ làm, chúng tôi coi mọi cảnh báo là lỗi, và không thể merge pull request nếu tất cả pipeline CI chạy tự động không pass
      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
    • Không bắt rồi bỏ qua exception cũng rất quan trọng
      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ề 500 hoặc hiển thị hộp thoại lỗi
    • Rốt cuộc phải chọn trận để đánh
      Trong 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ố
    • Ngay cả khi không có hỏng dữ liệu, điều kiện tranh chấp vẫn có thể khiến cùng một công việc được thực hiện nhiều lần rồi chỉ để lại một kết quả, gây ra vấn đề hiệu năng lớn
      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ình
    Theo 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òng
    Hơ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à su hiện prompt nhập mật khẩu sau 20–30 phút
    Sau thêm 5–10 phút nữa, tôi có được session root, dùng top xá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

    • Vì vậy khi dùng đối tượng không thread-safe từ nhiều thread, bạn phải tự quản lý truy cập nguyên tử, hoặc dùng phiên bản thread-safe
      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
    • Tôi đã gặp khá nhiều bug concurrency, nhưng chưa từng cố ý tái hiện sự bất nhất xảy ra khi bỏ modifier volatile khỏi một field mutable trong Java
      Cũng có thể JVM tôi dùng khi đó được làm quá tốt
    • Ngay cả khi tôi chắc chắn rằng mình có thể chấp nhận hậu quả của việc chia sẻ thứ không được đồng bộ hóa, kết quả thực tế lúc nào cũng nằm ngoài dự đoán
    • Nghe hơi cực đoan, nhưng vì lý do này tôi lại thích debug race trong C/C++ hơn
      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
    • Một phần, có lẽ là phần lớn, các thao tác của Java Collections thực hiện kiểm tra toàn vẹn để cảnh báo các vấn đề như thế này
      Ví dụ như map ném ra ConcurrentModificationException
  • Tô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ân
    Khi đó 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

    • Cuối cùng hóa ra là do logic sai
      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.HashMap khi thiếu đồng bộ hóa
    Dù đó 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ớ, HashMap dùng chaining để xử lý va chạm, và có lẽ đã có một vòng lặp hình thành trong chain
    Tuy 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

    • Tôi không biết chuyện như vậy cũng có thể xảy ra với HashMap
      Có 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

    • Không phải ý tưởng tệ, nhưng trong tình huống có data race, việc thêm một tiền điều kiện hữu ích nói chung khá khó
      Cây còn có nhiều cách hỏng khác ngoài cách này
    • Ý tưởng hay. Tôi từng làm việc này trước đây với binary search và cấu trúc câ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ách đó tốt hơn nhiều. Chỉ cần dùng bộ nhớ hằng số
      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=42532979

    • Tôi nhớ đã đọc bài đó, nhưng vì thiếu kiến thức trong lĩnh vực đó nên không hiểu
      Tôi sẽ phải đọc lại