Best practice on mixing Async and Lwt?

Sorry if this is a FAQ or a point of contention, I couldn’t find an answer typing keywords into various search boxes.

I’m starting a new app in Async but will need to use a library or two written in Lwt, so I’d like to minimize Lwt and wrap it in Async stuff if possible.

I have a pretty good handle on Async’s conceptual model and making it co-exist in non-Async environments but not sure how it would collide with Lwt.

Has anyone been down this road already? Any pointers besides “lol just use Lwt for everything”? :smiley:

(I notice there’s an lwt-async library on github but it’s not released into opam. Wondering if that’s a sign…)

1 Like

Actually I forgot this was the internet, so I’ll start this by posting the wrong answer.

I hereby decree that the best practice for mixing Async and Lwt is to spawn a child sub-process that reads requests on stdin to run Lwt actions and writes Lwt result responses to stdout. The parent can use Async and communicate with the Lwt sub-process through a pipe (using Async too). Both async libraries are nicely isolated from one another. Sure you have to come up with an ad-hoc pipe protocol but it’ll be easy because it’s OCaml. For extra speed and convenience at the expense of safety you can just use Marshal. Yeah, problem solved.

This introduces an inter-process communication latency but surely it’ll be negligible. UNIX is, like, totally designed for really really fast pipe I/O right?

1 Like

This is the right answer.

1 Like

I’m going to be annoying and ask: what would happen if you stuck with Lwt for the entire stack?

1 Like

It’s actually not clear to me why Async and Lwt can’t coexist in the same process context. As long as both libraries don’t try to claim fds or threads they didn’t create and don’t try to use signals they should be good.

Except for the brain bending required to be inside of an Async deferred waiting on an Lwt deferred. You might still need to use a UNIX pipe to broker between worlds but it should be possible to pass a pointer instead of marshalling data structures through it.

I just know Async already and would rather not trade in the sometimes bitter learned experiences for a fresh set of roughly equivalent unknowns with Lwt.

The safest way to do this is wrap Lwt code within Async, and to invoke the Lwt_main runtime thread via Async_unix.In_thread. But you need to be careful to not introduce deadlocks between Async and Lwt promises, so try to keep the Lwt evaluation fairly well contained.

Which Lwt libraries do you need to use from Async, out of curiosity?

Cool. What does a deadlock here look like? Perhaps I’m misunderstanding Lwt, but if both schedulers live in their own separate threads shouldn’t there be no additional deadlock risk?

Right now it’s the USB library that adds an Lwt runtime to my project but if exploratory stuff goes well there may be others.

It’s more about this warning in Async:

WARNING: Async code MUST NOT be used from within f . By Async code we mean pretty-much all functions of libraries making use of Async. Only a few functions of the Async library can be called inside In_thread.run . These are explicitly marked as such, using the phrase “thread-safe”.

So when you dispatch to Lwt, only do Lwt in that thread. Not many people have tried this before, so let us know how it goes. The lwt-in-async project you found was an experiment that @diml did to turn the actual definition of Lwt.t into an Async.Deferred, but we ended up deciding that the semantics between the two were sufficiently different that it wasn’t practical to upstream such a merger. That might have changed in recent years as @antron has been steadily cleaning up the innards of Lwt…

1 Like

Ah, I thought you might have been driving at that In_thread.run warning but wondered if it was actually about something else because every Async user ought to know that already :x-smug-grin-emoji:

Will happily report back on how it goes!

2 Likes

Related: how do you do this the other way round (putting a little bit of Async into an Lwt project)? Specifically I want to use Async_smtp to send emails from an Opium web server. Also open to other libraries for emails.

If you want other libraries, there is sendmail or ocamlnet.

An implementation of SMTP agnostic to Async or LWT exists here. An overlay on LWT is provided and an UNIX tool on top of that is done here. It want to follow RFC6409 but it still is an experimental project.

I will really happy to improve it and let some others to use it. So let me know some tasks (at least, a documentation :smile:). These software were released too.

Thanks, both of those look good!

The safest way to do this is wrap Lwt code within Async, and to invoke the Lwt_main runtime thread via Async_unix.In_thread

Reporting back on how it went.

So far so good. I originally just knocked out a function that constructed the Lwt deferreds and ran Lwt_main.run inside of the Async.In_thread.run to get things going, and it has worked out well enough that I haven’t had to touch it.

let read_usb t len =
  In_thread.run (fun () ->
    Lwt_main.run
      begin
        let open Lwt.Infix in
        let s = String.make len (Char.of_int_exn 0x0) in
        USB.interrupt_recv ~handle:t.device_handle
          ~endpoint:endpoint_address s 0 len
        >>= fun transfer ->
        if Int.(<>) transfer len then
          failwithf "read_usb: failed to read %d bytes: %d" len transfer ();
        Lwt.return (Bytes.of_string s)
      end)

I’m a little squeamish about starting and finishing Lwt_main.run on every call. No idea how much overhead that actually involves, but, again, seems to work so I haven’t investigated further.

Down the line, it doesn’t seem like this will go well if I need to talk to two USB devices at the same time, because we could end up with two different threads launching Lwt_main.run, and I perhaps they will stomp on each other. It is possible to create a dedicated helper thread for In_thread.run to force serialization there, but that’s not a much better solution.

If I end up in such a place I imagine I’ll rewrite the underlying ocaml usb bindings to be more Async friendly but this seems good enough for now.

It’s reasonably efficient to called Lwt_main.run, but you can also implement a lighterweight one yourself. The main overhead is the enter/exit hooks. From the implementation:

let rec run t =
  (* Wakeup paused threads now. *)
  Lwt.wakeup_paused ();
  match Lwt.poll t with
  | Some x ->
    x
  | None ->
    (* Call enter hooks. *)
    Lwt_sequence.iter_l (fun f -> f ()) enter_iter_hooks;
    (* Do the main loop call. *)
    Lwt_engine.iter (Lwt.paused_count () = 0 && Lwt_sequence.is_empty yielded);
    (* Wakeup paused threads again. *)
    Lwt.wakeup_paused ();
    (* Wakeup yielded threads now. *)
    if not (Lwt_sequence.is_empty yielded) then begin
      let tmp = Lwt_sequence.create () in
      Lwt_sequence.transfer_r yielded tmp;
      Lwt_sequence.iter_l (fun wakener -> Lwt.wakeup wakener ()) tmp
    end;
    (* Call leave hooks. *)
    Lwt_sequence.iter_l (fun f -> f ()) leave_iter_hooks;
    run t

So if you just strip out the hooks, and copy the yield and pause logic, that’ll be simpler. MirageOS does something similar for its embedded runtimes. You can find a non-deprecated version of Lwt_sequence in the lwt-dllist package that is signature-compatible.

1 Like

Sweet. What makes the Lwt enter/leave hooks expensive? Why can they be safely ripped out? Having a bit of trouble finding references to those hooks from the rest of the Lwt-world. Do they impose surprising amounts of overhead even when no hooks are registered?