Skip to content

DA1. Multiple Thread Execution

Statement

  • Discuss multiple threads executing the code (Figure 26.6, Chapter 26 Concurrency: An Introduction) which can result in a race condition, which is a critical section.
  • A critical section is a piece of code that accesses a shared variable (or more generally, a shared resource) and must not be concurrently executed by more.
  • How do you solve this issue when it occurs?

Answer

  • the code mentioned in the question (Figure 26.6, Chapter 26 Concurrency: An Introduction) is doing the following:
    1. Creating two separate threads using pthread_create() function.
    2. Passing different arguments to each thread, 1 and 2 respectively; however, both threads are accessing the same variable counter and incrementing it by 1 (million times each).
    3. Waiting for both threads to finish using pthread_join() function; then exiting.
  • The counter variable is the critical section in this code since it is accessed and modified by two parallel threads.
  • The luck and the type of schedular used by the OS can affect the result of the execution of this code; however, the result will be different every time you run it making the result non-deterministic.
  • The two threads may run:
    • Arbitrarily, in any order; thread 1 may finish first, or thread 2 may finish first.
    • They may pull the same value from the counter variable, and then increment it by 1 and then write it back to the counter variable; which causes the counter to increase by 1 instead of 2.
    • Any issue may occur during one of the threads execution, which makes the results unpredictable.
  • To solve this issue we can use two techniques:

    • Locks:
      1. we defined a lock variable and pass it to both threads; then when the first thread (can be A or B) accesses the critical section, it locks the lock variable and unlocks it once finished writing back to the counter variable.
      2. Once the second thread starts execution, it checks if the lock variable is locked or not; if it is locked, it waits until the lock is unlocked, then it locks it and unlocks it once finished writing back to the counter variable.
    • Signals (aka, semaphores or condition variables):
      1. we defined a signal variable and pass it to both threads; then when the first thread (can be A or B) accesses the critical section, it decrements the signal variable and increments it once finished writing back to the counter variable.
      2. Once the second thread starts execution, it checks if the signal variable is 0 or not; if it is 0, it waits until the signal variable is incremented, then it decrements it and increments it once finished writing back to the counter variable.
  • Below is the code for the first technique (locks):

#include <stdio.h>
#include <pthread.h>
#include "common.h"
#include "common_threads.h"

static volatile int counter = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 1. initialize the lock variable

 void *mythread(void *arg) {
     printf("%s: begin\n", (char *) arg);
     pthread_mutex_lock(&lock); // 2. lock the lock variable or keep waiting until you acquire the lock
     int i;
     for (i = 0; i < 1e7; i++) {
     counter = counter + 1;
     }
     printf("%s: done\n", (char *) arg);
     pthread_mutex_unlock(&lock); // 3. unlock  when done
     return NULL;
 }


 int main(int argc, char *argv[]) {
     pthread_t p1, p2;
     printf("main: begin (counter = %d)\n", counter);
     Pthread_create(&p1, NULL, mythread, "A");
     Pthread_create(&p2, NULL, mythread, "B");

     // join waits for the threads to finish
     Pthread_join(p1, NULL);
     Pthread_join(p2, NULL);
     printf("main: done with both (counter = %d)\n",
     counter);
     return 0;
 }
  • Below is the code for the second technique (signals):
#include <stdio.h>
#include <pthread.h>
#include "common.h"
#include "common_threads.h"

static volatile int counter = 0;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 1. initialize the condition helper variable
int signal = 1; // 2. initialize the signal variable

 void *mythread(void *arg) {
     printf("%s: begin\n", (char *) arg);
     while (signal == 0) { // 3. check if the signal variable is 0 or not
        pthread_cond_wait(&cond); // 4. if it is 0, wait until the signal variable is incremented
     }
     signal = 0; // 5. decrement the signal variable, preventing other threads from accessing the critical section
     int i;
     for (i = 0; i < 1e7; i++) {
     counter = counter + 1;
     }
     printf("%s: done\n", (char *) arg);
     signal = 1; // 6. increment the signal variable, allowing other threads to access the critical section
     pthread_cond_signal(&cond); // 7. signal the other thread that the critical section is free
     return NULL;
 }


 int main(int argc, char *argv[]) {
     pthread_t p1, p2;
     printf("main: begin (counter = %d)\n", counter);
     Pthread_create(&p1, NULL, mythread, "A");
     Pthread_create(&p2, NULL, mythread, "B");

     // join waits for the threads to finish
     Pthread_join(p1, NULL);
     Pthread_join(p2, NULL);
     printf("main: done with both (counter = %d)\n",
     counter);
     return 0;
 }

References