chapter 6 synchronization - seattle university

59
Synchronization Dr. Yingwu Zhu

Upload: others

Post on 30-Jan-2022

4 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Chapter 6 Synchronization - Seattle University

Synchronization

Dr. Yingwu Zhu

Page 2: Chapter 6 Synchronization - Seattle University

Synchronization

• Threads cooperate in multithreaded programs– To share resources, access shared data structures

• Threads accessing a memory cache in a Web server

– To coordinate their execution

• One thread executes relative to another (recall ping-pong)

• For correctness, we need to control this cooperation– Threads interleave executions arbitrarily and at different rates

– Scheduling is not under program control

• We control cooperation using synchronization– Synchronization enables us to restrict the possible interleavings of

thread executions

• Discuss in terms of threads, also applies to processes

Page 3: Chapter 6 Synchronization - Seattle University

The Problem with Concurrent Execution

• Concurrent threads (& processes) often access shared data and resources

– Need controlled access to the shared data; otherwise result in an inconsistent view of this data

• Maintaining data consistency must ensure orderly execution of cooperating processes

• We will look at

– Mechanisms to control access to shared resources• Locks, mutexes, semaphores, monitors, condition variables, …

– Patterns for coordinating accesses to shared resources• Bounded buffer, producer-consumer, etc.

Page 4: Chapter 6 Synchronization - Seattle University

Classic Example

• Suppose we have to implement a function to handle withdrawals from a bank account:

• Now suppose that you and your significant other share a bank account with a balance of $1000.

• Then you each go to separate ATM machines and simultaneously withdraw $100 from the account.

Page 5: Chapter 6 Synchronization - Seattle University

Example Continued…

• We’ll represent the situation by creating a separate thread for each person to do the withdrawals

– These threads run on the same bank machine:

• What’s the problem with this implementation?

– Think about potential schedules of these two threads

Page 6: Chapter 6 Synchronization - Seattle University

Interleaved Schedules

• The problem is that the execution of the two threads can be interleaved:

• What is the balance of the account now?

• Is the bank happy with our implementation?

Page 7: Chapter 6 Synchronization - Seattle University

Shared Resources

• The problem is that two concurrent threads (or processes) accessed a shared resource (account) without any synchronization– Known as a race condition (memorize this buzzword)

• We need mechanisms to control access to these shared resources in the face of concurrency– So we can reason about how the program will operate

• Our example was updating a shared bank account

• Also necessary for synchronizing access to any shared data structure– Buffers, queues, lists, hash tables, etc.

Page 8: Chapter 6 Synchronization - Seattle University

When Are Resources Shared?

• Local variables are not shared (private)

– 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 and static objects are shared

– Stored in the static data segment, accessible by any thread

• Dynamic objects and other heap objects are shared

– Allocated from heap with malloc/free or new/delete

Page 9: Chapter 6 Synchronization - Seattle University

Race Condition

• Multiple processes manipulate same data concurrently

• The outcome of execution depends on the particular order in which the data access takes place

Page 10: Chapter 6 Synchronization - Seattle University

Race Condition: Example

• count++ could be implemented as

register1 = countregister1 = register1 + 1count = register1

• count-- could be implemented as

register2 = countregister2 = register2 - 1count = register2

• Consider this execution interleaving with “count = 5” initially:S0: producer execute register1 = count {register1 = 5}S1: producer execute register1 = register1 + 1 {register1 = 6} S2: consumer execute register2 = count {register2 = 5} S3: consumer execute register2 = register2 - 1 {register2 = 4} S4: producer execute count = register1 {count = 6 } S5: consumer execute count = register2 {count = 4}

Page 11: Chapter 6 Synchronization - Seattle University

Mutual Exclusion & Critical-Section

• We want to use mutual exclusion to synchronize access to shared resources

• Code that uses mutual exclusion to synchronize its execution is called a critical section

– 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

Page 12: Chapter 6 Synchronization - Seattle University

Critical-Section Problem

Each process looks like:do {

CRITICAL SECTION

REMAINDER SECTION

} while (TRUE);

Entry Section

Exit Section

Page 13: Chapter 6 Synchronization - Seattle University

Solution to Critical-Section Problem

MUST satisfy the following three/four requirements:

1. Mutual Exclusion -- If one thread is in the critical section, then no other is

2. Progress - If some thread T is not in the critical section, then T cannot prevent some other thread S from entering the critical section

3. Bounded Waiting - If some thread T is waiting on the critical section, then T will eventually enter the critical section

4. Performance -- The overhead of entering and exiting the critical section is small with respect to the work being done within it

Page 14: Chapter 6 Synchronization - Seattle University

Locks

• While one thread executes “withdraw”, we want some way to prevent other threads from executing in it

• Locks are one way to do this

• A lock is an object in memory providing two operations– acquire(): before entering the critical section

– release(): after leaving a critical section

• Threads pair calls to acquire() and release()– Between acquire()/release(), the thread holds the lock

– acquire() does not return until any previous holder releases

– What can happen if the calls are not paired?

• Locks can spin (a spinlock) or block (a mutex)

Page 15: Chapter 6 Synchronization - Seattle University

Using Locks

• What happens when blue tries to acquire the lock?

• Why is the “return” outside the critical section? Is this ok?

Page 16: Chapter 6 Synchronization - Seattle University

Implementing Locks

• How do we implement locks? Here is one attempt:

• This is called a spinlock because a thread spins waiting for the lock to be released

• Does this work?

Page 17: Chapter 6 Synchronization - Seattle University

Implementing Locks

• No. Two independent threads may both notice that a lock has been released and thereby acquire it.

Page 18: Chapter 6 Synchronization - Seattle University

Implementing Locks

• The problem is that the implementation of locks has critical sections, too

• How do we stop the recursion?

• The implementation of acquire/release must be atomic– An atomic operation is one which executes as though it could not be

interrupted

– Code that executes “all or nothing”

• How do we make them atomic?

• Need help from hardware– Atomic instructions (e.g., test-and-set)

– Disable/enable interrupts (prevents context switches)

Page 19: Chapter 6 Synchronization - Seattle University

Hardware Solution – Test and Set

• Test and modify the content of a word atomically

boolean TestAndSet (boolean*flag)

{

boolean old = *flag;

*flag = true;

return old;

}

Implement locks using test and set

Page 20: Chapter 6 Synchronization - Seattle University

Problems with Spinlocks

• The problem with spinlocks is that they are wasteful

– If a thread is spinning on a lock, then the thread holding the lock cannot make progress

• How did the lock holder give up the CPU in the first place?

– Lock holder calls yield or sleep

– Involuntary context switch

• Only want to use spinlocks as primitives to build higher-level synchronization constructs

Page 21: Chapter 6 Synchronization - Seattle University

Hardware Solution – Disable Interrupts

• Correct solution for uni-processor machine– Atomic instructions

• During critical section, multiprogramming is not utilized perf. penalty

• Too inefficient for multi-processor machines, interrupt

disabling message passing to

all processors, delay entrance

to critical section

Page 22: Chapter 6 Synchronization - Seattle University

On Disabling Interrupts

• Disabling interrupts blocks notification of external events that could trigger a context switch (e.g., timer)– This is what Nachos uses as its primitive

• In a “real” system, this is only available to the kernel– Why?

– What could user-level programs use instead?

• Disabling interrupts is insufficient on a multiprocessor– Back to atomic instructions

• Like spinlocks, only want to disable interrupts to implement higher-level synchronization primitives– Don’t want interrupts disabled between acquire and release

Page 23: Chapter 6 Synchronization - Seattle University

Summarize Where We Are

• Goal: Use mutual exclusion to protect critical sections of code that access shared resources

• Method: Use locks (spinlocks or disable interrupts)

• Problem: Critical sections can be long

Page 24: Chapter 6 Synchronization - Seattle University

PART 2: High-Level Synchronization

Page 25: Chapter 6 Synchronization - Seattle University

High-Level Synchronization

• Spinlocks and disabling interrupts are useful only for very short and simple critical sections– Wasteful otherwise

– These primitives are “primitive” – don’t do anything besides mutual exclusion

Page 26: Chapter 6 Synchronization - Seattle University

High-Level Synchronization

• We looked at using locks to provide mutual exclusion

• Locks work, but they have some drawbacks when critical sections are long– Spinlocks – inefficient

– Disabling interrupts – can miss or delay important events

• Instead, we want synchronization mechanisms that– Block waiters

– Leave interrupts enabled inside the critical section

• Look at two common high-level mechanisms– Semaphores: binary (mutex) and counting

– Condition variables w/ locks

• Use them to solve common synchronization problems

Page 27: Chapter 6 Synchronization - Seattle University

Semaphores• Semaphores are another data structure that provides

mutual exclusion to critical sections– Block waiters, interrupts enabled within CS

– Described by Dijkstra in THE system in 1968

• Semaphores can also be used as atomic counters– More later

• Semaphores support two operations:– wait(semaphore): decrement, block until semaphore is open

• Also P(), after the Dutch word for test, or down()

– signal(semaphore): increment, allow another thread to enter• Also V() after the Dutch word for increment, or up()

Page 28: Chapter 6 Synchronization - Seattle University

Blocking in Semaphores

• Associated with each semaphore is a queue of waiting processes

• When wait() is called by a thread:– If semaphore is open, thread continues

– If semaphore is closed, thread blocks on queue

• Then signal() opens the semaphore:– If a thread is waiting on the queue, the thread is unblocked

– If no threads are waiting on the queue, the signal is remembered for the next thread

• In other words, signal() has “history” (c.f. condition vars later)

• This “history” is a counter

Page 29: Chapter 6 Synchronization - Seattle University

Semaphore Types

• Counting semaphore – integer value can range over an unrestricted domain– Used to control access to a given resource consisting of a finite

number of instances

– The semaphore is initialized to the number of resources available

– Multiple threads can pass the semaphore

• Binary semaphore – integer value can range only between 0 and 1; – Also known as mutex locks

– Represents single access to a resource

– Guarantees mutual exclusion to a critical section

Page 30: Chapter 6 Synchronization - Seattle University

Semaphore Implementation

• Implementation of wait:

wait (S){ value--;if (value < 0) {

add this thread T to waiting queueblock(P);

}}

• Implementation of signal:

signal (S){ value++;if (value <= 0) {

remove a thread T from the waiting queuewakeup(P);

}}

Struct Semaphore {

int value;

Queue q;

} S;

Page 31: Chapter 6 Synchronization - Seattle University

Using Semaphores

• Use is similar to our locks, but semantics are different

Page 32: Chapter 6 Synchronization - Seattle University

Producer-Consumer: Bounded Buffer

• Problem: There is a set of resource buffers shared by producer and consumer threads

• Producer inserts resources into the buffer set– Output, disk blocks, memory pages, processes, etc.

• Consumer removes resources from the buffer set– Whatever is generated by the producer

• Producer and consumer execute at different rates

• Cyclic buffer:

0 1 2 3 4

out in

Page 33: Chapter 6 Synchronization - Seattle University

Using Semaphores

• Use three semaphores:

• mutex – mutual exclusion to shared set of buffers

– Binary semaphore

• empty – count of empty buffers

– Counting semaphore

• full – count of full buffers

– Counting semaphore

Page 34: Chapter 6 Synchronization - Seattle University

Producer-Consumer: bounded buffer

void append(int d) {

buffer[in] = d;

in = (in + 1) % N;

}

int take() {

int x = out;

out = (ouit+1) %N;

return buffer[x];

}

Initialization: semaphores: mutex = 1, full = 0; empty = N;integers: int = 0, out = 0;

Producer:

While (1) {produce x; wait(empty);wait(mutex);append(x);signal(full);

signal(mutex);}

Consumer:

While (1) {wait(full); wait(mutex);x = take();signal(empty);

signal(mutex);consume x;

}

What happens if operations on mutex and full/empty are switched around?

Page 35: Chapter 6 Synchronization - Seattle University

Semaphore Summary

• Semaphores can be used to solve any of the traditional synchronization problems

• However, they have some drawbacks– They are essentially shared global variables

• Can potentially be accessed anywhere in program

– No connection between the semaphore and the data being controlled by the semaphore

– Used both for critical sections (mutual exclusion) and coordination (scheduling)

– No control or guarantee of proper usage

• Sometimes hard to use and prone to bugs– Another approach: Use programming language support

Page 36: Chapter 6 Synchronization - Seattle University

Condition Variables

• Condition variables provide a mechanism to wait for events (a “rendezvous point”)– Resource available, no more writers, etc.

• Condition variables support three operations:– Wait – release monitor lock, wait for C/V to be signaled

• So condition variables have wait queues, too

– Signal – wakeup one waiting thread

– Broadcast – wakeup all waiting threads

• Note: Condition variables are not boolean objects– “if (condition_variable) then” … does not make sense

– “if (num_resources == 0) then wait(resources_available)” does

– An example will make this more clear

Page 37: Chapter 6 Synchronization - Seattle University

Condition Variables != Semaphores

• Condition variables != semaphores– Although their operations have the same names, they have entirely

different semantics

– However, they each can be used to implement the other

• Usage: Combined with a lock– wait() blocks the calling thread, and gives up the lock

• To call wait, the thread has lock

• Semaphore::wait just blocks the thread on the queue

– signal() causes a waiting thread to wake up• If there is no waiting thread, the signal is lost

• Semaphore::signal increases the semaphore count, allowing future entry even if no thread is waiting

• Condition variables have no history

Page 38: Chapter 6 Synchronization - Seattle University

Signal Semantics

• There are two flavors of monitors that differ in the scheduling semantics of signal()

– Hoare monitors (original)• signal() immediately switches from the caller to a waiting thread

• The condition that the waiter was anticipating is guaranteed to hold when waiter executes

• Signaler must restore monitor invariants before signaling

– Mesa monitors (Mesa, Java)• signal() places a waiter on the ready queue, but signaler continues

inside monitor

• Condition is not necessarily true when waiter runs again– Returning from wait() is only a hint that something changed

– Must recheck conditional case

Page 39: Chapter 6 Synchronization - Seattle University

Hoare vs. Mesa

• Hoareif (empty)

wait(condition);

• Mesawhile (empty)

wait(condition);

• Tradeoffs– Mesa monitors easier to use, more efficient

• Fewer context switches, easy to support broadcast

– Hoare monitors leave less to chance• Easier to reason about the program

Page 40: Chapter 6 Synchronization - Seattle University

Condition Variables vs. Locks

• Condition variables are used in conjunction with blocking locks

Page 41: Chapter 6 Synchronization - Seattle University

Using Condition Variables & Locks

• Alternation of two threads (ping-pong)

• Each executes the following:

Page 42: Chapter 6 Synchronization - Seattle University

Summary

• Semaphores– wait()/signal() implement blocking mutual exclusion

– Also used as atomic counters (counting semaphores)

– Can be inconvenient to use

• Condition variables– Used by threads as a synchronization point to wait for events

– Used with locks

Page 43: Chapter 6 Synchronization - Seattle University

Part III: Case Study ---Pthread Synchronization

Page 44: Chapter 6 Synchronization - Seattle University

Mutual Exclusion

• Bad things can happen when two threads “simultaneously” access shared data structures:Race condition critical section problem

– Data inconsistency!

– These types of bugs are really nasty!• Program may not blow up, just produces wrong results

• Bugs are not repeatable

• Associate a separate lock (mutex) variable with the shared data structure to ensure “one at a time access”

Page 45: Chapter 6 Synchronization - Seattle University

Mutual Exclusion in PThreads

• pthread_mutex_t mutex_var;– Declares mutex_var as a lock (mutex) variable

– Holds one of two values: “locked” or “unlocked”

• pthread_mutex_lock (&mutex_var)– Waits/blocked until mutex_var in unlocked state

– Sets mutex_var into locked state

• pthread_mutex_unlock (&mutex_var)– Sets mutex_var into unlocked state

– If one or more threads are waiting on lock, will allow one thread to acquire lock

Example: pthread_mutex_t m; //pthread_mutex_init(&m, NULL);

pthread_mutex_lock (&m);

<access shared variables>

pthread_mutex_unlock(&m);

//pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

Page 46: Chapter 6 Synchronization - Seattle University

Pthread Semaphores

• #include <semaphore.h>

• Each semaphore has a counter value, which is a non-negative integer

Page 47: Chapter 6 Synchronization - Seattle University

Pthread Semaphores

• Two basic operations:– A wait operation decrements the value of the semaphore by 1. If the

value is already zero, the operation blocks until the value of the semaphore becomes positive (due to the action of some other thread).When the semaphore’s value becomes positive, it is decremented by 1 and the wait operation returns. sem_wait()

– A post operation increments the value of the semaphore by 1. If the semaphore was previously zero and other threads are blocked in a wait operation on that semaphore, one of those threads is unblocked and its wait operation completes (which brings the semaphore’s value back to zero). sem_post()

Slightly different from our discussion on semaphores

Page 48: Chapter 6 Synchronization - Seattle University

Pthread Semaphores

• sem_t s; //define a variable

• sem_init(); //must initialize

– 1st para: pointer to sem_t variable

– 2nd para: must be zero • A nonzero value would indicate a semaphore that can be shared

across processes, which is not supported by GNU/Linux for this type of semaphore.

– 3rd para: initial value

• sem_destroy(): destroy a semaphore if do not use it anymore

Page 49: Chapter 6 Synchronization - Seattle University

Pthread Semaphores

• int sem_wait(): wait operation

• int sem_post(): signal operation

• int sem_trywait():

– A nonblocking wait function

– if the wait would have blocked because the semaphore’s value was zero, the function returns immediately, with error value EAGAIN, instead of blocking.

Page 50: Chapter 6 Synchronization - Seattle University

Example

#include <malloc.h>

#include <pthread.h>

#include <semaphore.h>

struct job {

/* Link field for linked list. */

struct job* next;

/* Other fields describing work to be done...*/

};

/* A linked list of pending jobs. */

struct job* job_queue;

/* A mutex protecting job_queue. */

pthread_mutex_t job_queue_mutex =

PTHREAD_MUTEX_INITIALIZER;

/* A semaphore counting the number of jobs in the queue. */sem_t job_queue_count;/* Perform one-time initialization of the job queue. */void initialize_job_queue (){

/* The queue is initially empty. */job_queue = NULL;/* Initialize the semaphore which counts jobs in the queue. Its initial value should be zero. */sem_init (&job_queue_count, 0, 0);

}

Assume infinite queue capacity.

Page 51: Chapter 6 Synchronization - Seattle University

Example/* Process dequeued jobs until the queue is empty. */

void* thread_function (void* arg)

{

while (1) {

struct job* next_job;

/* Wait on the job queue semaphore. If its value is positive,indicating that the queue is not empty, decrement the count by 1. If the queue is empty, block until a new job is enqueued. */

sem_wait (&job_queue_count);

/* Lock the mutex on the job queue. */

pthread_mutex_lock (&job_queue_mutex);

/* Because of the semaphore, we know the queue is not empty. Get the next available job. */

next_job = job_queue;

/* Remove this job from the list. */

job_queue = job_queue->next;

/* Unlock the mutex on the job queue because we’re done with the queue for now. */

pthread_mutex_unlock (&job_queue_mutex);

/* Carry out the work. */

process_job (next_job);

/* Clean up. */

free (next_job);

}

return NULL;

}

Page 52: Chapter 6 Synchronization - Seattle University

Example/* Add a new job to the front of the job queue. */

void enqueue_job (/* Pass job-specific data here... */)

{

struct job* new_job;

/* Allocate a new job object. */

new_job = (struct job*) malloc (sizeof (struct job));

/* Set the other fields of the job struct here... */

/* Lock the mutex on the job queue before accessing it. */

pthread_mutex_lock (&job_queue_mutex);

/* Place the new job at the head of the queue. */

new_job->next = job_queue;

job_queue = new_job;

/* Post to the semaphore to indicate that another job is available. If

threads are blocked, waiting on the semaphore, one will become

unblocked so it can process the job. */

sem_post (&job_queue_count);

/* Unlock the job queue mutex. */

pthread_mutex_unlock (&job_queue_mutex);

}

Can they switch order?

Page 53: Chapter 6 Synchronization - Seattle University

Waiting for Events: Condition Variables

• Mutex variables are used to control access to shared data

• Condition variables are used to wait for specific events

– Buffer has data to consume

– New data arrived on I/O port

– 10,000 clock ticks have elapsed

Page 54: Chapter 6 Synchronization - Seattle University

Condition Variables

• Enable you to implement a condition under which a thread executes and, inversely, the condition under which the thread is blocked

Page 55: Chapter 6 Synchronization - Seattle University

Condition Variables in PThreads• pthread_cond_t c_var;

– Declares c_var as a condition variable

– Always associated with a mutex variable (say m_var)

• pthread_cond_wait (&c_var, &m_var)– Atomically unlock m_var and block on c_var

– Upon return, mutex m_var will be re-acquired

– Spurious wakeups may occur (i.e., may wake up for no good reason -always recheck the condition you are waiting on!)

• pthread_cond_signal (&c_var)– If no thread blocked on c_var, do nothing

– Else, unblock a thread blocked on c_var to allow one thread to be released from a pthread_cond_wait call

• pthread_cond_broadcast (&c_var)– Unblock all threads blocked on condition variable c_var

– Order that threads execute unspecified; each reacquires mutex when it resumes

Page 56: Chapter 6 Synchronization - Seattle University

Waiting on a Conditionpthread_mutex_t

m_var=PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t c_var=PTHREAD_COND_INITIALIZER;

//pthread_cond_init()

pthread_mutex_lock (m_var);

while (<some blocking condition is true>)

pthread_cond_wait (c_var, m_var);

<access shared data structrure>

pthread_mutex_unlock(m_var);

Note: Use “while” not “if”; Why?

Page 57: Chapter 6 Synchronization - Seattle University

Revisit on the example

Page 58: Chapter 6 Synchronization - Seattle University

Example continued…

Page 59: Chapter 6 Synchronization - Seattle University

Exercise

• Design a multithreaded program which handles bounded buffer problem using semaphores or conditional variables– int buffer[10]; //10 buffers

– rand() to produce an item

– int in, out;

– Implement producers and consumers process