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:
image-38.png270
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

...

Mutex in C++ for Thread Safety

...

...

Notes:

Now when we do:

./a.out 1000000
data = 2000000
...

Mutex in C++ -- Finer Locking

...

Notes:

When we do:

./a.out 1000000
data = 2000000

Timeline of Coarse/Fine Grained Locking

Notes:

Producer-Consumer Synchronization

...

Notes:

Producer-Consumer: problems

image-27.png550

Notes:

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

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...

What we need is "condition variables"

A condition variable (CV) is

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

image-29.png473

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();
    }
}

image-30.png

Notes:

"Broadcasting" on a condition variable

image-31.png457x190

Notes:

A detour through the C++ Thread API

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:

  1. When your thread object is created, you are already runnable!
  2. Having a method in a class that implements the operator method
    • Then another way to create a thread is by giving an instance of that class and what is going to happen is that the function that is going to be invoked is the operator?
    • The way you provide the self is to provide the pointer to the argument
    • For us the most important use is for lambdas (anonymous function)
    • This anonymous function takes no arguments and captures the variable s from its scope
      • C++ did a slightly different design to lambdas
      • You will copy by value
        • Captures s within the scope of you lambda

Producer-Consumer: Correctly

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

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) {
			...
		}
		...
	}
}

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

...