Powered by AppSignal & Oban Pro

Atomics (mutable variables)

livebooks/atomics.livemd

Atomics (mutable variables)

Mix.install([
  {:inplace, path: Path.join([System.get_env("HOME"), "projects", "inplace"])},
  :kino,
  {:kino_vega_lite, "~> 0.1.11"}
])

defmodule AtomicsHelper do
  def get_values(array_ref) do
    array_size = :atomics.info(array_ref)[:size]  
    Enum.map(1..array_size, fn idx -> :atomics.get(array_ref, idx) end)
end
end

What is :atomics?

The Erlang module that provides a set of functions to do atomic operations towards mutable atomic variables (will refer to them as atomics).

  • The atomics are organized into arrays

  • Atomics are 64 bit integers.

  • Atomics can be represented as either signed or unsigned.

  • Atomics are lock-free, shared and leverage hardware-level atomic instructions.

Create an array of atomics

import AtomicsHelper
# Initialize
array_size = 5
array_ref = :atomics.new(5, [])

## :atomics array values are initialized with 0
get_values(array_ref)

Modify array values in place

## Modify in place
## Use reference to the array to modify values
Enum.each(1..array_size, fn idx -> 
  value = if rem(idx, 2) == 1 do
    1
  else
    -1
  end
  :atomics.put(array_ref, idx, value)
  end)

get_values(array_ref)

Safe concurrency reads/writes

 @spec compare_exchange(ref, ix, expected, desired) :: :ok | integer()
        when ref: atomics_ref(),
             ix: pos_integer(),
             expected: integer(),
             desired: integer()

since: OTP 21.2

Atomically compare the atomic with Expected, and if those are equal, set atomic
to Desired.

Return ok if Desired was written. Return the actual atomic value if not equal
to Expected.

Example: synchronizing across multiple processes

Use resource by one process at a time:

defmodule SingletonResource do
  @available 0
  @taken 1

  ## Tries to get a resource
  ## If successfull, uses it, then releases.
  ## Otherwise, repeats within a timeout
  def acquire(resource_flag, process_id) do
    ## try to get a resource
    case :atomics.compare_exchange(resource_flag, 1, @available, @taken) do
      :ok ->
        IO.inspect("#{process_id}: :-)) got it!")
        use_resource(process_id)
        release(resource_flag)
      @taken ->
        IO.inspect("#{process_id}: didn't get it :-(( ... will try again shortly")
        :timer.sleep(100)
        acquire(resource_flag, process_id)
    end
  end

  defp use_resource(process_id) do
    IO.inspect("#{process_id}: working...")
    :timer.sleep(Enum.random(100..200))
    IO.inspect("#{process_id}: done...")
  end

  defp release(resource_flag) do
    :atomics.put(resource_flag, 1, @available)
  end
end

## We have two processes
## , and we want only one process at a time to have an access to a specific resource.

resource_flag = :atomics.new(1, [])

Enum.each(
  [:process1, :process2, :process3],
  fn process_id ->
    spawn(fn ->
      :timer.sleep(Enum.random(100..200))
      SingletonResource.acquire(resource_flag, process_id)
    end)
  end
)

InPlace library

Github: bokner/inplace

The wrapper around atomics, with some ADTs implemented

  • array
  • stack
  • queue
  • heap
  • priority queue
  • bitset
  • sparse set
  • linked lists

Circular linked lists

File.read!("livebooks/circular-linked-list.svg.png")
|> Kino.Image.new(:png)

Josephus problem

https://en.wikipedia.org/wiki/Josephus_problem

Here is a story from Roman-Jewish historian Flavius Josephus (c. AD 37 – c. 100):

During the war between Roman and Jewish forces in the 1st century, Josephus and 40 soldiers ended up being trapped in a cave surrounded by Roman soldiers. They chose to die rather than surrender.

All the soldiers would stand in a circle, and the first person would kill the second in the circle. Then, the next remaining living person (the third) would kill the next person (the fourth), and the next remaining living person after that (the fifth) would kill the next person (the sixth), and so on.

Josephus managed to pick a position in the circle that allowed him to be a survivor.

#Path.join([System.get_env("HOME"), "projects", "inplace", "livebooks", "Josephus_problem_41_3.svg"])

Path.join([System.get_env("HOME"), "projects", "inplace", "livebooks", "josephus_animation.html"])
|> File.read!()
|> Kino.HTML.new()

Josephus problem: functional solution

defmodule Josephus.Functional do
  def permutation(list, k) when k > 0 do
    do_permutation(list, k, [], 0)
  end

  defp do_permutation([], _k, acc, _idx), do: Enum.reverse(acc)

  defp do_permutation(list, k, acc, idx) do
    len = length(list)
    kill_index = rem(idx + k - 1, len)

    {picked, rest} = List.pop_at(list, kill_index)
    do_permutation(rest, k, [picked | acc], kill_index)
  end
end

n = 10_000; k = 3
:timer.tc(fn -> Josephus.Functional.permutation(1..n |> Enum.to_list(), k) end) |> elem(0) |> IO.inspect(label: :functional)
:timer.tc(fn -> InPlace.Examples.Josephus.solve(n, k) end) |> elem(0) |> IO.inspect(label: :inplace)
Path.join([System.get_env("HOME"), "projects", "inplace", "livebooks", "bitset.png"])
|> File.read!()
|> Kino.Image.new("image/jpeg")