programming with threadscourses.daiict.ac.in/pluginfile.php/5096/mod_resource/...programming with...
TRANSCRIPT
Programming with Threads
Ref: • “Computer Systems: A Programmer's Perspective”, Bryant and O'Hallaron, First edition, Pearson Education 2003
Topics
Shared variables
The need for synchronization
Synchronizing with semaphores
Thread safety and reentrancy
Races and deadlocks
– 2 –
Shared Variables in Threaded C Programs
Question: Which variables in a threaded C program are shared variables?
The answer is not as simple as “global variables are shared”
and “stack variables are private”.
We must understand what is shared and what is not shared.
Requires answers to the following questions:
What is the memory model for threads?
How are instances of the variables mapped to memory?
How many threads reference each of these instances?
The variable is shared if and only if multiple threads reference some of instance of the variable.
– 3 –
Threads Memory Model
Conceptual model:
Each thread runs in the context of a process.
Each thread has its own separate thread context.
Thread ID, stack, stack pointer, program
counter, condition codes, and general purpose
registers.
All threads share the remaining process context.
Code, R/W data, heap, and shared library code
and data areas (segments of the process virtual
address space.)
Open files and installed handlers
– 4 –
Threads Memory Model (cont)
Operationally, this model is not strictly enforced:
While register values are truly separate and protected....
It is impossible for one thread to read or write the register
values of another thread
Any thread can access any location in the shared virtual
memory
- read and write the stack of any other thread
- If one thread modifies a memory location, then every other
thread will see the change, if it reads that location.
Registers are never shared, virtual memory is always shared.
Mismatch between the conceptual and operation model is a source of confusion and errors.
– 5 –
Example of Threads Accessing Another Thread’s Stack#include "csapp.h"
#define N 2
void *thread(void *vargp);
char **ptr; /* global */
int main()
{
int i;
pthread_t tid;
char *msgs[N] = {
"Hello from Block - A",
"Hello from Block - B"
};
ptr = msgs;
for (i = 0; i < 2; i++)
Pthread_create(&tid,
NULL,
thread,
(void *)i);
Pthread_exit(NULL);
}
/* thread routine */
void *thread(void *vargp)
{
int myid = (int)vargp;
static int cnt = 0;
printf("[%d]: %s (cnt=%d)\n",
myid, ptr[myid], ++cnt);
}
Peer threads access main thread’s stack
indirectly through global ptr variable
– 6 –
[sanjay@dslabsrv17 conc]$ ./sharing
[0]: Hello from Block - A (cnt=1)
[1]: Hello from Block - B (cnt=2)
[sanjay@dslabsrv17 conc]$
Thread Stacks:
• Contained in the stack area of the virtual
address space, and are usually accessed
independently of their respective threads.
• Different thread stacks are not protected from
other threads, i.e. if a thread can somehow
manage to acquire a pointer to another
thread’s stack, then it can read and write any
part of that stack.
– 7 –
Mapping Variables to Memory Variables
Global Variables
• Any variable declared outside of a function.
• At run time, the read/write area of virtual memory contains exactly one instance of each global variable that can be referenced by any thread.
• For example, the global ptr variable has one run-time instance in the R/W area of virtual memory.
• When there is only one instance of a variable, we will denote the instance by simply using the variable name, e.g. ptr
– 8 –
Mapping Variables to Memory Variables (cont)
Local automatic variables
• A local automatic variable is one that is declared inside a function without the static attribute.
• At run time, each thread’s stack contains its own instances of any local automatic variables. It is true even if multiple threads execute the same thread routine.
For example,
• There is one instance of the local variable tid, and it resides on
the stack of the main thread. We will denote this instance as tid.m.
• There are two instances of the local variable myid, one instance
on the stack of peer thread 0 and the other on the stack of peer thread 1. We will denote these instances as myid.p0 and myid.p1, respectively.
– 9 –
Mapping Variables to Memory Variables (cont)
Local Static Variables
• A local static variable is one that is declared inside a function with the static attribute.
• As with global variables, R/W area of virtual memory contains exactly one instance of each local static variable declared in the program.
• For example, even though each peer thread in our program declares cnt in line 25, at run time there is only one instance of cnt residing in the R/W area of
virtual memory. Each peer thread reads and writes this instance.
– 10 –
Shared Variables
• A variable v is shared if and only if one of its instances is referenced by more than one thread.
For example,
• Variable cnt is shared because it has one run-time
instance, and this instance is referenced in both peer threads.
• Variable myid is not shared because each of its two
instances is referenced by exactly on thread.
• It is important to note that local automatic variables such as msgs can also be shared.
– 11 –
Mapping Variables to Mem. Instances
char **ptr; /* global */
int main()
{
int i;
pthread_t tid;
char *msgs[N] = {
"Hello from Block - A",
"Hello from Block - B"
};
ptr = msgs;
for (i = 0; i < 2; i++)
Pthread_create(&tid,
NULL,
thread,
(void *)i);
Pthread_exit(NULL);
}
/* thread routine */
void *thread(void *vargp)
{
int myid = (int)vargp;
static int cnt = 0;
printf("[%d]: %s (cnt=%d)\n",
myid, ptr[myid], ++cnt);
}
Global var: 1 instance (ptr [data])
Local static var: 1 instance (cnt [data])
Local automatic vars: 1 instance (i.m, msgs.m )
Local automatic var: 2 instances (myid.p0[peer thread 0’s stack],
myid.p1[peer thread 1’s stack]
)
– 12 –
Shared Variable Analysis
Which variables are shared?
Variable Referenced by Referenced by Referenced by
instance main thread? peer thread 0? peer thread 1?
ptr yes yes yes
svar no yes yes
i.m yes no no
msgs.m yes yes yes
myid.p0 no yes no
myid.p1 no no yes
Answer: A variable x is shared iff multiple threads reference at least one instance of x. Thus:
ptr, cnt, and msgs are shared.
i and myid are NOT shared.
– 13 –
badcnt.c: An Improperly Synchronized Threaded Program#include "csapp.h“
#define NITERS 200000000
void *count(void *arg);
unsigned int cnt = 0; /* shared */
int main() {
pthread_t tid1, tid2;
Pthread_create(&tid1, NULL,
count, NULL);
Pthread_create(&tid2, NULL,
count, NULL);
Pthread_join(tid1, NULL);
Pthread_join(tid2, NULL);
if (cnt != (unsigned)NITERS*2)
printf(“Value of cnt variable not
proper!! cnt=%d\n“, cnt);
else
printf("OK cnt=%d\n",
cnt);
}
/* thread routine */
void *count(void *arg) {
int i;
for (i=0; i<NITERS; i++)
cnt++;
return NULL;
}
– 14 –
[sanjay@dslabsrv17 conc]$ ./badcnt
Value of cnt variable not proper! cnt=242529250
[sanjay@dslabsrv17 conc]$ ./badcnt
Value of cnt variable not proper! cnt=269131504
[sanjay@dslabsrv17 conc]$ ./badcnt
Value of cnt variable not proper! cnt=251801434
[sanjay@dslabsrv17 conc]$
cnt should be equal to 200,000,000.
What went wrong?!
– 15 –
Assembly Code for Counter Loop
.L9:
movl -4(%ebp),%eax
cmpl $99999999,%eax
jle .L12
jmp .L10
.L12:
movl cnt,%eax # Load
leal 1(%eax),%edx # Update
movl %edx,cnt # Store
.L11:
movl -4(%ebp),%eax
leal 1(%eax),%edx
movl %edx,-4(%ebp)
jmp .L9
.L10:
Corresponding asm code(gcc -O0 -fforce-mem)
for (i=0; i<NITERS; i++)
cnt++;
C code for counter loop
Head (Hi)
Tail (Ti)
Load cnt (Li)
Update cnt (Ui)
Store cnt (Si)
– 16 –
Concurrent Execution
Five parts of loop code:
Hi: The block of instructions at the head of the loop.
Li: The instruction that loads the shared variable cnt
into register %eaxi, where %eax, denotes the value of register %eax in thread i.
Ui: The instruction that updates (increments) %eaxi.
Si: The instruction that stores the updated value of %eaxi back to the shared variable cnt.
Ti: the block of instructions at the tail of the loop.
– 17 –
Concurrent Execution (cont)
Key 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 i’s context
H1
L1
U1
S1
H2
L2
U2
S2
T2
T1
1
1
1
1
2
2
2
2
2
1
-
0
1
1
-
-
-
-
-
1
0
0
0
1
1
1
1
2
2
2
i (thread) instri cnt%eax1
Correct Ordering
-
-
-
-
-
1
2
2
2
-
%eax2
– 18 –
Concurrent Execution (cont)
Incorrect ordering: two threads increment the counter, but the result is 1 instead of 2.
H1
L1
U1
H2
L2
S1
T1
U2
S2
T2
1
1
1
2
2
1
1
2
2
2
-
0
1
-
-
1
1
-
-
-
0
0
0
0
0
1
1
1
1
1
i (thread) instri cnt%eax1
-
-
-
-
0
-
-
1
1
1
%eax2
Incorrect Ordering
– 19 –
Concurrent Execution (cont)
How about this ordering?
H1
L1
H2
L2
U2
S2
U1
S1
T1
T2
1
1
2
2
2
2
1
1
1
2
i (thread) instri cnt%eax1 %eax2
We can clarify our understanding of concurrent
execution with the help of the progress graph
– 20 –
Progress Graphs
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.H1 L1 U1 S1 T1
H2
L2
U2
S2
T2
Thread 1
Thread 2
(L1, S2)
– 21 –
Trajectories in Progress Graphs
A trajectory is a sequence
of legal state transitions
that describes one possible
concurrent execution of
the threads.
Example:
H1, L1, U1, H2, L2,
S1, T1, U2, S2, T2
H1 L1 U1 S1 T1
H2
L2
U2
S2
T2
Thread 1
Thread 2
– 22 –
Critical Sections and Unsafe Regions
L, U, and S form a
critical section with
respect to the sharedvariable cnt.
Instructions in critical
sections (wrt to some
shared variable) should
not be interleaved.
Sets of states where such
interleaving occurs
form unsafe regions.
H1 L1 U1 S1 T1
H2
L2
U2
S2
T2
Thread 1
Thread 2
Unsafe region
critical section wrt cnt
critical
section wrt cnt
– 23 –
Safe and Unsafe Trajectories
Def: A trajectory is safe
iff it doesn’t touch any
part of an unsafe region.
Claim: A trajectory is correct (wrt cnt) iff it is
safe.
H1 L1 U1 S1 T1
H2
L2
U2
S2
T2
Thread 1
Thread 2
Unsafe region Unsafe
trajectory
Safe trajectory
critical section wrt cnt
critical
section wrt cnt
– 24 –
Semaphores
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)
– 25 –
Safe Sharing with Semaphores
Here is how we would use P and V operations to synchronize the threads that update cnt.
/* Semaphore s is initially 1 */
/* Thread routine */
void *count(void *arg)
{
int i;
for (i=0; i<NITERS; i++) {
P(s);
cnt++;
V(s);
}
return NULL;
}
– 26 –
Safe Sharing With Semaphores
Provide mutually
exclusive access to
shared variable by
surrounding critical
section with P and V
operations on semaphores (initially set to 1).
Semaphore invariant
creates a forbidden region
that encloses unsafe
region and is never
touched by any trajectory.
H1 P(s) V(s) T1
Thread 1
Thread 2
L1 U1 S1
H2
P(s)
V(s)
T2
L2
U2
S2
Unsafe region
Forbidden region
1 1 0 0 0 0 1 1
1 1 0 0 0 0 1 1
0 0 -1 -1 -1 -1 0 0
0 0-1 -1 -1 -1
0 0
0 0-1 -1 -1 -1
0 0
0 0
-1 -1 -1 -1
0 0
1 1 0 0 0 0 1 1
1 1 0 0 0 0 1 1
Initially
s = 1
– 27 –
POSIX Semaphores
#include <semaphore.h>
Note: Refer /usr/include/semaphore.h
int sem_init(sem_t *sem, o, unsinged int value);
int sem_wait(sem_t *s); /* P(s) */
int sem_post(sem_t *s); /* V(s) */
• P and V Operations will be performed by calling the sem_wait and sem_postfunctions respectively.
• Following wrapper functions are defined:
#include “csapp.h”
void P(sem_t *s); /Wrapper function for sem_wait */
void S(sem_t *s); /Wrapper function for sem_post */
– 28 –
POSIX Semaphores (cont)
/* 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");
– 29 –
Sharing With POSIX Semaphores/*
* goodcnt.c - A properly synchronized counter program
*/
/* $begin goodcnt */
#include "csapp.h"
#define NITERS 10000000
void *count(void *arg);
/* shared variables */
unsigned int cnt; /* counter */
sem_t mutex; /* semaphore that protects counter */
int main()
{ pthread_t tid1, tid2;
Sem_init(&mutex, 0, 1); /* mutex = 1 */
Pthread_create(&tid1, NULL, count, NULL);
Pthread_create(&tid2, NULL, count, NULL);
Pthread_join(tid1, NULL);
Pthread_join(tid2, NULL);
if (cnt != (unsigned)NITERS*2)
printf("Value of cnt variable not proper !! %d\n", cnt);
else
printf("OK cnt=%d\n", cnt);
exit(0); }
– 30 –
Sharing With POSIX Semaphores (cont)
/* thread routine */
void *count(void *arg)
{
int i;
for (i = 0; i < NITERS; i++) {
P(&mutex);
cnt++;
V(&mutex);
}
return NULL;
}
– 31 –
[sanjay@dslabsrv17 conc]$ ./goodcnt
OK cnt=20000000
[sanjay@dslabsrv17 conc]$ ./goodcnt
OK cnt=20000000
[sanjay@dslabsrv17 conc]$ ./goodcnt
OK cnt=20000000
– 32 –
Signaling With Semaphores
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.
“signals” in this context has nothing to do with Unix signals
Examples
Multimedia processing:
Producer creates MPEG video frames, consumer renders the frames
Event-driven 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.
producer
thread
shared
bufferconsumer
thread
– 33 –
Using semaphores to Schedule Shared Resources
• SBUF package: to build producer-consumer programs. It manipulates buffers of type sbuf_t
• Items are stored in a dynamically allocated integer array (buf) with n items.
• front and rear indices keep track of the first and last items in the array.
• Three semaphores control synchronize access to the buffer.
• mutex semaphore provides mutually exclusive buffer access.
• slots and items semaphores count the number of empty slots and available items respectively.
– 34 –
Using semaphores to Schedule Shared Resources
• The sbuf_init function allocates heap memory for the buffer, sets front and rear to indicate an empty buffer, and assigns initial values to the three semaphores
• The sbuf_deinit function frees the buffer storage when the application is completed
• The sbuf_insert function waits for an available slot, locks the mutex, adds the item, unlock the mutex and the announces the availability of a new item.
• The sbuf_remove function is symmetric. After waiting for an available buffer item, it locks the mutex, removes the item from the front of the buffer, unlocks mutex, and then signal the availability of a new slot.
– 35 –
Using semaphores to Schedule Shared Resources
#ifndef __SBUF_H__
#define __SBUF_H__
#include "csapp.h“
/* $begin sbuft Shared Buffer */
typedef struct {
int *buf; /* Buffer array */
int n; /* Maximum number of slots */
int front; /* buf[(front+1)%n] is first item */
int rear; /* buf[rear%n] is last item */
sem_t mutex; /* Protects accesses to buf */
sem_t slots; /* Counts available slots */
sem_t items; /* Counts available items */
} sbuf_t;
/* $end sbuft */
void sbuf_init(sbuf_t *sp, int n);
void sbuf_deinit(sbuf_t *sp);
void sbuf_insert(sbuf_t *sp, int item);
int sbuf_remove(sbuf_t *sp);
#endif /* __SBUF_H__ */
– 36 –
/* sbuf.c
$begin sbufc */
#include "csapp.h"
#include "sbuf.h"
/* Create an empty, bounded, shared FIFO buffer with n slots */
/* $begin sbuf_init */
void sbuf_init(sbuf_t *sp, int n)
{ sp->buf = Calloc(n, sizeof(int));
sp->n = n; /* Buffer holds max of n items */
sp->front = sp->rear = 0; /* Empty buffer iff front == rear */
Sem_init(&sp->mutex, 0, 1); /* Binary semaphore for locking */
Sem_init(&sp->slots, 0, n); /* Initially, buf has n empty slots */
Sem_init(&sp->items, 0, 0); /* Initially, buf has zero data items
*/
}
/* $end sbuf_init */
/* Clean up buffer sp */
/* $begin sbuf_deinit */
void sbuf_deinit(sbuf_t *sp)
{
Free(sp->buf);
}
/* $end sbuf_deinit */
– 37 –
/* Insert item onto the rear of shared buffer sp */
/* $begin sbuf_insert */
void sbuf_insert(sbuf_t *sp, int item)
{
P(&sp->slots); /* Wait for available slot */
P(&sp->mutex); /* Lock the buffer */
sp->buf[(++sp->rear)%(sp->n)] = item; /* Insert the item */
V(&sp->mutex); /* Unlock the buffer */
V(&sp->items); /* Announce available item */
}
/* $end sbuf_insert */
/* Remove and return the first item from buffer sp */
/* $begin sbuf_remove */
int sbuf_remove(sbuf_t *sp)
{
int item;
P(&sp->items); /* Wait for available item */
P(&sp->mutex); /* Lock the buffer */
item = sp->buf[(++sp->front)%(sp->n)]; /* Remove the item */
V(&sp->mutex); /* Unlock the buffer */
V(&sp->slots); /* Announce available slot */
return item;
}
/* $end sbuf_remove */
/* $end sbufc */
– 38 –
Concurrent server based on Prethreading
• Concurrent server: creating a new thread for each new client.
• DisAdv: Nontrivial cost of creating a new thread for each
new client
• Prethreaded Concurrent Server
• Server consists of main thread and a set of worker threads
• Main thread repeatedly accepts connection requests from
clients and places the resulting connected descriptors in a
shared buffer.
• Each worker thread repeatedly removes a descriptors from
the buffer, services the client and then waits for the next
descriptors
• Refer: echoservert_pre.c and echo_cnt.c
– 39 –
Organization of a prethreaded concurent server:
A set of existing threads repeatedly remove and process
connected descriptions from a shared buffer
– 40 –
Concurrent server based on Prethreading(cont)
• SBUF package is used to implement a prethreadedconcurrent server echo server
• Initialize buffer sbuf
• Main thread creates the set of worker threads
• Enters the infinite loop, accepting connection requests and inserting resulting connectioneddescriptors in sbuf.
• Each worker thread waits until it is able to remove a connected descriptor from the buffer and calls the echo_cnt function to eacho client input
• Accesses to the shared byte_cnt variable are protected by P and V operations.
– 41 –
Event-driven programs based on threads
Concurrent prethreaded server can be an event-driven server with simple state machines for the main and worker thread.
Main thread
Two states: “Waiting for connection request” and
“Waiting for available buffer slot”
Two I/O events: “Connection request arrives” and
“Buffer slot becomes available”
Two transitions: “Accept connection request” and
“Insert buffer item”
Worker thread
One state: “Waiting for available buffer slot”
One I/O event: “Buffer slot becomes available”
One transition: “Remove buffer item”
– 42 –
Producer-Consumer on a Buffer That Holds One Item
/* buf1.c - producer-consumer
on 1-element buffer */
#include “csapp.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(&shared.empty, 0, 1);
Sem_init(&shared.full, 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);
}
– 43 –
Producer-Consumer (cont)
/* 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(&shared.empty);
shared.buf = item;
V(&shared.full);
}
return NULL;
}
/* consumer thread */
void *consumer(void *arg) {
int i, item;
for (i=0; i<NITERS; i++) {
/* read item from buf */
P(&shared.full);
item = shared.buf;
V(&shared.empty);
/* consume item */
printf("consumed %d\n",
item);
}
return NULL;
}
Initially: empty = 1, full = 0.
– 44 –
Thread Safety
Functions called from a thread must be thread-safe.
We identify four (non-disjoint) classes of thread-unsafe functions:
Class 1: Failing to protect shared variables.
Class 2: Relying on persistent state across invocations.
Class 3: Returning a pointer to a static variable.
Class 4: Calling thread-unsafe functions.
– 45 –
Thread-Unsafe Functions
Class 1: Failing to protect shared variables.
Relatively easy to make thread-safe
Fix: Use P and V semaphore operations.
Issue: Synchronization operations will slow down code.
Example: goodcnt.c
– 46 –
Thread-Unsafe Functions (cont)Class 2: Relying on persistent state across multiple
function invocations.
rand(): repeatedly from from single thread, same sequence of
numbers, not if multiple threads are calling it
Random number generator relies on static state
Fix: Rewrite function so that caller passes in all necessary state.
/* rand - return pseudo-random integer on 0..32767 */
int rand(void)
{ static unsigned int next = 1;
next = next*1103515245 + 12345;
return (unsigned int)(next/65536) % 32768; }
/* srand - set seed for rand() */
void srand(unsigned int seed)
{ next = seed; }
Thread-unsafe pseudorandom number generator
– 47 –
Thread-Unsafe Functions (cont)
Class 3: Returning a ptr to a static variable.
Such function from concurrent threads, results used by one thread will be overwritten by another thread
Fixes:
1. Rewrite code so caller passes pointer to struct.
» Issue: Requires
changes in caller and
callee.
2. Lock-and-copy
Each call lock mutex, call thread-unsafe function, dynamically allocate memory, copy result and then unlock
Issue: Requires only simple changes
in caller (and none in callee)
However, caller must free memory.
hostp = Malloc(...));
gethostbyname_r(name, hostp);
struct hostent
*gethostbyname(char name)
{
static struct hostent h;
<contact DNS and fill in h>
return &h;
}
struct hostent
*gethostbyname_ts(char *p)
{
struct hostent *q = Malloc(...);
P(&mutex); /* lock */
p = gethostbyname(name);
*q = *p; /* copy */
V(&mutex);
return q;
}
Thread-safe wrapper for gethostnamebyname() uses
the lock-and-copy technique to call a Class 2 thread-unsafe function
– 48 –
Thread-Unsafe Functions
Class 4: Calling thread-unsafe functions.
Calling one thread-unsafe function makes an entire function
thread-unsafe.
If a function f calls a thread-unsafe function g, is f thread-
unsafe?
If g is a class 2 function that relies on state across multiple
invocations, then f is also thread-unsafe and there is no
recourse of rewriting g.
If g is a class 1 or 3 function, then f can still be thread-safe it
your protect the call site and any resulting shared data with a
mutex.
Fix: Modify the function so it calls only thread-safe functions
We used lock-and-copy to write a thread-safe function that calls
a thread-unsafe function (previous slide)
– 49 –
Thread-Safe Library Functions
All functions in the Standard C Library (at the back of your K&R text) are thread-safe.
Examples: malloc, free, printf, scanf
Most Unix system calls are thread-safe, with a few exceptions:
Thread-unsafe function Class Reentrant versionasctime 3 asctime_r
ctime 3 ctime_r
gethostbyaddr 3 gethostbyaddr_r
gethostbyname 3 gethostbyname_r
inet_ntoa 3 (none)
localtime 3 localtime_r
rand 2 rand_r
– 50 –
Reentrant FunctionsA function is reentrant iff it accesses NO shared variables when
called from multiple threads.
Do not reference any shared data when called by multiple threads
Reentrant functions are a proper subset of the set of thread-safe functions.
• The set of reentrant functions if a proper subset of the thread-safe functions.
• NOTE: The fixes to Class 2 and 3 thread-unsafe functions require modifying the function to make it reentrant.
Reentrant
functions
All functions
Thread-unsafe
functions
Thread-safe
functions
– 51 –
Reentrant Functions (cont)
• Example of reentrant version of the rand function. Here static next variable is replaced with a pointer that is passed in by the caller
int rand_r(unsigned int *nextp)
{ *nextp = *nextp * 1103515245 + 12345;
return (unsigned int) (*nextp / 65536) % 32768; }
• Is it possible to inspect the code of a function and declare a priori that it is reentrant?... It depends!
• If all function arguments are passed
• by value (e.g. no pointers) and
• all data references are to local automatic stack variables (i.e.
no references to static or global variables) then the function
is explicitly reentrant, we can assert its reentrancy
regardless of how it is called.
– 52 –
Reentrant Functions (cont)• If we allow some parameters in explicitly reentrant function to be
passed by reference (allow them to pass pointers) then we have an implicitly reentrant function;
• in the sense that it is only reentrant if the calling threads are
careful to pass pointers to nonshared data, e.g. rand_r()
• It is important to realize that reentrancy is sometimes a property of both the caller and the callee, and not only the callee alone.
• Thread-unsafe functions listed earlier are of the class 3 variety and return a pointer to a static variable.
• Use lock-and-copy approach, but it will slow down the
execution of a program
• Lock-and-copy will not work for Class 2 thread-unsafe
functions such as rand() which relies on static state across
calls
• Unix systems provide reentrant versions of most thread-
unsafe functions, such functions end with _r suffix. These
functions are poorly documented.
– 53 –
Races
A race occurs when the correctness of the program depends on one thread reaching point x before another thread reaches point y.
Races usually occur because programmers assume that threads will take some particular trajectory through the execution state space, forgetting the golden rule that threaded programs must work correctly for any feasible trajectory.
race.c: the main thread creates four peer threads and passes pointer to a unique integer ID to each one. Each peer thread copies the ID passed in its argument to a local variable, and then prints a message containing the ID.
– 54 –
Races (cont)
race.c:
/* a threaded program with a race */
int main() {
pthread_t tid[N];
int i;
for (i = 0; i < N; i++)
Pthread_create(&tid[i], NULL, thread, &i);
for (i = 0; i < N; i++)
Pthread_join(tid[i], NULL);
exit(0);
}
/* thread routine */
void *thread(void *vargp) {
int myid = *((int *)vargp);
printf("Hello from thread %d\n", myid);
return NULL;
}
– 55 –
Races (cont)
• The problem is caused by a race between each peer thread and the main thread.
• When the main thread creates a peer thread:Pthread_create(&tid[i], NULL, thread, &i);
• It passes a pointer to the local stack variable i.
• At this point the race is between the next call to Pthread_create and the deferencing and
assignment of the argument in line :int myid = *((int *)vargp);
• If peer thread executes above code before main thread creates next peer thread, then myid variable will get correct ID. Otherwise it will contain ID of some other thread.
– 56 –
Races (cont)
• Correct answer depends on how the kernel schedules the execution of threads. It may differ from system to system.
Solution:
• We can dynamically allocate a separate block for each integer ID and pass the thread routine a pointer to this block:
for (i = 0; i < N; i++) { ptr = Malloc(sizeof(int));
*ptr = i;
Pthread_create(&tid[i], NULL, thread, ptr); }
….
void *thread(void *vargp) { int myid = *((int *)vargp);
Free(vargp); }
• Note: Do not forget to free the block in order to avoid memory leak.
– 57 –
deadlock
region
Deadlock
P(s) V(s)
V(t)
Thread 1
Thread 2
Initially, s=t=1
P(t)
P(t) V(t)
forbidden
region for s
forbidden
region for t
P(s)
V(s) deadlock
state
Locking introduces 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.
Unfortunate fact: deadlock
is often non-deterministic.
– 58 –
Deadlock (cont)
• When binary semaphores are used, following rule can be applied:
• Mutex lock ordering rule: A program is deadlock-free if, for each pair of mutexes (s, t) in the program, each thread that holds both s and t simultaneously locks them in the same order.
• Lock s first, then t in each thread.
– 59 –
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.
Tread carefully with threads!
For more info:
D. Butenhof, “Programming with Posix Threads”, Addison-
Wesley, 1997.