Janet 1.13.1-392d5d5 Documentation
(Other Versions: 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

Starting in version 1.6.0, Janet has the ability to do true, non-cooperative multithreading with the thread/ functions. Janet threads correspond to native threads on the host operating system, which may be either pthreads on POSIX systems or Windows 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.

Creating threads

Use (thread/new func &opt capacity) to create a new thread. This thread will start and wait for a message containing a function that it will run as the main body. This function must also be able to take 1 parameter, the parent thread. Each thread has its own mailbox, which can asynchronously receive messages. These messages are added to a queue, which has a configurable maximum capacity according to the second optional argument passed to thread/new. If unspecified, the mailbox will have an initial capacity of 10.

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

# New thread's mailbox has capacity for 32 messages.
(def thread (thread/new worker 32))

Sending and receiving messages

Threads in Janet do not share a memory heap and must communicate via message passing. Use thread/send to send a message to a thread, and thread/receive to get messages sent to the current thread.

(defn worker
  [parent]
  (print "waiting for message...")
  (def msg (thread/receive))
  (print "got message: " msg))

(def thread (thread/new worker))
# Thread objects support the :send method as an alias for thread/send.
(:send thread "Hello!")

Limitations of messages

Since threads do not share Janet heaps, all values sent as messages are first marshalled to a byte sequence by the sending thread, and then unmarshalled by the receiving thread. For marshalling and unmarshalling, threads use the two tables make-image-dict and load-image-dict. This means you can send over core bindings, even if the underlying value cannot be marshalled. Semantically, messages sent with (thread/send to msg) are first converted to a buffer via (marshal msg make-image-dict mailbox-buf), and then unmarshalled by the receiving thread via (unmarshal mailbox-buf load-image-dict).

Values that cannot be marshalled, including thread values, cannot be sent as messages to other threads, or even as part of a message to another thread. For example, the following will not work because open file handles cannot be marshalled.

(def file (file/open "myfile.txt" :w))
(defn worker
  [parent]
  (with-dyns [:out file]
    (print "Writing to file.")))

# Will throw an error, as worker contains a reference to file, which cannot be marshalled.
(thread/new worker)

The fix here is to move the file creation inside the worker function, since every worker thread is going to have its own copy of the file handle.

(defn worker
  [parent]
  (def file (file/open "myfile.txt" :w))
  (with-dyns [:out file]
    (print "Writing to file.")))

# No error
(thread/new worker)

This limitation has been removed for some values (C functions and raw pointers) in version 1.9.0, to make use of threads with native modules more seamless, although certain abstract types like open files can still not be sent to threads via thread/send or thread/new.

Blocking and non-blocking sends and receives

All sends and receives can be blocking or non-blocking by providing an optional timeout value. A timeout of 0 makes (thread/send to msg &opt timeout) and (thread/receive &opt timeout) non-blocking, while a positive timeout value indicates a blocking send or receive that will resume the current thread by throwing an error after timeout seconds. Timeouts are limited to 30 days, with exception the using math/inf for a timeout means that a timeout will never occur.

(defn worker
  [parent]
  (for i 0 10
    (os/sleep 0.5)
    (:send parent :ping))
  # sleep a long while to make the parent timeout
  (os/sleep 2)
  (:send parent :done))

# Will print the first 10 pings, but timeout and throw an error before :done
(try
  (let [t (thread/new worker)]
    (for i 0 11
      (print "got message: " (thread/receive 1))))
  ([err] (print "error: " err)))

# Flush :done message
(thread/receive math/inf)

# Will not error and work successfully
(let [t (thread/new worker)]
  (for i 0 11
    (print "got message: " (thread/receive math/inf))))