Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Many Ways to Concur

Many Ways to Concur

Easy concurrency is one of the main prophesied benefits of the modern functional programming (FP) languages. But the implementation of concurrency differs widely between different FP languages. In this talk, we shall explore the methods and primitives of concurrency across three FP languages: Haskell, Erlang, and Clojure (with core.async).

We shall learn about and compare the trade-offs between
- the green threads and STM channels oriented concurrency of Haskell
- everything-is-a-process and message-passing actor pattern of Erlang
- macro-based CSP of Clojure/core.async

Abhinav Sarkar

December 15, 2018
Tweet

More Decks by Abhinav Sarkar

Other Decks in Programming

Transcript

  1. Concurrency is a program- structuring technique in which there are

    multiple threads of control which execute "at the same time". — Simon Marlow, Parallel and Concurrent Programming in Haskell
  2. Threads » A sequence of instructions along with a context.

    » Run by processors. » Managed by schedulers. Source: Wikipedia
  3. Green Threads » Green threads are scheduled by the runtime

    system or the virtual machine instead of the OS kernel. » Starting green threads is cheaper and faster. Context switches are faster too. » Making system calls blocks the thread. So the runtime needs to support asynchronous IO. » Cannot exploit multiprocessor parallelism in their simplest form.
  4. Synchronization » The process by which multiple threads agree (or

    concur) on something. » Locks prevent concurrent access to critical sections/memory. » Locks do not compose.
  5. Synchronization » Lock-free shared-state concurrency » Software Transactional Memory »

    Communicating Sequential Processes » No shared-state concurrency » Actor Model
  6. Actor Model » An actor is an entity which can

    » create new actors » send messages to actors » behaves in a certain way on receiving a message
  7. -module(echo). -export([loop/0]). loop() -> receive quit -> io:fwrite("Bye~n"); Num ->

    io:fwrite("Received: ~p~n", [Num]), loop() end. > Pid = spawn(fun echo:loop/0). > Pid ! 232. Received: 232 > Pid ! quit. Bye
  8. -module(counter). -export([loop/1]). loop(N) -> receive {inc} -> loop(N+1); {get, Sender}

    -> Sender ! N, loop(N) end. > Pid = spawn(counter, loop, [0]). > Pid ! {inc}. > Pid ! {get, self()}. > receive Value -> io:fwrite("~p~n", [Value]) end. 1 > Pid ! {getx, self()}. > receive Value -> io:fwrite("~p~n", [Value]) > after 1000 -> io:fwrite("Timeout~n") end. Timeout
  9. +----------------------------------+ | | | PID / Status / Registered Name

    | Process | | Control | Initial Call / Current Call +----> Block | | (PCB) | Mailbox Pointers | | | +----------------------------------+ | | | Function Parameters | | | Process | Return Addresses +----> Stack | | | Local Variables | | | +-------------------------------+--+ | | | | ^ v +----> Free | | | Space +--+-------------------------------+ | | | Mailbox Messages (Linked List) | | | Process | Compound Terms (List, Tuples) +----> Private | | Heap | Terms Larger than a word | | | +----------------------------------+
  10. Erlang Actors » Pros: » Communication is synchronization. » No

    need to worry about mutual exclusion. » Models distributed systems very well. » Cons: » Can be slower compared to Shared-state Concurrency. » Multi entity consensus is difficult to achieve. » Mailboxes may overflow if the message processing is slow.
  11. Software Transactional Memory » Software transactional memory (STM) allows changing

    multiple mutable variables together in a single atomic operation. » Atomicity: All the state changes in an atomic operation become visible too all the threads at once. » Isolation: The atomic operation is completely unaffected by whatever other threads are doing.
  12. data STM a -- abstract instance Monad STM -- among

    other things data TVar a -- abstract newTVar :: a -> STM (TVar a) readTVar :: TVar a -> STM a writeTVar :: TVar a -> a -> STM () atomically :: STM a -> IO a retry :: STM a orElse :: STM a -> STM a -> STM a
  13. import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Account { private int id,

    amount; private Lock lock = new ReentrantLock(); Account(int id, int initialAmount) { this.id = id; this.amount = initialAmount; } public void withdraw(int n) { this.lock.lock(); try { this.amount -= n; } finally { this.lock.unlock(); } } public void deposit(int n) { this.withdraw(-n); } public void transfer(Account other, int n) { this.withdraw(n); other.deposit(n); } }
  14. class Account { public void transfer(Account other, int n) {

    if (this.id < other.id) { this.lock.lock(); other.lock.lock(); } else { other.lock.lock(); this.lock.lock(); } try { this.amount -= n; other.amount += n; } finally { if (this.id < other.id) { this.lock.unlock(); other.lock.unlock(); } else { other.lock.unlock(); this.lock.unlock(); } } } }
  15. import System.IO import Control.Concurrent.STM type Account = TVar Int withdraw

    :: Account -> Int -> STM () withdraw acc amount = do bal <- readTVar acc writeTVar acc (bal - amount) deposit :: Account -> Int -> STM () deposit acc amount = withdraw acc (- amount) transfer :: Account -> Account -> Int -> IO () transfer from to amount = atomically $ do deposit to amount withdraw from amount amount :: Account -> IO Int amount acc = atomically $ readTVar acc
  16. main = do -- create accounts from <- atomically (newTVar

    200) to <- atomically (newTVar 100) -- transfer amount transfer from to 50 -- get amounts and print v1 <- amount from v2 <- amount to putStrLn $ (show v1) ++ ", " ++ (show v2) -- prints "150, 150"
  17. retry :: STM a checkedWithdraw :: Account -> Int ->

    STM () checkedWithdraw acc amount = do bal <- readTVar acc if amount >= bal then writeTVar acc (bal - amount) else retry orElse :: STM a -> STM a -> STM a backupWithdraw :: Account -> Account -> Int -> STM () backupWithdraw acc1 acc2 amount = checkedWithdraw acc1 amt `orElse` checkedWithdraw acc2 amt
  18. data TChan a newTChan :: STM (TChan a) writeTChan ::

    TChan a -> a -> STM () readTChan :: TChan a -> STM a
  19. Haskell STM » Pros » Composable atomicity and blocking. »

    Type system prevents side effects. » Dataflow programming with TChans. » Cons » Transactions which touch many variables are expensive. » A long-running transaction may re-execute indefinitely because it may be repeatedly aborted by shorter transactions.
  20. Communicating Sequential Processes » Independent threads of activity. » Synchronous

    communication through channels. » Multiplexing of channels with alternation.
  21. (go (println "hi")) (def echo-chan (chan)) (go (println (<! echo-chan)))

    (go (>! echo-chan "hello")) ; => hello (def echo-chan (chan 10))
  22. (let [c1 (chan) c2 (chan) c3 (chan)] (dotimes [n 3]

    (go (let [[v ch] (alts! [c1 c2 c3])] (println "Read" v "from" ch)))) (go (>! c1 "hello")) (go (>! c2 "allo") (go (>! c2 "hola"))) ; => Read allo from #<ManyToManyChannel ...> ; => Read hola from #<ManyToManyChannel ...> ; => Read hello from #<ManyToManyChannel ...>
  23. [inst_10125 {:bindings {:terminators ()}, :block-id 1, :blocks {1 [{:ast (let

    [c__7560__auto__ (core.async/chan 1) captured-bindings__7561__auto__ (Var/getThreadBindingFrame)] (core.async.impl.dispatch/run (fn* [] (let [f__7562__auto__ (fn state-machine__7066__auto__ ([] (core.async.impl.ioc-macros/aset-all! (java.util.concurrent.atomic.AtomicReferenceArray. 7) 0 state-machine__7066__auto__ 1 1)) ([state_10123] (let [old-frame__7067__auto__ (Var/getThreadBindingFrame) ret-value__7068__auto__ (try (Var/resetThreadBindingFrame (core.async.impl.ioc-macros/aget-object state_10123 3)) (loop [] (let [result__7069__auto__ (case (int (core.async.impl.ioc-macros/aget-object state_10123 1)) 1 (let [inst_10121 (if (= :x 1) :true :false)] (core.async.impl.ioc-macros/return-chan state_10123 inst_10121)))] (if (identical? result__7069__auto__ :recur) (recur) result__7069__auto__))) (catch java.lang.Throwable ex__7070__auto__ (core.async.impl.ioc-macros/aset-all! state_10123 2 ex__7070__auto__) (if (seq (core.async.impl.ioc-macros/aget-object state_10123 4)) (core.async.impl.ioc-macros/aset-all! state_10123 1 (first (core.async.impl.ioc-macros/aget-object state_10123 4)) 4 (rest (core.async.impl.ioc-macros/aget-object state_10123 4))) (throw ex__7070__auto__)) :recur) (finally (Var/resetThreadBindingFrame old-frame__7067__auto__)))] (if (identical? ret-value__7068__auto__ :recur) (recur state_10123) ret-value__7068__auto__)))) state__7563__auto__ (-> (f__7562__auto__) (core.async.impl.ioc-macros/aset-all! core.async.impl.ioc-macros/USER-START-IDX c__7560__auto__ core.async.impl.ioc-macros/BINDINGS-IDX captured-bindings__7561__auto__))] (core.async.impl.ioc-macros/run-state-machine-wrapped state__7563__auto__)))) c__7560__auto__), :locals nil, :id inst_10124} {:value inst_10124, :id inst_10125}]}, :block-catches {1 nil}, :start-block 1, :current-block 1}]
  24. Clojure core.async » Pros » Just a library. No special

    runtime needed. » Easy Dataflow programming with many combinators available. » Works on JVM and browser. » Cons » Doing blocking IO in go-threads blocks them. » Error handling is complicated.
  25. Learnings » Green threads FTW. » Decomplect using higher level

    concurrency models. » Different techniques are suitable for different situations.
  26. References » Software Transactional Memory chapter from the Parallel and

    Concurrent Programming in Haskell book » Processes chapter from The Erlang Runtime System book » Timothy Baldridge's screencasts on Clojure/ core.async internals » Rich Hickey's talk on Clojure/core.async internals