Wait a minute! Why threads? Aren’t event loops supposed to be the way to do web-scale programming? Well… not always. Threads can still be very useful, though you will have to make use of various synchronization primitives. For example, one way to bind a library with a blocking API, without risking blocking your whole program, is to run its functions in threads.

Today, there are two predominant threading APIs: Windows threads and pthreads on Unix. libuv’s thread API is a cross-platform approximation of pthreads, with similar semantics.

libuv threads don’t follow the model of most of the rest of libuv — event loops and callbacks. Instead, the libuv thread API is more of a direct wrapper over your system’s threading library. It is a low-level part of libuv, over which some other parts of libuv are implemented, but which is also exposed for applications to use.

This means that libuv thread functions can block, the same as functions in pthreads.

Starting and waiting#

Start a thread using Luv.Thread.create and wait for it to exit using Luv.Thread.join:

 1let () =
 2  let track_length = 3 in
 4  let rec run_tortoise = function
 5    | 0 ->
 6      print_endline "Tortoise done running!"
 7    | n ->
 8      Luv.Time.sleep 2000;
 9      print_endline "Tortoise ran another step";
10      run_tortoise (n - 1)
11  in
13  let rec run_hare = function
14    | 0 ->
15      print_endline "Hare done running!"
16    | n ->
17      Luv.Time.sleep 1000;
18      print_endline "Hare ran another step";
19      run_hare (n - 1)
20  in
22  let tortoise =
23    Luv.Thread.create (fun () -> run_tortoise track_length)
24    |> Result.get_ok
25  in
27  let hare =
28    Luv.Thread.create (fun () -> run_hare track_length)
29    |> Result.get_ok
30  in
32  ignore (Luv.Thread.join tortoise);
33  ignore (Luv.Thread.join hare)

Note that this program does not call This is because, again, threading is a separate, simpler, and lower-level part of libuv.

libuv thread pool#

Luv.Thread_pool.queue_work can be used to run functions in threads from libuv’s thread pool, the same thread pool libuv uses internally for filesystem and DNS requests:

1let () =
2  Luv.Thread_pool.queue_work (fun () ->
3    Luv.Time.sleep 1000;
4    print_endline "Finished")
5    ignore;
7  ignore ( () : bool)

This can be easier than creating individual threads, because it is not necessary to keep track of each thread to later call Luv.Thread.join. The thread pool requests are registered with a libuv event loop, so a single call to is enough to wait for all of them.

Use Luv.Thread_pool.set_size to set the size of the libuv thread pool. This function should be called by applications (not libraries) as early as possible during their initialization.

Synchronization primitives#

libuv offers several cross-platform synchronization primitives, which work largely like their pthreads counterparts:

Here’s an example that uses a mutex to wait for a thread to finish its work:

 1let () =
 2  let mutex = Luv.Mutex.init () |> Result.get_ok in
 3  Luv.Mutex.lock mutex;
 5  ignore @@ Luv.Thread.create begin fun () ->
 6    Luv.Time.sleep 1000;
 7    Luv.Mutex.unlock mutex
 8  end;
10  Luv.Mutex.lock mutex;
11  print_endline "Worker finished"

Inter-thread communication#

In addition to all the synchronization primitives listed above, libuv offers one more method of inter-thread communication. If you have a thread that is blocked inside, as your program’s main thread typically would be, you can cause to “wake up” and run a callback using Luv.Async:

 1let () =
 2  let progress = ref 0. in
 3  let show_progress () =
 4    Printf.printf "%i%%\n%!" (int_of_float (!progress *. 100.)) in
 6  let notification =
 7    Luv.Async.init (fun _ -> show_progress ()) |> Result.get_ok in
 9  let rec do_work total n =
10    if n >= total then
11      ()
12    else begin
13      Luv.Time.sleep 1000;
14      progress := float_of_int (n + 1) /. float_of_int total;
15      ignore (Luv.Async.send notification);
16      do_work total (n + 1)
17    end
18  in
20  let finished _ =
21    Luv.Handle.close notification ignore;
22    print_endline "Done"
23  in
25  Luv.Thread_pool.queue_work (fun () -> do_work 3 0) finished;
27  ignore ( () : bool)

Technically, this is a form of inter-loop communication, since the notification is received by a loop, in whatever thread happens to be calling for that loop. The notification can be sent by any thread.


The callback may be invoked immediately after Luv.Async.send is called, or after some time. libuv may combine multiple calls to Luv.Async.send into one callback call.

Multiple event loops#

You can run multiple libuv event loops. A complex application might have several “primary” threads, each running its own event loop, several ordinary worker threads, and be communicating with some external processes, some of which might also be running libuv.

To run multiple event loops, create them with Luv.Loop.init. Then, pass them as the ?loop arguments to the various Luv APIs. Here are some sample calls:

let secondary_loop = Luv.Loop.init () |> Stdlib.Result.get_ok in (* ... *)

ignore @@ ~loop:secondary_loop ();

Luv.File.open_ ~loop:secondary_loop "foo" [`RDONLY] (fun _ -> (* ... *))

In the future, Luv may lazily create a loop on demand in each thread when it first tries to use libuv, store a reference to it in a TLS key, and pass that as the default value of the ?loop argument throughout the API.

OCaml runtime lock#

Luv integrates libuv with the OCaml runtime lock. This means that, as in any other OCaml program, two threads cannot be running OCaml code at the same time. However, Luv releases the lock when calling a potentially-blocking libuv API, so that other threads can run while the calling thread is blocked. In particular, the lock is released during calls to, which means that other threads can run in between when you make a call to a non-blocking API, and when its callback is called by libuv.