Chuỗi Disruptor Phần Hai - những game nổ hũ uy tín
Bắt đầu từ đây, chúng ta sẽ đi sâu hơn vào việc tìm hiểu về Disruptor. Trước tiên là khái niệm không khóa
(lock free
). So với hai hàng đợi bị chặn mà chúng ta đã giới thiệu trước đó, Disruptor không sử dụng khóa trực tiếp trong thiết kế của mình. Điều này được thực hiện bằng cách chỉ định một luồng duy nhất để sản xuất dữ liệu và sử dụng cơ chế so sánh-và-đặt (CAS) để duy trì con trỏ đầu, thay vì trực tiếp quản lý con trỏ cuối. Cách tiếp cận này giúp giảm thiểu việc sử dụng khóa, từ đó cải thiện hiệu suất.
Tiếp theo, điểm chính mà tôi muốn trình bày trong phần này là làm thế nào để giảm thiểu tình trạng chia sẻ giả
hay còn gọi là false sharing. Vậy thì false sharing là gì? Để hiểu rõ điều này, chúng ta cần đề cập đến kiến trúc bộ nhớ đệm của CPU. Trong cấu trúc CPU, có ba cấp độ bộ nhớ đệm: L1, L2 và L3. Các cấp bộ nhớ càng gần CPU thì tốc độ truy cập càng nhanh nhưng dung lượng lưu trữ lại càng nhỏ. Đặc biệt, L1 và L2 là những bộ nhớ đệm chuyên dụng cho từng lõi CPU, mỗi lõi đều có riêng L1 và L2 của mình. L1 còn chia thành hai phần: dữ liệu và lệnh. Như hình ảnh minh họa ở trên, L1 Cache chỉ có dung lượng 64KB (32KB dành cho dữ liệu và 32KB dành cho lệnh), trong khi L2 Cache có dung lượng 256KB và L3 Cache có dung lượng lên tới 4MB.
Một giá trị quan trọng mà chúng ta cần chú ý ở đây là kích thước dòng bộ nhớ đệm (Cache Line Size
). CPU thường trực tiếp bóng đá ưu tiên đọc dữ liệu từ bộ nhớ đệm gần nhất. Khi xảy ra tình trạng lỗi bộ nhớ đệm
(Cache Miss
), CPU sẽ tiếp tục đọc từ các cấp bộ nhớ đệm thấp hơn. Mỗi lần đọc sẽ diễn ra theo đơn vị dòng bộ nhớ đệm (Cache Line
) và tuân theo nguyên tắc "gần kề". Điều này có nghĩa là các dữ liệu gần nhau có khả năng cao sẽ được đọc cùng lúc. Tuy nhiên, chính cách thức này cũng có thể dẫn đến tình trạng chia sẻ giả
.
Ví dụ như trong trường hợp của ArrayBlockingQueue
, các biến takeIndex
, putIndex
và count
thường nằm trong cùng một lớp và có khả năng cao sẽ tồn tại trong cùng một dòng bộ nhớ đệm (Cache Line
). Nếu các biến này được sửa đổi bởi các luồng khác nhau, dữ liệu trong bộ nhớ đệm sẽ nhanh chóng trở nên lỗi thời sau khi được lấy ra khỏi bộ nhớ đệm. Nguyên tắc "gần kề" lúc này trở nên vô ích do phải liên tục đánh dấu các những game nổ hũ uy tín bit bẩn (dirty bit
) và xóa bộ nhớ đệm, gây ra tình trạng chia sẻ giả
. Trong Disruptor, vấn đề này được giải quyết bằng cách sử dụng kỹ thuật thêm đệm (padding
).
1class LhsPadding {
2 protected long p1, p2, p3, p4, p5, p6, p7;
3}
4
5class Value extends LhsPadding {
6 protected volatile long value;
7}
8
9class RhsPadding extends Value {
10 protected long p9, p10, p11, p12, p13, p14, p15;
11}
12
13/**
14 * <p>Lớp chuỗi đồng bộ dùng để theo dõi tiến trình của
15 * buffer vòng và các trình xử lý sự kiện. Hỗ trợ nhiều
16 * hoạt động đồng thời bao gồm CAS và ghi thứ tự.
17 *
18 * <p>Cũng cố gắng tăng hiệu quả hơn về mặt chia sẻ giả
19 * bằng cách thêm đệm xung quanh trường biến động.
20 */
21public class Sequence extends RhsPadding {
22}
Từ đoạn mã trên, chúng ta có thể thấy rằng trong lớp Sequence
, trường value
thực sự là phần quan trọng nhất. Trường này được khai báo với từ khóa volatile
để đảm bảo tính khả kiến trong môi trường đa luồng. Các lớp LhsPadding
và RhsPadding
cung cấp đệm trước và sau trường value
bằng cách thêm bảy biến kiểu long
ở mỗi bên. Do mỗi biến kiểu long
trong Java chiếm 8 byte, cách bố trí này đảm bảo rằng trường value
luôn sử dụng một dòng bộ nhớ đệm riêng biệt, nhờ đó tránh được tình trạng chia sẻ giả
.
Kết quả là, Disruptor không chỉ cải thiện hiệu suất thông qua việc loại bỏ khóa mà còn tối ưu hóa cách sử dụng bộ nhớ đệm để giảm thiểu các tác động tiêu cực từ chia sẻ giả
. Điều này góp phần làm cho Disruptor trở thành một công cụ mạnh mẽ trong lập trình đa luồng.