====== 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 [[https://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread.h.html| 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).
* //**thread**//: the thread you want to wait for
* //**value_ptr**//: pointer to a ''void*'' variable where the thread's return value will be stored. Can be ''NULL'' if you don't care about the return value.
++++
**Example:**
#include
#include
#include
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
#include
#include
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**:
* Does simply passing a pointer to the variable ''i'' work? Why?
* How could you modify the code so that each thread reliably prints its own index? (//malloc/array of integers//)
----
===== 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
#include
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
* gets its number from the array
* compares it with ''globalMax'' and if it is larger, updates ''globalMax''
//When comparing the values and updating the maximum value, use a **mutex**.//
++++ Code to copy|
#include
#include
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
#include
#include
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
#include
#include
#include
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;
}
++++