Skip to content

Commit 89c8d6b

Browse files
committed
docs(proposal): LE-004 explain concurrency
1 parent e2e29c8 commit 89c8d6b

File tree

1 file changed

+68
-17
lines changed

1 file changed

+68
-17
lines changed

proposals/LE-004-new-concurrency-model.md

+68-17
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,80 @@ When comparing this with the Go concurrency model, where async function calls ar
1616

1717
## Proposed Solution
1818

19-
The new concurrency model is based on channels and coroutines. Coroutines are similar to async functions, but they are not blocking. They can be called using the `async` function. The `async` function is followed by an expression, which is executed in a new coroutine. The coroutine can be suspended using the `await` function. The `await` keyword is followed by an expression, which is evaluated in the current coroutine. The result of the expression is returned to the coroutine, which called `await`.
19+
The new concurrency model is based on channels and coroutines. Coroutines are similar to async functions, but they are not blocking. They can be called using the `async` function. The `async` function is followed by an expression, which is executed in background.
2020

2121
```lithia
22-
let task = async { =>
23-
let channel = Channel 0
24-
select { on, closed =>
25-
on channel, { value =>
26-
print value
27-
}
28-
on None, { _ =>
29-
print "No value"
30-
}
31-
closed channel, { reason =>
32-
print reason
33-
}
22+
// create an unbuffered channel
23+
let channel = Channel 0
24+
25+
// start a new coroutine
26+
async { =>
27+
// send a value to the channel
28+
// as unbuffered, send is blocking until received
29+
channel.send "Hello World"
30+
}
31+
32+
// start selecting on the channel
33+
// blocking until fulfilled
34+
select { on, closed =>
35+
// on new value in channel
36+
on channel, { value =>
37+
print value
38+
}
39+
// channel is closed
40+
closed channel, { reason =>
41+
print reason
42+
}
43+
// default case
44+
on None, { _ =>
45+
print "No value"
3446
}
3547
}
36-
await task
3748
```
3849

50+
When spinning up a new async task, a `Task` is returned. This can be used to `await` for the task to finish.
51+
But in contrast to other languages, long running tasks are not async by default and returning Tasks is considered an anti-pattern. Invocations of `async` and `await` are designed to take place on the call site.
52+
53+
```lithia
54+
// start all jobs in background
55+
let tasks = lists.map jobs, { job => async run job }
56+
// wait for all tasks to finish
57+
lists.map tasks, await
58+
```
59+
60+
The goal of a `Task` is to avoid local channels to wait for multiple results, requested in parallel. They should not escape your function scope.
61+
3962
## Detailed Design
4063

41-
`Channel` is just a wrapper around a Go-channel. But in contrast to `close` in Go, `close` in Lithia takes a reason.
64+
The new concurrency model is based around three basic building blocks:
65+
66+
1. Channels - represent a communication channel between different parts of the program
67+
2. Select - allows to wait for certain events to take place
68+
3. Async Tasks - represent a small unit of work, which can be executed in background
4269

43-
The `async` function directly creates a new Goroutine with a `Task` under the hood. It is implemented similar to `rx.Future`.
70+
### Channels
71+
72+
Channels are used to communicate between different parts of the program. They are created using the `Channel` type. The `Channel` type is a wrapper around a Go-channel. It can be created with a buffer size. If the buffer size is 0, the channel is unbuffered. Otherwise it is buffered.
73+
74+
- Sending a value to a channel is blocking if the channel is unbuffered or if the buffer is full until there is a receiver.
75+
- Receiving a value from a channel is blocking if the channel is unbuffered or if the buffer is empty until there is a sender.
76+
- Closing a channel not blocking.
77+
- Waiting for a channel to be closed is blocking until it has been blocked.
78+
- Closing a channel requires a reason.
79+
- Sending a value to a closed channel is a runtime error.
80+
- Closing an already closed channel is a runtime error. Even if the reason is the same.
81+
82+
### Select
83+
84+
The `select` function allows to handle the fastest case in a set of channels or Async Tasks. It is blocking until one of the cases is fulfilled. The `select` function takes a function as argument. This function is called with two functions `on` and `closed`. The `on` function is used to register a case for a channel. The `closed` function is used to register a case for a closed channel.
85+
86+
If `on` is called with `None` or `Nil`, it is called if no other case is fulfilled immediately. If it has been omitted, the `select` function will block until one of the cases is fulfilled.
87+
88+
### Async Tasks
89+
90+
Async Tasks are used to execute a small unit of work in background. They are created using the `async` function. The `async` function takes an expression as argument. This expression is executed in background. The `async` function returns a `Task` which can be used to `await` for the task to finish.
91+
92+
When awaiting a task, the current coroutine is blocked until the task has finished. The result of the task is returned. Tasks cannot fail.
4493

4594
## Changes to the Standard Library
4695

@@ -66,4 +115,6 @@ The `Task` could be dropped, but it might be useful.
66115

67116
## Acknowledgements
68117

69-
This is heavily inspired by Go. `Task` is inspired by Swift.
118+
`Channel`, `select`, but also `async` are heavily inspired by Go. But in contrast to Go, they require a reason to close a channel.
119+
120+
`Task`, `async` and `await` are inspired by many implementations like in TypeScript or Swift. Especially decoupling errors from async operations like in Swift really fit into Lithia's design.

0 commit comments

Comments
 (0)