# thread

> Run functions on threads with async functions and lambdas, wait for results, run work in parallel and pass values over channels.

This is the Markdown version of https://osl-lang.org/packages/thread. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:thread"
```

`std:thread` runs functions alongside the rest of your program. For concurrent work, write an `async` function or an async lambda: it needs no import and returns a typed thread. Reach for `std:thread` itself for the lower-level cases: `thread.new` for a one-off job, channels, `thread.parallel` and `thread.race`. The package's functions, such as `thread.sleep`, `thread.parallel` and `thread.channel`, are available in any file that has an async function or lambda. In a file without one, add `import "std:thread"`.

For a guided introduction, see the [concurrency lesson](/learn/concurrency.md).

## Async functions

Write `async` where you would write `def`. Calling the function starts its body on a thread and returns at once. `wait()` gives the value the body returned:

```osl
async square(int n) int (
  thread.sleep(50)
  return n * n
)

auto job = square(9)
log "started"
log job.wait()
```

Output:

```text
started
81
```

`job` is a `*thread.Thread<int>`. The `int` comes from the return type written on the function. A one-line async function uses `->`, a function with no return type returns a thread that gives nothing, and methods in a class can be async too:

```osl
async add(int a, int b) int -> a + b

async announce(string text) (
  log "announcing " ++ text
)

class Box (
  int n = 4
  async doubled() int -> self.n * 2
)

log add(2, 3).wait()
announce("hi").wait()
log Box().doubled().wait()
```

Output:

```text
5
announcing hi
8
```

There is no `await`. A thread is a value, and `.wait()` is how you get what is inside it. Forgetting to call it is a compile error that says so:

```osl
async name() string -> "ada"

log name().toUpper()
```

This doesn't compile. The compiler reports:

```text
TypeError: 'toUpper' isn't a method of a thread; this is a thread that gives a string once it finishes, so call .wait() first: .wait().toUpper(...)
   |
 3 | log name().toUpper()
   |            ^^^^^^^
in: main.osl:3
```

## Async lambdas

An async lambda is the lambda form of the same thing, with `async` in place of `def`. Both the arrow form and the block form work:

```osl
auto double = async(int n) int -> n * 2
auto shout = async(string s) string (
  thread.sleep(10)
  return s.toUpper()
)
auto quiet = async() (
  log "ran"
)

log double(4).wait()
log shout("hello").wait()
quiet().wait()
```

Output:

```text
8
HELLO
ran
```

Pass one wherever a function is expected. A parameter that takes an async lambda has the type `def(int) *thread.Thread<int>`:

```osl
def retryTwice(def(int) *thread.Thread<int> job, int value) int -> job(job(value).wait()).wait()

log retryTwice(async(int n) int -> n + 10, 1)
```

Output:

```text
21
```

## What async is shorthand for

An async function returns the thread that `thread.new` makes for its body. These two functions are the same:

```osl
async load(string name) string (
  return "profile of " ++ name
)

def load(string name) *thread.Thread<string> (
  return thread.new(def() string (
    return "profile of " ++ name
  ))
)
```

Prefer `async` when the work is a function of its own. Use `thread.new` directly for a one-off background job.

## Lower level: thread.new

`thread.new(fn, args...)` starts `fn` on a new thread, passes it the extra arguments, and returns the thread:

```osl
import "std:thread"

def cube(int n) int -> n * n * n

auto direct = thread.new(cube, 3)
auto inline = thread.new(def() string -> (
  thread.sleep(20)
  return "background result"
))
log direct.wait()
log inline.wait()
```

Output:

```text
27
background result
```

A thread is typed by the return type written on its function. `thread.new(def() int -> 1)` is a `*thread.Thread<int>`. If the function's return type is left for the compiler to work out, as in `def() -> 1`, you get a plain `*thread.Thread` whose `wait` gives `any`. Write the type where a thread is declared or returned to keep it.

When the main program finishes, running threads stop where they are. Keep your threads and wait for them.

## Waiting and then

`wait()` blocks until the thread is done. If the body threw, `wait()` rethrows the error, so wrap it in `try` to get a [result](/learn/errors.md):

```osl
async parse(string text) int (
  auto n = text.tryAs<int>
  if n.isErr() (
    throw error("ParseError", "not a number: " ++ text)
  )
  return n.unwrap()
)

log parse("42").wait()
auto bad = try(parse("nope").wait())
log bad.unwrapErr().kind bad.unwrapErr().message
```

Output:

```text
42
ParseError not a number: nope
```

`then(fn)` calls `fn` with the value once the thread finishes and returns another thread, typed by what `fn` returns. If the thread threw, `fn` doesn't run and the returned thread carries the error:

```osl
async load(string name) string -> "profile of " ++ name

auto length = load("ada").then(text -> text.len)
log length.wait()

async broken() string (
  throw error("StateError", "not ready")
)

auto skipped = broken().then(text -> text.len)
log try(skipped.wait()).unwrapErr().message
```

Output:

```text
14
not ready
```

`isDone()` says whether a thread has finished, and `age()` is how many milliseconds ago it started:

```osl
async quick() int -> 1

auto job = quick()
void job.wait()
log job.isDone()
log job.age() >= 0
```

Output:

```text
true
true
```

## Timeouts

`wait({timeout: ms})` gives up after that many milliseconds by throwing a `TimeoutError`. The thread keeps running, so you can wait again:

```osl
async slow() string (
  thread.sleep(300)
  return "finally"
)

auto job = slow()
auto early = try(job.wait({timeout: 20}))
log early.isErr()
log early.unwrapErr().kind
log job.wait()
```

Output:

```text
true
TimeoutError
finally
```

## Several threads

`thread.waitAll(threads)` waits for every thread and returns their values in the order the threads were given. If any threw, it rethrows the first failure after all have finished:

```osl
async fetch(string name) string (
  thread.sleep(100)
  return name ++ " done"
)

*thread.Thread<string>[] jobs = []
for name in ["alpha", "beta", "gamma"] (
  jobs.append(fetch(name))
)
log thread.waitAll(jobs)
```

Output:

```text
["alpha done","beta done","gamma done"]
```

`thread.race(threads)` returns the value of the first thread to finish. The others keep running:

```osl
async after(int ms, string name) string (
  thread.sleep(ms)
  return name
)

log thread.race([after(300, "slow"), after(20, "quick")])
```

Output:

```text
quick
```

## Parallel maps

`thread.parallel(items, fn, limit?)` calls `fn(item, index)` for every item on separate threads and returns the results in item order. The index starts at 1. The optional limit caps how many run at once:

```osl
import "std:thread"

int[] sizes = [3, 1, 2]

auto results = thread.parallel(sizes, def(int n, int i) string -> (
  thread.sleep(n * 20)
  return i ++ ":" ++ ("x" * n)
))
log results

auto limited = thread.parallel([1, 2, 3, 4, 5, 6], def(int n) int -> n * n, 2)
log limited
```

Output:

```text
["1:xxx","2:x","3:xx"]
[1,4,9,16,25,36]
```

If any call throws, `parallel` rethrows the first failure after every call has finished.

## Channels

A channel passes values between threads. `thread.channel(capacity?)` makes one that holds up to `capacity` values (default 0) before `send` waits for a receiver. `send` returns a result, which is an error when the channel is closed. `recv` waits for the next value and returns `null` once the channel is closed and empty:

```osl
auto ch = thread.channel()

async produce() int (
  for n in 1 to 5 (
    void ch.send(n * n)
  )
  ch.close()
  return 5
)

auto producer = produce()

int total = 0
while true (
  auto value = ch.recv()
  if value == null break
  total += value.<int>
)
log total
log producer.wait()
```

Output:

```text
55
5
```

`values()` iterates over the received values and ends when the channel is closed, which reads better in a `for` loop, and still works when `null` is one of the values you send:

```osl
auto ch = thread.channel(3)

async fill() (
  for word in ["a", "b", "c"] (
    void ch.send(word)
  )
  ch.close()
)

auto filler = fill()
for word in ch.values() (
  log word
)
filler.wait()
log ch.isClosed()
```

Output:

```text
a
b
c
true
```

`tryRecv()` takes a value if one is ready and returns `null` straight away if not. `len()` says how many values are waiting in the buffer.

## Fan out, fan in

A common pattern: several workers pull jobs from one channel and push answers into another. Sort the answers before printing, since workers finish in any order:

```osl
auto jobs = thread.channel(10)
auto answers = thread.channel(10)

async worker() int (
  int handled = 0
  for n in jobs.values() (
    void answers.send(n * n)
    handled++
  )
  return handled
)

*thread.Thread<int>[] workers = []
loop 3 (
  workers.append(worker())
)

for n in 1 to 9 (
  void jobs.send(n)
)
jobs.close()

int handled = 0
for w in workers (
  handled += w.wait()
)
answers.close()

int[] squares = []
for s in answers.values() (
  squares.append(s.<int>)
)
log handled
log squares.sort()
```

Output:

```text
9
[1,4,9,16,25,36,49,64,81]
```

## Shared data

Once a program starts threads, each statement that touches shared data runs as one step, so eight threads adding to the same counter never lose an update and you write no lock:

```osl
string[int] counts = {}

async countUp(int times) int (
  loop times (
    counts["total"] = (counts["total"] ?? 0) + 1
  )
  return times
)

*thread.Thread<int>[] jobs = []
for t 8 (
  jobs.append(countUp(1000))
)
void thread.waitAll(jobs)
log counts["total"]
```

Output:

```text
8000
```

A sequence of statements isn't one step. When several must happen together, wrap them in `atomic ( ... )`. A `return` can't leave the block, so set a variable inside it:

```osl
int stock = 10

async buy() boolean (
  boolean bought = false
  atomic (
    if stock > 0 (
      stock--
      bought = true
    )
  )
  return bought
)

*thread.Thread<boolean>[] jobs = []
loop 15 (
  jobs.append(buy())
)

int happy = 0
for job in jobs (
  if job.wait() (
    happy++
  )
)
log happy stock
```

Output:

```text
10 0
```

For locks you manage yourself, wait groups and run-once setup, see [`std:sync`](/packages/sync.md).

## Sleeping

`thread.sleep(ms)` pauses the current thread. Other threads keep running. A zero or negative time returns at once.

**Quiz.** What does this print?

```osl
async slow() int (
  thread.sleep(100)
  return 7
)

auto job = slow()
log "started"
log job.wait()
```

- [ ] `7`, then `started`
- [x] `started`, then `7`
- [ ] Only `7`

Answer: Calling an async function starts it and returns at once, so `started` prints while the body sleeps. `wait()` then blocks for the value.

### Exercise: Three cubes at once

Make `cube` an async function that returns `n * n * n`, so each call starts a thread. The loop collects the threads and waits for them all.

Starting code:

```osl
async cube(int n) int -> n

*thread.Thread<int>[] jobs = []
for n in 1 to 3 (
  jobs.append(cube(n))
)
log thread.waitAll(jobs)
```

It should print:

```text
[1,8,27]
```

Hint 1: It is already `async`. Fix the body so it cubes `n`.

A solution:

```osl
async cube(int n) int -> n * n * n

*thread.Thread<int>[] jobs = []
for n in 1 to 3 (
  jobs.append(cube(n))
)
log thread.waitAll(jobs)
```

## API reference

## API reference

Every member of `std:thread`, generated from the source of OSL.

### Functions

#### `thread.channel(any ...capacity) *thread.Channel`

channel returns a Channel that holds up to capacity values (default 0)
before send waits for a receiver. Throws ValueError when capacity isn't a
whole number of 0 or more.

#### `thread.new(any fn, any ...args) *thread.Thread`

new starts fn on a new thread, calling it with args, and returns the
Thread. Throws TypeError when fn isn't a function or accepts fewer
parameters than there are args.

#### `thread.parallel(array items, any fn, any ...limit) array`

parallel calls fn(item, index) for every item on separate threads, at
most limit at a time when limit is given, and returns the results in item
order. index is 1-based. If any call throws, parallel rethrows the first
failure after every call has finished. Throws TypeError when fn isn't a
function and ValueError when limit isn't a whole number of 1 or more.

#### `thread.race(array threads) any`

race waits for the first of threads to finish and returns its value, or
rethrows its error. The others keep running. Throws ValueError for an
empty array and TypeError when an item isn't a thread.

#### `thread.sleep(number ms)`

sleep pauses the current thread for ms milliseconds. Other threads keep
running. Zero, negative and NaN return at once; a longer time than Go
can count (about 292 years) waits that long.

```osl
thread.sleep(250)
```

#### `thread.waitAll(array threads) array`

waitAll waits for every thread and returns their values in the same
order. If any thread threw, it rethrows the first failure, listing the
others, after all have finished. Throws TypeError when an item isn't a
thread.

### `*thread.Thread`

#### `thread.age() number`

age returns how many milliseconds ago the thread was started.

#### `thread.isDone() boolean`

isDone reports whether the thread has finished, successfully or not.

#### `thread.then(any fn) *thread.Thread`

then calls fn with what the thread's function returned once it finishes,
and returns a Thread that finishes with what fn returns. On a
*thread.Thread<T>, an untyped parameter of fn is a T, and the Thread
then returns is typed by fn's declared return. In a program showing a
std:ui interface, fn runs on the UI's thread, like an action,
and every screen updates after it; otherwise it runs on a thread of its
own. When the thread threw, fn doesn't run and the returned Thread
carries the error, which its wait rethrows. Throws TypeError when fn
isn't a function.

```osl
thread.new(def() -> load(url)).then(def(data) -> (
  self.items = data
))
```

#### `thread.timeout(number ms) any`

**Deprecated.** Deprecated: use t.wait({timeout: ms}) instead, which throws TimeoutError rather than returning null, so a thread that returned null can't be mistaken for one that timed out.

timeout waits at most ms milliseconds for the thread to finish and
returns what its function returned, or null when the thread is still
running (it keeps running and can be waited on again). Like wait, it
rethrows the thread's own error. A negative ms doesn't wait.

#### `thread.wait(object? options) any`

wait blocks until the thread finishes and returns what its function
returned, as a T for a *thread.Thread<T>. If the function threw, wait
rethrows that error with the thread's stack. Option: timeout, the most
milliseconds to wait; when the
thread is still running after it, wait throws TimeoutError (catch it, or
use try(t.wait({timeout: ms})) for a result) and the thread keeps running,
so it can be waited on again. The timeout is a throw rather than a result
so that wait keeps returning the thread's plain value. Throws ValueError
for a negative timeout.

### `*thread.Channel`

#### `channel.close()`

close stops new sends, makes waiting senders fail, and lets receivers
drain the values already buffered. Closing twice does nothing.

#### `channel.isClosed() boolean`

isClosed reports whether close has been called.

#### `channel.len() int`

len returns how many values are buffered and waiting to be received.

#### `channel.recv() any`

recv waits for the next value and returns it, or returns null once the
channel is closed and empty. Use values() when null itself is sent.

#### `channel.send(any value) result<unit, error>`

send delivers value, waiting for a receiver or a free buffer slot. Fails
with StateError when the channel is closed, or is closed while send
waits; in a condition the result reads as "was it delivered".

#### `channel.tryRecv() any`

tryRecv returns the next value if one is ready, or null without waiting
when none is or the channel is closed and empty.

#### `channel.values() iter<any>`

values iterates over received values, waiting for each, and ends once the
channel is closed and empty, so for x in ch.values() drains a channel.
