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¶
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) |> Result.get_ok in let hare = Luv.Thread.create (fun () -> run_hare track_length) |> 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:
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 () : 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 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.
libuv offers several cross-platform synchronization primitives, which work largely like their pthreads counterparts:
Luv.Mutex — mutexes
Luv.Rwlock — read-write locks
Luv.Semaphore — semaphores
Luv.Condition — condition variables
Luv.Barrier — barriers
Luv.Once — once-only barriers
Luv.TLS — thread-local storage
Here’s an example that uses a mutex to wait for a thread to finish its work:
1 2 3 4 5 6 7 8 9 10 11
let () = let mutex = Luv.Mutex.init () |> 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"
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:
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 ()) |> 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 () : 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 Luv.Loop.run for that loop. The notification can be sent by any thread.
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.