Skip to content

Commit 4f7dfa4

Browse files
authored
Merge pull request #34 from bartoszmodelski/dscheck-tests
dscheck tests for current data structures.
2 parents 9d5ef7f + 0c97d3a commit 4f7dfa4

17 files changed

+309
-1
lines changed

atomic/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Virtual atomic
2+
3+
Virtual atomic lets us vary the implementation of `Atomic` module. The are two options:
4+
5+
* `Stdlib.Atomic` - the standard library implementation.
6+
* `Dscheck.TracedAtomic` - wrapper provided by a model checker ([DSCheck](https://github.com/ocaml-multicore/dscheck)) for exhaustively testing all interleavings.
7+
8+
## User perspective
9+
10+
`Lockfree` defaults to standard library implementation, thus user does not need to do select anything explicitely to use our data structures.
11+
12+
However, it can be useful to write your own DSCheck tests (e.g. when composing two lock-free structures). In such a case, import `lockfree.atomic_traced` to override the default choice. See [../test/dscheck/dune](../test/dscheck/dune) for example.

atomic/dune

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(library
2+
(name virtual_atomic)
3+
(public_name lockfree.virtual_atomic)
4+
(virtual_modules virtual_atomic)
5+
(default_implementation atomic_stdlib))

atomic/stdlib/dune

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
(library
2+
(name atomic_stdlib)
3+
(public_name lockfree.atomic_stdlib)
4+
(implements virtual_atomic))

atomic/stdlib/virtual_atomic.ml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Atomic = struct
2+
include Stdlib.Atomic
3+
end

atomic/traced/dune

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(library
2+
(name atomic_traced)
3+
(public_name lockfree.atomic_traced)
4+
(libraries dscheck)
5+
(implements virtual_atomic))

atomic/traced/virtual_atomic.ml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Atomic = struct
2+
include Dscheck.TracedAtomic
3+
end

atomic/virtual_atomic.mli

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
(** Copied from standard library. *)
2+
3+
module Atomic : sig
4+
type !'a t
5+
(** An atomic (mutable) reference to a value of type ['a]. *)
6+
7+
val make : 'a -> 'a t
8+
(** Create an atomic reference. *)
9+
10+
val get : 'a t -> 'a
11+
(** Get the current value of the atomic reference. *)
12+
13+
val set : 'a t -> 'a -> unit
14+
(** Set a new value for the atomic reference. *)
15+
16+
val exchange : 'a t -> 'a -> 'a
17+
(** Set a new value for the atomic reference, and return the current value. *)
18+
19+
val compare_and_set : 'a t -> 'a -> 'a -> bool
20+
(** [compare_and_set r seen v] sets the new value of [r] to [v] only
21+
if its current value is physically equal to [seen] -- the
22+
comparison and the set occur atomically. Returns [true] if the
23+
comparison succeeded (so the set happened) and [false]
24+
otherwise. *)
25+
26+
val fetch_and_add : int t -> int -> int
27+
(** [fetch_and_add r n] atomically increments the value of [r] by [n],
28+
and returns the current value (before the increment). *)
29+
30+
val incr : int t -> unit
31+
(** [incr r] atomically increments the value of [r] by [1]. *)
32+
33+
val decr : int t -> unit
34+
(** [decr r] atomically decrements the value of [r] by [1]. *)
35+
end

lockfree.opam

+2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ depends: [
1313
"dune" {>= "3.0"}
1414
"qcheck" {with-test & >= "0.18.1"}
1515
"qcheck-alcotest" {with-test & >= "0.18.1"}
16+
"alcotest" {>= "1.6.0"}
1617
"yojson" {>= "2.0.2"}
18+
"dscheck" {>= "0.0.1"}
1719
]
1820
depopts: []
1921
build: ["dune" "build" "-p" name "-j" jobs]

src/dune

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
(library
22
(name lockfree)
3-
(public_name lockfree))
3+
(public_name lockfree)
4+
(libraries dscheck virtual_atomic))

src/mpsc_queue.ml

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
It is simplified slightly because we don't need multiple consumers.
88
Therefore [head] is not atomic. *)
99

10+
open Virtual_atomic
11+
1012
exception Closed
1113

1214
module Node : sig

src/spsc_queue.ml

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
* https://dl.acm.org/doi/pdf/10.1145/3437801.3441583
2222
*)
2323

24+
open Virtual_atomic
25+
2426
type 'a t = {
2527
array : 'a Option.t Array.t;
2628
tail : int Atomic.t;

src/ws_deque.ml

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
* https://dl.acm.org/doi/abs/10.1145/2442516.2442524
2828
*)
2929

30+
open Virtual_atomic
31+
3032
module type S = sig
3133
type 'a t
3234

test/dscheck/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This directory contains tests using [DSCheck](https://github.com/ocaml-multicore/dscheck). DSCheck lets us verify that relevant invariants indeed hold under all possible interleavings.

test/dscheck/dune

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
(test
2+
(name spsc_queue)
3+
(libraries lockfree lockfree.atomic_traced dscheck alcotest)
4+
(modules spsc_queue))
5+
6+
(test
7+
(name ws_deque)
8+
(libraries lockfree lockfree.atomic_traced dscheck alcotest)
9+
(modules ws_deque))
10+
11+
(test
12+
(name mpsc_queue)
13+
(libraries lockfree lockfree.atomic_traced dscheck alcotest)
14+
(modules mpsc_queue))

test/dscheck/mpsc_queue.ml

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
module Atomic = Dscheck.TracedAtomic
2+
open Lockfree
3+
4+
let drain queue =
5+
let remaining = ref 0 in
6+
while not (Mpsc_queue.is_empty queue) do
7+
remaining := !remaining + 1;
8+
assert (Option.is_some (Mpsc_queue.pop queue))
9+
done;
10+
!remaining
11+
12+
let producer_consumer () =
13+
Atomic.trace (fun () ->
14+
let queue = Mpsc_queue.create () in
15+
let items_total = 4 in
16+
17+
(* producer *)
18+
Atomic.spawn (fun () ->
19+
for _ = 1 to items_total - 1 do
20+
Mpsc_queue.push queue 0
21+
done);
22+
23+
(* consumer *)
24+
let popped = ref 0 in
25+
Atomic.spawn (fun () ->
26+
Mpsc_queue.push_head queue 1;
27+
for _ = 1 to items_total do
28+
match Mpsc_queue.pop queue with
29+
| None -> ()
30+
| Some _ -> popped := !popped + 1
31+
done);
32+
33+
(* checks*)
34+
Atomic.final (fun () ->
35+
Atomic.check (fun () ->
36+
let remaining = drain queue in
37+
!popped + remaining = items_total)))
38+
39+
let two_producers () =
40+
Atomic.trace (fun () ->
41+
let queue = Mpsc_queue.create () in
42+
let items_total = 4 in
43+
44+
(* producers *)
45+
for _ = 1 to 2 do
46+
Atomic.spawn (fun () ->
47+
for _ = 1 to items_total / 2 do
48+
Mpsc_queue.push queue 0
49+
done)
50+
done;
51+
52+
(* checks*)
53+
Atomic.final (fun () ->
54+
Atomic.check (fun () ->
55+
let remaining = drain queue in
56+
remaining = items_total)))
57+
58+
let () =
59+
let open Alcotest in
60+
run "mpsc_queue_dscheck"
61+
[
62+
( "basic",
63+
[
64+
test_case "1-producer-1-consumer" `Slow producer_consumer;
65+
test_case "2-producers" `Slow two_producers;
66+
] );
67+
]

test/dscheck/spsc_queue.ml

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
open Lockfree
2+
module Atomic = Dscheck.TracedAtomic
3+
4+
let create_test ~shift_by () =
5+
let queue = Spsc_queue.create ~size_exponent:2 in
6+
let items_count = 3 in
7+
8+
(* shift the queue, that helps testing overlap handling *)
9+
for _ = 1 to shift_by do
10+
Spsc_queue.push queue (-1);
11+
assert (Option.is_some (Spsc_queue.pop queue))
12+
done;
13+
14+
(* enqueuer *)
15+
Atomic.spawn (fun () ->
16+
for i = 1 to items_count do
17+
Spsc_queue.push queue i
18+
done);
19+
20+
(* dequeuer *)
21+
let dequeued = ref 0 in
22+
Atomic.spawn (fun () ->
23+
for _ = 1 to items_count + 1 do
24+
match Spsc_queue.pop queue with
25+
| None -> ()
26+
| Some v ->
27+
assert (v = !dequeued + 1);
28+
dequeued := v
29+
done);
30+
31+
(* ending assertions *)
32+
Atomic.final (fun () ->
33+
Atomic.check (fun () -> Spsc_queue.size queue == items_count - !dequeued))
34+
35+
let with_trace ?(shift_by = 0) f () = Atomic.trace (fun () -> f ~shift_by ())
36+
37+
let size_linearizes_with_1_thr () =
38+
Atomic.trace (fun () ->
39+
let queue = Spsc_queue.create ~size_exponent:4 in
40+
Spsc_queue.push queue (-1);
41+
Spsc_queue.push queue (-1);
42+
43+
Atomic.spawn (fun () ->
44+
for _ = 1 to 4 do
45+
Spsc_queue.push queue (-1)
46+
done);
47+
48+
let size = ref 0 in
49+
Atomic.spawn (fun () ->
50+
assert (Option.is_some (Spsc_queue.pop queue));
51+
size := Spsc_queue.size queue);
52+
53+
Atomic.final (fun () -> Atomic.check (fun () -> 1 <= !size && !size <= 5)))
54+
55+
let () =
56+
let open Alcotest in
57+
run "Spsc_queue_dscheck"
58+
[
59+
("basic", [ test_case "simple-test" `Slow (with_trace create_test) ]);
60+
( "wrap-arounds",
61+
let with_shift s =
62+
test_case
63+
(Printf.sprintf "shift-by-%d" s)
64+
`Slow
65+
(with_trace ~shift_by:s create_test)
66+
in
67+
[ with_shift 1; with_shift 6; with_shift 11 ] );
68+
( "size",
69+
[ test_case "linearizes-with-1-thr" `Slow size_linearizes_with_1_thr ]
70+
);
71+
]

test/dscheck/ws_deque.ml

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
module Atomic = Dscheck.TracedAtomic
2+
open Lockfree
3+
4+
let drain_remaining queue =
5+
let remaining = ref 0 in
6+
(try
7+
while true do
8+
Ws_deque.M.pop queue |> ignore;
9+
remaining := !remaining + 1
10+
done
11+
with _ -> ());
12+
!remaining
13+
14+
let owner_stealer () =
15+
Atomic.trace (fun () ->
16+
let queue = Ws_deque.M.create () in
17+
let total_items = 3 in
18+
19+
let popped = ref 0 in
20+
21+
(* owner thr *)
22+
Atomic.spawn (fun () ->
23+
for _ = 1 to total_items do
24+
Ws_deque.M.push queue 0
25+
done;
26+
for _ = 1 to total_items / 2 do
27+
match Ws_deque.M.pop queue with
28+
| exception _ -> ()
29+
| _ -> popped := !popped + 1
30+
done);
31+
32+
(* stealer *)
33+
Atomic.spawn (fun () ->
34+
for _ = 1 to total_items / 2 do
35+
match Ws_deque.M.steal queue with
36+
| exception _ -> ()
37+
| _ -> popped := !popped + 1
38+
done);
39+
40+
Atomic.final (fun () ->
41+
Atomic.check (fun () ->
42+
let remaining = drain_remaining queue in
43+
remaining + !popped == total_items)))
44+
45+
let popper_stealer () =
46+
Atomic.trace (fun () ->
47+
let queue = Ws_deque.M.create () in
48+
let total_items = 3 in
49+
for _ = 1 to total_items do
50+
Ws_deque.M.push queue 0
51+
done;
52+
53+
(* stealers *)
54+
let popped = ref 0 in
55+
let stealer () =
56+
match Ws_deque.M.steal queue with
57+
| exception _ -> ()
58+
| _ -> popped := !popped + 1
59+
in
60+
Atomic.spawn stealer |> ignore;
61+
Atomic.spawn stealer |> ignore;
62+
63+
Atomic.final (fun () ->
64+
Atomic.check (fun () ->
65+
let remaining = drain_remaining queue in
66+
remaining = 1 && !popped = 2)))
67+
68+
let () =
69+
let open Alcotest in
70+
run "ws_deque_dscheck"
71+
[
72+
( "basic",
73+
[
74+
test_case "1-owner-1-stealer" `Slow owner_stealer;
75+
test_case "1-pusher-2-stealers" `Slow popper_stealer;
76+
(* we'd really want to test cases with more threads here,
77+
but dscheck is not optimized enough for that yet *)
78+
] );
79+
]

0 commit comments

Comments
 (0)