Threads

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:

threads.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let () =
  let track_length = 3 in

  let rec run_tortoise = function
    | 0 ->
      print_endline "Tortoise done running!"
    | n ->
      Luv.Time.sleep 2000;
      print_endline "Tortoise ran another step";
      run_tortoise (n - 1)
  in

  let rec run_hare = function
    | 0 ->
      print_endline "Hare done running!"
    | n ->
      Luv.Time.sleep 1000;
      print_endline "Hare ran another step";
      run_hare (n - 1)
  in

  let tortoise =
    Luv.Thread.create (fun () -> run_tortoise track_length)
    |> Stdlib.Result.get_ok
  in

  let hare =
    Luv.Thread.create (fun () -> run_hare track_length)
    |> Stdlib.Result.get_ok
  in

  ignore (Luv.Thread.join tortoise);
  ignore (Luv.Thread.join hare)

Note that this program does not call Luv.Loop.run. 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:

thread_pool.ml

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

  ignore (Luv.Loop.run ())

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 Luv.Loop.run 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:

mutex.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let () =
  let mutex = Luv.Mutex.init () |> Stdlib.Result.get_ok in
  Luv.Mutex.lock mutex;

  ignore @@ Luv.Thread.create begin fun () ->
    Luv.Time.sleep 1000;
    Luv.Mutex.unlock mutex
  end;

  Luv.Mutex.lock mutex;
  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 Luv.Loop.run, as your program’s main thread typically would be, you can cause Luv.Loop.run to “wake up” and run a callback using Luv.Async:

progress.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let () =
  let progress = ref 0. in
  let show_progress () =
    Printf.printf "%i%%\n%!" (int_of_float (!progress *. 100.)) in

  let notification =
    Luv.Async.init (fun _ -> show_progress ()) |> Stdlib.Result.get_ok in

  let rec do_work total n =
    if n >= total then
      ()
    else begin
      Luv.Time.sleep 1000;
      progress := float_of_int (n + 1) /. float_of_int total;
      ignore (Luv.Async.send notification);
      do_work total (n + 1)
    end
  in

  let finished _ =
    Luv.Handle.close notification ignore;
    print_endline "Done"
  in

  Luv.Thread_pool.queue_work (fun () -> do_work 3 0) finished;

  ignore (Luv.Loop.run ())

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

Warning

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 @@ Luv.Loop.run ~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 Luv.Loop.run, 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.