Please note that this lab is subject to change.
====== Lab 11 - Threading Part 2 ======
==== Trylock Example ====
Previously we covered **pthread_mutex_lock** and **pthread_mutex_unlock**.
There is also a third locking mechanism: **pthread_mutex_trylock**.
Try lock will not wait until the lock is available, it will return immediately.
It will return zero if it acquired the lock, else something else if not (specifically an error code, most commonly a number meaning "Lock is busy").
**Exercise 1:** Modify the code from last week.
- Create an array of several numbers.
- Create a thread for each value.
- Create a global “maximum” variable.
- The threads will test if their number is bigger than the maximum, and if so replace maximum with their value.
- At the end of the program, print “the biggest number was x”.
Change the locks to use **pthread_mutex_trylock** instead of **pthread_mutex_lock**.
If the lock cannot be acquired, wait/sleep and try again.
Sleeping is available in ''#include '' library as the functions ''sleep(int seconds)'' and ''usleep(int microseconds)''.
==== Conditional Variables ====
Conditional variables are often used in multithreaded programming to synchronize threads. They allow threads to wait for certain conditions to be met. A conditional variable works with a mutex to ensure safe access and modification of shared resources.
* ''pthread_cond_t'' - variable type
* ''int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)'' - function to wait/sleep on condition to be satisfied
* ''int pthread_cond_signal(pthread_cond_t *cond)'' - signalling function to wake up waiting threads
==== Semaphore ====
Different to a mutex. It should be used to multithread/multiprocess signalling rather than shared resource protection. One should see it as an n-member resource access or just as a signalling tool. It does not guarantee any exclusivity (only in a simple binary configuration).
It is implemented in the POSIX library ''semaphore.h'' with docs [[https://pubs.opengroup.org/onlinepubs/7908799/xsh/semaphore.h.html|here]]
==== Producer-Consumer Model ====
Threads are obviously used where we want multiple things to happen at the same time.
Therefore they are commonly employed for slow processes like in the following schemes:
* 1 thread doing heavy calculation, 1 thread can interact with the user - threading means the program remains responsive to user input
* 5 threads reading a file each, correcting spelling errors, and then saving the result - threading means all 5 can be done at the same time on different CPU cores, rather than one after the other
* 15 producer threads reading reading data and placing it in buffers, and 3 consumer threads reading these buffers and doing something with it - threading means that we can have many producers ensuring the buffers are always full for the consumers, the consumers can easily check many buffers and have the data ready for them when they need it
The final layout is called a producer-consumer model.
But is this a good idea?
Hint: What happens if no locks are available?
Think about how we can solve this.
**Exercise 2:** Create 3 threads which will generate a random integer *once per second*. The threads will loop forever. These will be our producers. They will safely put the random number into one global buffer. Use ''#include '' and the function ''int rand()''.
There will then be three other threads, our consumers, which will check if there is data in the buffer. If so, they will grab the result, and calculate if it is a prime number, and output the result.
int is_prime(int num) // yes=1, no=0
{
if (num <= 1) return 0;
if (num % 2 == 0 && num > 2) return 0;
for(int i = 3; i < floor(sqrt(num)); i+= 2)
{
if (num % i == 0)
return 0;
}
return 1;
}
==== Extension Tasks ====
* Consider coding in such a way that we can easily change the number of producers/buffers/consumers with a single variable (each). ie. int nProducerThreads = 15;
* Create a global variable that all threads will check regularly. If it is set to a specific value (called a sentinel), the threads will close. After creating the producers and consumers, the main thread can then wait for the user to press enter (scanf) before closing. When it closes, it will set the global variable to the sentinel so each thread will exit, join them all, and then return zero.
==== Example: Conditional Variables ====
#include
#include
#include
#include
// Shared data
int data_ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
sleep(1); // Simulate work
pthread_mutex_lock(&mutex);
data_ready = 1; // Data is ready
printf("Producer: Data is ready!\n");
pthread_cond_signal(&cond); // Signal the consumer
pthread_mutex_unlock(&mutex);
return NULL;
}
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (data_ready == 0) { // Wait for the condition to be true
printf("Consumer: Waiting for data...\n");
pthread_cond_wait(&cond, &mutex);
}
printf("Consumer: Consuming the data!\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
==== Example: Semaphore ====
#include
#include
#include
#include
sem_t S;
void* thread(void* arg)
{
//wait
sem_wait(&S);
printf("\nEntered..\n");
//critical section
sleep(4);
//signal
printf("\nJust Exiting...\n");
sem_post(&S);
}
int main()
{
sem_init(&S, 0, 1);
pthread_t t1,t2;
pthread_create(&t1,NULL,thread,NULL);
sleep(2);
pthread_create(&t2,NULL,thread,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
sem_destroy(&S);
return 0;
}