Thay Đổi Các Hoạt Động Nguyên Tử Của Skynet Sang Stdatomic
Tôi đã thay thế các thao tác nguyên tử trong skynet bằng thư viện stdatomic. Đây là một phần tiêu chuẩn của C11 và cũng đã được tích hợp vào chuẩn C++. Microsoft Visual C++ cũng sẽ hỗ trợ stdatomic trong tương lai. Khi dự án skynet bắt đầu, tính năng này vẫn chưa tồn tại, vì vậy tôi đã sử dụng các mở rộng sync của GCC từ trước đó.
Việc chuyển đổi sang stdatomic sẽ mang lại nhiều lợi ích lâu dài cho sự phát triển của skynet. Vào tuần trước, tôi đã dành thời gian làm quen với bộ API này và triển khai chúng vào skynet. Đồng thời vẫn giữ lại phiên bản cũ để phòng trường hợp trình biên dịch định nghĩa __STD_NO_ATOMICS__
, khi đó hệ thống sẽ tự động chuyển về phiên bản cũ.
Trước tiên, C11 đã bổ sung kiểu _Atomic(T)
. Đây là điểm khác biệt lớn so với các nguyên tử của GCC trước đây, vốn chỉ sử dụng các kiểu dữ liệu nguyên thủy. Bây giờ, nếu một biến int cần thao tác nguyên tử, chúng ta phải dùng atomic_int
(thực chất là _Atomic(int)
).
Tất cả các hàm API thao tác nguyên tử đều chỉ nhận biến nguyên tử. Ngay cả khi đọc/ghi đơn giản cũng phải dùng atomic_load
và atomic_store
. Trong khi phiên bản cũ không yêu cầu điều này. Cách tiếp cận mới khiến mã nguồn chặt chẽ hơn, vì không cần giả định kích thước từ nào có thể đọc/ghi nguyên tử - điều này sẽ được trình biên dịch kiểm tra tự động.
Điều thú vị là GCC không kiểm tra chặt chẽ kiểu nguyên tử của biến truyền vào, nhưng Clang lại rất nghiêm ngặt. Phiên bản đầu tiên tôi triển khai đã gặp lỗi biên dịch trên Clang do một số biến bị quên khai báo nguyên tử. Rất may nhờ sự góp ý của các nhà phát triển khác, mọi vấn đề đã được khắc phục đầy đủ.
Lệnh CAS (Compare-And-Swap) là nền tảng cho các cấu trúc không khóa và cơ chế khóa cơ bản. Ví dụ: CAS(ref, 0, 1) sẽ kiểm tra giá trị *ref có bằng 0 không. Nếu không bằng thì thất bại; nếu bằng thì cập nhật *ref thành 1 và trả về thành công. Điều này đảm bảo chỉ có duy nhất một tiến trình có thể thay đổi giá trị từ 0 sang 1 khi có nhiều tác nhân truy cập đồng thời.
Theo tiêu chuẩn mới, CAS có hai phiên bản: atomic_compare_exchange_weak
và atomic_compare_exchange_strong
. Phiên bản “weak” cho phép xảy ra lỗi ngẫu nhiên ngay cả khi giá trị bằng nhau (trong ví dụ trên, có thể thất bại khi *ref == 0). Thông thường chúng ta nên dùng phiên bản weak vì hiệu suất tốt hơn.
Tuy nhiên tôi thấy lạ là API mới lại truyền giá trị cũ qua con trỏ, trong khi giá trị mới lại truyền trực tiếp. Cụ thể, tham số “oval” là con trỏ truyền thống, không phải kiểu nguyên tử. Điều này khiến tôi băn khoăn về lý do thiết kế, bởi cả các nguyên tử built-in của GCC và hàm InterlockedCompareExchange của Windows đều sử dụng truyền giá trị.
Hàm __sync_lock_test_and_set
trước đây giờ được thay bằng atomic_flag_test_and_set
với kiểu atomic_flag
. Thành phần này có thể dùng để xây dựng spinlock. Nhưng đáng ngạc nhiên là C11 lại thiếu atomic_flag_test
(mới có ở C++20). Khi cần thao tác đọc mà không ghi (như trong khóa đọc/ghi), chúng ta có thể dùng CAS để thay thế.
Phiên bản atomic built-in trước đây có cả __sync_fetch_and_add
lẫn __sync_add_and_fetch
để phân biệt giá trị trả về là trước hay sau khi cộng. Trong stdatomic chỉ giữ lại atomic_fetch_and_add
và loại bỏ hàm còn lại. Tuy nhiên điều này không quan trọng vì nếu cần add_and_fetch(ref,1)
, ta chỉ việc lấy atomic_fetch_and_add(ref,1)+1
.
Một chi tiết nhỏ là kiểu con trỏ không được hỗ trợ nguyên tử trực tiếp. Tôi đã dùng atomic_uintptr_t
thay thế, sau đó chuyển đổi về kiểu con trỏ mong muốn khi sử dụng.