mirror of
https://github.com/c-cube/moonpool.git
synced 2025-12-06 03:05:30 -05:00
initial commit
This commit is contained in:
commit
1619f8b773
12 changed files with 535 additions and 0 deletions
38
.github/workflows/main.yml
vendored
Normal file
38
.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
run:
|
||||
name: build
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- macos-latest
|
||||
- ubuntu-latest
|
||||
#- windows-latest
|
||||
ocaml-compiler:
|
||||
- '4.03.x'
|
||||
- '4.14.x'
|
||||
- '5.0.x'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
- name: Use OCaml ${{ matrix.ocaml-compiler }}
|
||||
uses: ocaml/setup-ocaml@v2
|
||||
with:
|
||||
ocaml-compiler: ${{ matrix.ocaml-compiler }}
|
||||
dune-cache: true
|
||||
|
||||
- run: opam install -t moonpool --deps-only
|
||||
|
||||
- run: opam exec -- dune build '@install'
|
||||
|
||||
- run: opam exec -- dune runtest
|
||||
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
_build
|
||||
_opam
|
||||
15
.ocamlformat
Normal file
15
.ocamlformat
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
version = 0.24.1
|
||||
profile=conventional
|
||||
margin=80
|
||||
if-then-else=k-r
|
||||
parens-ite=true
|
||||
parens-tuple=multi-line-only
|
||||
sequence-style=terminator
|
||||
type-decl=sparse
|
||||
break-cases=toplevel
|
||||
cases-exp-indent=2
|
||||
field-space=tight-decl
|
||||
leading-nested-match-parens=true
|
||||
module-item-spacing=compact
|
||||
quiet=true
|
||||
ocaml-version=4.08.0
|
||||
11
Makefile
Normal file
11
Makefile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
DUNE_OPTS?=
|
||||
all:
|
||||
dune build @all $(DUNE_OPTS)
|
||||
|
||||
clean:
|
||||
@dune clean
|
||||
|
||||
WATCH?=@all
|
||||
watch:
|
||||
dune build $(DUNE_OPTS) -w $(WATCH)
|
||||
5
dune
Normal file
5
dune
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
(env
|
||||
(_ (flags :standard -strict-sequence -warn-error -a+8 -w +a-40-70)))
|
||||
|
||||
(mdx)
|
||||
33
dune-project
Normal file
33
dune-project
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
(lang dune 3.0)
|
||||
|
||||
(using mdx 0.2)
|
||||
|
||||
(name moonpool)
|
||||
|
||||
(generate_opam_files true)
|
||||
|
||||
(source
|
||||
(github c-cube/moonpool))
|
||||
|
||||
(authors "Simon Cruanes")
|
||||
|
||||
(maintainers "Simon Cruanes")
|
||||
|
||||
(license MIT)
|
||||
|
||||
;(documentation https://url/to/documentation)
|
||||
|
||||
(package
|
||||
(name moonpool)
|
||||
(synopsis "Pools of threads supported by a pool of domains")
|
||||
(depends
|
||||
ocaml
|
||||
dune
|
||||
(mdx
|
||||
(and
|
||||
(>= 1.9.0)
|
||||
:with-test)))
|
||||
(tags
|
||||
(thread pool domain)))
|
||||
|
||||
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project
|
||||
30
moonpool.opam
Normal file
30
moonpool.opam
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# This file is generated by dune, edit dune-project instead
|
||||
opam-version: "2.0"
|
||||
synopsis: "Pools of threads supported by a pool of domains"
|
||||
maintainer: ["Simon Cruanes"]
|
||||
authors: ["Simon Cruanes"]
|
||||
license: "MIT"
|
||||
tags: ["thread" "pool" "domain"]
|
||||
homepage: "https://github.com/c-cube/moonpool"
|
||||
bug-reports: "https://github.com/c-cube/moonpool/issues"
|
||||
depends: [
|
||||
"ocaml"
|
||||
"dune" {>= "3.0"}
|
||||
"mdx" {>= "1.9.0" & with-test}
|
||||
"odoc" {with-doc}
|
||||
]
|
||||
build: [
|
||||
["dune" "subst"] {dev}
|
||||
[
|
||||
"dune"
|
||||
"build"
|
||||
"-p"
|
||||
name
|
||||
"-j"
|
||||
jobs
|
||||
"@install"
|
||||
"@runtest" {with-test}
|
||||
"@doc" {with-doc}
|
||||
]
|
||||
]
|
||||
dev-repo: "git+https://github.com/c-cube/moonpool.git"
|
||||
18
src/dune
Normal file
18
src/dune
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
(library
|
||||
(public_name moonpool)
|
||||
(name moonpool)
|
||||
(private_modules atomic_ domain_)
|
||||
(libraries threads))
|
||||
|
||||
(rule
|
||||
(targets atomic_.ml)
|
||||
(action
|
||||
(with-stdout-to %{targets}
|
||||
(run ./gen/gen.exe --ocaml %{ocaml_version} --atomic))))
|
||||
|
||||
(rule
|
||||
(targets domain_.ml)
|
||||
(action
|
||||
(with-stdout-to %{targets}
|
||||
(run ./gen/gen.exe --ocaml %{ocaml_version} --domain))))
|
||||
|
||||
3
src/gen/dune
Normal file
3
src/gen/dune
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
(executable
|
||||
(name gen))
|
||||
104
src/gen/gen.ml
Normal file
104
src/gen/gen.ml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
let atomic_pre_412 =
|
||||
{|
|
||||
type 'a t = { mutable x: 'a }
|
||||
|
||||
let[@inline] make x = { x }
|
||||
let[@inline] get { x } = x
|
||||
let[@inline] set r x = r.x <- x
|
||||
|
||||
let[@inline never] exchange r x =
|
||||
(* atomic *)
|
||||
let y = r.x in
|
||||
r.x <- x;
|
||||
(* atomic *)
|
||||
y
|
||||
|
||||
let[@inline never] compare_and_set r seen v =
|
||||
(* atomic *)
|
||||
if r.x == seen then (
|
||||
r.x <- v;
|
||||
(* atomic *)
|
||||
true
|
||||
) else
|
||||
false
|
||||
|
||||
let[@inline never] fetch_and_add r x =
|
||||
(* atomic *)
|
||||
let v = r.x in
|
||||
r.x <- x + r.x;
|
||||
(* atomic *)
|
||||
v
|
||||
|
||||
let[@inline never] incr r =
|
||||
(* atomic *)
|
||||
r.x <- 1 + r.x
|
||||
(* atomic *)
|
||||
|
||||
let[@inline never] decr r =
|
||||
(* atomic *)
|
||||
r.x <- r.x - 1
|
||||
(* atomic *)
|
||||
|
||||
|}
|
||||
|
||||
let atomic_post_412 = {|
|
||||
include Atomic
|
||||
|}
|
||||
|
||||
let domain_pre_5 =
|
||||
{|
|
||||
let recommended_number () = 1
|
||||
|
||||
type t = Thread.t
|
||||
|
||||
let get_id (self:t) : int = Thread.id self
|
||||
|
||||
let spawn_on f : t =
|
||||
Thread.create f ()
|
||||
|}
|
||||
|
||||
let domain_post_5 =
|
||||
{|
|
||||
let recommended_number () = Domain.recommended_domain_count ()
|
||||
|
||||
type t = unit Domain.t
|
||||
|
||||
let get_id (self:t) : int = (Domain.get_id self :> int)
|
||||
|
||||
let spawn f : t =
|
||||
Domain.spawn f ()
|
||||
|}
|
||||
|
||||
let p_version s = Scanf.sscanf s "%d.%d" (fun x y -> x, y)
|
||||
|
||||
let () =
|
||||
let atomic = ref false in
|
||||
let domain = ref false in
|
||||
let ocaml = ref Sys.ocaml_version in
|
||||
Arg.parse
|
||||
[
|
||||
"--atomic", Arg.Set atomic, " atomic";
|
||||
"--domain", Arg.Set domain, " domain";
|
||||
"--ocaml", Arg.Set_string ocaml, " set ocaml version";
|
||||
]
|
||||
ignore "";
|
||||
|
||||
let major, minor = p_version !ocaml in
|
||||
|
||||
if !atomic then (
|
||||
let code =
|
||||
if (major, minor) < (4, 12) then
|
||||
atomic_pre_412
|
||||
else
|
||||
atomic_post_412
|
||||
in
|
||||
print_endline code
|
||||
) else if !domain then (
|
||||
let code =
|
||||
if (major, minor) < (5, 0) then
|
||||
domain_pre_5
|
||||
else
|
||||
domain_post_5
|
||||
in
|
||||
print_endline code
|
||||
)
|
||||
212
src/moonpool.ml
Normal file
212
src/moonpool.ml
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
module A = Atomic_
|
||||
|
||||
type 'a or_error = ('a, exn * Printexc.raw_backtrace) result
|
||||
|
||||
(** Simple blocking queue *)
|
||||
module S_queue : sig
|
||||
type 'a t
|
||||
|
||||
val create : unit -> _ t
|
||||
val push : 'a t -> 'a -> unit
|
||||
val pop : 'a t -> 'a
|
||||
end = struct
|
||||
type 'a t = {
|
||||
mutex: Mutex.t;
|
||||
cond: Condition.t;
|
||||
q: 'a Queue.t;
|
||||
}
|
||||
|
||||
let create () : _ t =
|
||||
{ mutex = Mutex.create (); cond = Condition.create (); q = Queue.create () }
|
||||
|
||||
let push (self : _ t) x : unit =
|
||||
Mutex.lock self.mutex;
|
||||
Queue.push x self.q;
|
||||
Condition.signal self.cond;
|
||||
Mutex.unlock self.mutex
|
||||
|
||||
let pop (self : 'a t) : 'a =
|
||||
Mutex.lock self.mutex;
|
||||
let rec loop () =
|
||||
if Queue.is_empty self.q then (
|
||||
Condition.wait self.cond self.mutex;
|
||||
(loop [@tailcall]) ()
|
||||
) else (
|
||||
let x = Queue.pop self.q in
|
||||
Mutex.unlock self.mutex;
|
||||
x
|
||||
)
|
||||
in
|
||||
loop ()
|
||||
end
|
||||
|
||||
(** Static pool of domains *)
|
||||
module D_pool_ = struct
|
||||
type domain = Domain_.t
|
||||
|
||||
let work_ _i q : unit =
|
||||
while true do
|
||||
let f = S_queue.pop q in
|
||||
try f () with _ -> ()
|
||||
done
|
||||
|
||||
(* A domain level worker. It should not do too much except for starting
|
||||
new threads for pools. *)
|
||||
type worker = { q: (unit -> unit) S_queue.t } [@@unboxed]
|
||||
|
||||
let domains_ : worker array lazy_t =
|
||||
lazy
|
||||
(let n = Domain_.recommended_number () in
|
||||
Array.init n (fun i ->
|
||||
let q = S_queue.create () in
|
||||
let _domain : domain = Domain_.spawn_on (fun () -> work_ i q) in
|
||||
{ q }))
|
||||
|
||||
(** Number of domains in the pool *)
|
||||
let[@inline] n_domains () : int = Array.length (Lazy.force domains_)
|
||||
|
||||
let run_on (i : int) (f : unit -> unit) : unit =
|
||||
let (lazy arr) = domains_ in
|
||||
assert (i < Array.length arr);
|
||||
S_queue.push arr.(i).q f
|
||||
end
|
||||
|
||||
module Pool = struct
|
||||
(* TODO: use a better queue for the tasks *)
|
||||
|
||||
type t = {
|
||||
active: bool A.t;
|
||||
threads: Thread.t array;
|
||||
q: (unit -> unit) S_queue.t;
|
||||
}
|
||||
|
||||
let[@inline] run self f : unit = S_queue.push self.q f
|
||||
|
||||
let worker_thread_ ~on_exn (active : bool A.t) (q : _ S_queue.t) : unit =
|
||||
while A.get active do
|
||||
let task = S_queue.pop q in
|
||||
try task ()
|
||||
with e ->
|
||||
let bt = Printexc.get_raw_backtrace () in
|
||||
on_exn e bt
|
||||
done
|
||||
|
||||
let default_thread_init_exit_ ~dom_id:_ ~t_id:_ () = ()
|
||||
|
||||
let create ?(on_init_thread = default_thread_init_exit_)
|
||||
?(on_exit_thread = default_thread_init_exit_) ?(on_exn = fun _ _ -> ())
|
||||
?(min = 1) ?(per_domain = 0) () : t =
|
||||
(* number of threads to run *)
|
||||
let min = max 1 min in
|
||||
let n_domains = D_pool_.n_domains () in
|
||||
assert (n_domains >= 1);
|
||||
let n = max min (n_domains * per_domain) in
|
||||
|
||||
(* make sure we don't bias towards the first domain(s) in {!D_pool_} *)
|
||||
let offset = Random.int n_domains in
|
||||
|
||||
let active = A.make true in
|
||||
let q = S_queue.create () in
|
||||
|
||||
let receive_threads = S_queue.create () in
|
||||
|
||||
(* start the thread with index [i] *)
|
||||
let start_thread_with_idx i =
|
||||
let dom_idx = (offset + i) mod n_domains in
|
||||
|
||||
let create () =
|
||||
let thread =
|
||||
Thread.create
|
||||
(fun () ->
|
||||
let t_id = Thread.id @@ Thread.self () in
|
||||
on_init_thread ~dom_id:dom_idx ~t_id ();
|
||||
worker_thread_ ~on_exn active q;
|
||||
on_exit_thread ~dom_id:dom_idx ~t_id ())
|
||||
()
|
||||
in
|
||||
(* send the thread from the domain back to us *)
|
||||
S_queue.push receive_threads (i, thread)
|
||||
in
|
||||
|
||||
D_pool_.run_on dom_idx create
|
||||
in
|
||||
|
||||
(* start all threads, placing them on the domains
|
||||
according to their index and [offset] in a round-robin fashion. *)
|
||||
let threads =
|
||||
let dummy = Thread.self () in
|
||||
Array.init n (fun i ->
|
||||
start_thread_with_idx i;
|
||||
dummy)
|
||||
in
|
||||
|
||||
(* receive the newly created threads back from domains *)
|
||||
for _j = 1 to n do
|
||||
let i, th = S_queue.pop receive_threads in
|
||||
threads.(i) <- th
|
||||
done;
|
||||
|
||||
{ active; threads; q }
|
||||
|
||||
let shutdown (self : t) : unit =
|
||||
let was_active = A.exchange self.active false in
|
||||
(* make sure to wakeup all the sleeping threads by scheduling one task each.
|
||||
This way, a thread that is asleep, waiting for tasks,
|
||||
will wakeup to process this trivial task, check [self.active], and terminate. *)
|
||||
if was_active then Array.iter (fun _ -> run self ignore) self.threads;
|
||||
Array.iter Thread.join self.threads
|
||||
end
|
||||
|
||||
module Fut = struct
|
||||
type 'a waiter = 'a or_error -> unit
|
||||
|
||||
type 'a state =
|
||||
| Done of 'a or_error
|
||||
| Waiting of 'a waiter list
|
||||
|
||||
type 'a t = { st: 'a state A.t } [@@unboxed]
|
||||
type 'a promise = 'a t
|
||||
|
||||
let make () =
|
||||
let fut = { st = A.make (Waiting []) } in
|
||||
fut, fut
|
||||
|
||||
let of_result x : _ t = { st = A.make (Done x) }
|
||||
let[@inline] return x : _ t = of_result (Ok x)
|
||||
let[@inline] fail e bt : _ t = of_result (Error (e, bt))
|
||||
|
||||
let on_result (self : _ t) (f : _ waiter) : unit =
|
||||
while
|
||||
let st = A.get self.st in
|
||||
match st with
|
||||
| Done x ->
|
||||
f x;
|
||||
false
|
||||
| Waiting l ->
|
||||
let must_retry =
|
||||
not (A.compare_and_set self.st st (Waiting (f :: l)))
|
||||
in
|
||||
must_retry
|
||||
do
|
||||
()
|
||||
done
|
||||
|
||||
exception Already_fulfilled
|
||||
|
||||
let fulfill (self : _ t) (r : _ result) : unit =
|
||||
while
|
||||
let st = A.get self.st in
|
||||
match st with
|
||||
| Done _ -> raise Already_fulfilled
|
||||
| Waiting l ->
|
||||
let did_swap = A.compare_and_set self.st st (Done r) in
|
||||
if did_swap then (
|
||||
(* success, now call all the waiters *)
|
||||
List.iter (fun f -> try f r with _ -> ()) l;
|
||||
false
|
||||
) else
|
||||
true
|
||||
do
|
||||
()
|
||||
done
|
||||
end
|
||||
64
src/moonpool.mli
Normal file
64
src/moonpool.mli
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
(** Moonpool
|
||||
|
||||
A pool within a bigger pool (ie the ocean). Here, we're talking about
|
||||
pools of [Thread.t] which live within a fixed pool of [Domain.t].
|
||||
*)
|
||||
|
||||
type 'a or_error = ('a, exn * Printexc.raw_backtrace) result
|
||||
|
||||
(** Thread pool *)
|
||||
module Pool : sig
|
||||
type t
|
||||
|
||||
val create :
|
||||
?on_init_thread:(dom_id:int -> t_id:int -> unit -> unit) ->
|
||||
?on_exit_thread:(dom_id:int -> t_id:int -> unit -> unit) ->
|
||||
?on_exn:(exn -> Printexc.raw_backtrace -> unit) ->
|
||||
?min:int ->
|
||||
?per_domain:int ->
|
||||
unit ->
|
||||
t
|
||||
(** [create ()] makes a new thread pool.
|
||||
@param on_init_thread called at the beginning of each new thread
|
||||
in the pool.
|
||||
*)
|
||||
|
||||
val shutdown : t -> unit
|
||||
(** Shutdown the pool and wait for it to terminate. Idempotent. *)
|
||||
|
||||
val run : t -> (unit -> unit) -> unit
|
||||
(** [run pool f] schedules [f] for later execution on the pool
|
||||
in one of the threads. *)
|
||||
end
|
||||
|
||||
(** Futures *)
|
||||
module Fut : sig
|
||||
type 'a t
|
||||
(** A future with a result of type ['a] *)
|
||||
|
||||
type 'a promise
|
||||
(** A promise, which can be fulfilled exactly once to set
|
||||
the corresponding future *)
|
||||
|
||||
val make : unit -> 'a t * 'a promise
|
||||
(** Make a new future with the associated promise *)
|
||||
|
||||
val on_result : 'a t -> ('a or_error -> unit) -> unit
|
||||
(** [on_result fut f] registers [f] to be called in the future
|
||||
when [fut] is set ;
|
||||
or calls [f] immediately if [fut] is already set. *)
|
||||
|
||||
exception Already_fulfilled
|
||||
|
||||
val fulfill : 'a promise -> 'a or_error -> unit
|
||||
(** Fullfill the promise, setting the future at the same time.
|
||||
@raise Already_fulfilled if the promise is already fulfilled. *)
|
||||
|
||||
val return : 'a -> 'a t
|
||||
(** Already settled future, with a result *)
|
||||
|
||||
val fail : exn -> Printexc.raw_backtrace -> _ t
|
||||
(** Already settled future, with a failure *)
|
||||
|
||||
val of_result : 'a or_error -> 'a t
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue