Elixir’s Actor Model

Concurrent computation, it’s unescapable. Elixir is known as a highly available concurrent language, but what does this really mean? Elixir’s concurrency model is built on the Actor Model, but what is it?

What is and Why Use the Actor Model?

The Actor Model is defined as a conceptual model of concurrent computation. An Actor is then a single unit of computation. This model of computation was designed in order to use all the available cores on a machine, increase fault tolerance and isolate state. Threads are arguably the most widely used model of concurrent computation. Threads can be tricky though, as you can mutate state out from under yourself. Jim Weirich has a famous talk on common issues with threads in Ruby. What All Rubyists Should Know About Threads In his example, a bank account is credited and debited concurrently using threads. On each run, not only did we see the account balance not reflect a sum expected, but the sum was different on each run. What a headache. Threads share memory. This can lead to “Dirty Reads” and incorrect state. Actors, also known as processes in Elixir are isolated. They do not share memory, garbage collection or runtime. From this point forward I will use Actor and Process interchangeably.

How Actors Work and Advantages

Actors have 3 main operations.

  1. Create another Actor/Process
  2. Send Message
  3. Handle Message

The Power of Message Passing

Each Process has its own private state. Processes that want to modify the state of another process do so by sending messages. Messages are received in the process’s mailbox and handled one at a time with FIFO (First In First Out). This means that we won’t have any “Dirty Reads” updating the state of our concurrent computation. We can use this mechanism, handling one message at a time, to update state not only in the process, but also in a memory store or DB without having inaccuracies. No need to craft clever locks!

fifo-actors

Supervision

One large advantage to processes is that they have the ability to supervise other processes. Just like you have a manager who supervises your work, so can a process supervise another process. Joe Armstrong, creator of Erlang, once said “Erlang was designed to program fault tolerant programs.” Supervision is what gives our Actor Model its fault tolerance. The supervisor, another lightweight process, receives messages from its child processes and handles the message.

supervision-tree

In the case where a process has died, the supervisor can restart, turn off and on, its child processes based on the messages sent back to the parent/supervisor process. This is done via exit signals in Elixir. I highly recommend understanding-exit-signals for more information on handling exit signals.

turn_off_and_on

Scaleable

Processes are also lightweight and require less resources than threads. This means we can spawn thousands if not millions of isolated processes. Erlang does this by starting one scheduler for each CPU, these schedule processes and assign to machine CPU. In a 4 core system, you would have 4 schedulers. Schedulers attempt to distribute load evenly across available cores.

Example Time

I’ve recreated a simple example of Jim Weirich’s BankAccount example, except in Elixir using Actor Model. Leveraging the OTP framework, you can think of this as a concurrency framework, I created a Generic Server to represent a bank account. “A GenServer is a process like any other Elixir process and it can be used to keep state, execute code asynchronously and so on, Elixir docs version 1.7”

defmodule BankAccount do
  use GenServer

  # Client

  def credit(pid, amount) do
    GenServer.cast(pid, {:credit, amount})
  end

  def debit(pid, amount) do
    GenServer.cast(pid, {:debit, amount})
  end

  # Callbacks

  def init(_) do
    {:ok, 0}
  end

  def handle_cast({:credit, amount}, state) do
    {:noreply, state + amount}
  end

  def handle_cast({:debit, amount}, state) do
    {:noreply, state - amount}
  end
end

Our Generic Server process, although quite contrived, demonstrates how the Actor Model tracks and updates state. Next we need to pass messages to our BankAccount concurrently. We will simulate by using Tasks. “Tasks are processes meant to execute one particular action throughout their lifetime, often with little or no communication with other processes. The most common use case for tasks is to convert sequential code into concurrent code by computing a value asynchronously. Elixir Docs 1.7”

Who wants to be a millionaire? With 10 processes, each processes will run asynchronously iterating 100_000 times crediting $1 each. This should make us a millionaire! In Jim’s talk, we see that no matter how many times he runs the code, the account has a different value less than our goal of 1 million. Where did my money go!?

iex(1)> {:ok, pid} = GenServer.start_link(BankAccount, []) # start BankAccount server
{:ok, #PID<0.131.0>}
iex(2)> 1..10 |> Enum.each(fn _ ->
...(2)> Task.start_link(fn -> # Spawn 10 concurrent Task processes to run code asynchronously
...(2)> for n <- 1..100_000, do: BankAccount.credit(pid, 1) # credit with $1
...(2)> end)
...(2)> end)
:ok
iex(3)> :sys.get_state(pid)
1000000

On every run of the above code, we will end up with the expected result of 1 million. All your hard earned cash is accounted for!

That’s a wrap folks.

Actor Model has a lot of powerful features and benefits. This is just the tip of the iceberg, next time we will work through supervising our BankAccount, walkthrough process communication types, and distributing our BankAccount across multiple nodes.