The Thread API provides a variation of coroutines for openos. A thread is superior to basic coroutines in many ways and, for many workflows, is easier to work with. An openos thread is an autonomous non-blocking detachable process.
join
the thread. That is, for a parent process to close it must first close all running child threads. A thread is considered a child thread when it is attached to a parent process (a thread is also a process of its own). A process does not have to close a thread when:
t:detach()
. Note that a thread can also attach to another parent. see t:attach()
os.exit
in which case all attached threads are killed. Note that rebooting also kills all threads.
t:kill()
t:suspend()
There are two main use cases for using threads over other viable options
There are two sections of API defined here.
thread
api, or the static functions, provided by require("thread")
thread handle
api, or the api available the thread objects created by the thread
api. In this documentation, a thread handle will be denoted by just t
. The examples also prefer to use t
for instances of a thread handle.thread.create(thread_proc: function[, ...]) returns thread handle
Starts a new thread executing the function thread_proc
and returns its thread handle, see [Thread Handle API]. This method takes an optional ...
which is passed to thread_proc
. The runtime of the thread continues autonomously.
local thread = require("thread") print("Main program start") thread.create(function(a, b) print(a) os.sleep() print(b) end, 'A', 'B') print("Main program end")
Output:
Main program start A Main program end B
thread.waitForAll(threads: array of thread handles[, timeout: number) returns success, error message
Waits for all threads
to complete. This blocking call can return in timeout
seconds if provided. Returns success and an error message on failure. A thread is “completed” under multiple conditions, see t:join()
for details. threads
is treated as an array table.
local thread = require("thread") print("Main program start") local t = thread.create(function(a, b) print(a) os.sleep() print(b) end, 'A', 'B') thread.waitForAll({t}) print("Main program end")
Output:
Main program start A B Main program end
thread.waitForAny(threads: array of thread handles[, timeout: number) returns success, error message
Waits for any single thread complete and is otherwise equivalent to thread.waitForAll()
local thread = require("thread") print("Main program start") local t1 = thread.create(function(a, b) print(a) os.sleep() print(b) end, 'A', 'B') local t2 = thread.create(function(c, d) print(c) os.sleep() os.sleep() print(d) end, 'C', 'D') thread.waitForAny({t1, t2}) print("Main program end")
Output:
Main program start A C B Main program end D
t:resume() returns success, error message
Resumes (or thaws) a suspended thread. Returns success and an error message on failure. A thread begins its life already in a running state and thus basic thread workflows will not ever need to call t:resume()
. A “running” thread will autonomously continue until it completes. t:resume()
is only necessary to resume a thread that has been suspended(t:suspend()
). Note that because you are not directly resuming the thread any exceptions thrown from the thread are absorbed by the threading library and not exposed to your process. At this time there is no way to hook in an exception handler for threads but for now event.onError
is used to print the error message to “/tmp/event.log”. Please note that currently the hard interrupt exception is only thrown once, and the behavior of a process with threads when a hard interrupt is thrown is unspecified. At this time, any one of the threads or the parent process may take the exception.
t:suspend() returns success, error message
Suspends (or freezes) a running thread. Returns success and an error message on failure. A “suspended” thread never autonomously wakes up and dies as soon as its parent process (if attached) closes. A suspended thread ignores events. That means any event listeners or timers created inside the thread will not respond to event notifications. Note that threads do not buffer event signals and a suspended thread may miss event signals it was waiting for. For example, if a thread was last waiting on event.pull("modem_message")
and is “suspended” and a “modem_message” is received by the computer then the thread will miss the event and never know it happened.
Special notes about t:resume, t:suspend
Do not think of these methods as coroutine.resume()
nor coroutine.yield()
. These methods are indirect and a thread will asynchronously start or stop running on its own. Contrast this to coroutine methods which directly and immediately invoke execution or leave execution of a coroutine. Consider this example:
local thread = require("thread") local t -- this example needs an upvalue to t t = thread.create(function() print("start") t:suspend() print("after suspend") os.sleep() print("after sleep") end)
Output:
start after suspend
t:kill() returns success, error message
Stabby stab! Kills the thread dead. Returns success and an error message on failure. The thread is terminated and will not continue its thread function. Any event registrations it made die with it. Keep in mind that the core underlying Lua type is a coroutine, which is not a preemptive thread. Thus, the thread's stopping points are deterministic, meaning that you can predict exactly where the thread will stop. Consider this example:
local thread = require("thread") local t = thread.create(function() while true do print("running") print("still running") os.sleep() print("after sleep") end print("unreachable code") end) print("before kill") t:kill() print("after kill")
Output:
running still running before kill after kill
t:status() returns status string
Returns the thread status as a string.
t:suspend()
) or killed (t:kill()
) but not resumed(t:resume()
). An running thread will block calls to t:join()
and block its parent from closing. Unlike a coroutine which appears “suspended” when not executing in this very moment, a thread state remains “running” even when waiting for an event. Consider this example
t:join()
it. A suspended thread ignores event signals, and any event registrations made from the context of the thread, or any child threads created therein, also ignore any event signals. A suspended thread's children behave as if suspended even if their status is “running”. A suspended thread can be resumed(t:resume()
) or killed (t:kill()
) but not suspended(t:suspend()
).
t:resume()
) nor suspended(t:suspend()
). A dead thread does not block a parent process from closing. Killing a dead thread is not an error but does nothing.Status Examples
local thread = require("thread") local t = thread.create(function() print("before sleep") os.sleep() print("after sleep") end) print(t:status())
Output:
before sleep running after sleep
local thread = require("thread") local t = thread.create(function() print("before sleep") os.sleep() print("after sleep") end) t:suspend() print(t:status()) os.sleep(10) print(t:status()) t:resume() print("after resume") print(t:status())
Output:
before sleep suspended suspended after resume dead
t:attach([level: number]) returns success, error message
Attaches a thread to a process, conventionally known as a child thread or attached thread. level
is an optional used to get parent processes, 0 or nil uses the currently running process. When initially created a thread is already attached to the current process. This method returns nil and an error message if level
refers to a nonexistent process, otherwise it returns truthy.
t:detach()
Detaches a thread from its parent if it has one. Returns nil and an error message if no action was taken, otherwise returns self (handy if you want to create and detach a thread in one line).
local detached_thread = thread.create(function() end):detach()
t:join()
Blocks the caller until t
is no longer running. After a call to t:join()
the thread state is “dead”. The following circumstance allow join
to finish and unblock the caller
Calling thread.waitForAll({t})
is functionally equivalent to calling t:join()
. Also, when a processs is closing it will call thread.waitForAll
on the group of its child threads if it has any. Also, a child threads created inside a thread block their parent thread in this same manner.
This example demonstrates what happens when a thread throws an exception. A thread stops executing and becomes “dead” when it throws an uncaught exception. The exception message is not printed to stdout nor stderr (see t:resume()
) for details.
local thread = require("thread") local tty = require("tty") print("p start") local reader = thread.create(function() print("reader start") error("thread abort") -- throws an exception print("reader done") end) print("p end", t:status())
Output
p start reader start p end dead
This example demonstrates how you would register a function that handles soft interrupts (^c) to close file handles, release resources, etc, and then exit the whole program.
local thread = require("thread") local event = require("event") local cleanup_thread = thread.create(function() event.pull("interrupted") print("cleaning up resources") end) local main_thread = thread.create(function() print("main program") while true do io.write("input: ") io.read() end end) thread.waitForAny({cleanup_thread, main_thread}) os.exit(0) -- closes all remaining threads
Assuming the user presses ^c to send an interrupt Output
main program input: ^c cleaning up resources
This example demonstrates that now OpenOS supports non blocking threads.
local thread = require("thread") thread.create(function() a,b,c,d,e,f,g = coroutine.yield() print(a,b,c,d,e,f,g) print(event.pull()) end) event.push("custom_event_a") print("done") event.push("custom_event_b", 2)
Output
custom_event_a done custom_event_b 2