การทดสอบโครงสร้างข้อมูลแบบพร้อมกันอย่างเหมาะสม
หนึ่ง สอง สาม สอง
- สามารถใช้
loom ซึ่งเป็นไลบรารีของ Rust เพื่อทดสอบโครงสร้างข้อมูลแบบ lock-free ได้อย่างละเอียด
- มีตัวอย่างโค้ดตัวนับแบบพร้อมกันอย่างง่าย
- บั๊กของโค้ดคือการดำเนินการเพิ่มค่าไม่เป็นอะตอมมิก
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)
}
}
การทดสอบอย่างง่าย
- ทดสอบโดยเพิ่มค่าตัวนับเดียวกันซ้ำ ๆ จากหลายเธรดแล้วตรวจสอบผลลัพธ์
- การทดสอบสามารถล้มเหลวได้สำเร็จ แต่ขึ้นอยู่กับจังหวะเวลา จึงทำให้ทำซ้ำได้ยาก
#[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);
}
การทดสอบเชิงคุณสมบัติ (PBT)
- ลองนำการทดสอบเชิงคุณสมบัติที่เหมาะกับการทดสอบ state machine มาใช้
- หากสามารถรันเธรดทีละขั้นด้วยมือได้ ก็จะสอดแทรกการทำงานระหว่าง
load และ store ของอีกเธรดได้ง่าย
#[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 แบบง่าย
- วิธีทำให้เธรดสามารถ "หยุดชั่วคราว" ระหว่างการดำเนินการอะตอมมิก
pub fn increment(&self) {
pause();
let value = self.value.load(SeqCst);
pause();
self.value.store(value + 1, SeqCst);
pause();
}
fn pause() {
// ¯\\_(ツ)_/¯
}
Managed thread API
- กฎข้อหนึ่งของการออกแบบ API คือเริ่มจากผู้ใช้เพียงคนเดียวเพื่อทำความเข้าใจความรู้สึกของ API ก่อน แล้วค่อยลงมือทำ implementation จริง
- เขียนการทดสอบเชิงคุณสมบัติโดยใช้ managed thread
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();
}
}
}
การทำงานของ managed thread
- จำเป็นต้องมีการสื่อสารระหว่างเธรดควบคุมกับ managed thread
- ใช้ mutex และ condition variable ที่ปกป้องสถานะในการติดตั้งใช้งาน
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);
}
}
รวมโค้ดทั้งหมดเข้าด้วยกัน
- รวม managed thread และโค้ดทดสอบเข้าด้วยกัน
#[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(())
})
});
}
สรุปโดย GN⁺
- บทความนี้อธิบายวิธีทดสอบโครงสร้างข้อมูลแบบพร้อมกัน
- สำรวจวิธีใช้ไลบรารี
loom ของ Rust เพื่อทดสอบการดำเนินการที่ไม่เป็นอะตอมมิก
- ใช้ managed thread เพื่อทดสอบปัญหาความพร้อมกันในรูปแบบที่ทำซ้ำได้และดีบักได้
- บทความนี้น่าจะเป็นประโยชน์สำหรับนักพัฒนาที่สนใจการเขียนโปรแกรมแบบพร้อมกัน
- โปรเจ็กต์ที่มีความสามารถคล้ายกันคือ
JCStress ของ Java
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
กำลังพัฒนาไลบรารีชื่อ Temper ด้วย Rust และต้องใช้ความพยายามอย่างมากเพื่อรับมือกับส่วนที่ซับซ้อนของโมเดลหน่วยความจำของ Rust
ได้ทำ implementation ของ shared-memory atomic snapshot ใน Rust และมองว่าการทดสอบอัตโนมัติมีความสำคัญมาก
ข้อเสียของแนวทางนี้คือจำเป็นต้องปรับแก้โค้ดเองให้เข้ากับโค้ดทดสอบ
Lincheck ของ JetBrains เป็นไลบรารีที่ดีในโลก Kotlin/Java
สงสัยว่ามีไลบรารีคล้าย "Loom" สำหรับ C++ หรือไม่
แนวทางนี้อาจมีข้อจำกัดกับการรับประกันความคืบหน้าแบบ soft
ต้องมีความรู้เชิงปฏิบัติจริง และจำเป็นต้องสร้างเธรดจริง
สามารถใช้ ptrace ให้เธรดรันแบบ single-step เพื่อสร้าง interleaving แบบต่าง ๆ ในระดับคำสั่งได้
การใช้ Loom จำเป็นต้องใช้ conditional compilation ซึ่งค่อนข้าง intrusive
อยากรู้วิธีทำสิ่งเดียวกันนี้ใน Python