Janet 1.27.0-01aab66 Documentation
(Other Versions: 1.26.0 1.25.1 1.24.0 1.23.0 1.22.0 1.21.0 1.20.0 1.19.0 1.18.1 1.17.1 1.16.1 1.15.0 1.13.1 1.12.2 1.11.1 1.10.1 1.9.1 1.8.1 1.7.0 1.6.0 1.5.1 1.5.0 1.4.0 1.3.1 )

Multithreading

Multithreading is the process of running a program on multiple threads at the same time, usually to improve throughput. Threads allow your program to take full advantage of the multiple processors on modern CPUs letting you do work in the background without stopping the main program flow, or breaking up an expensive operation to run on multiple processors.

Janet's ev/ module supports spawning native operating system threads in a way that is compatible with other ev/ functions and will not block the event loop.

For the most part, Janet values are not shared between threads. Each thread has its own Janet heap, which means threads behave more like processes that communicate by message passing. However, this does not prevent native code from sharing memory across these threads. Without native extensions, however, the only way for two Janet threads to communicate directly is through message passing with threaded channels.

By default, a Janet program will not exit until all threads have terminated.

Creating threads

The most primitive way to create a thread is (ev/thread fiber &opt value flags supervisor). This will start and wait for a message containing a function that it will run as the main body.

(defn worker
  []
  (print "New thread started!"))

# Create a new thread and wait for it to complete.
(ev/thread (fiber/new worker :t))

By itself, the above code isn't very useful because the main fiber will suspend until the new thread is complete. But it is quite useful to have threads suspend execution of the calling fiber by default - we can then easily have a thread wrapped with a fiber to be handled like other asynchronous tasks in the ev/ module. To run the thread in the background, you can either use the :n flag, or wrap the call to ev/thread in its own fiber.

(ev/thread (fiber/new worker :t) nil :n)

(ev/spawn
  (ev/thread (fiber/new worker :t)))

To make this process easier, Janet comes with a few built-in macros, ev/spawn-thread to run a block of code in a new thread, return immediately, and ev/do-thread to run a block of code but wait for it to return.

(ev/spawn-thread
  (print "New thread started!"))

(ev/do-thread
  (print "New thread started!"))

Sending and receiving messages

Threads in Janet do not share a memory heap and must communicate via threaded channels. Threaded channels behave much like normal channels in the ev/ module, with the only difference being that they can send values between threads by copying messages. Threaded channels are used for both communication and coordination between threads.

Threaded channels can be created with ev/thread-chan.

# Create a threaded channel with space for 10 values
(def thread-channel (ev/thread-chan 10))

(ev/spawn-thread
 (repeat 10
  (def item (ev/take thread-channel))
  (print "got " item)))

(repeat 10
 (os/sleep 0.2)
 (ev/give thread-channel :item))

Thread Supervisors

Threaded channels can also be used as supervisors for spawned threads. A supervisor is a channel that receives messages whenever an event, like an error, occurs in the supervised thread. Another fiber or thread can then read from this supervisor channel and handle the errors, usually by either logging the event, retrying the operation, or canceling other operations.

Thread supervisors need to be specified when creating the thread.

(def supervisor (ev/thread-chan 10))

(defn worker
 []
 (repeat 10
  (if (< 0.9 (math/random))
   (error "oops!")))
 (print "done!"))

# Start a worker thread that will signal events on the supervisor channel
(ev/thread worker nil :n supervisor)

# Get one event from the supervisor channel (on the initial thread here)
# It will either be (:error "oops!" nil) or (:ok nil nil).
(def event (ev/take supervisor))
(pp event)

Events from thread supervisors are much like events from normal fiber supervisors, but the first argument is not an entire copy of the fiber or thread, it is the event name. In the above example, depending on whether or not the "oops!" error was triggered, event will be either (:error "oops!" nil) or (:ok nil nil). :error and :ok correspond to (fiber/status f), while "oops!" and nil to (fiber/last-value) of the main fiber of the child thread. The third element of the tuple is the associated task id, if the fiber has an associated environment, or nil otherwise.