0% found this document useful (0 votes)
10 views40 pages

13 Locks

The document discusses concurrency and synchronization in programming, particularly focusing on the issues that arise when multiple threads access shared resources. It explains the concept of critical sections, the need for locks to ensure mutual exclusion, and various algorithms for implementing locks, including hardware support for atomic operations. Additionally, it highlights the importance of correctness, fairness, and performance in lock implementations.

Uploaded by

wlgml7219
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views40 pages

13 Locks

The document discusses concurrency and synchronization in programming, particularly focusing on the issues that arise when multiple threads access shared resources. It explains the concept of critical sections, the need for locks to ensure mutual exclusion, and various algorithms for implementing locks, including hardware support for atomic operations. Additionally, it highlights the importance of correctness, fairness, and performance in lock implementations.

Uploaded by

wlgml7219
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 40

Pusan National University

Locks
Reading materials: Chap.28

Instructor: Sungyong Ahn

1
Pusan National University

The Classic Example


■ Withdrawing money from a bank account
▪ Suppose you and your girl (or boy) friend share a bank account with a
balance of 1,000,000won
▪ What happens if both go to separate ATM machines and
simultaneously withdraw 100,000won from the account?

int withdraw (account, amount)


{
balance = get_balance (account);
balance = balance - amount;
put_balance (account, balance);
return balance;
}

2
Pusan National University

The Classic Example (Cont’d)


■ The execution of the two threads can be interleaved,
assuming preemptive scheduling:

balance = get_balance (account);


balance = balance - amount; Context
Execution switch
sequence balance = get_balance (account);
as seen by balance = balance - amount;
CPU put_balance (account, balance); Context
switch
put_balance (account, balance);

3
Pusan National University

The Real Example

extern int g;
void inc() movl 0x1000, %eax
{ addl $1, %eax
g++; movl %eax, 0x1000
}

Thread T1 Thread T2

movl 0x1000, %eax


addl $1, %eax
Context switch movl 0x1000, %eax
addl $1, %eax
movl %eax, 0x1000
Context switch
movl %eax, 0x1000
4
Pusan National University

Sharing Resources
■ Local variables are not shared among threads
▪ Refer to data on the stack
▪ Each thread has its own stack
▪ Never pass/share/store a pointer to a local variable on another thread’s
stack
■ Global variables are shared among threads
▪ Stored in static data segment, accessible by any thread
■ Dynamic objects are shared among threads
▪ Stored in the heap, shared through the pointers
■ Also, processes can share memory (shmem)

5
Pusan National University

Synchronization Problem
■ Concurrency leads to non-deterministic results
▪ Two or more concurrent threads accessing a shared resource create a
race condition
▪ The output of the program is not deterministic; it varies from run to run
even with same inputs, depending on timing
▪ Hard to debug (“Heisenbugs”)

■ We need synchronization mechanisms for controlling access


to shared resources
▪ Synchronization restricts the concurrency
▪ Scheduling is not under programmer’s control

6
Pusan National University

Concurrency in the Kernel

7
Pusan National University

Critical Section
■ A critical section is a piece of code that accesses a shared
resource, usually a variable or data structure

movl 0x1000, %eax


addl $1, %eax Critical section
movl %eax, 0x1000

■ Need mutual exclusion for critical sections


▪ Execute the critical section atomically (all-or-nothing)
▪ Only one thread at a time can execute in the critical section
▪ All other threads are forced to wait on entry
▪ When a thread leaves a critical section, another can enter

8
Pusan National University

Locks: The Basic Idea


■ Ensure that any critical section executes as if it were a single
atomic instruction.
▪ An example: the canonical update of a shared variable
balance = balance + 1;

▪ Add some code around the critical section


1 lock_t mutex; // some globally-allocated lock ‘mutex’
2 …
3 lock(&mutex);
4 balance = balance + 1;
5 unlock(&mutex);

9
Pusan National University

Locks: The Basic Idea


■ Lock variable holds the state of the lock.
▪ available (or unlocked or free)
▪ No thread holds the lock.

▪ acquired (or locked or held)


▪ Exactly one thread holds the lock and presumably is in a critical
section.

10
Pusan National University

The semantics of the lock()


■ lock()
▪ Try to acquire the lock.
▪ If no other thread holds the lock, the thread will acquire the lock.
▪ Enter the critical section.
▪ This thread is said to be the owner of the lock.

▪ Other threads are prevented from entering the critical section while the
first thread that holds the lock is in there.

■ unlock()
▪ Once the owner of the lock calls unlock(), the lock is now available
(free) again.
▪ Wake up any thread waiting in lock()

11
Pusan National University

Using locks
■ Lock is initially free
■ Call lock() before entering a critical section, and unlock()
after leaving it
■ lock() does not return until the caller holds the lock
■ On unlock(), a thread can spin (spinlock) or block (mutex)
■ At most one thread can hold a lock at a time

12
Pusan National University

Using Locks

int withdraw (account, amount)


{
A lock (lock);
S1 balance = get_balance (account);
S2 balance = balance - amount; Critical section
S3 put_balance (account, balance);
R unlock (lock);
return balance;
}

13
Pusan National University

Requirements for Locks


■ Correctness
▪ Mutual exclusion: only one thread in critical section at a time
▪ Progress (deadlock-free): if several threads want to enter the critical
section, must allow one to proceed
▪ Bounded waiting (starvation-free): must eventually allow each waiting
thread to enter

■ Fairness
▪ Each thread gets a fair chance at acquiring the lock

■ Performance
▪ Time overhead for a lock without and with contentions (possibly on
multiple CPUs)?

14
Pusan National University

Implementing Locks
■ Controlling interrupts

■ Software-only algorithms
▪ Dekker’s algorithm (1962)
▪ Peterson’s algorithm (1981)
▪ Lamport’s Bakery algorithm for more than two processes (1974)

■ Hardware atomic instructions


▪ Test-And-Set
▪ Compare-And-Swap
▪ Load-Linked (LL) and Store-Conditional (SC)
▪ Fetch-And-Add

15
Pusan National University

Controlling Interrupts
■ Disable Interrupts for critical sections
▪ One of the earliest solutions used to provide mutual exclusion
▪ Invented for single-processor systems.
▪ Disabling interrupts blocks external events that could trigger a context
switch (e.g. timer)
▪ The code inside the critical section will not be interrupted
▪ There is no state associated with the lock
1 void lock() {
2 DisableInterrupts();
3 }
4 void unlock() {
5 EnableInterrupts();
6 }

16
Pusan National University

Controlling Interrupts
■ Disable Interrupts for critical sections
▪ Simple
▪ Useful for a single-processor system
▪ Problem:
▪ Require too much trust in applications
– Greedy (or malicious) program could monopolize the processor.
▪ Do not work on multiprocessors
▪ Code that masks or unmasks interrupts be executed slowly by
modern CPUs

17
Pusan National University

Software-only Algorithm
■ The second attempt to implement spinlocks
▪ Note: each load and store instruction is atomic
int flag[2];

void init() {
flag[0] = flag[1] = 0; // 1->thread wants to grab lock
}
void lock() {
int other = 1 – self;
flag[self] = 1; // self: thread ID of caller
while (flag[other] == 1); // spin-wait
}
void unlock() {
flag[self] = 0; // simply undo your intent
}

■ Does this work?

18
Pusan National University

Peterson’s Algorithm
■ Solves the critical section problem for two processes
int flag[2];
int turn;

void init() {
flag[0] = flag[1] = 0; // 1->thread wants to grab lock
turn = 0; // whose turn? (thread 0 or 1?)
}
void lock() {
int other = 1 – self;
flag[self] = 1; // self: thread ID of caller
turn = other; // make it other thread’s turn
while ((flag[other] == 1) && (turn == other)); // spin-wait
}
void unlock() {
flag[self] = 0; // simply undo your intent
}

19
Pusan National University

Why hardware support needed?


■ First attempt: Using a flag denoting whether the lock is held
or not.
▪ The code below has problems.
1 typedef struct __lock_t { int flag; } lock_t;
2
3 void init(lock_t *mutex) {
4 // 0 → lock is available, 1 → held
5 mutex->flag = 0;
6 }
7
8 void lock(lock_t *mutex) {
9 while (mutex->flag == 1) // TEST the flag
10 ; // spin-wait (do nothing)
11 mutex->flag = 1; // now SET it !
12 }
13
14 void unlock(lock_t *mutex) {
15 mutex->flag = 0;
16 }

20
Pusan National University

Why hardware support needed? (Cont.)


▪ Problem 1: No Mutual Exclusion (assume flag=0 to begin)
Thread1 Thread2
call lock()
while (flag == 1)
interrupt: switch to Thread 2
call lock()
while (flag == 1)
flag = 1;
interrupt: switch to Thread 1
flag = 1; // set flag to 1 (too!)

▪ Problem 2: Spin-waiting wastes time waiting for another thread.

■ So, we need an atomic instruction supported by Hardware!


▪ test-and-set instruction, also known as atomic exchange

21
Pusan National University

Test And Set (Atomic Exchange)


■ An instruction to support the creation of simple locks
1 int TestAndSet(int *ptr, int new) {
2 int old = *ptr; // fetch old value at ptr
3 *ptr = new; // store ‘new’ into ptr
4 return old; // return the old value
5 }

▪ return(testing) old value pointed to by the ptr.


▪ Simultaneously update(setting) said value to new.
▪ This sequence of operations is performed atomically.

22
Pusan National University

A Simple Spin Lock using test-and-set


1 typedef struct __lock_t {
2 int flag;
3 } lock_t;
4
5 void init(lock_t *lock) {
6 // 0 indicates that lock is available,
7 // 1 that it is held
8 lock->flag = 0;
9 }
10
11 void lock(lock_t *lock) {
12 while (TestAndSet(&lock->flag, 1) == 1)
13 ; // spin-wait
14 }
15
16 void unlock(lock_t *lock) {
17 lock->flag = 0;
18 }

▪ Note: To work correctly on a single processor, it requires a preemptive


scheduler.

23
Pusan National University

Evaluating Spin Locks


■ Correctness: yes
▪ The spin lock only allows a single thread to entry the critical section.

■ Fairness: no
▪ Spin locks don’t provide any fairness guarantees.
▪ Indeed, a thread spinning may spin forever.

■ Performance:
▪ In the single CPU, performance overheads can be quire painful.
▪ If the number of threads roughly equals the number of CPUs, spin locks
work reasonably well.

24
Pusan National University

Compare-And-Swap
■ Test whether the value at the address(ptr) is equal to
expected.
▪ If so, update the memory location pointed to by ptr with the new
value.
▪ In either case, return the actual value at that memory location.
1 int CompareAndSwap(int *ptr, int expected, int new) {
2 int actual = *ptr;
3 if (actual == expected)
4 *ptr = new;
5 return actual;
6 }
Compare-and-Swap hardware atomic instruction (C-style)

1 void lock(lock_t *lock) {


2 while (CompareAndSwap(&lock->flag, 0, 1) == 1)
3 ; // spin
4 }
Spin lock with compare-and-swap
25
Pusan National University

Load-Linked and Store-Conditional


1 int LoadLinked(int *ptr) {
2 return *ptr;
3 }
4
5 int StoreConditional(int *ptr, int value) {
6 if (no one has updated *ptr since the LoadLinked to this address) {
7 *ptr = value;
8 return 1; // success!
9 } else {
10 return 0; // failed to update
11 }
12 }

Load-linked And Store-conditional

▪ The store-conditional only succeeds if no intermittent store to the


address has taken place.
▪ success: return 1 and update the value at ptr to value.
▪ fail: the value at ptr is not updates and 0 is returned.

26
Pusan National University

Load-Linked and Store-Conditional (Cont.)


1 void lock(lock_t *lock) {
2 while (1) {
3 while (LoadLinked(&lock->flag) == 1)
4 ; // spin until it’s zero
5 if (StoreConditional(&lock->flag, 1) == 1)
6 return; // if set-it-to-1 was a success: all done
7 otherwise: try it all over again
8 }
9 }
10
11 void unlock(lock_t *lock) {
12 lock->flag = 0;
13 }
Using LL/SC To Build A Lock

1 void lock(lock_t *lock) {


2 while (LoadLinked(&lock->flag)||!StoreConditional(&lock->flag, 1))
3 ; // spin
4 }
A more concise form of the lock() using LL/SC

27
Pusan National University

Fetch-And-Add
■ Atomically increment a value while returning the old value at
a particular address.
1 int FetchAndAdd(int *ptr) {
2 int old = *ptr;
3 *ptr = old + 1;
4 return old;
5 }

Fetch-And-Add Hardware atomic instruction (C-style)

28
Pusan National University

Ticket Lock
■ Ticket lock can be built with fetch-and add.
▪ First get a ticket and wait until its turn
▪ Ensure progress for all threads. → fairness
1 typedef struct __lock_t {
2 int ticket;
3 int turn;
4 } lock_t;
5
6 void lock_init(lock_t *lock) {
7 lock->ticket = 0;
8 lock->turn = 0;
9 }
10
11 void lock(lock_t *lock) {
12 int myturn = FetchAndAdd(&lock->ticket);
13 while (lock->turn != myturn)
14 ; // spin
15 }
16 void unlock(lock_t *lock) {
17 FetchAndAdd(&lock->turn);
18 }

29
Pusan National University

So Much Spinning
■ Hardware-based spin locks are simple and they work.

■ In some cases, these solutions can be quite inefficient.


▪ Any time a thread gets caught spinning, it wastes an entire time slice
doing nothing but checking a value.

How To Avoid Spinning?


We’ll need OS Support too!

30
Pusan National University

A Simple Approach: Just Yield


■ When you are going to spin, give up the CPU to another
thread.
▪ OS system call moves the caller from the running state to the ready
state.
▪ The cost of a context switch can be substantial and the starvation
problem still exists.
1 void init() {
2 flag = 0;
3 }
4
5 void lock() {
6 while (TestAndSet(&flag, 1) == 1)
7 yield(); // give up the CPU
8 }
9
10 void unlock() {
11 flag = 0;
12 }

Lock with Test-and-set and Yield


31
Pusan National University

Using Queues: Sleeping Instead of Spinning


■ Queue to keep track of which threads are waiting to enter the
lock.
■ park()
▪ Put a calling thread to sleep
■ unpark(threadID)
▪ Wake a particular thread as designated by threadID.

32
Pusan National University

Using Queues: Sleeping Instead of Spinning


1 typedef struct __lock_t { int flag; int guard; queue_t *q; } lock_t;
2
3 void lock_init(lock_t *m) {
4 m->flag = 0;
5 m->guard = 0;
6 queue_init(m->q);
7 }
8
9 void lock(lock_t *m) {
10 while (TestAndSet(&m->guard, 1) == 1)
11 ; // acquire guard lock by spinning
12 if (m->flag == 0) {
13 m->flag = 1; // lock is acquired
14 m->guard = 0;
15 } else {
16 queue_add(m->q, gettid());
17 m->guard = 0;
18 park();
19 }
20 }
21 …

Lock With Queues, Test-and-set, Yield, And Wakeup


33
Pusan National University

Using Queues: Sleeping Instead of Spinning


22 void unlock(lock_t *m) {
23 while (TestAndSet(&m->guard, 1) == 1)
24 ; // acquire guard lock by spinning
25 if (queue_empty(m->q))
26 m->flag = 0; // let go of lock; no one wants it
27 else
28 unpark(queue_remove(m->q)); // hold lock (for next thread!)
29 m->guard = 0;
30 }

Lock With Queues, Test-and-set, Yield, And Wakeup (Cont.)

34
Pusan National University

Wakeup/waiting race
■ In case of releasing the lock (thread A) just before the call to
park() (thread B) → Thread B would sleep forever
(potentially).

■ Solaris solves this problem by adding a third system call:


setpark().
▪ By calling this routine, a thread can indicate it is about to park.
▪ If it happens to be interrupted and another thread calls unpark before
park is actually called, the subsequent park returns immediately
instead of sleeping.
1 queue_add(m->q, gettid());
2 setpark(); // new code
3 m->guard = 0;
4 park();
Code modification inside of lock()

35
Pusan National University

Futex
■ Linux provides a futex (is similar to Solaris’s park and
unpark).
▪ futex_wait(address, expected)
▪Put the calling thread to sleep
▪ If the value at address is not equal to expected, the call
returns immediately.
▪ futex_wake(address)
▪ Wake one thread that is waiting on the queue.

36
Pusan National University

Futex (Cont.)
■ Snippet from lowlevellock.h in the nptl library
▪ The high bit of the integer v: track whether the lock is held or not
▪ All the other bits : the number of waiters

1 void mutex_lock(int *mutex) {


2 int v;
3 /* Bit 31 was clear, we got the mutex (this is the fastpath) */
4 if (atomic_bit_test_set(mutex, 31) == 0)
5 return;
6 atomic_increment(mutex);
7 while (1) {
8 if (atomic_bit_test_set(mutex, 31) == 0) {
9 atomic_decrement(mutex);
10 return;
11 }
12 /* We have to wait now. First make sure the futex value
13 we are monitoring is truly negative (i.e. locked). */
14 v = *mutex;
15 …
Linux-based Futex Locks

37
Pusan National University

Futex (Cont.)
16 if (v >= 0)
17 continue;
18 futex_wait(mutex, v);
19 }
20 }
21
22 void mutex_unlock(int *mutex) {
23 /* Adding 0x80000000 to the counter results in 0 if and only if
24 there are not other interested threads */
25 if (atomic_add_zero(mutex, 0x80000000))
26 return;
27 /* There are other threads waiting for this mutex,
28 wake one of them up */
29 futex_wake(mutex);
30 }

Linux-based Futex Locks (Cont.)

38
Pusan National University

Two-Phase Locks
■ A two-phase lock realizes that spinning can be useful if the
lock is about to be released.
▪ First phase
▪The lock spins for a while, hoping that it can acquire the lock.
▪ If the lock is not acquired during the first spin phase, a second
phase is entered,
▪ Second phase
▪ The caller is put to sleep.
▪ The caller is only woken up when the lock becomes free later.

39
Pusan National University

Next Class..
A Parent Waiting For Its Child
1 void *child(void *arg) {
2 printf("child\n");
3 // XXX how to indicate we are done?
4 return NULL;
5 }
6
7 int main(int argc, char *argv[]) {
8 printf("parent: begin\n");
9 pthread_t c;
10 Pthread_create(&c, NULL, child, NULL); // create child
11 // XXX how to wait for child?
12 printf("parent: end\n");
13 return 0;
14 }

■ There are many cases where a thread wishes to check whether


a condition is true before continuing its execution.
■ Please read the Chap.30 until next class.

40

You might also like