# Week 4 - PThreads

Last edited: 2024-09-04

# Additional reading

# Background

POSIX

PThreads

# Thread interface

The core data structure in the PThreads library is pthread_t which represents a thread. This can be created through:

int pthread_create(
	pthread_t *thread,
	const pthread_attr_t *attr,
	void * (*start_routine)(void *),
	void *arg
)
Void is C’s any type

The first argument thread will be filled with the thread once it is created. (We will come back to pthread_attr_t later - if you pass NULL you will get default behaviour.) The third argument start_routine is a function to call this arguments arg. It will return the status code of the operation.

This can be joined to the main thread using:

int pthread_join(
	pthread_t thread,
	void **status
)

where thread is the thread you want to join and status will be the return object. It will return the status code of the operation.

# Pthread attributes

The pthread_attr_t type controls the type of thread created with the properties:

  • Stack size,
  • Scheduling policy,
  • Priority,
  • System/process thread,
  • inheritance (if it should inherit attributes from the parent thread), and
  • joinable. The object can be created and destroyed through the following interface.
int pthread_attr_init(pthread_attr_t *attr)
int pthread_attr_destroy(pthread_attr_t *attr)

You can then set/get attribute values through the following function.

pthread_attr_{set/get}{attribute}

For example pthread_attr_setjoinable.

# Detachable threads

Threads that are created from a parent need to be joined by that parent in order for them to be cleaned up by the OS . Though if the parent thread exits before this happens it can create zombie threads that can not be cleaned up.

If you need to create a child thread that will outlive its parent then you can make it detachable. This means it is no longer needs to be joined to exit - it can instead use the following function.

void pthread_exit()

Threads can be made detachable using the joinable property or by calling detach from within the executing function.

pthread_attr_setjoinable(attr, PTHREAD_CREATE_DETACHED)
int pthread_detach()

# Example

#include <stdio.h>
#include <pthread.h>

void *foo (void *arg) {		/* thread main */
	printf("Foobar!\n");
	pthread_exit(NULL);
}

int main (void) {

	int i;
	pthread_t tid;

	pthread_attr_t attr;
	pthread_attr_init(&attr); // Required!!!
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
	pthread_create(&tid, &attr, foo, NULL);

	return 0;
}

When using Pthreads:

  • Include the pthread library #include <pthread.h>
  • Compile code using the pthreads flag: either -lpthread or -pthread.
  • Check the return value of pthread function calls in case of errors.

# Mutex

The PThreads library allows you to create and use mutexes .

pthread_mutex_t aMutex;
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

For example to implement safe lock and unlock using this would be:

list<int> my_list;
pthread_mutex_t list_mutex;

void safe_insert(int to_add){
	pthread_lock(list_mutex);
	my_list.insert(to_add);
	pthread_unlock(list_mutex);
}

You must initialise and cleanup mutexes.

int pthread_mutex_init(
	pthread_mutex_t *mutex,
	pthread_mutexattr_t *attr,
);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

Mutex have 3 attributes:

  • Mutex Type: can be changed to allow for deadlock detection.
  • Mutex Protocol: Determines the thread priority when holding the mutex.
  • Process-Shared Attribute: Determines if the mutex applies just to this process or can be shared.
  • Robustness Attribute: Determines if the mutex should be recoverable from a crash or not.

Another nice feature is the trylock which allows a thread to check if a mutex is free without getting blocked by it.

int pthread_mutex_trylock(pthread *mutex);

This will return 0 if it is free or EBUSY if not.

Lastly a couple reminders about working with mutexes:

  • Shared data must be accessed through a single mutex.
  • A mutex must be visible to all threads. i.e. declare them as global variables.
  • Globally order locks to prevent deadlock .
  • Always unlock the correct mutex.

# Conditional variables

PThreads supports the API we saw in the last lecture.

pthread_cond_t aCond;
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

Conditional variables need to be created and destroyed.

int pthread_cond_init(
	pthread_cond_t *cond,
	pthread_condattr_t *attr,
);
int pthread_cond_destroy(pthread_cond_t *cond);

The attributes you can provide are:

  • Process-Shared (PTHREAD_PROCESS_SHARED, PTHREAD_PROCESS_PRIVATE): Determines whether the condition variable is shared between processes or only within a single process.
  • Clock (CLOCK_REALTIME, CLOCK_MONOTONIC): Specifies which clock to use for timed wait operations on the condition variable.

Some notes on conditional variables :

  • Make sure any predicate change is followed by the required signal/broadcast.
  • If you do not know whether to use signal or broadcast, use broadcast (though check this later as you will lose performance).
  • To avoid spurious wakeups think about if you need to hold the mutex when doing your signal or broadcast.