You are on page 1of 23

CENG334

Introduction to Operating Systems

Threads and Synchronization


Topics:
•Using threads
•Implementation of threads
•Synchronization problem
•Race conditions and Critical Sections
•Mutual exclusion
•Locks
•Spinlocks
•Mutexes

Erol Sahin

Dept of Computer Eng.


Middle East Technical University
Ankara, TURKEY

URL: http://kovan.ceng.metu.edu.tr/ceng334
Week 2 13/03/07 1

Single and Multithreaded Processes

13/03/07

1
#include <pthread.h>
Using Pthreads
#include <stdio.h>
int sum; /* this data is shared by the thread(s) */
void *runner(void *param); /* the thread */

int main(int argc, char *argv[]){


pthread_t tid; /* the thread identifier */
pthread_attr_t attr; /* set of attributes for the thread */
pthread_attr_init(&attr);/* get the default attributes */
pthread_create(&tid,&attr,runner,argv[1]);/* create the thread */
pthread_join(tid,NULL); /* now wait for the thread to exit */
printf("sum = %d\n",sum);
}

void *runner(void *param){ /* The thread will begin control in this function
int i, upper = atoi(param);
sum = 0;
if (upper > 0) {
for (i = 1; i <= upper; i++)
sum += i;
}
pthread_exit(0);
}

Adapted from Matt Welsh’s (Harvard University) slides. 3

Synchronization
Threads cooperate in multithreaded programs in several ways:

Access to shared state


● e.g., multiple threads accessing a memory cache in a Web server

To coordinate their execution


● e.g., Pressing stop button on browser cancels download of current page
● “stop button thread” has to signal the “download thread”

For correctness, we have to control this cooperation


● Must assume threads interleave executions arbitrarily and at different rates
● scheduling is not under application’s control

● We control cooperation using synchronization


● enables us to restrict the interleaving of executions

Adapted from Matt Welsh’s (Harvard University) slides. 4

2
Shared Resources
We’ll focus on coordinating access to shared resources

Basic problem:
● Two concurrent threads are accessing a shared variable
● If the variable is read/modified/written by both threads, then access to the variable must
be controlled
● Otherwise, unexpected results may occur

We’ll look at:


● Mechanisms to control access to shared resources
● Low-level mechanisms: locks

● Higher level mechanisms: mutexes, semaphores, monitors, and condition variables

● Patterns for coordinating access to shared resources


● bounded buffer, producer-consumer, …

This stuff is complicated and rife with pitfalls


● Details are important for completing assignments
● Expect questions on the midterm/final!

Adapted from Matt Welsh’s (Harvard University) slides. 5

Shared Variable Example


Suppose we implement a function to withdraw money
from a bank account:

int withdraw(account, amount) {


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

Now suppose that you and your friend share a bank account with a
balance of $1500.00
● What happens if you both go to separate ATM machines, and simultaneously withdraw
$100.00 from the account?

Adapted from Matt Welsh’s (Harvard University) slides. 6

3
Example continued
We represent the situation by creating a separate thread for each ATM
user doing a withdrawal
● Both threads run on the same bank server system

Thread 1 Thread 2
int withdraw(account, amount) { int withdraw(account, amount) {
balance = get_balance(account); balance = get_balance(account);
balance -= amount; balance -= amount;
put_balance(account, balance); put_balance(account, balance);
return balance; return balance;
} }

What’s the problem with this?


● What are the possible balance values after each thread runs?

Adapted from Matt Welsh’s (Harvard University) slides. 7

Interleaved Execution
The execution of the two threads can be interleaved
● Assume preemptive scheduling
● Each thread can context switch after each instruction
● We need to worry about the worst-case scenario!

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

What’s the account balance after this sequence?


● And who's happier, the bank or you???

Adapted from Matt Welsh’s (Harvard University) slides. 8

4
Interleaved Execution
The execution of the two threads can be interleaved
● Assume preemptive scheduling
● Each thread can context switch after each instruction
● We need to worry about the worst-case scenario!

Balance = $1500
balance = get_balance(account);
balance -= amount; Local = $1400
Execution sequence
as seen by CPU balance = get_balance(account);
balance -= amount; Local = $1400
put_balance(account, balance); Balance = $1400

put_balance(account, balance); Balance = $1400!

What’s the account balance after this sequence?


● And who's happier, the bank or you???

Adapted from Matt Welsh’s (Harvard University) slides. 9

Race Conditions
The problem is that two concurrent threads access a shared resource
without any synchronization
● This is called a race condition
● The result of the concurrent access is non-deterministic
● Result depends on:
● Timing

● When context switches occurred

● Which thread ran at context switch

● What the threads were doing

We need mechanisms for controlling access to shared resources in the


face of concurrency
● This allows us to reason about the operation of programs
● Essentially, we want to re-introduce determinism into the thread's execution

Synchronization is necessary for any shared data structure


● buffers, queues, lists, hash tables, …

Adapted from Matt Welsh’s (Harvard University) slides. 10

5
Which resources are shared?
Local variables in a function are not shared
● They exist on the stack, and each thread has its own stack
● You can't safely pass a pointer from a local variable to another thread
● Why?

(Reserved for OS)


Unshared
Global variables are shared Stack for thread 0
● Stored in static data portion of the address space
Stack for thread 1
● Accessible by any thread
Stack for thread 2
Dynamically-allocated data is shared
● Stored in the heap, accessible by any thread Shared
Heap

Uninitialized vars
(BSS segment)
Initialized vars
(data segment)
Code
(text segment)

Adapted from Matt Welsh’s (Harvard University) slides. 11

Mutual Exclusion
We want to use mutual exclusion to synchronize access to shared
resources
● Meaning: When only one thread can access a shared resource at a time.

Code that uses mutual exclusion to synchronize its execution is


called a critical section
● Only one thread at a time can execute code in the critical section
● All other threads are forced to wait on entry
● When one thread leaves the critical section, another can enter

Critical Section

Thread 1
(modify account balance)

Adapted from Matt Welsh’s (Harvard University) slides. 12

6
Mutual Exclusion
We want to use mutual exclusion to synchronize access to shared
resources
● Meaning: When only one thread can access a shared resource at a time.

Code that uses mutual exclusion to synchronize its execution is


called a critical section
● Only one thread at a time can execute code in the critical section
● All other threads are forced to wait on entry
● When one thread leaves the critical section, another can enter

Critical Section

Thread 2 Thread 1
(modify account balance)
2nd thread must wait
for critical section to clear

Adapted from Matt Welsh’s (Harvard University) slides. 13

Mutual Exclusion
We want to use mutual exclusion to synchronize access to shared
resources
● Meaning: When only one thread can access a shared resource at a time.

Code that uses mutual exclusion to synchronize its execution is


called a critical section
● Only one thread at a time can execute code in the critical section
● All other threads are forced to wait on entry
● When one thread leaves the critical section, another can enter

Critical Section

Thread 2 Thread 1
(modify account balance)

2nd thread free to enter 1st thread leaves critical section

Adapted from Matt Welsh’s (Harvard University) slides. 14

7
Critical Section Requirements
Mutual exclusion
● At most one thread is currently executing in the critical section

Progress
● If thread T1 is outside the critical section, then T1 cannot prevent T2
from entering the critical section

Bounded waiting (no starvation)


● If thread T1 is waiting on the critical section, then T1 will eventually enter the critical
section
● Assumes threads eventually leave critical sections

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

Adapted from Matt Welsh’s (Harvard University) slides. 15

Locks
A lock is a object (in memory) that provides the following two
operations:
– acquire( ): a thread calls this before entering a critical section
● May require waiting to enter the critical section

– release( ): a thread calls this after leaving a critical section


● Allows another thread to enter the critical section

A call to acquire( ) must have a corresponding call to release( )


● Between acquire( ) and release( ), the thread holds the lock
● acquire( ) does not return until the caller holds the lock
● At most one thread can hold a lock at a time (usually!)

● We'll talk about the exceptions later...

What can happen if acquire( ) and release( ) calls are not paired?

Adapted from Matt Welsh’s (Harvard University) slides. 16

8
Using Locks

int withdraw(account, amount) {


acquire(lock);
balance = get_balance(account);
balance -= amount; critical
section
put_balance(account, balance);
release(lock);
return balance;
}

Adapted from Matt Welsh’s (Harvard University) slides. 17

Execution with Locks


acquire(lock); Thread 1 runs
balance = get_balance(account);
balance -= amount;

acquire(lock); Thread 2 waits on lock

put_balance(account, balance);
Thread 1 completes
release(lock);

balance = get_balance(account); Thread 2 resumes


balance -= amount;
put_balance(account, balance);
release(lock);

What happens when the blue thread tries to acquire the lock?

Adapted from Matt Welsh’s (Harvard University) slides. 18

9
Spinlocks
Very simple way to implement a lock:

struct lock {
int held = 0;
}
void acquire(lock) {
while (lock->held); The caller busy waits
lock->held = 1; for the lock to be released
}
void release(lock) {
lock->held = 0;
}

Why doesn't this work?


● Where is the race condition?

Adapted from Matt Welsh’s (Harvard University) slides. 19

Implementing Spinlocks
Problem is that the internals of the lock acquire/release have critical
sections too!
● The acquire( ) and release( ) actions must be atomic
● Atomic means that the code cannot be interrupted during execution
● “All or nothing” execution

struct lock {
int held = 0;
}
void acquire(lock) {
while (lock->held); What can happen if there
lock->held = 1; is a context switch here?
}
void release(lock) {
lock->held = 0;
}

Adapted from Matt Welsh’s (Harvard University) slides. 20

10
Implementing Spinlocks
Problem is that the internals of the lock acquire/release have critical
sections too!
● The acquire( ) and release( ) actions must be atomic
● Atomic means that the code cannot be interrupted during execution
● “All or nothing” execution

struct lock {
int held = 0;
}
void acquire(lock) {
while (lock->held); This sequence needs
lock->held = 1; to be atomic
}
void release(lock) {
lock->held = 0;
}

Adapted from Matt Welsh’s (Harvard University) slides. 21

Implementing Spinlocks
Problem is that the internals of the lock acquire/release have critical
sections too!
● The acquire( ) and release( ) actions must be atomic
● Atomic means that the code cannot be interrupted during execution
● “All or nothing” execution

Doing this requires help from hardware!


● Disabling interrupts
● Why does this prevent a context switch from occurring?

● Atomic instructions – CPU guarantees entire action will execute atomically


● Test-and-set

● Compare-and-swap

Adapted from Matt Welsh’s (Harvard University) slides. 22

11
Spinlocks using test-and-set
CPU provides the following as one atomic instruction:

bool test_and_set(bool *flag) {


… // Hardware dependent implementation
}

So to fix our broken spinlocks, we do this:


struct lock {
int held = 0;
}
void acquire(lock) {
while(test_and_set(&lock->held));
}
void release(lock) {
lock->held = 0;
}

Adapted from Matt Welsh’s (Harvard University) slides. 23

What's wrong with spinlocks?

OK, so spinlocks work (if you implement them correctly), and


they are simple. So what's the catch?

struct lock {
int held = 0;
}
void acquire(lock) {
while(test_and_set(&lock->held));
}
void release(lock) {
lock->held = 0;
}

Adapted from Matt Welsh’s (Harvard University) slides. 24

12
Problems with spinlocks
Horribly wasteful!
● Threads waiting to acquire locks spin on the CPU
● Eats up lots of cycles, slows down progress of other threads
● Note that other threads can still run ... how?

● What happens if you have a lot of threads trying to acquire the lock?

Only want spinlocks as primitives to build higher-level synchronization


constructs

Adapted from Matt Welsh’s (Harvard University) slides. 25

Disabling Interrupts
An alternative to spinlocks:
struct lock {
// Note – no state!
}
void acquire(lock) {
cli(); // disable interrupts
}
void release(lock) {
sti(); // reenable interupts
}

● Can two threads disable/reenable interrupts at the same time?

What's wrong with this approach?

Adapted from Matt Welsh’s (Harvard University) slides. 26

13
Disabling Interrupts
An alternative to spinlocks:
struct lock {
// Note – no state!
}
void acquire(lock) {
cli(); // disable interrupts
}
void release(lock) {
sti(); // reenable interupts
}

● Can two threads disable/reenable interrupts at the same time?

What's wrong with this approach?


● Can only be implemented at kernel level (why?)
● Inefficient on a multiprocessor system (why?)
● All locks in the system are mutually exclusive
● No separation between different locks for different bank accounts

Adapted from Matt Welsh’s (Harvard University) slides. 27

Mutexes – Blocking Locks


Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

???
1) Check lock state

Thread 1

Lock state unlocked


unlocked

Lock wait queue


Ø

Adapted from Matt Welsh’s (Harvard University) slides. 28

14
Mutexes – Blocking Locks
Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

1) Check lock state

Thread 1
2) Set state to locked

3) Enter critical section

Lock state locked


locked

Lock wait queue


Ø

Adapted from Matt Welsh’s (Harvard University) slides. 29

Mutexes – Blocking Locks


Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

???
1) Check lock state

Thread 2 Thread 1

Lock state locked


locked

Lock wait queue


Ø

Adapted from Matt Welsh’s (Harvard University) slides. 30

15
Mutexes – Blocking Locks
Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

1) Check lock state

Thread 2 Thread 1
2) Add self to wait queue (sleep)

Lock state locked


locked

Lock wait queue


Ø
Thread 2

Adapted from Matt Welsh’s (Harvard University) slides. 31

Mutexes – Blocking Locks


Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

???
1) Check lock state

Thread 3 Thread 1
2) Add self to wait queue (sleep)

Lock state locked


locked

Lock wait queue

Thread 2 Thread 3

Adapted from Matt Welsh’s (Harvard University) slides. 32

16
Mutexes – Blocking Locks
Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

1) Thread 1 finishes critical section

Thread 1

Lock state locked


locked

Lock wait queue

Thread 2 Thread 3

Adapted from Matt Welsh’s (Harvard University) slides. 33

Mutexes – Blocking Locks


Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

1) Thread 1 finishes critical section

Thread 3 Thread 1
2) Reset lock state to unlocked

3) Wake one thread from wait queue

Lock state unlocked


unlocked

Lock wait queue

Thread 2 Thread 3

Adapted from Matt Welsh’s (Harvard University) slides. 34

17
Mutexes – Blocking Locks
Really want a thread waiting to enter a critical section to block
● Put the thread to sleep until it can enter the critical section
● Frees up the CPU for other threads to run

Straightforward to implement using our TCB queues!

Thread 3 can now grab lock and


enter critical section
Thread 3

Lock state locked


locked

Lock wait queue

Thread 2

Adapted from Matt Welsh’s (Harvard University) slides. 35

Limitations of locks
Locks are great, and simple. What can they not easily accomplish?

What if you have a data structure where it's OK for many threads
to read the data, but only one thread to write the data?
● Bank account example.
● Locks only let one thread access the data structure at a time.

Adapted from Matt Welsh’s (Harvard University) slides. 36

18
Limitations of locks
Locks are great, and simple. What can they not easily accomplish?

What if you have a data structure where it's OK for many threads
to read the data, but only one thread to write the data?
● Bank account example.
● Locks only let one thread access the data structure at a time.

What if you want to protect access to two (or more)


data structures at a time?
● e.g., Transferring money from one bank account to another.
● Simple approach: Use a separate lock for each.
● What happens if you have transfer from account A -> account B, at the same time
as transfer from account B -> account A?
● Hmmmmm ... tricky.

● We will get into this next time.

Adapted from Matt Welsh’s (Harvard University) slides. 37

Next Lecture
Higher level synchronization primitives:
How do to fancier stuff than just locks

Semaphores, monitors, and condition variables


● Implemented using basic locks as a primitive

Allow applications to perform more complicated coordination schemes

Adapted from Matt Welsh’s (Harvard University) slides. 38

19
dup() and dup2()
#include <unistd.h>

int dup(int filedes);

int dup2(int filedes, int filedes2);

Both will duplicate an existing file descriptor

dup() returns lowest available file descriptor, now referring to whatever


filedes refers to

dup2() - filedes2 (if open) will be closed and then set to refer to
whatever filedes refers to

39

Redirecting stdin & stdout in UNIX (cont’d)

Method 2: use dup2() (check “man 2 dup2”)


● dup2(oldfd, newfd) closes newfd and makes it point to
whatever oldfd points to.
1. int fd = open(“outfile”, O_WRONLY);
2. dup2(fd, 1);
3. close(fd);
● Example: suppose fd = 3

BEFORE dup2() AFTER dup2() AFTER close()

0 stdin terminal 0 stdin terminal 0 stdin terminal


1 stdout 1 stdout 1 stdout
2 stderr 2 stderr 2 stderr
3 3 3
outfile
outfile outfile
outfile outfile
outfile

list of open
file descriptors
(in PCB)
40

20
Pipes
● A form of interprocess communication between processes that have
a common ancestor

● Typical use:
● Pipe created by a process
● Process calls fork()
● Pipe used between parent and child
● A pipe provides a one-way flow of data
- example: who | sort| lpr
output of who is input to sort
+

output of sort is input to lpr


+

● The difference between a file and a pipe:


- pipe is a data structure in the kernel.
● A pipe is created by using the pipe system call
int pipe(int* filedes);
● Two file descriptors are returned
- filedes[0] is open for reading
- filedes[1] is open for writing
● Typical size is 512 bytes (Minimum limit defined by POSIX)

41

Pipes
#include <unistd.h>
#include <stdio.h>
int main(void){
int n; // to keep track of num bytes read
int fd[2]; // to hold fds of both ends of pipe
pid_t pid; // pid of child process
char line[80]; // buffer to hold text read/written
if (pipe(fd) < 0) // create the pipe
perror("pipe error");

if ((pid = fork()) < 0) { // fork off a child


perror("fork error");
} else if (pid > 0) { // parent process
close(fd[0]); // close read end
write(fd[1], "hello world\n", 12); // write to it
}else { // child process
close(fd[1]); // close write end
n = read(fd[0], line, 80); // read from pipe
write(1, line, n); // echo to screen
}
exit(0);
}

42

21
After the pipe() call

0 stdin

1 stdout

2 stderr

4
Filedes 0 1
5
3 4

43

The process then calls fork()

0 stdin 0 stdin

stdout 1 stdout
1

stderr 2 stderr
2

3 3

4 4

5 5

Child
Parent

44

22
Parent writing to child

0 stdin 0 stdin

stdout 1 stdout
1

stderr 2 stderr
2

3 X 3

4 4 X

5 5

Child
Parent

45

Child writing to parent

0 stdin 0 stdin

stdout 1 stdout
1

stderr 2 stderr
2

3 3 X

4 X 4

5 5

Child
Parent

46

23

You might also like