15-213
The course that gives CMU its Zip!
Concurrency II: Synchronization Nov 14, 2000
Topics
Progress graphs Semaphores Mutex and condition variables Barrier synchronization Timeout waiting
[Link]
A version of badcnt.c with a simple counter loop
int ctr = 0; /* shared */ /* main routine creates*/ /* two count threads */ /* count thread */ void *count(void *arg) { int i; for (i=0; i<NITERS; i++) ctr++; return NULL; }
note: counter should be equal to 200,000,000
linux> badcnt BOOM! ctr=198841183 linux> badcnt BOOM! ctr=198261801 linux> badcnt BOOM! ctr=198269672
What went wrong?
[Link] 2 CS 213 F00
Assembly code for counter loop
C code for counter loop
for (i=0; i<NITERS; i++) ctr++; .L9:
Corresponding asm code (gcc -O0 -fforce-mem)
movl -4(%ebp),%eax cmpl $99999999,%eax jle .L12 jmp .L10 .L12: movl ctr,%eax # Load leal 1(%eax),%edx # Update movl %edx,ctr # Store .L11: movl -4(%ebp),%eax leal 1(%eax),%edx movl %edx,-4(%ebp) jmp .L9 .L10:
Head (Hi)
Load ctr (Li) Update ctr (Ui) Store ctr (Si) Tail (Ti)
[Link]
CS 213 F00
Concurrent execution
Key thread idea: In general, any sequentially consistent interleaving is possible, but some are incorrect!
Ii denotes that thread i executes instruction I %eaxi is the contents of %eax in thread is context
i (thread) 1 1 1 1 2 2 2 2 2 1 instri H1 L1 U1 S1 H2 L2 U2 S2 T2 T1 %eax1 0 1 1 1 %eax2 1 2 2 2 ctr 0 0 0 1 1 1 1 2 2 2
OK
CS 213 F00
[Link]
Concurrent execution (cont)
Incorrect ordering: two threads increment the counter, but the result is 1 instead of 2.
i (thread) 1 1 1 2 2 1 1 2 2 2
instri H1 L1 U1 H2 L2 S1 T1 U2 S2 T2
%eax1 0 1 1 1 -
%eax2 0 1 1 1
ctr 0 0 0 0 0 1 1 1 1 1
Oops!
[Link]
CS 213 F00
Concurrent execution (cont)
How about this ordering?
i (thread) 1 1 2 2 2 2 1 1 1 2 instri H1 L1 H2 L2 U2 S2 U1 S1 T1 T2 %eax1 %eax2 ctr
We can clarify our understanding of concurrent execution with the help of the progress graph
[Link] 6 CS 213 F00
Progress graphs
Thread 2 T2 S2 (L1, S2)
A progress graph depicts the discrete execution state space of concurrent threads. Each axis corresponds to the sequential order of instructions in a thread. Each point corresponds to a possible execution state (Inst1, Inst2). E.g., (L1, S2) denotes state where thread 1 has completed L1 and thread 2 has completed S2.
U2 L2 H2
(O,O)
H1
L1
U1
S1
T1
Thread 1
[Link]
CS 213 F00
Legal state transitions
Interleaved concurrent execution (one processor):
or
Parallel concurrent execution (multiple processors)
or or (parallel execution)
Key point: Always reason about concurrent threads as if each thread had its own CPU.
[Link] 8 CS 213 F00
Trajectorie s
Thread 2 T2 S2
A trajectory is a sequence of legal state transitions that describes one possible concurrent execution of the threads. Example:
U2 L2 H2
H1, L2, U1, H2, L2, S1, T1, U2, S2, T2
(O,O)
H1
L1
U1
S1
T1
Thread 1
[Link]
CS 213 F00
Critical sections and unsafe regions
Thread 2 T2
Unsafe region critical section wrt shared variable ctr
S2
L, U, and S form a critical section with respect to the shared variable ctr. Instructions in critical sections (wrt to some shared variable) should not be interleaved. Sets of states where such interleaving occurs form unsafe regions.
U2 L2 H2
(O,O)
H1
L1
U1
S1
T1
Thread 1
critical section wrt shared variable ctr
[Link] 10 CS 213 F00
Thread 2 T2
Safe trajectories
Unsafe region Def: A safe trajectory is a sequence of legal transitions that does not touch any states in an unsafe region. Claim: Any safe trajectory results in a correct value for the shared variable ctr.
S2
critical section
U2 L2 H2
(O,O)
H1
L1
U1
S1
T1
Thread 1
critical section
[Link] 11 CS 213 F00
Unsafe trajectories
Thread 2 T2
Touching a state of type x is always incorrect. Touching a state of type y may or may not be OK: correct because store completes before load. incorrect because order of load and store are indeterminate. Moral: be conservative and disallow all unsafe trajectories.
Unsafe region
S2 y x x y x x y y
critical section
U2 L2 H2
(O,O)
H1
L1
U1
S1
T1 Thread 1
critical section
[Link] 12 CS 213 F00
Semaphore operations
Question: How can we guarantee a safe trajectory?
We must synchronize the threads so that they never enter an unsafe state.
Classic solution: Dijkstra's P and V operations on semaphores.
semaphore: non-negative integer synchronization variable. P(s): [ while (s == 0) wait(); s--; ] Dutch for "Proberen" (test) V(s): [ s++; ] Dutch for "Verhogen" (increment) OS guarantees that operations between brackets [ ] are executed indivisibly. Only one P or V operation at a time can modify s. When while loop in P terminates, only that P can decrement s. Semaphore invariant: (s >= 0)
[Link]
13
CS 213 F00
Sharing with semaphores
Thread 2 T2
s=1
V(s)
s=0
s=1
ctr++; P(s) H2
s=0
s = -1 (forbidden s = 0 region)
Provide mutually exclusive access to shared variable by surrounding critical section with P and V operations on semaphore s (initially set to 1). Semaphore invariant creates a forbidden region that encloses unsafe region and is never touched by any trajectory. Semaphore used in this way is often called a mutex (mutual exclusion).
s=1
s=0
s=1
(O,O)
H1
P(s) ctr++; V(s)
Initially, s = 1
[Link]
T1 Thread 1
14
CS 213 F00
Posix semaphores /* initialize semaphore sem to value */
/* pshared=0 if thread, pshared=1 if process */ void Sem_init(sem_t *sem, int pshared, unsigned int value) { if (sem_init(sem, pshared, value) < 0) unix_error("Sem_init"); } /* P operation on semaphore sem */ void P(sem_t *sem) { if (sem_wait(sem)) unix_error("P"); } /* V operation on semaphore sem */ void V(sem_t *sem) { if (sem_post(sem)) unix_error("V"); }
[Link]
15
CS 213 F00
Sharing with Posix semaphores
/* goodcnt.c - properly synch'd */ /* version of badcnt.c */ #include <ics.h> #define NITERS 10000000 void *count(void *arg); struct { int ctr; /* shared ctr */ sem_t mutex; /* semaphore */ } shared; int main() { pthread_t tid1, tid2; /* init mutex semaphore to 1 */ Sem_init(&[Link], 0, 1); /* create 2 ctr threads and wait */ ... }
[Link] 16 CS 213 F00
/* counter thread */ void *count(void *arg) { int i; for (i=0; i<NITERS; i++) { P(&[Link]); [Link]++; V(&[Link]); } return NULL; }
Progress graph for goodcnt.c
Thread 2 V(m) f.r. P(m) V(m) f.r. P(m) V(m) f.r. P(m)
P(m)
V(m)
P(m)
V(m)
P(m)
V(m)
Thread 1
Initially, mutex = 1
[Link] 17 CS 213 F00
Deadloc k
Thread 2 V(s) forbidden region for s V(t)
deadlock state
Semaphores introduce the potential for deadlock: waiting for a condition that will never be true. Any trajectory that enters the deadlock region will eventually reach the deadlock state, waiting for either s or t to become nonzero. Other trajectories luck out and skirt the deadlock region.
P(s) deadlock region P(t) forbidden region for t
P(s)
P(t)
V(s)
V(t) Thread 1
Initially, s=t=1
[Link] 18
Unfortunate fact: deadlock is often non-deterministic.
CS 213 F00
A deterministic deadlock
Thread 2 V(t) f.r. for t P(t) f.r. for t f.r. for s P(s) deadlock region deadlock state
... Sometimes though, we get "lucky" and the deadlock is deterministic. Here is an example of a deterministic deadlock caused by improperly initializing semaphore t. Problem: correct this program and draw the resulting forbidden regions.
P(t) V(t) Thread 1
...
V(s)
P(s)
V(s)
Initially, s = 1, t = 0.
[Link] 19 CS 213 F00
Signaling with semaphores
producer thread shared buffer consumer thread
Common synchronization pattern:
Producer waits for slot, inserts item in buffer, and signals consumer. Consumer waits for item, removes it from buffer, and signals producer.
Examples
Multimedia processing: producer creates MPEG video frames, consumer renders the frames Graphical user interfaces producer detects mouse clicks, mouse movements, and keyboard hits and inserts corresponding events in buffer. consumer retrieves events from buffer and paints the display.
[Link]
20
CS 213 F00
Producer-consumer (1buffer)
/* buf1.c - producer-consumer on 1-element buffer */ #include <ics.h> #define NITERS 5 void *producer(void *arg); void *consumer(void *arg); struct { int buf; /* shared var */ sem_t full; /* sems */ sem_t empty; } shared; int main() { pthread_t tid_producer; pthread_t tid_consumer; /* initialize the semaphores */ Sem_init(&[Link], 0, 1); Sem_init(&[Link], 0, 0); /* create threads and wait */ Pthread_create(&tid_producer, NULL, producer, NULL); Pthread_create(&tid_consumer, NULL, consumer, NULL); Pthread_join(tid_producer, NULL); Pthread_join(tid_consumer, NULL); exit(0); }
[Link]
21
CS 213 F00
Producer-consumer (cont) Initially: empty = 1, full = 0.
/* producer thread */ void *producer(void *arg) { int i, item; for (i=0; i<NITERS; i++) { /* produce item */ item = i; printf("produced %d\n", item); /* write item to buf */ P(&[Link]); [Link] = item; V(&[Link]); } return NULL; }
[Link] 22 CS 213 F00
/* consumer thread */ void *consumer(void *arg) { int i, item; for (i=0; i<NITERS; i++) { /* read item from buf */ P(&[Link]); item = [Link]; V(&[Link]); /* consume item */ printf("consumed %d\n", item); } return NULL; }
Producer-consumer progress graph Consumer
V(e) P(f) V(e) P(f) V(e) P(f)
The forbidden regions prevent the producer from writing into a full buffer. They also prevent the consumer from reading an empty buffer. Problem: Write version for n-element buffer with multiple producers and consumers.
P(e) V(f) P(e) V(f) P(e) V(f) Producer
23 CS 213 F00
Initially, empty = 1, full = 0.
[Link]
Limitations of semaphores
Semaphores are sound and fundamental, but they have limitations.
Difficult to broadcast a signal to a group of threads. e.g., barrier synchronization: no thread returns from the barrier function until every other thread has called the barrier function. Impossible to do timeout waiting. e.g., wait for at most 1 second for a condition to become true.
For these we must use Pthreads mutex and condition variables.
[Link]
24
CS 213 F00
Basic operations on mutex variables int pthread_mutex_init(pthread_mutex_t *mutex,
pthread_mutexattr_t *attr)
Initializes a mutex variable (mutex) with some attributes (attr). attributes are usually NULL. like initializing a mutex semaphore to 1.
int pthread_mutex_lock(pthread_mutex_t *mutex)
Indivisibly waits for mutex to be unlocked and then locks it. like P(mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
Unlocks mutex. like V(mutex)
[Link] 25 CS 213 F00
Basic operations on condition variables
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr)
Initializes a condition variable (cond) with some attributes (attr). attributes are usually NULL.
int pthread_cond_signal(pthread_cond_t *cond)
Awakens one thread waiting on condition cond. if no threads waiting on condition, then it does nothing. key point: signals are not queued!
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
Indivisibly unlocks mutex and waits for signal on condition cond When awakened, indivisibly locks mutex.
[Link]
26
CS 213 F00
Advanced operations on condition variables
int pthread_cond_broadcast(pthread_cond_t *cond)
Awakens all threads waiting on condition cond. if no threads waiting on condition, then it does nothing.
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, struct timespec *abstime)
Waits for condition cond until absolute wall clock time is abstime Unlocks mutex on entry, locks mutex on awakening. Use of absolute time rather than relative time is strange.
[Link]
27
CS 213 F00
Signaling and waiting on conditions
Basic pattern for signaling
Pthread_mutex_lock(&mutex); Pthread_cond_signal(&cond); Pthread_mutex_unlock(&mutex);
A mutex is always associated with a condition variable. Guarantees that the condition cannot be signaled (and thus ignored) in the interval when the waiter locks the mutex and waits on the condition.
Basic pattern for waiting
Pthread_mutex_lock(&mutex); Pthread_cond_wait(&cond, &mutex); Pthread_mutex_unlock(&mutex);
[Link]
28
CS 213 F00
#include <ics.h> static static static static pthread_mutex_t mutex; pthread_cond_t cond; int nthreads; int barriercnt = 0;
Barrier synchronization
Call to barrier will not return until every other thread has also called barrier. Needed for tightlycoupled parallel applications that proceed in phases. E.g., physical simulations.
void barrier_init(int n) { nthreads = n; Pthread_mutex_init(&mutex, NULL); Pthread_cond_init(&cond, NULL); } void barrier() { Pthread_mutex_lock(&mutex); if (++barriercnt == nthreads) { barriercnt = 0; Pthread_cond_broadcast(&cond); } else Pthread_cond_wait(&cond, &mutex); Pthread_mutex_unlock(&mutex); }
[Link]
29
CS 213 F00
timebomb.c: timeout waiting example
A program that explodes unless the user hits a key within 5 seconds.
#include <ics.h> #define TIMEOUT 5 /* function prototypes */ void *thread(void *vargp); struct timespec *maketimeout(int secs); /* condition variable and its associated mutex */ pthread_cond_t cond; pthread_mutex_t mutex; /* thread id */ pthread_t tid;
[Link]
30
CS 213 F00
timebomb.c (cont)
A routine for building a timeout structure for pthread_cond_timewait.
/* * maketimeout - builds a timeout object that times out * in secs seconds */ struct timespec *maketimeout(int secs) { struct timeval now; struct timespec *tp = (struct timespec *)malloc(sizeof(struct timespec)); gettimeofday(&now, NULL); tp->tv_sec = now.tv_sec + secs; tp->tv_nsec = now.tv_usec * 1000; return tp; }
[Link]
31
CS 213 F00
Main routine for timebomb.c
int main() { int i, rc; /* initialize the mutex and condition variable */ Pthread_cond_init(&cond, NULL); Pthread_mutex_init(&mutex, NULL); /* start getchar thread and wait for it to timeout */ Pthread_mutex_lock(&mutex); Pthread_create(&tid, NULL, thread, NULL); for (i=0; i<TIMEOUT; i++) { printf("BEEP\n"); rc = pthread_cond_timedwait(&cond, &mutex, maketimeout(1)); if (rc != ETIMEDOUT) { printf("WHEW!\n"); exit(0); } } printf("BOOM!\n"); exit(0); }
[Link] 32 CS 213 F00
Thread routine for timebomb.c
/* * thread - executes getchar in a separate thread */ void *thread(void *vargp) { (void) getchar(); Pthread_mutex_lock(&mutex); Pthread_cond_signal(&cond); Pthread_mutex_unlock(&mutex); return NULL; }
[Link]
33
CS 213 F00
Threads summary Threads provide another mechanism for writing
concurrent programs. Threads are growing in popularity
Somewhat cheaper than processes. Easy to share data between threads.
However, the ease of sharing has a cost:
Easy to introduce subtle synchronization errors.
For more info:
man pages (man -k pthreads) D. Butenhof, Programming with Posix Threads, Addison-Wesley, 1997.
[Link] 34 CS 213 F00