Skip to content

Commit 5b5d644

Browse files
authoredNov 5, 2023
Merge pull request #92 from lyrm/more_functions
`pop_opt`, `peek`, `peek_opt` (and if possible `try_push`) functions for all queues.
2 parents 49e4e59 + 6699df5 commit 5b5d644

24 files changed

+850
-170
lines changed
 

‎bench/bench_spsc_queue.ml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ let run () =
1616
start_time)
1717
in
1818
for _ = 1 to item_count do
19-
while Option.is_none (Spsc_queue.pop queue) do
19+
while Option.is_none (Spsc_queue.pop_opt queue) do
2020
()
2121
done
2222
done;

‎src_lockfree/michael_scott_queue.ml

+24-19
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ let create () =
3131

3232
let is_empty { head; _ } = Atomic.get (Atomic.get head) == Nil
3333

34-
let pop { head; _ } =
34+
exception Empty
35+
36+
let pop_opt { head; _ } =
3537
let b = Backoff.create () in
3638
let rec loop () =
3739
let old_head = Atomic.get head in
@@ -45,6 +47,27 @@ let pop { head; _ } =
4547
in
4648
loop ()
4749

50+
let pop { head; _ } =
51+
let b = Backoff.create () in
52+
let rec loop () =
53+
let old_head = Atomic.get head in
54+
match Atomic.get old_head with
55+
| Nil -> raise Empty
56+
| Next (value, next) when Atomic.compare_and_set head old_head next -> value
57+
| _ ->
58+
Backoff.once b;
59+
loop ()
60+
in
61+
loop ()
62+
63+
let peek_opt { head; _ } =
64+
let old_head = Atomic.get head in
65+
match Atomic.get old_head with Nil -> None | Next (value, _) -> Some value
66+
67+
let peek { head; _ } =
68+
let old_head = Atomic.get head in
69+
match Atomic.get old_head with Nil -> raise Empty | Next (value, _) -> value
70+
4871
let rec fix_tail tail new_tail =
4972
let old_tail = Atomic.get tail in
5073
if
@@ -66,24 +89,6 @@ let push { tail; _ } value =
6689
if not (Atomic.compare_and_set tail old_tail new_tail) then
6790
fix_tail tail new_tail
6891

69-
let clean_until { head; _ } f =
70-
let b = Backoff.create () in
71-
let rec loop () =
72-
let old_head = Atomic.get head in
73-
match Atomic.get old_head with
74-
| Nil -> ()
75-
| Next (value, next) ->
76-
if not (f value) then
77-
if Atomic.compare_and_set head old_head next then (
78-
Backoff.reset b;
79-
loop ())
80-
else (
81-
Backoff.once b;
82-
loop ())
83-
else ()
84-
in
85-
loop ()
86-
8792
type 'a cursor = 'a node
8893

8994
let snapshot { head; _ } = Atomic.get (Atomic.get head)

‎src_lockfree/michael_scott_queue.mli

+18-6
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,26 @@ val is_empty : 'a t -> bool
3737
val push : 'a t -> 'a -> unit
3838
(** [push q v] adds the element [v] at the end of the queue [q]. *)
3939

40-
val pop : 'a t -> 'a option
41-
(** [pop q] removes and returns the first element in queue [q], or
40+
exception Empty
41+
(** Raised when {!pop} or {!peek} is applied to an empty queue. *)
42+
43+
val pop : 'a t -> 'a
44+
(** [pop q] removes and returns the first element in queue [q].
45+
46+
@raise Empty if [q] is empty. *)
47+
48+
val pop_opt : 'a t -> 'a option
49+
(** [pop_opt q] removes and returns the first element in queue [q], or
4250
returns [None] if the queue is empty. *)
4351

44-
val clean_until : 'a t -> ('a -> bool) -> unit
45-
(** [clean_until q f] drops the prefix of the queue until the element [e],
46-
where [f e] is [true]. If no such element exists, then the queue is
47-
emptied. *)
52+
val peek : 'a t -> 'a
53+
(** [peek q] returns the first element in queue [q].
54+
55+
@raise Empty if [q] is empty. *)
56+
57+
val peek_opt : 'a t -> 'a option
58+
(** [peek_opt q] returns the first element in queue [q], or
59+
returns [None] if the queue is empty. *)
4860

4961
type 'a cursor
5062
(** The type of cursor. *)

‎src_lockfree/mpsc_queue.ml

+28-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ let rec close (t : 'a t) =
102102
(* Retry *)
103103
close t)
104104

105-
let pop t =
105+
let pop_opt t =
106106
let p = t.head in
107107
(* [p] is the previously-popped item. *)
108108
let node = Atomic.get p.next in
@@ -115,6 +115,33 @@ let pop t =
115115
(* So it can be GC'd *)
116116
Some v)
117117

118+
exception Empty
119+
120+
let pop t =
121+
let p = t.head in
122+
(* [p] is the previously-popped item. *)
123+
let node = Atomic.get p.next in
124+
Node.fold node
125+
~none:(fun () -> raise Empty)
126+
~some:(fun node ->
127+
t.head <- node;
128+
let v = node.value in
129+
node.value <- Obj.magic ();
130+
(* So it can be GC'd *)
131+
v)
132+
133+
let peek_opt t =
134+
let p = t.head in
135+
(* [p] is the previously-popped item. *)
136+
let node = Atomic.get p.next in
137+
Node.fold node ~none:(fun () -> None) ~some:(fun node -> Some node.value)
138+
139+
let peek t =
140+
let p = t.head in
141+
(* [p] is the previously-popped item. *)
142+
let node = Atomic.get p.next in
143+
Node.fold node ~none:(fun () -> raise Empty) ~some:(fun node -> node.value)
144+
118145
let is_empty t =
119146
Node.fold (Atomic.get t.head.next)
120147
~none:(fun () -> true)

‎src_lockfree/mpsc_queue.mli

+38-13
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,58 @@ exception Closed
1212
val create : unit -> 'a t
1313
(** [create ()] returns a new empty queue. *)
1414

15+
val is_empty : 'a t -> bool
16+
(** [is_empty q] is [true] if calling [pop] would return [None].
17+
18+
@raise Closed if [q] is closed and empty. *)
19+
20+
val close : 'a t -> unit
21+
(** [close q] marks [q] as closed, preventing any further items from
22+
being pushed by the producers (i.e. with {!push}).
23+
24+
@raise Closed if [q] has already been closed. *)
25+
1526
val push : 'a t -> 'a -> unit
16-
(** [push q v] adds the element [v] at the end of the queue [q]. This
27+
(** [push q v] adds the element [v] at the end of the queue [q]. This
1728
can be used safely by multiple producer domains, in parallel with
1829
the other operations.
1930
2031
@raise Closed if [q] is closed. *)
2132

22-
val pop : 'a t -> 'a option
23-
(** [pop q] removes and returns the first element in queue [q] or
33+
(** {2 Consumer-only functions} *)
34+
35+
exception Empty
36+
(** Raised when {!pop} or {!peek} is applied to an empty queue. *)
37+
38+
val pop : 'a t -> 'a
39+
(** [pop q] removes and returns the first element in queue [q].
40+
41+
@raise Empty if [q] is empty.
42+
43+
@raise Closed if [q] is closed and empty. *)
44+
45+
val pop_opt : 'a t -> 'a option
46+
(** [pop_opt q] removes and returns the first element in queue [q] or
2447
returns [None] if the queue is empty.
2548
2649
@raise Closed if [q] is closed and empty. *)
2750

28-
val push_head : 'a t -> 'a -> unit
29-
(** [push_head q v] adds the element [v] at the head of the queue
30-
[q]. This can only be used by the consumer (if run in parallel
31-
with {!pop}, the item might be skipped).
51+
val peek : 'a t -> 'a
52+
(** [peek q] returns the first element in queue [q].
53+
54+
@raise Empty if [q] is empty.
3255
3356
@raise Closed if [q] is closed and empty. *)
3457

35-
val is_empty : 'a t -> bool
36-
(** [is_empty q] is [true] if calling [pop] would return [None].
58+
val peek_opt : 'a t -> 'a option
59+
(** [peek_opt q] returns the first element in queue [q] or
60+
returns [None] if the queue is empty.
3761
3862
@raise Closed if [q] is closed and empty. *)
3963

40-
val close : 'a t -> unit
41-
(** [close q] marks [q] as closed, preventing any further items from
42-
being pushed by the producers (i.e. with {!push}).
64+
val push_head : 'a t -> 'a -> unit
65+
(** [push_head q v] adds the element [v] at the head of the queue
66+
[q]. This can only be used by the consumer (if run in parallel
67+
with {!pop}, the item might be skipped).
4368
44-
@raise Closed if [q] has already been closed. *)
69+
@raise Closed if [q] is closed and empty. *)

‎src_lockfree/spsc_queue.ml

+41
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,31 @@ let push { array; head; tail; mask; _ } element =
4848
Array.set array (tail_val land mask) (Some element);
4949
Atomic.set tail (tail_val + 1))
5050

51+
let try_push { array; head; tail; mask; _ } element =
52+
let size = mask + 1 in
53+
let head_val = Atomic.get head in
54+
let tail_val = Atomic.get tail in
55+
if head_val + size == tail_val then false
56+
else (
57+
Array.set array (tail_val land mask) (Some element);
58+
Atomic.set tail (tail_val + 1);
59+
true)
60+
61+
exception Empty
62+
5163
let pop { array; head; tail; mask; _ } =
64+
let head_val = Atomic.get head in
65+
let tail_val = Atomic.get tail in
66+
if head_val == tail_val then raise Empty
67+
else
68+
let index = head_val land mask in
69+
let v = Array.get array index in
70+
(* allow gc to collect it *)
71+
Array.set array index None;
72+
Atomic.set head (head_val + 1);
73+
match v with None -> assert false | Some v -> v
74+
75+
let pop_opt { array; head; tail; mask; _ } =
5276
let head_val = Atomic.get head in
5377
let tail_val = Atomic.get tail in
5478
if head_val == tail_val then None
@@ -61,4 +85,21 @@ let pop { array; head; tail; mask; _ } =
6185
assert (Option.is_some v);
6286
v
6387

88+
let peek_opt { array; head; tail; mask; _ } =
89+
let head_val = Atomic.get head in
90+
let tail_val = Atomic.get tail in
91+
if head_val == tail_val then None
92+
else
93+
let v = Array.get array @@ (head_val land mask) in
94+
assert (Option.is_some v);
95+
v
96+
97+
let peek { array; head; tail; mask; _ } =
98+
let head_val = Atomic.get head in
99+
let tail_val = Atomic.get tail in
100+
if head_val == tail_val then raise Empty
101+
else
102+
let v = Array.get array @@ (head_val land mask) in
103+
match v with None -> assert false | Some v -> v
104+
64105
let size { head; tail; _ } = Atomic.get tail - Atomic.get head

‎src_lockfree/spsc_queue.mli

+43-10
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,59 @@ type 'a t
44
(** Type of single-producer single-consumer non-resizable domain-safe
55
queue that works in FIFO order. *)
66

7-
exception Full
8-
(** Raised when {!push} is applied to a full queue. *)
9-
107
val create : size_exponent:int -> 'a t
118
(** [create ~size_exponent:int] returns a new queue of maximum size
129
[2^size_exponent] and initially empty. *)
1310

11+
val size : 'a t -> int
12+
(** [size] returns the size of the queue. This method linearizes only when called
13+
from either consumer or producer domain. Otherwise, it is safe to call but
14+
provides only an *indication* of the size of the structure. *)
15+
16+
(** {1 Producer functions} *)
17+
18+
exception Full
19+
(** Raised when {!push} is applied to a full queue. *)
20+
1421
val push : 'a t -> 'a -> unit
1522
(** [push q v] adds the element [v] at the end of the queue [q]. This
1623
method can be used by at most one domain at the time.
1724
18-
@raise [Full] if the queue is full.
25+
@raise Full if the queue is full.
26+
*)
27+
28+
val try_push : 'a t -> 'a -> bool
29+
(** [try_push q v] tries to add the element [v] at the end of the
30+
queue [q]. It fails it the queue [q] is full. This method can be
31+
used by at most one domain at the time.
32+
*)
33+
34+
(** {2 Consumer functions} *)
35+
36+
exception Empty
37+
(** Raised when {!pop} or {!peek} is applied to an empty queue. *)
38+
39+
val pop : 'a t -> 'a
40+
(** [pop q] removes and returns the first element in queue [q]. This
41+
method can be used by at most one domain at the time.
42+
43+
@raise Empty if [q] is empty.
1944
*)
2045

21-
val pop : 'a t -> 'a option
22-
(** [pop q] removes and returns the first element in queue [q], or
46+
val pop_opt : 'a t -> 'a option
47+
(** [pop_opt q] removes and returns the first element in queue [q], or
2348
returns [None] if the queue is empty. This method can be used by
2449
at most one domain at the time. *)
2550

26-
val size : 'a t -> int
27-
(** [size] returns the size of the queue. This method linearizes only when called
28-
from either consumer or producer domain. Otherwise, it is safe to call but
29-
provides only an *indication* of the size of the structure. *)
51+
val peek : 'a t -> 'a
52+
(** [peek q] returns the first element in queue [q]. This method can be
53+
used by at most one domain at the time.
54+
55+
@raise Empty if [q] is empty.
56+
*)
57+
58+
val peek_opt : 'a t -> 'a option
59+
(** [peek_opt q] returns the first element in queue [q], or [None]
60+
if the queue is empty. This method can be used by at most one
61+
domain at the time.
62+
*)

‎src_lockfree/treiber_stack.ml

+24
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,31 @@ let push q v =
2828
in
2929
loop b
3030

31+
exception Empty
32+
3133
let pop q =
34+
let rec loop b =
35+
let s = Atomic.get q.head in
36+
match s with
37+
| Nil -> raise Empty
38+
| Next (v, next) ->
39+
if Atomic.compare_and_set q.head s next then v
40+
else (
41+
Backoff.once b;
42+
loop b)
43+
in
44+
45+
let s = Atomic.get q.head in
46+
match s with
47+
| Nil -> raise Empty
48+
| Next (v, next) ->
49+
if Atomic.compare_and_set q.head s next then v
50+
else
51+
let b = Backoff.create () in
52+
Backoff.once b;
53+
loop b
54+
55+
let pop_opt q =
3256
let rec loop b =
3357
let s = Atomic.get q.head in
3458
match s with

‎src_lockfree/treiber_stack.mli

+14-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ val is_empty : 'a t -> bool
1515
val push : 'a t -> 'a -> unit
1616
(** [push s v] adds the element [v] at the top of stack [s]. *)
1717

18-
val pop : 'a t -> 'a option
19-
(** [pop a] removes and returns the topmost element in the
20-
stack [s], or returns [None] if the stack is empty. *)
18+
exception Empty
19+
(** Raised when {!pop} or {!peek} is applied to an empty queue. *)
20+
21+
val pop : 'a t -> 'a
22+
(** [pop s] removes and returns the topmost element in the
23+
stack [s].
24+
25+
@raise Empty if [a] is empty.
26+
*)
27+
28+
val pop_opt : 'a t -> 'a option
29+
(** [pop_opt s] removes and returns the topmost element in the
30+
stack [s], or returns [None] if the stack is empty.
31+
*)

0 commit comments

Comments
 (0)
Please sign in to comment.