Chapter 27.
Interlude: Thread API
Operating System: Three Easy Pieces
Thread
A thread is a single sequence stream within a process.
Single process can have multiple threads, but each thread belongs to exactly one process.
Threads are also called lightweight processes as they possess some properties of processes.
Threads are not independent from each other unlike processes. All threads belonging to the same
process share - code section, data section, and OS resources (e.g. open files and signals)
Each thread has its own (thread control block) - thread ID, program counter (PC), register set, and a stack
Multithreading in C
Multithreading is a programming technique where a process is divided into multiple
smaller units called threads, which can run simultaneously.
Threads can be effective only if the CPU is more than 1 otherwise two threads have to
context switch for that single CPU.
In C programming language, we use the POSIX Threads (pthreads) library to implement
multithreading.
The POSIX thread libraries are a C/C++ thread API based on standards.
The pthread library is defined inside <pthread.h> header file
#include <pthread.h>
Thread Creation
pthread_create(): initializes and starts the thread to run the given function.
#include <pthread.h>
int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void*),
void *arg);
thread: Pointer to a pthread_t variable where the system stores the ID of the new thread.
attr: Pointer to a thread attributes object that defines thread properties. Use NULL for default
attributes, such as stack size, scheduling priority, …
start_routine: a function pointer to the function that the thread will execute.
arg: A single argument passed to the thread function. Use NULL if no argument is needed. You
can pass a struct or pointer to pass multiple values.
a void pointer allows us to pass in any type of argument.
start_routine has a single
Thread Creation (Cont.) argument of type void * and
returns a value of type void *
int pthread_create(…, // first two args are the same
void* (*start_routine)(void*),
void* arg);
If start_routine requires another type argument, the declaration would look like this:
An integer argument:
int pthread_create(…, // first two args are the same
void* (*start_routine)(int),
int arg);
Return an integer:
int pthread_create(…, // first two args are the same
int (*start_routine)(void*),
void* arg);
Example: Creating a Thread
#include <pthread.h>
typedef struct __myarg_t {
int a;
int b;
} myarg_t; cast the argument to the type it expects
void *mythread(void *arg) {
myarg_t *m = (myarg_t *) arg;
printf(“%d %d\n”, m->a, m->b);
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t thread1; //Create a pthread_t variable to store thread ID
myarg_t args = {10, 20};
int rc = pthread_create(&thread1, NULL, mythread, &args); // Creating a new thread.
. . .
return 0;
}
// Wait for thread to finish
Wait for a Thread to Complete pthread_join(p, NULL);
int main(int argc, char *argv[]) {
pthread_t thread1; //Create a pthread_t variable to store thread ID
myarg_t args = {10, 20};
int rc = pthread_create(&thread1, NULL, mythread, &args); // Creating a new thread.
return 0;
}
Wait for the execution of the particular thread: pthread_join()
main thread may end before the execution of the thread1 and may lead to
unexpected behavior of the program.
pthread_join() function allows one thread to wait for the termination of another
thread. It is used to synchronize the execution of threads.
Wait for a Thread to Complete (Cont.)
int pthread_join(pthread_t thread, void **value_ptr);
pthread_join() :
allows one thread to wait for the termination of another thread
used to synchronize the execution of threads
Two arguments:
thread: Specify which thread to wait for
value_ptr: A pointer to the return value expected to get back
This is optional and can be set to NULL if you do not need the return value of the thread.
pthread_join() routine may change the return values. A pointer to that value is required.
#include <stdio.h>
#include <pthread.h> Example: Waiting for Thread Completion
#include <assert.h>
#include <stdlib.h> int main(int argc, char *argv[]) {
int rc;
typedef struct __myarg_t {
int a; pthread_t thread1;
int b; myret_t *m;
} myarg_t; myarg_t args = {10, 20};
typedef struct __myret_t { pthread_create(&thread1, NULL, mythread, &args);
int x; pthread_join(thread1, (void **) &m);
int y; printf(“returned %d %d %d\n”, m->x, m->y, m->z);
int z; return 0;
} myret_t; }
void *mythread(void *arg) {
thread1 has multiple arguments
myarg_t *m = (myarg_t *) arg;
packaged into a struct
printf(“%d %d\n”, m->a, m->b);
casted to the type it expects
myret_t *r = malloc(sizeof(myret_t));
r->x = 1; thread1 has multiple return values
r->y = 2; packaged into a struct
r->z = 3; pthread_join():
return (void *) r; Thread1 is finished running, the main thread then prints and returns.
}
Example: Simpler Argument Passing to a Thread
Just passing in or return a single value, we don’t have to package arguments and
return values inside of structures.
void *mythread(void *arg) {
int m = (int) arg;
printf(“%d\n”, m);
return (void *) (arg + 1);
}
int main(int argc, char *argv[]) {
pthread_t thread1;
int rc, m;
pthread_create(& thread1, NULL, mythread, (void *) 100);
pthread_join(thread1, (void **) &m);
printf(“returned %d\n”, m);
return 0;
}
Example: Dangerous Code
Be careful with how values are returned from a thread.
1 void *mythread(void *arg) {
2 myarg_t *m = (myarg_t *) arg;
3 printf(“%d %d\n”, m->a, m->b);
4 myret_t r; // ALLOCATED ON STACK: BAD!
5 r.x = 1;
6 r.y = 2;
7 return (void *) &r;
8 }
When the variable r returns, it is automatically de-allocated.
Never return a pointer which refers to something
allocated on the thread’s call stack.
Locks: Defining Critical Sections
Provide mutual exclusion to a critical section
Interface int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Usage (w/o lock initialization and error check)
pthread_mutex_t lock;
pthread_mutex_lock(&lock);
x = x + 1; // or whatever your critical section is
pthread_mutex_unlock(&lock);
No other thread holds the lock → the thread will acquire the lock and enter the critical section.
If another thread hold the lock → the thread will not return from the call until it has acquired
the lock.
Returned value If successful, returns 0. If unsuccessful, returns -1.
Locks: Lock Initialization
All locks must be properly initialized.
One way: using PTHREAD_MUTEX_INITIALIZER
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
The dynamic way: using pthread_mutex_init()
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr)
o Initializes the mutex referenced by mutex with attributes specified by attr.
o If attr is NULL, the default mutex attribute (NONRECURSIVE) is used
o If successful, pthread_mutex_init() returns 0, and the state of the mutex becomes
initialized and unlocked.
o If unsuccessful, pthread_mutex_init() returns -1.
Locks: Unlock & Destroy
The mutex can be unlocked and destroyed by calling following two functions :
int pthread_mutex_unlock(pthread_mutex_t *mutex)
o Release a mutex object.
o If one or more threads are waiting to lock the mutex, pthread_mutex_unlock() causes one
of those threads to return from pthread_mutex_lock() with the mutex object acquired.
o If no threads are waiting for the mutex, the mutex unlocks with no current owner.
o Returned value: If successful, returns 0. If unsuccessful, returns -1.
int pthread_mutex_destroy(pthread_mutex_t *mutex)
o Destroy a mutex object and mutex is set to an invalid value
o The mutex object can be reinitialized using pthread_mutex_init()
o Returned value: If successful, returns 0. If unsuccessful, returns -1.
Locks: Error Check
Mutex routines may fail also. The failure may allow multiple threads into a
critical section if the errors are not properly checked!
Check the return codes: int rc = pthread_mutex_init(&lock, NULL);
assert(rc == 0); // always check success!
❖ Lock initialization error check
❖ Check errors code when calling lock and unlock int rc = pthread_mutex_lock(mutex);
assert(rc == 0);
// An example wrapper:
// Use this to keep your code clean but check for failures
void Pthread_mutex_lock(pthread_mutex_t *mutex) {
int rc = pthread_mutex_lock(mutex);
assert(rc == 0);
}
Condition Variables
Condition variables are useful when some signaling must take place between threads.
One thread waits on the condition while other threads signal when complete the condition.
Example:
A parent thread might wish to check whether a child thread has completed, i.e., join()
Condition variable: queue of waiting threads
Waiting on the condition
An explicit queue that threads can put themselves on when some state of execution is not as desired.
Signaling on the condition
Some other thread, when it changes said state, can wake one or more of those waiting threads and allow
them to continue.
Condition Variables - Definition and Routines
Declare condition variable Pthread_cond_t c;
Proper initialization is required.
Pthread_cond_t c = PTHREAD_COND_INITIALZER;
Two operations: wait() and signal()
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m); // wait()
pthread_cond_signal(pthread_cond_t *c); // signal()
wait()
Used to put the calling thread to sleep on a condition
The wait() call takes a mutex as a parameter.
o assumes the lock is held when wait() is called
o releases the lock + puts caller to sleep (atomically)
o when awoken, reacquires lock before returning to the caller.
signal()
wake a single sleeping thread waiting on the condition (if >= 1 thread is waiting)
if there is no waiting thread, just return, doing not
Thread wait/signal Routine
Declare and initialize condition variable, lock, and a state variable
int ready = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
A thread calling wait() routine:
Check the state variable, if not ready, call wait()
The wait call releases the lock when putting said caller to sleep.
Before returning after being woken, the wait call re-acquire the lock.
A thread calling signal() routine:
Set the state variable and to wake signal the waiting thread
Use a lock to ensure no race between interacting with state and wait/signal
void thread_wait() { void thread_signal() {
Pthread_mutex_lock(&m); Pthread_mutex_lock(&m);
while (ready == 0) ready = 1;
Pthread_cond_wait(&c, &m); Pthread_cond_signal(&c);
Pthread_mutex_unlock(&m); Pthread_mutex_unlock(&m);
} }
Thread wait/signal Routine (Cont.)
The waiting thread re-checks the condition in a while loop, instead of a simple if statement.
void thread_wait() {
Pthread_mutex_lock(&m);
while (ready == 0)
Pthread_cond_wait(&c, &m);
Pthread_mutex_unlock(&m);
}
Some pthread implementations could spuriously wake up a waiting thread
Without rechecking, the waiting thread will continue thinking that the condition has
changed even though it has not.
Compiling and Running
To compile the previous example programs, you must include the header pthread.h
Explicitly link with the pthreads library, by adding the –pthread flag.
prompt> gcc –o main main.c –Wall -pthread
For more information,
man –k pthread