3 Underrated Concurrency Models

Topics

Clojure - Flexible single process concurrency      

Erlang/Elixir - Distributed,  fault tolerant  

OpenCL - highly parallel, not so flexible               

What is Concurrency?

The ability to be executed out-of-order or in partial order, 

without affecting the final outcome

Why Care?

  • Moore's Law is dead

  • Because we can: microservices

pthreads

  •  Easy to mess up, impossible to debug 

  •  Good luck with shared state

  •  Poor real world performance

(are not the answer) 

 Immutability: the FP superpower

let   x = 42;
const y = [1,2,3];
y.splice(0,1); // [1]
y // [2, 3]
// woops

But...

  • How do we change things?

  • How do we store changes? 

Clojure

  • A "functional"  (data oriented) LISP

  • Runs on the JVM (cljs in the browser)

  • Real world language, not an academic BS

  • Was built for single process concurrency

Clojure syntax

(defn add [a b]
  (+ a b))
function add(a, b) {
  return a + b;
}

Clojure syntax

(filter #{:red :green} [:red :green :blue])
const allowed = new Set(["green", "blue"]);
["red", "green", "blue"].filter(c => allowed.has(c));

How Do We Change Data structs in Clojure?

 

We Dont!

(-> {:x { :y [ {:z #{"apple"}}]}}
  (update-in [:x :y 0 :z] conj "banana")) 

;; {:x {:y [{:z #{"apple" "banana"}}]}}

Persistent Data Structures

  • Immutable, share structure

  • Bit partitioned hash tries

  • Same time complexity O(1) = O(log32)

  • Immutable.js is the (unusable) JS port 

(map inc (range 0 100))

Free Concurrency 

(concat
  (map inc (range 0 25))
  (map inc (range 25 50))
  (map inc (range 50 75))
  (map inc (range 75 100)))
  

map -> pmap

(map inc (range 0 10000000))
(pmap inc (range 0 10000000))

map -> pmap

(map inc (range 0 10000000))
"Elapsed time: 2274.327422 msecs"
(pmap inc (range 0 10000000))
"Elapsed time: 27530.360776 msecs"
(map sleep-100-ms-then-inc (range 0 1000))
(pmap sleep-100ms-then-inc (range 0 1000))

map -> pmap

(map sleep-100-ms-then-inc (range 0 1000))
"Elapsed time: 100195.859968 msecs"
(pmap sleep-100ms-then-inc (range 0 1000))
"Elapsed time: 3218.437153 msecs"

map -> pmap

So how do we save and share changes ?

 3 Concurrency Primitives

coordinated uncoordinated
Sync Ref Atom
Async Agents
  • Coordinated - can we change multiple values?
  • Sync                - should we block and wait?

Atoms

(def me 
  (atom {:name "vitali" 
         :age 32 
         :languages #{:clojure :python, :smalltalk}}))

Atoms

(deref me)
{:name "vitali", 
 :age 32, 
 :languages #{:smalltalk :clojure :python}}

@me
{:name "vitali", 
 :age 32, 
 :languages #{:smalltalk :clojure :python}}

Atoms

;; age
(swap! me update :age inc)
{:name "vitali",  
 :age 33, 
 :languages #{:smalltalk :clojure :python}}

;; learn css
(swap! me update :languages conj :css)
{:name "vitali", 
 :age 33, 
 :languages #{:smalltalk :clojure :css :python}}

Refs and Clojure's STM

(def player (ref {:health 500 :attack 10 :items #{}}))
(def mob1   (ref {:health 100 :attack 2  :items #{"big-banana"}}))
(def mob2   (ref {:health 100 :attack 4  :items #{"banana"}}))

Refs and Clojure's STM

(defn attack! [attacker enemy]
  (dosync
    (let [attacker-value (:attack @attacker)
          enemy-value (:attack @enemy)]
      (alter enemy    update :health - attacker-value)
      (alter attacker update :health - (/ enemy-value 2)))))

Refs and Clojure's STM

(defn loot! [to from]
  (dosync
    (when-let [item (first (:items @from))]
      (alter to update :items conj item)
      (alter from update :items disj item))))

Refs and Clojure's STM

(attack! player mob1)
(attack! player mob2)
(loot!  player  mob2)

@player
{:health 497, :attack 10, :items #{"banana"}}

@mob2
{:health 90, :attack 4, :items #{}}

@mob1
{:health 90, :attack 2, :items #{"big-banana"}}

Agents

(def sleeper (agent 0))

(send-off sleeper sleep-100ms-then-inc)

@sleeper                             
=> 0

(await sleeper)                     

@sleeper                              
=> 1

The Actor Model

The Actor Model

  • Theoretical model of computation

  • Carl Hewitt 1973

  • Concurrent from the ground up

  • NOT related to Clojure agents!

  • Many Implementations for the actor model

Actor

  • Can spawn new actors                     

Actor

  • Can spawn new actors

  • Can send messages to other actors

Actor

  • Can spawn new actors

  • Can send messages to other actors

  • Can send messages to himself

Actor

  • Can spawn new actors

  • Can send messages to other actors

  • Can send messages to himself

  • Can store private state

Messages

  • Data (Immutable)

  • The only way to share state

  • Sent to addresses not actors directly

Erlang

  • Was built for real time distributed systems

  • Has it's own VM - BEAM

  • Almost "Pure" Actor Model 

  • Developed by Ericsson in 1986

  • Compiles to bytecode for BEAM

  • Much newer (2011) 

  • Takes from Ruby and Clojure 

  • Has access to the host, like clojure

Elixir Counter

defmodule Counter do
  def loop(count) do
    receive do
     {:next} ->
       IO.puts("Current count: #{count}")
       loop(count + 1)
     end
  end
end
counter = spawn(Counter, :loop, [1])
#PID<0.47.0>

iex(2)> send(counter, {:next})
Current count: 1

iex(3)> send(counter, {:next})
Current count: 2

Counter v2.0

defmodule Demo do
  def counter() do
      receive do
       value ->
         IO.puts value
         Process.sleep(1000)
         send(self(), value + 1)
    end
    counter()
  end
end
defmodule Demo do
    def ctrl(t_pid) do
      receive do
        :start ->
            t_pid = spawn(&counter/0)
            send(t_pid, 0)
            ctrl(t_pid)
		
        :stop ->
           Process.exit(t_pid, :kill)
           ctrl(t_pid)
      end
    end
    
    def counter() do
      receive do
       value ->
         IO.puts value
         Process.sleep(1000)
         send(self(), value + 1)
    end
    counter()
  end
end

Erlang VM Killer Features

  • Real time GC

  • Hot code swapping

  • Preemptive multitasking

GPGPU

Data Parallelism

Data Parallelism

(map inc (range 0 10000000))
(pmap inc (range 0 10000000))

GPUs

  • Historically fixed pipline (OpenGL 1)

  • Shaders (OpenGL 2.0) changed the game

  • Many "cores", high latency high throughput 

  • SIMD (it's really SPMD)  vs CPUs MIMD

Stream processing

  • Steam - a read-only mem (texture - 2D grid)

  • Kernel - a function that runs on a stream

  • Thread - a running kernel instance
                                                      (aka "work item")

OpenCL

  • One of the many options for GPGPU

  • Open, not vendor specific

  • Hosted inside a C program

  • Abstracts GPUs and CPUs 

OpenCL Code

__kernel void vector_add(__global const int* A, 
                         __global const int* B, 
                         __global int* C) 
{ 
    int id = get_global_id(0);
    C[id] = A[id] + B[id];
  
}

OpenCL Execution

  • The actual code is in kernels

  • Executed by many threads (work items)

  • Each work item has an it's own index

  • All work items run the same code (SPMD)

OpenCL device

OpenCL device

OpenCL Context

OpenCL Hardware Abstraction

Compute Unit
 

PE

OpenCL Memory Regions

  • _private  - PE, on chip registers

  • _local      - working group

  • _global   - context (expensive)

deck

By Vitali Perchonok