Skip to content

Commit 895f187

Browse files
authored
Merge pull request #540 from ocaml-multicore/improve-thread-mode
Improve context switching chance in `Lin_thread` and `STM_thread` modes
2 parents 6afe7de + 0ef30bb commit 895f187

26 files changed

+229
-89
lines changed

CHANGES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## NEXT RELEASE
44

5-
- ...
5+
- #540: Significantly increase the probability of context switching in `Lin_thread`
6+
and `STM_thread` by utilizing `Gc.Memprof` callbacks. Avoid on 5.0-5.2
7+
without `Gc.Memprof` support.
68

79
## 0.7
810

lib/STM_thread.ml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ module MakeExt (Spec: SpecExt) = struct
1111

1212
let arb_cmds_triple = arb_cmds_triple
1313

14+
let alloc_callback _src =
15+
Thread.yield ();
16+
None
17+
18+
let yield_tracker =
19+
Gc.Memprof.{ null_tracker with alloc_minor = alloc_callback;
20+
alloc_major = alloc_callback; }
21+
1422
(* [interp_sut_res] specialized for [Threads] *)
1523
let rec interp_sut_res sut cs = match cs with
1624
| [] -> []
@@ -23,13 +31,16 @@ module MakeExt (Spec: SpecExt) = struct
2331
let agree_prop_conc (seq_pref,cmds1,cmds2) =
2432
let sut = Spec.init_sut () in
2533
let obs1,obs2 = ref (Error ThreadNotFinished), ref (Error ThreadNotFinished) in
34+
(* Gc.Memprof.{start,stop} raises Failure on OCaml 5.0 and 5.1 *)
35+
(try ignore (Gc.Memprof.start ~sampling_rate:1e-1 ~callstack_size:0 yield_tracker) with Failure _ -> ());
2636
let pref_obs = Spec.wrap_cmd_seq @@ fun () -> interp_sut_res sut seq_pref in
2737
let wait = ref true in
2838
let th1 = Thread.create (fun () -> Spec.wrap_cmd_seq @@ fun () -> while !wait do Thread.yield () done; obs1 := try Ok (interp_sut_res sut cmds1) with exn -> Error exn) () in
2939
let th2 = Thread.create (fun () -> Spec.wrap_cmd_seq @@ fun () -> wait := false; obs2 := try Ok (interp_sut_res sut cmds2) with exn -> Error exn) () in
30-
let () = Thread.join th1 in
31-
let () = Thread.join th2 in
32-
let () = Spec.cleanup sut in
40+
Thread.join th1;
41+
Thread.join th2;
42+
(try Gc.Memprof.stop () with Failure _ -> ());
43+
Spec.cleanup sut;
3344
let obs1 = match !obs1 with Ok v -> v | Error exn -> raise exn in
3445
let obs2 = match !obs2 with Ok v -> v | Error exn -> raise exn in
3546
check_obs pref_obs obs1 obs2 Spec.init_state
@@ -40,25 +51,25 @@ module MakeExt (Spec: SpecExt) = struct
4051

4152
let agree_test_conc ~count ~name =
4253
(* a bigger [rep_count] for [Threads] as it is more difficult to trigger a problem *)
43-
let rep_count = 100 in
54+
let rep_count = 3 in
4455
let seq_len,par_len = 20,12 in
4556
let max_gen = 3*count in (* precond filtering may require extra generation: max. 3*count though *)
46-
Test.make ~retries:15 ~max_gen ~count ~name
57+
Test.make ~retries:25 ~max_gen ~count ~name
4758
(arb_cmds_triple seq_len par_len)
4859
(fun ((seq_pref,cmds1,cmds2) as triple) ->
4960
assume (all_interleavings_ok seq_pref cmds1 cmds2 Spec.init_state);
5061
repeat rep_count agree_prop_conc triple) (* 100 times each, then 100 * 15 times when shrinking *)
5162

5263
let neg_agree_test_conc ~count ~name =
53-
let rep_count = 100 in
64+
let rep_count = 3 in
5465
let seq_len,par_len = 20,12 in
5566
let max_gen = 3*count in (* precond filtering may require extra generation: max. 3*count though *)
56-
Test.make_neg ~retries:15 ~max_gen ~count ~name
67+
Test.make_neg ~retries:25 ~max_gen ~count ~name
5768
(arb_cmds_triple seq_len par_len)
5869
(fun ((seq_pref,cmds1,cmds2) as triple) ->
5970
assume (all_interleavings_ok seq_pref cmds1 cmds2 Spec.init_state);
6071
repeat rep_count agree_prop_conc triple) (* 100 times each, then 100 * 15 times when shrinking *)
61-
end
72+
end
6273

6374
module Make (Spec: Spec) =
6475
MakeExt (struct

lib/STM_thread.mli

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1-
(** Module for building concurrent STM tests over {!Thread}s *)
1+
(** Module for building concurrent [STM] tests over {!Thread}s
2+
3+
Context switches in {!Thread}s may happen
4+
- at allocations and
5+
- at safepoints {:https://github.com/ocaml/ocaml/pull/10039}.
6+
7+
This module relies on [Gc.Memprof] support to trigger more frequent context
8+
switching between threads at allocation sites. This works well in OCaml
9+
4.11.0-4.14.x and 5.3.0 onwards where [Gc.Memprof] is available.
10+
11+
In OCaml 5.0-5.2 without [Gc.Memprof] support the context switching at
12+
allocation sites will be inferior. As a consequence the module may fail to
13+
trigger concurrency issues.
14+
15+
Context switches at safepoints will trigger much less frequently. This
16+
means the module may fail to trigger concurrency issues in connection with
17+
these. Consider yourself warned.
18+
*)
219

320
module Make : functor (Spec : STM.Spec) ->
421
sig
@@ -26,8 +43,6 @@ module Make : functor (Spec : STM.Spec) ->
2643
[count] is the test count and [name] is the printed test name. *)
2744

2845
end
29-
[@@alert experimental "This module is experimental: It may fail to trigger concurrency issues that are present."]
3046

3147
module MakeExt : functor (Spec : STM.SpecExt) ->
32-
module type of Make (Spec) [@@alert "-experimental"]
33-
[@@alert experimental "This module is experimental: It may fail to trigger concurrency issues that are present."]
48+
module type of Make (Spec)

lib/lin_domain.mli

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
open Lin
22

3-
(** functor to build an internal module representing parallel tests *)
3+
(** Functor to build an internal module representing parallel tests *)
44
module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
55
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
66
val lin_prop : (Spec.cmd list * Spec.cmd list * Spec.cmd list) -> bool
@@ -11,7 +11,7 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
1111
end
1212
[@@alert internal "This module is exposed for internal uses only, its API may change at any time"]
1313

14-
(** functor to build a module for parallel testing *)
14+
(** Functor to build a module for parallel testing *)
1515
module Make (Spec : Spec) : sig
1616
val lin_test : count:int -> name:string -> QCheck.Test.t
1717
(** [lin_test ~count:c ~name:n] builds a parallel test with the name [n] that

lib/lin_effect.mli

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
open Lin
22

3-
(** functor to build an internal module representing {!Stdlib.Effect}-based tests *)
3+
(** Functor to build an internal module representing {!Stdlib.Effect}-based tests *)
44
module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
55
module EffSpec : sig
66
type cmd
@@ -20,7 +20,7 @@ val yield : unit -> unit
2020
(** Helper function to yield control in the underlying {!Stdlib.Effect}-based scheduler
2121
*)
2222

23-
(** functor to build a module for [Effect]-based testing *)
23+
(** Functor to build a module for [Effect]-based testing *)
2424
module Make (Spec : Spec) : sig
2525
val lin_test : count:int -> name:string -> QCheck.Test.t
2626
(** [lin_test ~count:c ~name:n] builds an {!Stdlib.Effect}-based test with the name

lib/lin_thread.ml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
44
module M = Internal.Make(Spec) [@alert "-internal"]
55
include M
66

7+
let alloc_callback _src =
8+
Thread.yield ();
9+
None
10+
11+
let yield_tracker =
12+
Gc.Memprof.{ null_tracker with alloc_minor = alloc_callback;
13+
alloc_major = alloc_callback; }
14+
715
(* Note: On purpose we use
816
- a non-tail-recursive function and
917
- an (explicit) allocation in the loop body
@@ -21,12 +29,15 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
2129
let lin_prop (seq_pref, cmds1, cmds2) =
2230
let sut = Spec.init () in
2331
let obs1, obs2 = ref (Ok []), ref (Ok []) in
32+
(* Gc.Memprof.{start,stop} raises Failure on OCaml 5.0 and 5.1 *)
33+
(try ignore (Gc.Memprof.start ~sampling_rate:1e-1 ~callstack_size:0 yield_tracker) with Failure _ -> ());
2434
let pref_obs = interp_plain sut seq_pref in
2535
let wait = ref true in
2636
let th1 = Thread.create (fun () -> while !wait do Thread.yield () done; obs1 := try Ok (interp_thread sut cmds1) with exn -> Error exn) () in
2737
let th2 = Thread.create (fun () -> wait := false; obs2 := try Ok (interp_thread sut cmds2) with exn -> Error exn) () in
2838
Thread.join th1;
2939
Thread.join th2;
40+
(try Gc.Memprof.stop () with Failure _ -> ());
3041
Spec.cleanup sut;
3142
let obs1 = match !obs1 with Ok v -> ref v | Error exn -> raise exn in
3243
let obs2 = match !obs2 with Ok v -> ref v | Error exn -> raise exn in
@@ -39,10 +50,10 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) = struct
3950
(pref_obs,!obs1,!obs2)
4051

4152
let lin_test ~count ~name =
42-
lin_test ~rep_count:100 ~count ~retries:5 ~name ~lin_prop:lin_prop
53+
lin_test ~rep_count:3 ~count ~retries:25 ~name ~lin_prop:lin_prop
4354

4455
let neg_lin_test ~count ~name =
45-
neg_lin_test ~rep_count:100 ~count ~retries:5 ~name ~lin_prop:lin_prop
56+
neg_lin_test ~rep_count:3 ~count ~retries:25 ~name ~lin_prop:lin_prop
4657
end
4758

4859
module Make (Spec : Spec) = Make_internal(MakeCmd(Spec))

lib/lin_thread.mli

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
1+
(** Module for building concurrent [Lin] tests over {!Thread}s
2+
3+
Context switches in {!Thread}s may happen
4+
- at allocations and
5+
- at safepoints {:https://github.com/ocaml/ocaml/pull/10039}.
6+
7+
This module relies on [Gc.Memprof] support to trigger more frequent context
8+
switching between threads at allocation sites. This works well in OCaml
9+
4.11.0-4.14.x and 5.3.0 onwards where [Gc.Memprof] is available.
10+
11+
In OCaml 5.0-5.2 without [Gc.Memprof] support the context switching at
12+
allocation sites will be inferior. As a consequence the module may fail to
13+
trigger concurrency issues.
14+
15+
Context switches at safepoints will trigger much less frequently. This
16+
means the module may fail to trigger concurrency issues in connection with
17+
these. Consider yourself warned.
18+
*)
19+
120
open Lin
221

3-
(** functor to build an internal module representing concurrent tests *)
22+
(** Functor to build an internal module representing concurrent tests *)
423
module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
524
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
625
val lin_prop : (Spec.cmd list * Spec.cmd list * Spec.cmd list) -> bool
@@ -9,7 +28,7 @@ module Make_internal (Spec : Internal.CmdSpec [@alert "-internal"]) : sig
928
end
1029
[@@alert internal "This module is exposed for internal uses only, its API may change at any time"]
1130

12-
(** functor to build a module for concurrent testing *)
31+
(** Functor to build a module for concurrent testing *)
1332
module Make (Spec : Spec) : sig
1433
val lin_test : count:int -> name:string -> QCheck.Test.t
1534
(** [lin_test ~count:c ~name:n] builds a concurrent test with the name [n]
@@ -25,4 +44,3 @@ module Make (Spec : Spec) : sig
2544
afterwards.
2645
*)
2746
end
28-
[@@alert experimental "This module is experimental: It may fail to trigger concurrency issues that are present."]

src/bytes/lin_tests.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ module BConf = struct
4242
end
4343

4444
module BT_domain = Lin_domain.Make(BConf)
45-
module BT_thread = Lin_thread.Make(BConf) [@alert "-experimental"]
45+
module BT_thread = Lin_thread.Make(BConf)
4646
;;
4747
QCheck_base_runner.run_tests_main [
4848
BT_domain.neg_lin_test ~count:5000 ~name:"Lin Bytes test with Domain";

src/io/lin_tests_thread.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(* Tests of In_channels *)
33
(* ********************************************************************** *)
44

5-
module IC_thread = Lin_thread.Make(Lin_tests_spec_io.ICConf) [@@alert "-experimental"]
5+
module IC_thread = Lin_thread.Make(Lin_tests_spec_io.ICConf)
66

77
let _ =
88
QCheck_base_runner.run_tests_main [

src/neg_tests/CList.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ let rec add_node list_head n =
1414
else
1515
add_node list_head n
1616

17+
let rec add_node_thread list_head n =
18+
(* try to add a new node to head *)
19+
let old_head = Atomic.get list_head in
20+
if Atomic.get list_head = old_head then begin
21+
(* introduce bug: as above but context switch can happen when allocating *)
22+
let new_node = { value = n ; next = (Some old_head) } in
23+
Atomic.set list_head new_node;
24+
true
25+
end
26+
else
27+
add_node_thread list_head n
28+
1729
let list_init i = Atomic.make { value = i ; next = None }
1830

1931
let member list_head n =

src/neg_tests/dune

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,26 @@
4646
)
4747

4848
(test
49-
(name stm_tests_conclist)
50-
(modules stm_tests_conclist)
49+
(name stm_tests_clist_sequential)
50+
(modules stm_tests_clist_spec stm_tests_clist_sequential)
5151
(package multicoretests)
52-
(libraries CList qcheck-stm.sequential qcheck-stm.domain)
52+
(libraries CList qcheck-stm.sequential)
53+
(action (run %{test} --verbose))
54+
)
55+
56+
(test
57+
(name stm_tests_clist_domain)
58+
(modules stm_tests_clist_spec stm_tests_clist_domain)
59+
(package multicoretests)
60+
(libraries CList qcheck-stm.domain)
61+
(action (run %{test} --verbose))
62+
)
63+
64+
(test
65+
(name stm_tests_clist_thread)
66+
(modules stm_tests_clist_spec stm_tests_clist_thread)
67+
(package multicoretests)
68+
(libraries CList qcheck-stm.thread)
5369
(action (run %{test} --verbose))
5470
)
5571

@@ -84,8 +100,7 @@
84100
(package multicoretests)
85101
(flags (:standard -w -27))
86102
(libraries lin_tests_common qcheck-lin.thread)
87-
; (action (run %{test} --verbose))
88-
(action (progn))
103+
(action (run %{test} --verbose))
89104
)
90105

91106
(test
@@ -115,7 +130,8 @@
115130
(package multicoretests)
116131
(flags (:standard -w -27))
117132
(libraries lin_internal_tests_common qcheck-lin.thread)
118-
(action (run %{test} --verbose))
133+
; (action (run %{test} --verbose))
134+
(action (progn))
119135
)
120136

121137
(test

src/neg_tests/lin_internal_tests_common.ml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,20 @@ end
188188
(* ********************************************************************** *)
189189
(* Tests of the buggy concurrent list CList *)
190190
(* ********************************************************************** *)
191-
module CLConf (T : sig
192-
type t
193-
val zero : t
194-
val of_int : int -> t
195-
val to_string : t -> string
196-
val shrink : t Shrink.t
197-
end) =
191+
module CLConf
192+
(CList : sig
193+
type 'a conc_list
194+
val list_init : 'a -> 'a conc_list Atomic.t
195+
val add_node: 'a conc_list Atomic.t -> 'a -> bool
196+
val member : 'a conc_list Atomic.t -> 'a -> bool
197+
end)
198+
(T : sig
199+
type t
200+
val zero : t
201+
val of_int : int -> t
202+
val to_string : t -> string
203+
val shrink : t Shrink.t
204+
end) =
198205
struct
199206
module Lin = Lin.Internal [@alert "-internal"]
200207

src/neg_tests/lin_internal_tests_domain.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ open Lin_internal_tests_common
22

33
module RT_int_domain = Lin_domain.Make_internal(RConf_int) [@alert "-internal"]
44
module RT_int64_domain = Lin_domain.Make_internal(RConf_int64) [@alert "-internal"]
5-
module CLT_int_domain = Lin_domain.Make_internal(CLConf (Int)) [@alert "-internal"]
6-
module CLT_int64_domain = Lin_domain.Make_internal(CLConf (Int64)) [@alert "-internal"]
5+
module CLT_int_domain = Lin_domain.Make_internal(CLConf(CList)(Int)) [@alert "-internal"]
6+
module CLT_int64_domain = Lin_domain.Make_internal(CLConf(CList)(Int64)) [@alert "-internal"]
77

88
(** This is a driver of the negative tests over the Domain module *)
99

src/neg_tests/lin_internal_tests_effect.ml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ module RT_int64'_effect = Lin_effect.Make_internal(RConf_int64') [@alert "-inter
9292

9393
module CLConf_int' =
9494
struct
95-
include CLConf(Int)
95+
include CLConf(CList)(Int)
9696
type res =
9797
| RAdd_node of (bool, exn) result
9898
| RMember of bool
@@ -116,12 +116,12 @@ struct
116116
| Add_node i -> RAdd_node (try Lin_effect.yield (); Ok (CList.add_node r i) with exn -> Error exn)
117117
| Member i -> RMember (CList.member r i)
118118
end
119-
module CLT_int_effect = Lin_effect.Make_internal(CLConf (Int)) [@alert "-internal"]
119+
module CLT_int_effect = Lin_effect.Make_internal(CLConf(CList)(Int)) [@alert "-internal"]
120120
module CLT_int'_effect = Lin_effect.Make_internal(CLConf_int') [@alert "-internal"]
121121

122122
module CLConf_int64' =
123123
struct
124-
include CLConf(Int64)
124+
include CLConf(CList)(Int64)
125125
type res =
126126
| RAdd_node of (bool, exn) result
127127
| RMember of bool
@@ -145,7 +145,7 @@ struct
145145
| Add_node i -> RAdd_node (try Lin_effect.yield (); Ok (CList.add_node r i) with exn -> Error exn)
146146
| Member i -> RMember (CList.member r i)
147147
end
148-
module CLT_int64_effect = Lin_effect.Make_internal(CLConf(Int64)) [@alert "-internal"]
148+
module CLT_int64_effect = Lin_effect.Make_internal(CLConf(CList)(Int64)) [@alert "-internal"]
149149
module CLT_int64'_effect = Lin_effect.Make_internal(CLConf_int64') [@alert "-internal"]
150150
;;
151151
QCheck_base_runner.run_tests_main

0 commit comments

Comments
 (0)