Hello, concurrency!
Let's make printing "Hello, world!" a little more exciting by performing work
concurrently. We'll start with creating the file hello.inko
with the following
contents:
import std.process (sleep)
import std.stdio (Stdout)
import std.time (Duration)
class async Printer {
fn async print(message: String) {
let _ = Stdout.new.print(message)
}
}
class async Main {
fn async main {
Printer().print('Hello')
Printer().print('world')
sleep(Duration.from_millis(500))
}
}
This program prints "Hello" and "world" concurrently to the terminal, then waits 500 milliseconds for this to complete.
To showcase this, run the program several times as follows:
inko run hello.inko
The output may change slightly between runs: sometimes it will print "Hello" and "world" on separate lines, other times it may print "Helloworld", "worldHello" or "world" and "Hello" on separate lines.
Explanation
Inko uses "lightweight processes" for concurrency. Such processes are defined
using the syntax class async
, such as class async Printer { ... }
in our
program.
We create instances of these processes using the syntax Printer()
. For such a
process to do anything, we must send it a message. In our program we do this
using print(...)
, where "print" is the message, defined using the fn async
syntax. The details of how this works, what to keep in mind, etc, are covered
separately.
The sleep(...)
line is needed such that the main process (defined using
class async Main
) doesn't stop before the Printer
processes print the
messages to the terminal.
Futures and Promises
Instead of waiting for a fixed 500 milliseconds, we can change the program to stop right away when the output is produced. We achieve this by changing the program to the following:
import std.stdio (Stdout)
import std.sync (Future, Promise)
class async Printer {
fn async print(message: String, output: uni Promise[Nil]) {
let _ = Stdout.new.print(message)
output.set(nil)
}
}
class async Main {
fn async main {
let future1 = match Future.new {
case (future, promise) -> {
Printer().print('Hello', promise)
future
}
}
let future2 = match Future.new {
case (future, promise) -> {
Printer().print('world', promise)
future
}
}
future1.get
future2.get
}
}
Future
and Promise
are types used for waiting for and resolving values
concurrently. A Future
is a proxy for a value to be computed in the future,
while a Promise
is used to assign a value to a Future
.
In the above example, a Promise
is passed along with the Printer.print
message, which is assigned to nil
. The Main
process in turn waits for this
to complete by calling Future.get
on the two Future
values. The result is
that the Main
process doesn't stop until the Printer
processes finished
their work.
The Future
and Promise
types are useful if a parent process wants to wait
for some result produced by a child process, without knowing what that parent
process is.
An example of this is a library function that wishes to perform computations in
parallel. The function doesn't know what processes it will be called from,
meaning any child processes can't communicate their results back to the parent
using messages. Using the Future
and Promise
types we can achieve this.
Channels
Inko also provides a Channel
type in the std.sync
module, acting is an
unbounded multiple publisher, multiple subscriber channel. This type is useful
when M jobs need to be performed by N processes, where M > N
. While sending
messages is certainly possible, it may result in an uneven workload across the
processes, but by using std.sync.Channel
the workload is balanced
automatically.
We can rewrite the example from earlier using Channel
as follows:
import std.stdio (Stdout)
import std.sync (Channel)
class async Printer {
fn async print(message: String, output: uni Channel[Nil]) {
let _ = Stdout.new.print(message)
output.send(nil)
}
}
class async Main {
fn async main {
let chan = Channel.new
Printer().print('Hello', recover chan.clone)
Printer().print('world', recover chan.clone)
chan.receive
chan.receive
}
}