B3CC: Concurrency 03: Threads Trevor L. McDonell Utrecht University, B2 2020-2021
Announcement • The first practical assignment has been released - http://www.cs.uu.nl/docs/vakken/b3cc/assignments.html - Deadline: 2020-11-28 @ 23:59 2
Mutual Exclusion 3
Recall: concurrent access to a global queue head last • Thread A: • Thread B: - Create new object - Set last ->. next to &new - Create new object - Set last ->. next to &new - Set last to &new - Set last to &new 4
Mutual exclusion • Mutual exclusion (locking) protects shared resources - Only one process at a time is allowed to access the critical resource - Modifications to the resource appear to happen atomically • In this lecture: a software approach implementing concurrency control - That is, without relying on builtin language features or hardware support (we’ll cover that later) 5
Software approach to mutual exclusion • Premise - One or more threads with shared memory - Elementary mutual exclusion at the level of memory access • Simultaneous access to the same memory location are serialised • Requirements for mutual exclusion - Only one thread at a time is allowed in the critical section - No deadlock or starvation 6
Attempt #1 • The plan: - Threads take turns executing the critical section - Exploit serialisation of memory access to implement serialisation of access to the critical section • Employ a shared variable (memory location) turn that indicates whose turn it is to enter the critical section P 0 : P 1 : while (turn !!> 0) while (turn !!> 1) //+ do nothing **0 ; //+ do nothing **0 ; <critical section> <critical section> turn = 1; turn = 0; 7
Attempt #1 • Busy waiting (spin lock) - Process is always checking to see if it can enter the critical section - Implements mutual exclusion - Simple • Disadvantages - Process burns resources while waiting - Processes must alternate access to the critical section - If one process fails anywhere in the program, the other is permanently blocked 8
Attempt #2 • The problem: - turn stores who can enter the critical section, rather than whether anybody may enter the critical section • The new plan: - Store for each process whether it is in the critical section right now - flag[i] if process i is in the critical section P 0 : P 1 : while (flag[1]) while (flag[0]) //+ do nothing **0 ; //+ do nothing **0 ; flag[0] = true; flag[1] = true; <critical section> <critical section> flag[0] = false; flag[1] = false; 9
Attempt #2 • If a process fails? - Outside the critical section: the other is not blocked - Inside the critical section: the other is blocked (however, difficult to avoid) • Does it work? 1. Both flags are set to false 2. P 0 enters critical section 3. P 1 enters critical section 4. P 1 sets flag[1] 5. P 0 sets flag[0] - Does not guarantee exclusive access 10
Attempt #3 • The goal: - Remove the gap between toggling the two flags • The new updated plan: - Move setting the flag to before checking whether we can enter P 0 : P 1 : flag[0] = true; flag[1] = true; while (flag[1]) while (flag[0]) //+ do nothing **0 ; //+ do nothing **0 ; <critical section> <critical section> flag[0] = false; flag[1] = false; 11
Attempt #3 • Is it working now? - No. The gap can cause a deadlock now >_> - Deadlock: when each member of a group of processes is waiting for another to take action (e.g. waiting for another to release a lock) 12
Attempt #4 • Previous problem: - Process sets its own state before knowing the other processes’ states, and cannot back off • The new updated revised plan: - Process retracts its decision if it cannot enter P 0 : P 1 : flag[0] = true; flag[1] = true; while (flag[1]) { while (flag[0]) { flag[0] = false; flag[1] = false; delay(); delay(); flag[0] = true; flag[1] = true; } } <critical section> <critical section> flag[0] = false; flag[1] = false; 13
Attempt #4 • Is it working now? - Close, but we may have a livelock =_= - Livelock: The states of the group of processes are constantly changing with regard to each other, but none are progressing (e.g. trying to obtaining a lock, but backing off if it fails) - A special case of resource starvation, and a risk for algorithms which attempt to detect and recover from deadlock 14
Attempt #5 • Improvements - We can solve this problem by combining the fourth and first attempts - In addition to the flag s we use a variable indicating whose turn it is to have precedence in entering the critical section 15
Attempt #5: Peterson’s algorithm • Both processes are courteous and solve a tie in favour of the other • Algorithm can be generalised to work with n processes P 0 : P 1 : flag[0] = true; flag[1] = true; turn = 1; turn = 0; while (flag[1] while (flag[0] &&' turn ==> 1) &&' turn ==> 0) //+ do nothing **0 ; //+ do nothing **0 ; <critical section> <critical section> flag[0] = false; flag[1] = false; 16
Attempt #5: Peterson’s algorithm • Statement: mutual exclusion Threads 0 and 1 are never in the critical section at the same time • Proof: - If P 0 is in the critical section then • flag[0] is true • flag[1] is false OR turn is zero OR P 1 is trying to enter the critical section, after setting flag[1] to true but before setting turn to zero - For both P 0 and P 1 to be in the critical section • flag[0] AND flag[1] AND turn=0 AND turn=1 17
Hardware support • The compare-and-swap (CAS) operation is an atomic instruction which allows mutual exclusion for any number of threads using a single bit of memory - Compares the memory location to a given value - If they are the same, writes a new value to that location - Returns the old value of the memory location https://www.felixcloutier.com/x86/cmpxchg 18
Hardware support • The plan: - Use a bit lock where zero represents unlocked and one represents locked while (atomic_cas(lock, 0, 1) ==> 1) //+ do nothing **0 ; <critical section> lock = 0; • In Haskell we can use atomicModifyIORef’ or atomicModifyIORefCAS (from atomic - primps ) 19
Non-blocking algorithms • Obstruction-freedom - From any point after a thread begins executing in isolation, it finishes in a finite number of steps (a thread will be able to finish if no other thread makes progress) • Lock-freedom - Some method will finish in a finite number of steps • Wait-freedom - Every method will finish in a finite number of steps • Starvation - A process is denied access to a resource. Antonym: fairness 20
Non-blocking algorithms independent dependent dependent non-blocking non-blocking blocking every method Wait-free Obstruction-free Starvation-free makes progress maximal vs. mimimal some method Lock-free Deadlock-free ? makes progress blocking dependent vs. vs. non-blocking independent http://www.cs.tau.ac.il/~shanir/progress.pdf 21
Threads in Haskell 22
Threads • The fundamental action in concurrency: create a new thread of control forkIO ::; IO () ->. IO ThreadId - Takes a computation of type IO () as its argument - This IO action executes in a new thread concurrently with other threads - No specified order in which threads execute - Threads are very cheap: ~1.5 Kb / thread, easily run thousands of threads 23
Example • Interleaving of two threads import Control.Concurrent import Control.Monad main ::; IO () main = do let n = 1000 forkIO $ replicateM_ n (putChar 'A') forkIO $ replicateM_ n (putChar 'B') return () 24
Example • Interleaving of two threads - The term n ::; Int is shared between both threads (captured); this is safe because it is immutable - The program exits when main returns, even if there are other threads still running! • How to check whether the child thread has completed? 25
tot ziens Photo by Ugur Arpaci
Recommend
More recommend