2 điểm bởi GN⁺ 2024-07-07 | 1 bình luận | Chia sẻ qua WhatsApp

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 loadstore 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

 
GN⁺ 2024-07-07
Ý 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

    • Nó bao gồm bộ sưu tập các ca kiểm thử lớn nhất cho mô hình bộ nhớ C++/Rust
    • Loom là một thư viện hoàn thiện hơn, cho phép kiểm thử kỹ lưỡng các cấu trúc cấp cao như mutex và queue
    • Lấy cảm hứng từ bài nói chuyện về kiểm thử của Foundation DB, và tin rằng WebAssembly sẽ đóng vai trò quan trọng trong lĩnh vực này
  • 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

    • Ban đầu dùng Loom nhưng sau đó chuyển sang Shuttle
    • Shuttle sử dụng cách tiếp cận ngẫu nhiên hóa và cung cấp đảm bảo mang tính xác suất trong việc tìm lỗi
    • Shuttle nhanh hơn và mở rộng tốt với các kịch bản kiểm thử phức tạp
    • Khả năng tái hiện các bài kiểm thử thất bại 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ử

    • Cũng có thể làm bằng cách chạy hai luồng và dùng ptrace để trộn ngẫu nhiên việc thực thi lệnh thông qua single-step
  • Lincheck của JetBrains là một thư viện tốt trong thế giới Kotlin/Java

    • Tôi thích việc nó mang tính khai báo và cách nó xuất ra kết quả linearization
  • Tôi tự hỏi liệu có thư viện nào giống "Loom" cho C++ không

    • Tôi muốn kiểm thử các cấu trúc dữ liệu lock-free
  • 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

    • Trong vòng lặp cmpxchg, trên phần cứng và scheduler thực tế thì khả năng bị gián đoạn là rất thấp
    • Tuy nhiên, trong cách kiểm thử này, xác suất tiến triển thay đổi theo số lượng tác vụ và số lần tạm dừng
  • Cần có kiến thức thực tiễn và phải tạo các luồng thực sự

    • Tôi tự hỏi có thể dùng async runtime hay không
  • 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

    • Tôi tự hỏi có lựa chọn thay thế kiểm thử hộp đen nào không
  • Để dùng Loom phải sử dụng conditional compilation, điều này khá xâm lấn

    • Tôi tự hỏi liệu các ngôn ngữ khác có phù hợp hơn vì dùng scheduler riêng hay không
  • Tôi muốn biết cách thực hiện điều tương tự trong Python

    • Tôi tự hỏi có thể tạo một lớp luồng cho phép kiểu kiểm thử này hay không