Pthread Tutorial
Creating a POSIX thread.
Pthreads are created using pthread_create().
#include <pthread.h>
int
pthread_create (pthread_t *thread_id, const pthread_attr_t *attributes,
void *(*thread_function)(void *), void *arguments);
This function creates a new thread. pthread_t is an opaque type which acts as a handle for the new thread. attributes is another opaque data type which allows you to fine tune various parameters, to use the defaults pass NULL. thread_function is the function the new thread is executing, the thread will terminate when this function terminates, or it is explicitly killed. arguments is a void * pointer which is passed as the only argument to the thread_function.
Pthreads terminate when the function returns, or the thread can call pthread_exit() which terminates the calling thread explicitly.
int
pthread_exit (void *status);
status is the return value of the thread. (note a thread_function returns a void *, so calling return(void *) is the equivalent of this function.
One Thread can wait on the termination of another by using pthread_join()
int
pthread_join (pthread_t thread, void **status_ptr);
The exit status is returned in status_ptr.
A thread can get its own thread id, by calling pthread_self()
pthread_t
pthread_self ();
Two thread id's can be compared using pthread_equal()
int
pthread (pthread_t t1, pthread_t t2);
Returns zero if the threads are different threads, non-zero otherwise.
Mutexes
When writing multithreaded programs it is frequently necessary to enforce mutually exclusive access to a shared data object. This is done with mutex objects. The idea is to associate a mutex with each shared data object and then require that every thread that wishes to use the shared data object first lock the mutex before doing so. Here are the particular.
1. Declare an object of type pthread_mutex_t.
2. Initialize the object by calling pthread_mutex_init().
3. Call pthread_mutex_lock()to gain exclusive access to the shared data object.
4. Call pthread_mutex_unlock()to release the exclusive access and allow another thread to use the shared data object.
5. Get rid of the object by calling pthread_mutex_destroy().
Mutexes have two basic operations, lock and unlock. If a mutex is unlocked and a thread calls lock, the mutex locks and the thread continues. If however the mutex is locked, the thread blocks until the thread 'holding' the lock calls unlock.
There are 5 basic functions dealing with mutexes.
int
pthread_mutex_init (pthread_mutex_t *mut, const pthread_mutexattr_t *attr);
Note that you pass a pointer to the mutex, and that to use the default attributes just pass NULL for the second parameter.
int
pthread_mutex_lock (pthread_mutex_t *mut);
Locks the mutex.
int
pthread_mutex_unlock (pthread_mutex_t *mut);
Unlocks the mutex.
int
pthread_mutex_trylock (pthread_mutex_t *mut);
Either acquires the lock if it is available, or returns EBUSY.
int
pthread_mutex_destroy (pthread_mutex_t *mut);
Deallocates any memory or other resources associated with the mutex.
Be sure to observe these points:
1. No thread should attempt to lock or unlock a mutex that has not been initialized.
2. The thread that locks a mutex must be the thread that unlocks it.
3. No thread should have the mutex locked when you destroy the mutex.
4. Any mutex that is initialized should eventually be destroyed, but only afterany thread that uses it has either terminated or is no longer interested inusing it.
A short example
Consider the problem``z we had before, now lets use mutexes:
THREAD 1 THREAD 2
pthread_mutex_lock (&mut);
pthread_mutex_lock (&mut);
a = data; /* blocked */
a++; /* blocked */
data = a; /* blocked */
pthread_mutex_unlock (&mut); /* blocked */
b = data;
b--;
data = b;
pthread_mutex_unlock (&mut);
[data is fine. The data race is gone.]
Mutex Example:
#include pthread.h
#include unistd.h
Pthread_mutex_t lock;
int shared_data ;
// Often shared data is more complex than just an int.
void *thread_function ( void *arg ) {
int i ;
for ( i = 0 ; i 1024*1024; ++i ) {
// Access the shared data here.
pthread_mutex_lock(&l o ck ) ;
shared_data++;
pthread_mutex_unlock(&lock ) ;
}
return NULL;
}
int main (void)
{
pthread_t thread_ID ;
void *exit_status ;
int i ;
// Initialize the mutex before trying to use it.
Pthread_mutex_init (&lock , NULL) ;
Pthread_create(&thread_ID , NULL, thread_function , NULL) ;
// Try to use the shared data.
for ( i = 0 ; i 1 0 ; ++i ) {
sleep (1) ;
pthread_mutex_lock(&l o ck ) ;
printf( "\rShared integer ’s value = %d\n" , shared_data ) ;
pthread_mutex_unlock(&lock) ;
}
printf ( "\n" ) ;
pthread_join ( thread_ID , &exit_status) ;
// Clean up the mutex when we are finished with it.
pthread_mutex_destroy(&lock) ;
return 0 ;
}
Condition Variables
If you want one thread to signal an event to another thread, you need to use condition variables. The idea is that one thread waits until a certain condition is true. First it tests the condition and, if it is not yet true, calls pthread_cond_wait() to block until it is. At some later time another thread makes the condition true and calls pthread_cond_signal() to unblock the first thread. Every call to pthread_cond_wait() should be done as part of a conditional statement. If you aren’t doing that, then you are most likely using condition variables incorrectly. For example
if (flag = = 0) pthread_cond_wait ( . . . ) ;
Here I’m waiting until the flag is not zero. You can test conditions of any
complexity. For example
x = a + b − (2*c ) ;
i f ( x < 0 | | x > 9) pthread_cond_wait ( . . . ) ;
Here I’m waiting until x is in the range from zero to nine inclusive where x is computedin some complex way. Note that pthread_cond_wait() is only called if thecondition is not yet true. If the condition is already true, pthread_cond_wait()is not called. This is necessary because condition variables do not rememberthat they have been signaled.If you look at my examples, you will see that there is a serious race condition inthem. Suppose the condition is not true. Then suppose that after the conditionis tested but before pthread_cond_wait() is called, the condition becomes true.The fact that the condition is signaled (by some other thread) will be missedby pthread_cond_wait(). The first thread will end up waiting on a conditionthat is already true. If the condition is never signaled again the thread will bestuck waiting forever.
To deal with this problem, every time you use a condition variable you mustalso use a mutex to prevent the race condition. For example:
pthread_mutex_lock(&mutex ) ;
if (flag = = 0) pthread_cond_wait (&condition , &mutex ) ;
pthread_mutex_unlock(&mutex ) ;
The thread that signals this condition will use the same mutex to gain exclusiveaccess to the flag. Thus there is no way that the signalling could occur betweenthe test of the flag and the waiting on the condition.
For the above to work, pthread_cond_wait() needs to wait on the conditionand unlock the mutex as an atomic action. It does this, but it needs toknow which mutex to unlock. Hence the need for the second parameter of pthread_cond_wait(). When the condition is signaled, pthread_cond_wait()will lock the mutex again so that the pthread_mutex_unlock() in the aboveexample is appropriate regardless of which branch of the if is taken.Here is how the signalling thread might look
pthread_mutex_lock(&mutex ) ;
flag = 1 ;
pthread_mutex_unlock(&mutex ) ;
pthread_cond_signa l (&condition ) ;
Before setting the flag, and thus making the condition true, the signalling threadlocks the mutex to make sure the waiting thread can’t get caught in a racecondition.
The pthread_cond_signal function releases only one thread at a time. In some cases it is desirable to release all threads waiting on a condition. This can beaccomplished using pthread_cond_broadcast. For example
pthread_mutex_lock(&mutex ) ;
flag = 1 ;
pthread_mutex_unlock(&mutex ) ;
pthread_cond_broadcast (&condition) ;
pthread_mutex_lock(&mutex ) ;
while (flag = = 0) pthread_cond_wait (&condition , &mutex ) ; //signal interrupt will lead to error retrun
pthread_mutex_unlock(&mutex ) ;
Condition Variables solve this problem. There are six operations which you can do on a condition variable:
Initialization.
int
pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
Again to use the default attributes, just pass NULL as the second parameter.
Waiting.
int
pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
This function always blocks. In pseudo-code:
pthread_cond_wait (cond, mut)
begin
pthread_mutex_unlock (mut);
block_on_cond (cond);
pthread_mutex_lock (mut);
end
Note that it releases the mutex before it blocks, and then re-acquires it before it returns. This is very important. Also note that re-acquiring the mutex can block for a little longer, so the the condition which was signalled will need to be rechecked after the function returns.
More about this later.
Signalling.
int
pthread_cond_signal (pthread_cond_t *cond);
This wakes up at least one thread blocked on the condition variable. Remember that they must each re-acquire the mutex before they can return, so they will exit the block one at a time.
Broadcast Signalling.
int
pthread_cond_broadcast (pthread_cond_t *cond);
This wakes up all of the threads blocked on the condition variable. Note again they will exit the block one at a time.
Waiting with timeout.
int
pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut,
const struct timespec *abstime);
Identical to pthread_cond_wait(), except it has a timeout. This timeout is an absolute time of day.
struct timespec to {
time_t tv_sec;
long tv_nsec;
};
If a abstime has passed, then pthread_cond_timedwait() returns ETIMEDOUT.
Deallocation.
int
pthread_cond_destroy (pthread_cond_t *cond);
Bye Bye condition variable :).
thread lifecycle
- pthread_create / pthread_join / pthread_exit / pthread_detach
- int pthread_create( pthread_t *tid, const pthread_attr_t *attr, TFP start, void *arg )
- typedef void* (*TFP)(void*);
- example: pthread_create( &tid, NULL, start, NULL );
- more than 1 arg? use a struct
- make arg live longer than the thread!
- no parent/child relationship after creation
- pthread_t: opaque data type
- int pthread_equal( tid1, tid2 );
- pthread_t pthread_self();
- possible to join (wait for) any (non-detached) thread
- int pthread_join( pthread_t tid, void ** status );
- forget to join? zombie thread, unreleased resources (memory leak)
- detached threads:
- specify using attributes at creation or
- int pthread_detach()
- thread termination
- pthread_exit( void* status )
- return from start ==> implicit pthread_exit()
- exit() from any thread terminates process
- main is special ==> return calls exit()
- main can call pthread_exit() explicitly and leave other threads running
- pthread_cancel( tid ) is complex and to be avoided...
attribute objects
- POSIX uses attribute objects to allow easy extension of the API
- statically or dynamically allocate an attribute-thing (just a pointer)
- attribute-things must be "initialized" (allocates memory) and "destroyed" (deallocates)
- call functions to set specific values
- use the attribute-thing to create one or more threads or synchronization variables
- attribute values are copied to newly created object (no references or pointers are retained)
- allows new attributes by just adding more API calls
- thread attribute example:
- pthread_attr_t attr;
- pthread_attr_init( &attr );
- pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );
- pthread_attr_setdetachedstate( &attr, PTHREAD_CREATE_DETACHED );
- pthread_create( &tid, &attr, start, &args );
- pthread_attr_destroy( &attr );
scheduling
- user vs kernel threads
- 3 models: 1-1, many-1, many-many
- Linux: 1-1
- Solaris: many-many, LWPs (virtual cpus)
- two ways to schedule threads
- 1-1 / global / bound / system contention scope
- many-many / local / unbound / process contention scope
- LWPs are managed by pthreads library (usually no programmer intervention)
- scheduling policies, priorities: SCHED_FIFO, SCHED_RR, SCHED_OTHER
- Solaris only implements SCHED_OTHER (DEC, SGI implement others)
- preemptive, but NOT timesliced
- schedule points: blocking, yielding (sched_yield()), preemption
- aside: timeslicing is really more important for independent computations; threads are usually dependent
- possible to achieve other scheduling policies under Solaris
- specify SYSTEM_SCOPE
- use thr_ interfaces to specify LWP realtime priorities (requires root
- conventional wisdom: process scope is best, Lewis & Berg: system scope!
Semaphores
Semaphores are essentially glorified integer counters. They support two primary operations. One operation, called down or wait, attempts to decrement the counter. The other operation, called up or signal, attempts to increment the counter. What makes semaphores special is that if a thread tries to waiton a zero semaphore it is blocked instead. Later when another thread signals the semaphore the blocked thread is activated while the semaphore remains at zero. In effect, the signalling causes the semaphore to be incremented but then the thread that was blocked trying to do a wait is allowed to proceed, causing the semaphore to be immediately decremented again. If multiple threads are blocked waiting on a semaphore then the system chooses one to unblock. Exactly how this choice is made is generally system dependent.You can not assume that it will be in FIFO order. However, the order in which the threads are unblocked is not normally a concern. A semaphore with an initial value of one can be used like a mutex. When athread wishes to enter its critical section and access a shared data structure, it does a wait operation on the semaphore. If no other thread is in its critical section, the semaphore will have its initial value of one and the wait will return immediately. The semaphore will then be zero. If another thread tries to waiton the semaphore during this time it will be blocked. When the first thread is finished executing its critical section it does a signal operation on the semaphore. This will unblock one waiting thread or, if thereare no waiting threads, increment the semaphore back to its initial value of one. A semaphore used in this way is called a binary semaphore because it has exactly two states.
However, because semaphores are integers they can take on values larger than one. Thus they are often used to count scarce resources. For example a thread might wait on a semaphore to effective reserve one item of a resource. If there are no items left, the semaphore will be zero and the wait operation will block.When a thread is finished using an item of a resource it signals the semaphore to either increment the count of available items or to allow a blocked thread to access the now free item. A semaphore used in this way is called a counting semaphore. The Posix semaphore API is not really part of the normal pthread API. Instead Posix standardizes semaphores under a different API. Traditional Unix systems support shared memory, message queues, and semaphores as part of what is called “System V Interprocess Communication” (System V IPC). Posix also provides shared memory, message queues, and semaphores as a package that competes with, or replaces, the older standard. The functionality of the two systems is similar although the details of the two APIs are different. Note that Posix semaphores, like System V IPC semaphores, can be used to synchronize two or more separate processes. This is different than pthreadmutexes. A mutex can only be used by threads in the same process. Because Posix semaphores can be used for interprocess communication, they have the option of being named. One process can create a semaphore under a particular name and other processes can open that semaphore by name. In this tutorial, however, I will focus only on sychronizing threads in the same process. Thus here I will only go over what that requires and skip matters of semaphore naming. The skeleton program in Listing 3 shows how to initialize, clean up, and use a Posix semaphore. For brevity the skeleton program does not show the threads being created or joined nor does it show any error handling. See the manual pages for the various functions for more information on error returns.
Another difference between a pthread mutex and a semaphore is that, unlike a mutex, a semaphore can be signaled in a different thread than the thread that does the wait operation. This is necessary when using a semaphore to count instances of a scarce resource.
#include semaphore.h
int shared ;
sem_t binary_sem ; // Used like a mutex.
void *func ( void *arg )
{
// ...
sem_wai t (&binary_sem ) ; // Decrements count.
// Used shared resource.
sem_signa l (&binary_sem ) ; // Increments count.
// ...
}
void main ( void)
{
// ...
sem_init (&binary_sem , 1 ) ; // Give semaphore initial count.
// Start threads here.
// ...
sem_wait (&binary_sem ) ;
// Use shared resource.
sem_signal (&binary_sem ) ;
// ...
// Join with threads here.
sem_destroy(&binary_sem ) ;
// ...
return 0 ;
}
Synchronization
- mutex / semaphore / condition variable (monitor) / rwlocks
- mutex and cv use attribute objects
- mutex: blocking, not fifo!, release only by holder, recursive mutex, priority inheritance mutex (inversion)
- may be statically or dynamically intialized (static for default attributes)
- pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
- pthread_mutex_init( &m, &mutexattr );
- pthread_mutex_destroy( &m );
- mutex calls
- pthread_mutex_lock( &m );
- pthread_mutex_unlock( &m );
- pthread_mutex_trylock( &m );
- semaphores: must be initialized dynamically
- sem_t sem;
- sem_init( &sem, PTHREAD_PROCESS_PRIVATE, 5 );
- sem_destroy( &sem );
- standard counting semaphores
- sem_post( &sem ); // +
- sem_wait( &sem ); // - (block if 0)
- sem_trywait( &sem );
- sem_getvalue( &sem );
- semaphores are the only ASYNCH SAFE synch variables (ok in signal handlers)
- possible for sem_wait() to be interrupted EINTR -1
- always put it in a loop
- do { err = sem_wait(&sem); } while ( err=EINTR );
- condition variables: very common in threads programming
- idiom: combined mutex and condition variable for waiting
- really a generalized semaphore where condition can be anything, not just count>0
- nice comparison diagram in Lewis & Berg pages 94-95
- allocation, init similar to mutexes
- condition variable calls:
- pthread_cond_wait( &cv, &m );
- pthread_cond_timedwait( &cv, &m, ×pec );
- pthread_cond_signal( &cv ); // wake up one waiting thread
- pthread_cond_broadcast( &cv ); // wake up all waiting threads
- wakeup (signal/broadcast) is "lost" if no waiting thread
- Spurious wakeups are allowed; signal may wakeup more than one :-(
- standard "waiter" idiom:
- pthread_mutex_lock( &m );
- while ( !condition )
- pthread_cond_wait( &cv, &m );
- do_something();
- pthread_mutex_unlock( &m );
- standard "signaler" idiom:
- pthread_mutex_lock( &m );
- // make condition TRUE
- pthread_mutex_unlock( &m );
- pthread_cond_signal( &cv );
Exercise: bounded buffer problem using semaphore and condition variables respectively