crates.io docs.rs Rust CI MIT License

Lincheck

Lincheck is a Rust library for testing concurrent data structures for linearizability. Simply put, it checks whether a concurrent data structure behaves similarly to a simpler sequential implementation. It is inspired by Lincheck for Kotlin and is built on top of loom, a model-checker for concurrency.

Features

Tutorial

For this tutorial we will use the following: rust use lincheck::{ConcurrentSpec, Lincheck, SequentialSpec}; use loom::sync::atomic::{AtomicBool, Ordering}; use proptest::prelude::*;

Let's implement a simple concurrent data structure: a pair of boolean flags x and y that can be read and written by multiple threads. The flags are initialized to false and can be switched to true.

We start by defining the operations and their results: ```rust

[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 } ```

We need to implement the Arbitrary trait for our operations to be able to generate them randomly: ```rust impl Arbitrary for Op { type Parameters = (); type Strategy = BoxedStrategy;

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

} ```

We then define the concurrent implementation that we want to test: rust struct TwoSlotsParallel { x: AtomicBool, y: AtomicBool, }

We implement the ConcurrentSpec trait for our implementation: ```rust impl ConcurrentSpec for TwoSlotsParallel { type Op = Op; type Ret = Ret;

fn new() -> Self {
    Self {
        x: AtomicBool::new(false),
        y: AtomicBool::new(false),
    }
}

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)),
    }
}

} `` We must be able to create a new instance of our implementation and execute an operation on it. Theexec` method should not panic.

We then define the sequential implementation which we test against: rust struct TwoSlotsSequential { x: bool, y: bool, }

We implement the SequentialSpec trait for our implementation: ```rust impl SequentialSpec for TwoSlotsSequential { type Op = Op; type Ret = Ret;

fn new() -> Self {
    Self { x: false, y: false }
}

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),
    }
}

} ```

Notice that the concurrent specification receives a shared reference to itself (&self) while the sequential specification receives an exclusive reference to itself (&mut self). This is because the concurrent specification is shared between threads while the sequential specification is not.

We are now ready to write our test: ```rust

[test]

fn twoslots() { Lincheck { numthreads: 2, num_ops: 5, }.verify::(); } ```

If we run the test, we get a failure along with a trace of the execution: ``` running 1 test test two_slots ... FAILED

failures:

---- twoslots stdout ---- thread 'twoslots' 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 | | | |----------------| ```

Limitations

Semver compatibility and MSRV

Lincheck follows semver. However, the API is not yet stable and may change in a breaking way before the first stable release. There are also no guarantees about the MSRV (minimum supported Rust version) for now.

License

Lincheck is licensed under the MIT license.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Lincheck by you, shall be licensed as MIT, without any additional terms or conditions.