8 minute read
Notice a tyop typo? Please submit an issue or open a PR.
To represent threads, pthreads supports a pthread_t
data type. Variables of this type will be uniquely identified by an identifier and will describe a thread.
Threads are created by the following function
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void *), void *arg);
This function takes as arguments a pointer to a pthread_t
, as well as a start routine and an argument to pass to that start routine.
It also takes an argument which is of the type pthread_attr_t
which is a data structure that can be used to set various options on the thread during creation.
pthread_create
returns an int
which indicates whether the creation was a success or a failure.
Child threads can be joined back to a parent thread with
int pthread_join(pthread_t thread, void **status);
This function takes the thread to be joined as well as a status variable. The status variable will capture all of the important information that is returned from the thread to be joined.
pthread_join
returns an int
which indicates whether the join was a success or a failure.
The pthread_attr_t
data structure allows us to define features of the new thread we are creating, such as:
If NULL
is passed in the place of a pthread_attr_t
, pthreads falls back to default behavior for the new thread.
There are several calls that allow us to operate on a pthread_attr_t
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
pthread_attr_{set/get}{attribute};
These functions can be used, respectively, to allocate space for a pthread_attr_t
, to deallocate space for that pthread_attr_t
and to set/get various attributes of that structure.
One mechanism not considered by Birrell is detachable threads. In pthreads, the default behavior for thread creation is joinable threads. For a joinable (child) thread, the parent will not terminate until the child has completed their execution. If the parent thread exits early, the child threads may turn into zombies.
In pthreads, it is possible to allow child threads to become detached. Detached threads cannot be joined back into the parent, allowing the parent to exit early and the child threads to continue their execution.
To detach threads, use
int pthread_detach(pthread_t thread);
Or
pthread_attr_setdetachstate(attr, PTHREAD_CREATE_DETACHED);
// ...
pthread_create(..., attr, ...);
Since parent threads do not need to wait around until child threads complete, they can simply exit with pthread_exit
.
Example of creating a new thread with custom options:
We loop and create threads with pthread_create
passing in hello
as the start routine. Since this function takes no arguments we pass NULL
as the fourth argument to the function. Also, since we do not need any custom behavior, we pass NULL
as the second argument. After we create the threads we must join them all back in.
In this case, the call to pthread_create
passes in a value for the final parameter (the argument to pass to the start routine). This value is the pointer to current value of i
the loop counter. In threadFunc
, the pointer is first cast to an integer pointer, before a local variable is assigned to the value the pointer points to. Finally, the value is printed out.
This section explains how this is a valid input for the example above
Thread Number 0
Thread Number 2
Thread Number 2
Thread Number 3
The variable i
that is used in thread creation in the example above is a globally visible variable that is defined in main
. When its value changes in one thread, all the other threads will see the new value.
In this particular case, the second thread that is created with pthread_create
is created when i == 1
. In the thread function, p
will become equivalent to the address of i
and myNum
will take on the value of i
, which is presumably 1.
However, it is possible that before this thread had the chance to cast the pointer and define a local variable pointing to the pointer's value, the main thread went into the next iteration of the for
loop and incremented i
, making i
now 2. Since we pass the address of i
to the start routine, p
will still point to the address of i
, but myNum
will point to the (new) value of i
, which is 2.
We call the above situation a data race or race condition. In summary, these terms refers to situations in which one thread tries to read a value that another thread is modifying.
To correct the problem, we must first initialize an array tNum
that contains as many elements as we have threads. One each iteration of the for
loop, we can set the index of tNum
at i
to i
. We can then pass the address of tNum[i]
to the call to pthread_create
. This way we can increment i
, while ensuring that the value of i
that a thread needs to see does not change. We accomplish by copying the value of i
over to this private storage that we do not modify.
To deal with the mutual exclusion problem, pthreads supports mutexes. Mutual exclusion ensures that threads access shared state in a controlled manner, such that only thread at a time can apply modifications or otherwise access that shared variable.
In pthreads, the mutex data structure is represented via a pthread_mutex_t
type.
In pthreads, there is no implicit unlock: both lock and unlock must be explicitly called.
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Whatever code appears between these two statements will correspond to the critical section.
Mutexes must be explicitly initialized, as with the following function
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
The pthread_mutexattr_t *attr
argument allows us to specify the behavior that we want the mutex to have. Passing NULL
gives us the default behavior (similar to the case with pthread_create
and pthread_attr_t
).
Another interesting operation that can be used on mutexes is
int pthread_mutex_trylock(pthread_mutex_t *mutex);
Unlike pthread_mutex_lock
which will cause the calling thread to block, this function will return immediately if the mutex cannot be acquired.
Mutexes can be destroyed (freed) with
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Condition variables are synchronization mechanisms that allow blocked threads to be notified when a certain condition occurs.
In pthreads, condition variables are represented by the pthread_cond_t
data type.
To wait on a condition variable, we can call
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
When a thread enters this function, it immediately releases the mutex and places itself on the wait queue associated with the condition variable. When the thread is woken up, it will automatically reacquire the mutex before exiting the wait operation.
To signal or broadcast on a condition variable we call
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cont *cond);
To allocate/create a condition variable and de-allocate/destroy a condition variable, we can call
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_init
takes some attributes that can further specify the behavior of the condition variable (much like we saw with pthread_create
and pthread_mutex_init
). As before, setting this value to NULL
gives us the default behavior.
Let's walk through a real producer/consumer example using PThreads.
Our shared buffer is buffer
. add
and rem
refer to the indices at which we add and remove elements, respectively, while num
refers to the number of elements in the queue.
These shared variables are used in conjunction with our mutex m
and our condition variables c_cons
and c_prod
which are all initialized here.
Finally, we define our two procedures: producer
and consumer
, which will be executed by the producer and consumer threads respectively.
The first thread that we create will execute the producer
function, and the second thread will execute the consumer
function.
We use the default behavior for these threads (i.e. they are not detachable), so we must make sure to pthread_join
them before main
returns.
The producer loops twenty times, and tries to produce an element to the shared buffer on each iteration.
It tries to accomplish this by first acquiring m
with pthread_mutex_lock
. Once m
is acquired, the producer will potentially wait on c_prod
with pthread_cond_wait
if the shared buffer is full (num == BUFSIZE
). Otherwise, the producer will add its element to the buffer, and update the three shared variables to reflect this addition.
Once the producer unlocks the mutex, it signals on c_cons
with pthread_cond_signal
to let the consumers know that there is data to consume.
The consumer loops forever, continuously trying to remove elements from the shared buffer.
It tries to accomplish this by first acquiring m
with pthread_mutex_lock
. Once m
is acquired, the consumer will potentially wait on c_cons
with pthread_cond_wait
if the shared buffer is empty (num == 0
). Otherwise, the consumer will remove an element from the buffer, and update the three shared variables to reflect this removal.
Once the consumer unlocks the mutex, it signals on c_prod
with pthread_cond_signal
to let the producer know that there is space in the buffer to add more data.
OMSCS Notes is made with in NYC by Matt Schlenker.
Copyright © 2019-2023. All rights reserved.
privacy policy