Kiểm thử đúng cách các cấu trúc dữ liệu đồng thời
Một, hai, ba, hai
- Có thể dùng
loom, một thư viện Rust, để kiểm thử kỹ lưỡng các cấu trúc dữ liệu lock-free
- Cung cấp ví dụ mã cho một bộ đếm đồng thời đơn giản
- Lỗi của đoạn mã là phép tăng không mang tính nguyên tử
use std::sync::atomic::{AtomicU32, Ordering::SeqCst};
#[derive(Default)]
pub struct Counter {
value: AtomicU32,
}
impl Counter {
pub fn increment(&self) {
let value = self.value.load(SeqCst);
self.value.store(value + 1, SeqCst);
}
pub fn get(&self) -> u32 {
self.value.load(SeqCst)
}
}
Kiểm thử đơn giản
- Bài kiểm thử tăng lặp đi lặp lại cùng một bộ đếm từ nhiều luồng rồi kiểm tra kết quả
- Bài kiểm thử thất bại đúng như mong đợi, nhưng khó tái hiện vì phụ thuộc vào timing
#[test]
fn threaded_test() {
let counter = Counter::default();
let thread_count = 100;
let increment_count = 100;
std::thread::scope(|scope| {
for _ in 0..thread_count {
scope.spawn(|| {
for _ in 0..increment_count {
counter.increment()
}
});
}
});
assert_eq!(counter.get(), thread_count * increment_count);
}
Kiểm thử dựa trên thuộc tính (PBT)
- Thử áp dụng kiểm thử dựa trên thuộc tính, phù hợp để kiểm thử máy trạng thái
- Nếu có thể chạy thủ công từng bước của các luồng, sẽ dễ chèn vào giữa
load và store của luồng khác
#[test]
fn state_machine_test() {
arbtest::arbtest(|rng| {
let mut state: i32 = 0;
let step_count: usize = rng.int_in_range(0..=100)?;
for _ in 0..step_count {
match *rng.choose(&["inc", "dec"])? {
"inc" => state += 1,
"dec" => state -= 1,
_ => unreachable!(),
}
}
Ok(())
});
}
Instrumentation đơn giản
- Cách để một luồng có thể "tạm dừng" giữa các phép toán nguyên tử
pub fn increment(&self) {
pause();
let value = self.value.load(SeqCst);
pause();
self.value.store(value + 1, SeqCst);
pause();
}
fn pause() {
// ¯\_(ツ)_/¯
}
API luồng được quản lý
- Một quy tắc trong thiết kế API là bắt đầu từ một người dùng duy nhất để hiểu cảm giác của API, rồi mới tiến tới triển khai thực tế
- Viết kiểm thử dựa trên thuộc tính bằng cách dùng các luồng được quản lý
let counter = Counter::default();
let t1 = managed_thread::spawn(&counter);
let t2 = managed_thread::spawn(&counter);
while !rng.is_empty() {
let coin_flip: bool = rng.arbitrary()?;
if t1.is_paused() {
if coin_flip {
t1.unpause();
}
} else if t2.is_paused() {
if coin_flip {
t2.unpause();
}
}
}
Triển khai luồng được quản lý
- Cần giao tiếp giữa luồng điều khiển và luồng được quản lý
- Triển khai bằng mutex và biến điều kiện để bảo vệ trạng thái
struct SharedContext {
state: Mutex<State>,
cv: Condvar,
}
#[derive(PartialEq, Eq, Default)]
enum State {
#[default]
Running,
Paused,
}
impl SharedContext {
fn pause(&self) {
let mut guard = self.state.lock().unwrap();
assert_eq!(*guard, State::Running);
*guard = State::Paused;
self.cv.notify_all();
guard = self.cv.wait_while(guard, |state| *state == State::Paused).unwrap();
assert_eq!(*guard, State::Running);
}
}
Tích hợp toàn bộ mã
- Tích hợp các luồng được quản lý và mã kiểm thử
#[test]
fn test_counter() {
arbtest::arbtest(|rng| {
eprintln!("begin trace");
let counter = Counter::default();
let mut counter_model: u32 = 0;
std::thread::scope(|scope| {
let t1 = managed_thread::spawn(scope, &counter);
let t2 = managed_thread::spawn(scope, &counter);
let mut threads = [t1, t2];
while !rng.is_empty() {
for (tid, t) in threads.iter_mut().enumerate() {
if rng.arbitrary()? {
if t.is_paused() {
eprintln!("{tid}: unpause");
t.unpause()
} else {
eprintln!("{tid}: increment");
t.submit(|c| c.increment());
counter_model += 1;
}
}
}
}
for t in threads {
t.join();
}
assert_eq!(counter_model, counter.get());
Ok(())
})
});
}
Tóm tắt của GN⁺
- Bài viết này giải thích cách kiểm thử các cấu trúc dữ liệu đồng thời
- Khám phá cách dùng thư viện
loom của Rust để kiểm thử các phép toán không mang tính nguyên tử
- Dùng các luồng được quản lý để kiểm thử các vấn đề đồng thời theo cách có thể tái hiện và dễ debug
- Bài viết sẽ hữu ích với các lập trình viên quan tâm đến lập trình đồng thời
- Một dự án có chức năng tương tự là
JCStress của Java
1 bình luận
Ý kiến trên Hacker News
Tôi đang phát triển một thư viện tên là Temper bằng Rust, và cần rất nhiều nỗ lực để xử lý những phần phức tạp của mô hình bộ nhớ Rust
Tôi đã triển khai snapshot nguyên tử bộ nhớ chia sẻ trong Rust và xem kiểm thử tự động là cực kỳ quan trọng
Nhược điểm của cách tiếp cận này là phải chỉnh sửa chính mã nguồn để phù hợp với mã kiểm thử
Lincheck của JetBrains là một thư viện tốt trong thế giới Kotlin/Java
Tôi tự hỏi liệu có thư viện nào giống "Loom" cho C++ không
Cách tiếp cận này có thể có giới hạn đối với bảo đảm tiến triển mềm
Cần có kiến thức thực tiễn và phải tạo các luồng thực sự
Có thể dùng ptrace để chạy các luồng theo kiểu single-step nhằm tạo ra những interleaving khác nhau ở mức lệnh
Để dùng Loom phải sử dụng conditional compilation, điều này khá xâm lấn
Tôi muốn biết cách thực hiện điều tương tự trong Python