Multicore: How do spawn and join interact with try_with?

Since the release of ocaml 5.0 is imminent, I have finally been looking into the new multicore features and how I can use them in my code.

There is one thing that I don’t understand about the interaction of Effect.try_with and spawn and join from Domain: if I spawn a function that performs an effect, the corresponding handler in try_with is not invoked when I wrap the try_with around join or the combination of spawn and join. Instead, the exception Unhandled is raised. I if put the try_with inside of spawn, everything works - but that’s the boring version.

An exception that is raised inside of spawn can be caught by wrapping join in try ... with.

My question is now: is this the intended behaviour (thou shallst not handle effects across domain boundaries) or is it a bug in 4.12.0+domains and 5.00.0?


open EffectHandlers (* will be Effect with 5.0 *)
open Deep

exception Foo
type _ eff += Square : int -> int eff

open Printf

let raise_foo () =
  raise Foo

let perform_square n () =
  printf "perform (Square %d).\n" n;
  let n2 = perform (Square n) in
  printf "continuing with Square %d -> %d.\n" n n2

let handle_square =
  fun (type a) (e : a eff) ->
  match e with
  | Square n ->
     Some
       (fun (k : (a, _) continuation) ->
         printf "handling (Square %d)\n" n;
         continue k (n * n))
  | _ -> None

let perform_square_handled n () =
  try_with
    (perform_square n) ()
    { effc = handle_square }

let handle_inside () =
  let domain = Domain.spawn (perform_square_handled 42) in
  printf "joining domain ...\n";
  Domain.join domain;
  printf "done.\n"

let handle_around_join () =
  let domain = Domain.spawn (perform_square 42) in
  printf "joining domain ...\n";
  try_with
    (fun () ->
      Domain.join domain;
      printf "done.\n")
    ()
  { effc = handle_square }

let handle_outside () =
  try_with
    (fun () ->
      let domain = Domain.spawn (perform_square 42) in
      printf "joining domain ...\n";
      Domain.join domain;
      printf "done.\n")
    ()
  { effc = handle_square }

let catch_inside () =
  let domain = Domain.spawn raise_foo in
  printf "joining domain ...\n";
  match Domain.join domain with
  | () -> printf "done.\n"
  | exception Foo -> printf "caught exception in Domain.join.\n"

let catch_outside () =
  try
    let domain = Domain.spawn raise_foo in
    printf "joining domain ...\n";
    Domain.join domain;
    printf "done.\n"
  with
  | Foo -> Printf.printf "caught exception outside.\n"

type mode =
  | Handle_inside
  | Handle_outside
  | Handle_around_join
  | Catch_inside
  | Catch_outside

let _ =
  let mode = ref Handle_inside in
  let usage =
    "usage: " ^ my_name ^ " ..." in
  let options =
    Arg.align
      [ ("-handle_inside", Arg.Unit (fun () -> mode := Handle_inside),
         " handle effect inside of Domain.spawn");
        ("-handle_outside", Arg.Unit (fun () -> mode := Handle_outside),
         " handle effect outside of Domain.spawn");
        ("-handle_around_join", Arg.Unit (fun () -> mode := Handle_around_join),
         " handle effect outside of Domain.spawn");
        ("-catch_inside", Arg.Unit (fun () -> mode := Catch_inside),
         " catch the exception around Domain.join");
        ("-catch_outside", Arg.Unit (fun () -> mode := Catch_outside),
         " catch the exception around Domain.spawn/join") ] in
  Arg.parse options (fun s -> raise (Arg.Bad ("invalid argument: " ^ s))) usage;
  match !mode with
  | Handle_inside -> handle_inside ()
  | Handle_outside -> handle_outside ()
  | Handle_around_join -> handle_around_join ()
  | Catch_inside -> catch_inside ()
  | Catch_outside -> catch_outside ()

thou shallst not handle effects across domain boundaries

Indeed. Effects cannot cross domain boundaries.

1 Like

Thanks for the clarification.

I had misunderstood the separation of parallelism and concurreny in the papers to mean a conceptual separation that allows to combine them freely.

Now I can concentrate on parallelism for performance without worrying that the cool kids use effects to communicate between domains.

@kayceesrk Are there soundness or performance reasons for confining effects to a single domain? Or is work in progress on lifting this restriction when the syntax support for effects will be released?

Being used to message passing between a master process and many worker processes, I had thought of letting the workers report progress and ask the master for more work by performing effects. Of course, this is not symmetrical (the master cannot message the workers this way), might be inefficient and can be implemented easily by other means. But with the syntax support, it could result in very flexible and maintainable code.

Effect handlers are fundamentally about control flow between two points on the same stack. In the presented example, there are two different stacks corresponding to the two different threads. Effect handlers are not a communication mechanism between multiple domains.

Moreover, if multiple domains are waiting on Domain.join, does the effect propagate to each of the waiting domains? At that point would each of the waiters get a copy of the continuation? You would need some semantics which is quite different from what the current effect handlers present you.

1 Like

Thanks again. I hadn’t thought of the multiple waiters case.