Lab 10 - Threading Part 1
A process is an independently executing program with its own memory space. Processes are isolated from each other and do not share memory by default (safer, but makes data exchange harder).
A thread is a line of execution inside a process. Threads in the same process share memory and resources (can communicate easily but need synchronisation tools to avoid corrupting shared data).
In C, we will be using threads via the POSIX pthread library (see documentation), which provides the necessary functions to create and manage threads.
Thread handling
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
Click for explanation
Creates a new thread within a process.
thread: pointer to a pthread_t variable, where the system stores the thread handle
attr: pointer to a pthread_attr_t structure specifying thread attributes (stack size, detached/joinable, scheduling policy, etc.). If NULL, the default attributes are used.
start_routine: pointer to the function the thread will execute, must have the signature void* func(void *)
arg: pointer to any data you want to pass to the thread function. Can be NULL if no argument is needed.
int pthread_join(pthread_t thread, void **value_ptr);
Click for explanation
Suspends execution of the calling thread until the target thread terminates (unless the target thread has already terminated).
Example:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* myThreadFunction(void *arguments){
printf("Thread running.\n");
return NULL;
}
int main() {
pthread_t thread1;
pthread_create(&thread1, NULL, myThreadFunction, NULL);
printf("Main Thread.\n");
pthread_join(thread1, NULL);
return 0;
}
To compile, you will need to add -pthread to your compiler instructions.
Exercise 1
Copy and paste the code above.
What would happen if we didn't use pthread_join()? Try it.
Exercise 2
Code to copy
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* printIndex(void* arg) {
int* index = (int*) arg;
printf("Thread index: %d\n", *index);
return NULL;
}
int main() {
pthread_t threads[5];
for (int i = 0; i < 5; i++) {
// TODO: create a thread using pthread_create
}
for (int i = 0; i < 5; i++) {
// TODO: join each thread
}
return 0;
}
Fill in the TODO to create 5 threads in a loop.
Each thread should print its own index (0-4).
Hint: The final parameter of pthread_create() is a pointer which is passed to your function, so you will need to pass the ID number to the thread in this form.
Questions to consider:
Mutexes
We can use mutexes to control access to shared resources, such as variables.
Mutex is a lock that ensures only one thread is modifying the protected data at a time. Only the thread that locks the mutex can unlock it.
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
Click for explanation
Initialises the mutex referenced by mutex with attributes specified by attr.
mutex: pointer to a pthread_mutex_t variable you want to initialize.
attr: pointer to a pthread_mutexattr_t structure specifying mutex attributes. If you don’t need any special attributes, pass NULL to use the default attributes.
int pthread_mutex_lock(pthread_mutex_t *mutex);
Click for explanation
Locks the mutex object referenced by mutex.
If the mutex is already locked, the calling thread blocks until the mutex becomes available. This operation returns with the mutex object referenced by mutex in the locked state with the calling thread as its owner.
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Click for explanation
Releases the mutex object referenced by mutex.
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Click for explanation
Destroys the mutex object referenced by mutex.
Example:
The following code shows how a global variable can be modified safely using mutex:
#include <pthread.h>
#include <stdio.h>
int someVariable = 0; // shared global variable
pthread_mutex_t lock; // mutex to protect it
void* ThreadFunction(void* arg) {
int* value = (int*) arg; // cast the generic void* argument to int* so we can use it
// Lock the mutex: only one thread can enter this section at a time
pthread_mutex_lock(&lock);
// Critical section: safe modification of shared variable
someVariable = *value;
printf("Thread updated someVariable to %d\n", someVariable);
// Unlock the mutex so other threads can enter
pthread_mutex_unlock(&lock);
return NULL;
}
int main() {
pthread_t t1, t2;
// Initialize the mutex before using it
pthread_mutex_init(&lock, NULL);
// Create two threads that both try to modify someVariable
int a = 1;
int b = 2;
pthread_create(&t1, NULL, ThreadFunction, &a);
pthread_create(&t2, NULL, ThreadFunction, &b);
// Wait for both threads to finish
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final value of someVariable: %d\n", someVariable);
// Clean up the mutex
pthread_mutex_destroy(&lock);
return 0;
}
Exercise 3
Task 3.1
Use multiple threads to find the maximum in an array, updating a shared global variable protected by a mutex.
Create one thread per element of the array
Each thread
When comparing the values and updating the maximum value, use a mutex.
Code to copy
#include <pthread.h>
#include <stdio.h>
int numbers[] = {4, 7, 1, 9, 2};
int globalMax = 0;
pthread_mutex_t lock;
void* updateMax(void* arg) {
int* value = (int*) arg; // cast the generic void* argument to int*
// TODO: lock mutex
// TODO: update globalMax if *value > globalMax
// TODO: unlock mutex
return NULL;
}
int main() {
pthread_t threads[5];
pthread_mutex_init(&lock, NULL);
for (int i = 0; i < 5; i++) {
// TODO: create a thread for numbers[i]
}
for (int i = 0; i < 5; i++) {
// TODO: join each thread
}
printf("Final global maximum: %d\n", globalMax);
pthread_mutex_destroy(&lock);
return 0;
}
Task 3.2
In cases when we can't (or don't want to) use global variables, we might need to pass more than one value to the thread entry function.
The function pthread_create() has a single void* arg argument - we can bundle everything into a custom struct and pass a pointer to that struct.
Reuse the code from Task 3.1
Define and use struct ThreadArgs to carry: the value, a pointer to shared maximum and a pointer to the mutex
Instead of passing a pointer to a single number, pass a pointer to a ThreadArgs struct into the thread function.
Code to copy
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int value; // number to consider
int* globalMaxPtr; // pointer to the shared maximum
pthread_mutex_t* lockPtr; // pointer to the mutex
} ThreadArgs;
void* updateMax(void* arg) {
ThreadArgs* args = (ThreadArgs*) arg; // cast void* to our struct type
// TODO: lock mutex
// TODO: update maximum
// TODO: unlock mutex
// TODO: free args if they were allocated with malloc
return NULL;
}
int main() {
int numbers[] = {4, 7, 1, 9, 2};
int globalMax = 0;
pthread_mutex_t lock;
pthread_t threads[5];
pthread_mutex_init(&lock, NULL);
for (int i = 0; i < 5; i++) {
// TODO: allocate ThreadArgs (e.g. with malloc)
// TODO: fill value, globalMaxPtr and lockPtr
// TODO: create thread, passing the ThreadArgs pointer as argument
}
for (int i = 0; i < 5; i++) {
// TODO: join each thread
}
printf("Final global maximum: %d\n", globalMax);
pthread_mutex_destroy(&lock);
return 0;
}
Task 3.3
Finally, extend the code so that you can read the exit value returned by each thread.
We can give pthread_join() a pointer to a pointer to a variable, where the return value of the thread function will be stored.
Reuse the code from Task 3.2
When joining threads, pass a pointer to a void* variable to pthread_join().
In thread function, allocate memory for the return value, store the value there and return that pointer.
The return value is a boolean representing whether the global max value was updated or not.
Code to copy
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
int value; // number to consider
int* globalMaxPtr; // pointer to the shared maximum
pthread_mutex_t* lockPtr; // pointer to the mutex
} ThreadArgs;
void* updateMax(void* arg) {
ThreadArgs* args = (ThreadArgs*) arg; // cast void* to our struct type
// TODO: lock mutex
// TODO: update maximum
// TODO: unlock mutex
// TODO: free args if they were allocated with malloc
// TODO: allocate memory for the result and return it
// bool* result = ...
return result; // returned as void*
}
int main() {
int numbers[] = {4, 7, 1, 9, 2};
int globalMax = 0;
pthread_mutex_t lock;
pthread_t threads[5];
pthread_mutex_init(&lock, NULL);
for (int i = 0; i < 5; i++) {
// TODO: allocate ThreadArgs (e.g. with malloc)
// TODO: fill value, globalMaxPtr and lockPtr
// TODO: create thread, passing the ThreadArgs pointer as argument
}
for (int i = 0; i < 5; i++) {
void *ret;
// TODO: join each thread and get ret
bool* result = (bool*) ret; // cast ret back to int*
printf("Thread %d returned exit value %d\n", numbers[i], *result);
free(result);
}
printf("Final global maximum: %d\n", globalMax);
pthread_mutex_destroy(&lock);
return 0;
}