Skip to content

Commit

Permalink
docs: begin tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
leostera committed Nov 21, 2023
1 parent 8f6c100 commit cad941f
Show file tree
Hide file tree
Showing 47 changed files with 410 additions and 151 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
An actor-model multi-core scheduler for OCaml 5.
</p>

<p align="center">
<a href="#quick-start">Quick Start</a> |
<a href="https://github.com/leostera/riot/tree/master/examples#readme">
Tutorial</a> |
<a href="https://ocaml.org/p/riot/latest/doc/Riot/index.html">Reference</a>
&nbsp;&nbsp;
</p>

Riot is an [actor-model][actors] multi-core scheduler for OCaml 5. It brings
[Erlang][erlang]-style concurrency to the language, where lightweight processes communicate via message-passing.

Expand Down Expand Up @@ -60,9 +68,7 @@ several of its use-cases, like:
## Quick Start

```
git clone https://github.com/leostera/riot
cd riot
opam install .
opam install riot
```

After that, you can use any of the [examples](./examples) as a base for your app, and run them:
Expand Down
2 changes: 2 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
esy.lock
_esy
69 changes: 0 additions & 69 deletions examples/0-hello-world/README.md

This file was deleted.

1 change: 0 additions & 1 deletion examples/0-hello-world/hello_world.ml

This file was deleted.

55 changes: 9 additions & 46 deletions examples/1-hello-world/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# `1-hello-world`

A basic project that spins up 1 process and logs a message from it.

Normally, Riot programs will being by opening up the `Riot` module, which
exposes a lot of common functions to work with Processes, sending messages, and
receiving message.
A project so basic that fits on a single line!

```ocaml
open Riot
Riot.run @@ fun () -> print_endline "Hello, Joe!"
```

Every Riot program begins with a call to `Riot.run`. `Riot.run` takes a single
Expand All @@ -20,50 +16,17 @@ processes to terminate. We will cover graceful-shutdowns of applications later
in this tutorial.

```ocaml
let () = Riot.run @@ fun () ->
```

A Logger is included with Riot that is multi-core friendly, configurable at a
global and local level, and _non-blocking_. This means we can use this logger
from anywhere in the application, and always get messages in a reasonable
order, printed in a readable way (none of those pesky write-conflicts).

```ocaml
Logger.start () |> Result.get_ok;
Riot.run @@ fun () ->
print_endline "Hello, Joe!";
Riot.shutdown ()
```

Riot has a `spawn` function that can be used to create a new process. Riot
processes are _cheap_, and Riot programs can have millions of processes. They
are not like Operating System processes (or threads), and are closer to
green-threads or fibers.

`spawn` takes a `unit -> unit` function as an input, and will give us back a
_pid_. A Pid is a Process Identifier. Pids are unique during the execution of a
program and can be used to send messages to processes, to check if they are
still alive, and to terminate them.
The smallest Riot program, that starts and ends immediately, is then:

```ocaml
let pid1 = spawn say_hello in
Riot.(run shutdown)
```

A common scenario is waiting for a number of pids to terminate. For this Riot
offers a `wait_pids` function that will return after all the pids are finished.

Be mindful that if the pids do not terminate, this function will get the caller
process stuck in that loop. We will see later in this tutorial more flexible
mechanisms for detecting when other processes terminate.

```ocaml
wait_pids [pid1];
```


Finally, after our spawned process has terminated, we will simply log again.

```ocaml
Logger.info (fun f -> f "%a has terminated" Pid.pp pid1)
```
## Next Steps

Notice that when you run the program, the program itself _does not terminate_.
This is because we haven't ran `shutdown ()`. Calling it has the unfortunate
property that our Logger may not finish writing what it is meant to write.
* the [next step](../2-spawn-process/) introduces you to Processes
2 changes: 1 addition & 1 deletion examples/1-hello-world/dune
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(executable
(name hello_world)
(name main)
(libraries riot))
16 changes: 0 additions & 16 deletions examples/1-hello-world/hello_world.ml

This file was deleted.

1 change: 1 addition & 0 deletions examples/1-hello-world/main.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Riot.run @@ fun () -> print_endline "Hello, Joe!"
50 changes: 50 additions & 0 deletions examples/2-spawn-process/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `2-spawn-process`

A _Process_ is a long-lived function, that has access to a mailbox to receive
messages.

Here's how we can create a process to print out the message from the last tutorial:

```ocaml
Riot.run @@ fun () ->
let open Riot in
let pid = spawn (fun () -> print_endline "Hello, Joe!") in
```

Riot has a `spawn` function that can be used to create a new process. Riot
processes are _cheap_, and Riot programs can have millions of processes. They
are not like Operating System processes (or threads), and are closer to
green-threads or fibers.

`spawn` takes a `unit -> unit` function as an input, and will give us back a
_pid_. A `Pid` is a Process Identifier. Pids are unique during the execution of a
program and can be used to send messages to processes, to check if they are
still alive, and to terminate them.

```ocaml
let pid = spawn (fun () -> print_endline "Hello, Joe!") in
```

Inside of a process, we can get the pid of the process by calling `self ()`. A
Pid can also be pretty-printed with `Pid.pp` but it is not serializable.

```ocaml
let pid = spawn (fun () -> Printf.printff "Hello, %a!" Pid.pp (self ())) in
```

A common scenario is waiting for a number of pids to terminate. For this Riot
offers a `wait_pids` function that will return after all the pids are finished.

Be mindful that if the pids do not terminate, this function will get the caller
process stuck in that wait loop. We will see later in this tutorial more
flexible mechanisms for detecting when other processes terminate.

```ocaml
wait_pids [pid]
```

And as before, if we want the runtime to finish, we should call `shutdown ()`.

## Next Steps

* the [next step](../3-message-passing/) introduces you to communicating processes and Message passing
File renamed without changes.
5 changes: 5 additions & 0 deletions examples/2-spawn-process/main.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Riot.run @@ fun () ->
let open Riot in
let pid = spawn (fun () -> Format.printf "Hello, %a!" Pid.pp (self ())) in
wait_pids [ pid ];
shutdown ()
47 changes: 47 additions & 0 deletions examples/3-message-passing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# `3-message-passing`

Now that we've learned to spawn processes, we can start sending messages to
them.

Every message in Riot is _typed_. And all messages form part of the `Message.t`
type. To define a new message, you can write:

```ocaml
type Message.t += Hello_world
```

Your message can have any shape you want, so long as it fits into this message
type. Once a message is defined, we can start a process that knows how to
receive them. To receive a message we use the `receive` function, like this:

```ocaml
match receive () with
| Hello_world -> print_endline "Hello, World! :D"
```

`receive ()` will try to get a message from the _current process mailbox_. If
the mailbox is empty, `receive ()` _will suspend the process_ until a message
is delivered.

Since messages are represented with an open variant, when we pattern match on
`receive ()` we will have to make sure to handle or ignore _other messages_.

```ocaml
match receive () with
| Hello_world -> print_endline "Hello, World! :D"
| _ -> print_endline "Oh no, an unhandled message! D:"
```

Within a process, it is okay for us to do a _partial match_, since a process crashing isn't going to take the runtime down. So an alternative way to write this is:

```ocaml
match[@warning "-8"] receive () with
| Hello_world -> print_endline "Hello, World! :D"
```

Although I would recommend you to be careful where you disable this warning,
since exhaustive pattern-matching is one of OCaml's best features.

## Next Steps

* the [next step](../4-long-lived-processes/) introduces you to long lived processes
2 changes: 1 addition & 1 deletion examples/dune → examples/3-message-passing/dune
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(executable
(name spawn_many)
(name main)
(libraries riot))
12 changes: 12 additions & 0 deletions examples/3-message-passing/main.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
open Riot

type Message.t += Hello_world

let () =
Riot.run @@ fun () ->
let pid =
spawn (fun () ->
match[@warning "-8"] receive () with
| Hello_world -> print_endline "Hello, World! :D")
in
send pid Hello_world
50 changes: 50 additions & 0 deletions examples/4-long-lived-processes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `4-long-lived-processes`

Up until now, we have only dealt with processes that start, do some work, and
immediately die: either prints something and terminates, or waits for a message
and once the message arrives, it terminates.

But real systems are built out of long-living processes that can handle more
than a single message. How do we build those? Recursion, baby!

```ocaml
let pid = spawn (fun () -> loop ()) in
(* ... *)
```

To make a process that will live indefinitely, you just need to make a
recursive function. This has some advantages:

1. it is a very familiar way of programming in OCaml
2. it gives us State in a functional way (no mutation required!)

So let's do this! We'll write a process that recieves a message, says Hello to
someone, and continues awaiting.

```ocaml
let rec loop () =
(match receive () with
| Hello name -> print_endline ("Hello, " ^ name ^ "! :D")
| _ -> print_endline "Oh no, an unhandled message! D:");
loop ()
```

As we saw on the [message-passing tutorial](/3-message-passing/), processes can
receive all sorts of messages that we don't know about, although they will all
be typed, so we include a little catch-all to ignore unhandleable messages.

One caveat is that because function application can't be interrupted, we need
to make sure we _yield_ control back to the scheduler at some point before
recursing. Otherwise one of the cores will be _blocked_ by this process until it yields.

In our example, this is done automatically when we call `receive`

In fact, we are strategically placing yields all through the standard library
to make it as seamless as possible to write Riot programs without thinking
about scheduler starvation.

## Next Steps

* the [next step](../5-links-and-monitors/) introduces you to links and
monitors, to keep track of the lifecycle of a process or to make the
lifecycle of a process be linked to another
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(executable
(name hello_world)
(name main)
(libraries riot))
16 changes: 16 additions & 0 deletions examples/4-long-lived-processes/main.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
open Riot

type Message.t += Hello of string

let () =
Riot.run @@ fun () ->
let rec loop () =
(match receive () with
| Hello name -> print_endline ("Hello, " ^ name ^ "! :D")
| _ -> print_endline "Oh no, an unhandled message! D:");
loop ()
in
let pid = spawn loop in
send pid (Hello "Joe");
send pid (Hello "Mike");
send pid (Hello "Robert")
Loading

0 comments on commit cad941f

Please sign in to comment.