4 个版本

0.2.1 2023 年 8 月 3 日
0.2.0 2023 年 8 月 3 日
0.1.1 2023 年 7 月 14 日
0.1.0 2023 年 7 月 14 日

#265 in 测试


用于 mess_protector

MIT 许可证

63KB
1K SLoC

crates.io docs.rs Rust CI MIT License

Lincheck

Lincheck 是一个用于测试并发数据结构的 Rust 库,用于 线性化。简单来说,它检查并发数据结构的行为是否类似于更简单的顺序实现。它受到了 Kotlin 的 Lincheck 的启发,并建立在 loom(一个并发模型检查器)的基础上。

功能

  • Lincheck 使用 proptest 生成随机的并发场景,并将其自动缩小到最小失败场景。
  • Lincheck 在 loom 模型检查器中运行每个场景,以检查操作的所有可能的交错。
  • Lincheck 提供了一个简单的 API 来定义并发数据结构和它们的顺序对应物。
  • 执行跟踪的记录旨在引入尽可能少的线程间同步。

教程

在这个教程中,我们将使用以下内容

use lincheck::{ConcurrentSpec, Lincheck, SequentialSpec};
use loom::sync::atomic::{AtomicBool, Ordering};
use proptest::prelude::*;

让我们实现一个简单的并发数据结构:一对布尔标志 xy,它可以被多个线程读取和写入。标志初始化为 false,可以切换到 true

我们首先定义操作及其结果

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
    WriteX, // set x to true
    WriteY, // set y to true
    ReadX, // get the value of x
    ReadY, // get the value of y
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Ret {
    Write, // the result of a write operation
    Read(bool), // the result of a read operation
}

我们需要为我们的操作实现 Arbitrary 特性,以便能够随机生成它们

impl Arbitrary for Op {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
        prop_oneof![
            Just(Op::WriteX),
            Just(Op::WriteY),
            Just(Op::ReadX),
            Just(Op::ReadY),
        ]
        .boxed()
    }
}

我们定义了与测试对应的顺序实现

#[derive(Default)]
struct TwoSlotsSequential {
    x: bool,
    y: bool,
}

我们为我们的实现实现了 SequentialSpec 特性

impl SequentialSpec for TwoSlotsSequential {
    type Op = Op;
    type Ret = Ret;

    fn exec(&mut self, op: Op) -> Ret {
        match op {
            Op::WriteX => {
                self.x = true;
                Ret::Write
            }
            Op::WriteY => {
                self.y = true;
                Ret::Write
            }
            Op::ReadX => Ret::Read(self.x),
            Op::ReadY => Ret::Read(self.y),
        }
    }
}

然后定义我们想要测试的并发实现

#[derive(Default)]
struct TwoSlotsParallel {
    x: AtomicBool,
    y: AtomicBool,
}

然后为我们的实现实现 ConcurrentSpec 特性,并设置顺序规范

impl ConcurrentSpec for TwoSlotsParallel {
    type Seq = TwoSlotsSequential;

    fn exec(&self, op: Op) -> Ret {
        match op {
            Op::WriteX => {
                self.x.store(true, Ordering::Relaxed);
                Ret::Write
            }
            Op::WriteY => {
                self.y.store(true, Ordering::Relaxed);
                Ret::Write
            }
            Op::ReadX => Ret::Read(self.x.load(Ordering::Relaxed)),
            Op::ReadY => Ret::Read(self.y.load(Ordering::Relaxed)),
        }
    }
}

我们必须能够创建我们实现的实例并对其执行操作。 exec 方法不应引发 panic。

请注意,并发规范接收了对自身的共享引用(&self),而顺序规范接收了对自身的独占引用(&mut self)。这是因为并发规范在多个线程之间共享,而顺序规范不是。

我们现在可以编写我们的测试了

#[test]
fn two_slots() {
    Lincheck {
        num_threads: 2,
        num_ops: 5,
    }.verify::<TwoSlotsParallel, TwoSlotsSequential>();
}

如果我们运行测试,我们会得到一个失败和执行跟踪

running 1 test
test two_slots ... FAILED

failures:

---- two_slots stdout ----
thread 'two_slots' panicked at 'Non-linearizable execution: 

 INIT PART:
|================|
|  MAIN THREAD   |
|================|
|                |
| WriteX : Write |
|                |
|----------------|

PARALLEL PART:
|=====================|================|
|      THREAD 0       |    THREAD 1    |
|=====================|================|
|                     |                |
|                     |----------------|
|                     |                |
| ReadY : Read(false) | WriteY : Write |
|                     |                |
|                     |----------------|
|                     |                |
|---------------------|                |
|                     |                |
| ReadY : Read(false) |                |
|                     |                |
|---------------------|----------------|

POST PART:
|================|
|  MAIN THREAD   |
|================|
|                |
| WriteX : Write |
|                |
|----------------|

限制

  • Lincheck 运行器设置了它自己的 panic 钩。这与并行测试执行不兼容。要解决这个问题,您可以使用以下方式运行您的测试,带上 --test-threads=1 标志
$ cargo test -- --test-threads=1
  • loom 无法模拟所有弱内存模型效果。这意味着在某些真实硬件上可能出现的某些执行可能不会被 loom 探索。这就是为什么并发数据结构应该在真实硬件上额外进行模糊测试。Lincheck 计划支持模糊测试。
  • proptest 只探索所有可能场景的随机样本。这意味着可能不会探索一些失败的执行。

语义版本兼容性和 MSRV

Lincheck 遵循 semver。然而,API 尚未稳定,在第一个稳定版本发布之前可能会以破坏性的方式进行更改。目前也没有关于 MSRV(最低支持 Rust 版本)的保证。

许可证

Lincheck 在 MIT 许可证 下授权。

贡献

除非您明确声明,否则您提交给 Lincheck 的任何贡献,都应按照 MIT 许可证授权,没有任何额外的条款或条件。

依赖关系

~6–33MB
~455K SLoC