Synchronization primitives: Mutex and Semaphore (Condition Variables) - Article 11

Hi Folks,

In previous articles, we have understood the synchronization constructs Mutex and Semaphore. Semaphore is a superior construct compared to a Mutex. Mutex is just a lock where as Semaphore can be used to solve variety of problems.

I have explained Mutex & Semaphore using pseudo code and in recent articles, I have provided real PThreads & Mutex code which can be executed & tested in Linux. Going forward, we shall explore Condition Variables & the differences between Mutex & Semaphore. We shall also write some real Semaphore code in Linux. We shall also solve few more synchronization problems & conclude Mutex & Semaphore.

In this article, let's explore the concept of Condition Variable.

-> Mutex is just a locking mechanism.
-> Semaphore can be used as a lock or as a signalling mechanism.

Now, how can we use Mutex to solve serialization problems? That's where Condition Variables come into the picture. When we use the combination of Mutex & Condition Variables together, we can solve more synchronization problems.
Look at the sample program below:

void *sub_thread(void *arg) {
    printf("SUB THREAD\n");
    // How to signal main thread?
    return NULL;
}

int main(int argc, char *argv[]) {
    printf("MAIN THREAD: START\n");
    pthread_t t;
    Pthread_create(&t, NULL, sub_thread, NULL); // create child
    // How to wait for sub thread?
    printf("MAIN THREAD: FINISH\n");
    return 0;
}

Right now, many questions might be popping up in your mind,

-> Why can't we use pthread_join,  yes we can use this but above is just an example to showcase the use of condition variables.
-> When to use Mutex and when to use Semaphore when both are solving all the problems, let's save this for another day :-).

The expected output is,

MAIN THREAD: START
SUB THREAD
MAIN THREAD: FINISH

As we know, the above is not guaranteed. To solve this, we can use a global variable:

int signal_done;

void *sub_thread(void *arg) {
    printf("SUB THREAD\n");
    signal_done = 1;
    return NULL;
}

int main(int argc, char *argv[]) {
    printf("MAIN THREAD: START\n");
    pthread_t t;
    Pthread_create(&t, NULL, sub_thread, NULL); // create child
    while(signal_done == 0)
        ; // Spin & waste CPU cycles
    printf("MAIN THREAD: FINISH\n");
    return 0;
}

The above code wastes CPU cycles & global variable also needs to be protected which makes it even more inefficient.

A condition variable is an explicit queue that threads can put themselves on when some condition is not as desired by waiting on the condition. some other thread, when it changes said condition, can then wake one (or more) of those waiting threads and thus allow them to continue by signalling on the condition.

Following is the code:

int signal_done;

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;

void thread_exit() {
    pthread_mutex_lock(&m);
    signal_done = 1;
    pthread_cond_signal(&c);
    pthread_mutex_unlock(&m);
}

void *sub_thread(void *arg) {
    printf("SUB THREAD\n");
    thread_exit();
    return NULL;
}

void thread_join() {
    pthread_mutex_lock(&m);
    while (signal_done == 0)
        pthread_cond_wait(&c, &m);
    pthread_mutex_unlock(&m);
}

int main(int argc, char *argv[]) {
    printf("MAIN THREAD: START\n");
    pthread_t t;
    pthread_create(&t, NULL, sub_thread, NULL); // create child
    thread_join(); // Our own version of pthread_join()
    printf("MAIN THREAD: FINISH\n");
    return 0;
}

Let's understand the code. The main thread waits for the condition signal_done and goes to sleep. No CPU cycles are wasted. The sub thread changes the condition & signals main thread after which the main thread wakes up.

NOTE: The wait & signal is built around signal_done variable or it's value (or condition). That's the condition here. The main point one needs to understand is that wait() & signal() are explicit & not automatic. i.e., it's not like signal_done changes & automatically threads wake up. Why? Because if such a implementation is done, we need to explicitly check the condition again which wastes CPU cycles & that's the problem we are trying to solve. So, once the condition changes, some thread needs to wake up other threads.

In the coming articles, we shall go into more details.

Comments

Post a Comment

Popular posts from this blog

Synchronization primitives: Mutex and Semaphore (Serialization & Mutex problems using Semaphore) - Article 6

Synchronization primitives: Mutex and Semaphore (Readers & Writers problem) - Article 8

Copy constructor in C++ (Part 2)