12 - Thread Synchronization

Class: CSCE-313


Notes:

Atomic Operations

An atomic operation either "runs to completion" or "not at all".

Each instruction in the x86 instruction set is atomic. An instruction fully finishes before the current process/thread can be preempted/interrupted.

Notes:

Synchronization Variable - Lock to Provide Mutual Exclusion

General idea:
00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-38.png270
00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-39.png270

Notes:

What is a mutex?

Notes:

What is a critical section?

Notes:

Mutex thread synchronization

Thread 0 Thread 1
mutex lock mutex lock
critical section critical section
mutex unlock mutex unlock

Notes:

Mutex thread synchronization

image-93.png

Mutex in C++ for Thread Safety

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

void func(int * p, int x, mutex *m) {
    // increment *p x times
    m->lock();
    for (int i = 0; i < x; i++) {
        * p = 'p + 1;
    }
    m->unlock();
}

int main(int ac, char ** av) {
    int data = 0;
    int times = atol(av[1]);
    mutex m;
    
    thread t1(func, &data, times, &m);
    thread t2(func, &data, times, &m);
    t1.join(); // pauses until first finishes
    t2.join(); // pauses until second finishes
    cout << "data = " << data << endl;
}

Notes:

Now when we do:

./a.out 1000000
data = 2000000

Mutex in C++ -- Finer Locking

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
void func (int * p, int x, mutex* m) {
    // increment *p x times
    for (int i=0; i<x, i++){
	    m->lock();    // Critical section: This is more fine-grained,
	    *p = *p + 1;  // produces the same
	    m->unlock()
    }
}
int main(int ac, char** av) {
    int data = 0;
    int times = atoi (av [1]);
    mutex m;
    
    thread t1 (func, &data, times, &m);
    thread t2 (func, &data, times, &m);
    
    t1.join(); // pauses until first finishes
    t2.join(); // pauses until second finishes
    cout << "data = " < < data < < endl;
}
osboxes@osboxes:~/$ /a.out 10000000
data = 20000000
osboxes@osboxes:~/$ ./a.out 10000000
data = 20000000
osboxes@osboxes:*/$ /a.out 10000000
data=20000000

Notes:

When we do:

./a.out 1000000
data = 2000000

Timeline of Coarse/Fine Grained Locking

image-94.png463

Notes:

Producer-Consumer Synchronization

Simple mutual-exclusion is not adequate for many problems. Mutual-exclusion provides isolation, but not execution ordering.

For e.g.,

image-95.png456

Notes:

Producer-Consumer: problems

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-27.png550

Notes:

Solution:
You might attempt to solve multiple consumer’s race condition by trying to do
the following:

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-28.png500

This leads to a deadlock when list is empty, because “consumer” is blocked
with mutex held.

It would be nice if the lock could somehow be transferred to another thread while one consumer thread is “stuck”.

Notes:

What if...

Rewritten notes:

What we need is "condition variables"

A condition variable (CV) is

wait(cond_t *cv, mutex_t *lock)
signal(cond_t *cv)

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-29.png473

Notes:

Rewritten notes:

Example:

void Consumer () {
    mutex.lock();
    while (list.size() == 0) {
    mutex.unlock();
    // sleep until list = empty;
    mutex.lock();
    }
    int data = list.front();
    list.pop();
    mutex.unlock();
}
void Producer () {
    int data = 0;
    while (true) {
        data = produce_data();
        mutex.lock();
        list.push_back(data);
        mutex.unlock();
        nofify_consumer();
    }
}

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-30.png

Notes:

"Broadcasting" on a condition variable

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-31.png457x190

Notes:

Rewritten notes:

A detour through the C++ Thread API

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-32.png620

#include <iostream>
#include <string>
#include <thread>
void hello_worldstring s {
    cout << s << std::endl;
}
class HelloWorld {
public:
    void hello_worldstring s {
        cout << s << " from " <<
        "HelloWorld.hello_world." << endl;
    }
    void operator string s) const { (2
        cout << s << "from " <<
        " HelloWorld.operator()." << endl;
    }
};
int main() {
    std::string s = "Hello World";
    thread t1(hello_world, s); (1)
    HelloWorld hw;
    thread t2(hw, s);
    thread t3hello_world, &hw, s;
    std::thread t4([s] {
        cout << s << "from a lambda." << endl;
    });
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

Notes:

Producer-Consumer: Correctly

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-33.png

Step 1: Declare a condition variable and a mutex

Step 2: The producer produces data in a way such that the consumer can notice e.g., pushing into the vector that consumer checks

Step 3: Calls notify_one/all() on the condition to wake up the consumers so that they can check again

Notes:

Produce-Consumer

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-34.png697

Step 3: The consumer(s) do the following:

An example producer-consumer scenario

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-35.png459x211

Producer-consumer with bounded size?

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Lecture/Visual Aids/image-36.png458

Notes:

Producer-Consumer in C

#define QUEUE_SZ 10
int queue[QUEUE_SZ];
int queue_hd = 0, queue_tl = 0, queue_nitems = 0;

pthread_mutex_t m;
pthread_cond_t can_write, can_read;

void*producer(void *pv) {
	static int counter = 1;
	
	while (1) {
		pthread_mutex_lock(&m);
		while(queue_nitems == QUEUE_SZ) {
			...
		}
		...
	}
}

Notes:

Cond Var I

int done = 0;

void *child (void *arg) {
	printf ("child\n");
	done = 1;
	return NULL;
}

int main (int argc, char *argv[]) {
	pthread_t p;
	printf ("parent: begin\n");
	pthread_create (&p, 0, child, 0);
	while (done == 0);
	printf ("parent: end\n");
	return 0;
}

Notes:

Cond Var II

void *child (void *arg) {
	printf ("child\n");
	done = 1;
	pthread_cond_signal(&cv);
	return NULL;
}

int main (int argc, char *argv[]) {
	pthread_t p;
	printf ("parent: begin\n");
	pthread_create(&p, 0, child, 0);
	while(done == 0) {
		pthread_cond_wait(&cv, &m);
	}
	printf("parent: end\n");
	return 0;
}

Notes:

Cond Var III

void *child (void *arg) {
	printf ("child\n");
	pthread_mutex_lock(&m);
	pthread_cond_signal(&c);
	pthread_mutex_unlock(&m);
	return NULL;
}

int main (int argc, char *argv[]) {
	pthread_t p;
	printf ("parent: begin\n");
	pthread_create (&p, 0, child, 0);
	pthread_mutex_lock(&m);
	pthread_cond_wait(&c, &m);
	pthread_mutex_unlock(&m);
	printf ("parent: end\n");
	return 0;
}

Notes:

Cond Var IV (works)

void *child (void *arg) {
    printf ("child\n");
    othread_mutex_lock(&m);
    done = 1;
    pthread_cond_signal(&c);
    pthread_mutex_unlock(&m);
    return NULL;
}
int main (int argc, char *argv[]) {
    pthread_t p;
    printf ("parent : begin\n");
    pthread_create (&p, 0, child, 0);
    othread_mutex_lock(&m);
    while (done == 0)
        pthread_cond_wait(&c, &m);
    pthread_mutex_unlock(&m);
    printf ("parent: end\n");
    return 0;
}

Notes:

What if the two operations are not atomic?

release(mutex);
add yourself to the Q;

Lost update problem