Download Concurrency and Synchronization - Operating Systems | CMSC 412 and more Study notes Operating Systems in PDF only on Docsity!
CMSC 412
Spring 2007
Concurrency and Synchronization
Announcements
• Reading
• Project
Systems = Objects + Activities
- Safety is a property of objects, and groups of objects, that participate across multiple activities. - Can be a concern at many different levels: objects, composites, components, subsystems, hosts, …
- Liveness is a property of activities, and groups of activities, that span across multiple objects. - Levels: Messages, call chains, threads, sessions, scenarios, scripts workflows, use cases, transactions, data flows, mobile computations, …
Violating Safety
• Data can be shared by threads
- Scheduler can interleave or overlap
threads arbitrarily
- Can lead to interference
- Storage corruption (e.g. a data race / race condition)
- Violation of representation invariant
- Violation of a protocol (e.g. A occurs before B )
How does this apply to OSs?
• Any resource that is shared could be
accessed inappropriately
- Shared memory
- Kernel threads
- Processes (shared memory set up by kernel)
- Shared resources
- Printer, Video screen, Network card, …
• OS must protect shared resources
- And provide processes a means to protect
their own abstractions
Data Race Example
static int cnt = 0; t1.run() { int y = cnt; cnt = y + 1; } t2.run() { int y = cnt; cnt = y + 1; } cnt = 0 Start: both threads ready to run. Each will increment the global count. Shared state
Data Race Example
static int cnt = 0; t1.run() { int y = cnt; cnt = y + 1; } t2.run() { int y = cnt; cnt = y + 1; } cnt = 0 T1 executes, grabbing the global counter value into y. Shared state y = 0
Data Race Example
static int cnt = 0; t1.run() { int y = cnt; cnt = y + 1; } t2.run() { int y = cnt; cnt = y + 1; } cnt = 0 T1 is pre-empted. T executes, grabbing the global counter value into y. Shared state y = 0 y = 0
Data Race Example
static int cnt = 0; t1.run() { int y = cnt; cnt = y + 1; } t2.run() { int y = cnt; cnt = y + 1; } cnt = 1 T2 executes, storing the incremented cnt value. Shared state y = 0 y = 0
Data Race Example
static int cnt = 0; t1.run() { int y = cnt; cnt = y + 1; } t2.run() { int y = cnt; cnt = y + 1; } cnt = 1 T2 completes. T executes again, storing the old counter value (1) rather than the new one (2)! Shared state y = 0 y = 0
But When I Run it Again? Data Race Example
static int cnt = 0; t1.run() { int y = cnt; cnt = y + 1; } t2.run() { int y = cnt; cnt = y + 1; } cnt = 0 Start: both threads ready to run. Each will increment the global count. Shared state
Synchronization
static int cnt = 0; struct Mutex lock; Mutex_Init(&lock); void run() { Mutex_Lock (&lock); int y = cnt; cnt = y + 1; Mutex_Unlock (&lock); } Lock , for protecting The shared state Acquires the lock; Only succeeds if not held by another thread Releases the lock
Java-style synchronized block
static int cnt = 0; struct Mutex lock; Mutex_Init(&lock); void run() { synchronized (lock) { int y = cnt; cnt = y + 1; } } Lock , for protecting The shared state Acquires the lock; Only succeeds if not held by another thread Releases the lock
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 0 T1 acquires the lock
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 0 T1 reads cnt into y y = 0
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 0 T1 is pre-empted. T2 attempts to acquire the lock but fails because it’s held by T1, so it blocks y = 0
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 1 T1 runs, assigning to cnt y = 0
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 1 T1 releases the lock and terminates y = 0
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 1 T2 now can acquire the lock. y = 0
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 1 T2 reads cnt into y. y = 0 y = 1
Applying synchronization
int cnt = 0; t1.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } t2.run() { synchronized(lock) { int y = cnt; cnt = y + 1; } } Shared state cnt = 2 T2 assigns cnt, then releases the lock y = 0 y = 1
Mutexes (locks)
- Only one thread can “acquire” a mutex
- Other threads block until they can acquire it
- Used for implementing critical sections
- A critical section is a piece of code that
should not be interleaved with code from
another thread
- Executed atomically
- We’ll look at other ways to implement critical
sections later …
Mutex Policies
- What if a thread already holds the mutex it’s
trying to acquire?
- Re-entrant mutexes: The thread can reacquire the same lock many times. Lock is released when object unlocked the corresponding number of times - This is the case for Java
- Non-reentrant: Deadlock! (defined soon.)
- This is the case in GeekOS
- What happens if a thread is killed while
holding a mutex? Or if it just forgets to
release it
Deadlock: Wait graphs
A T1 Thread T1 holds lock A T2 B Thread T2 attempting to acquire lock B Deadlock occurs when there is a cycle in the graph
Wait graph example
A T
T2 B
T1 holds lock on A T2 holds lock on B T1 is trying to acquire a lock on B T2 is trying to acquire a lock on A
Key Ideas
• Multiple threads can run simultaneously
- Either truly in parallel on a multiprocessor
- Or can be scheduled on a single processor
- A running thread can be pre-empted at any time
• Threads can share data
- Need to prevent interference
- Synchronization is one way, but not the only way
- Overuse use of synchronization can create
deadlock
Implementing Synchronization
• Next we’re going to revisit the issues
of synchronization for the
producer/consumer problem.
• Another way of looking at what we’ve
just gone over.
• Will prepare us to see possible
solutions next time.
Bounded-Buffer
• Shared data
#define BUFFER_SIZE 10 typedef struct {
... } item ; item buffer[BUFFER_SIZE]; int in = 0; int out = 0; int counter = 0;
Bounded-Buffer
- Producer process item nextProduced; while (1) { while (counter == BUFFER_SIZE) ; / do nothing / buffer[in] = nextProduced; in = (in + 1) % BUFFER_SIZE; counter++; }
Bounded-Buffer
- Consumer process item nextConsumed; while (1) { while (counter == 0) ; / do nothing / nextConsumed = buffer[out]; out = (out + 1) % BUFFER_SIZE; counter--; }
Bounded Buffer
counter++;
counter--;
must be performed atomically.
- Atomic operation means an operation that
completes in its entirety without
interruption.
Bounded Buffer
- The statement “ count++ ” may be implemented in machine language as: register1 = counter register1 = register1 + 1 counter = register
- The statement “ count-- ” may be implemented as: register2 = counter register2 = register2 – 1 counter = register
Bounded Buffer
• If both the producer and consumer
attempt to update the buffer
concurrently, the assembly language
statements may get interleaved.
• Interleaving depends upon how the
producer and consumer processes are
scheduled.
Bounded Buffer
- Consider the three address code for the counter Counter Increment Counter Decrement reg 1 = counter reg 2 = counter reg 1 = reg 1 + 1 reg 2 = reg 2 - 1 counter = reg 1 counter = reg 2
- Now consider an ordering of these instructions T 0 producer reg 1 = counter { reg 1 = 5 } T 1 producer reg 1 = reg 1 + 1 { reg 1 = 6 } T 2 consumer reg 2 = counter { reg 2 = 5 } T 3 consumer reg 2 = reg 2 - 1 { reg 2 = 4 } T 4 producer counter = reg 1 { counter = 6 } T 5 consumer counter = reg 2 { counter = 4 } (^) shouldThis be 5!
Race Condition
- Race condition : The situation where several
processes access – and manipulate shared
data concurrently. The final value of the
shared data depends upon which process
finishes last.
- To prevent race conditions, concurrent
processes must be synchronized.