Lecture 5

last time - showed example of how same code could produce different results when run different times. result is no longer deterministic

#include<iostream>
#include<thread>

void Greet(std::string name) {
    std::cout << "Hello from " << name << std::endl;
}

int main() {
    std::thread t1{ Greet, "t1" };
    std::thread t2{ Greet, "t2" };
    std::cout << "Hello from original" << std::endl;
}

this caused error when we ran:

to make computer happy, do it in the correct way:

#include<iostream>
#include<thread>

void Greet(std::string name) {
    std::cout << "Hello from " << name << std::endl;
}

int main() {
    std::thread t1{ Greet, "t1" };
    std::thread t2{ Greet, "t2" };
    std::cout << "Hello from original" << std::endl;

    // join: stop here, and wait for thread to terminate
    // (if thread is done already, no pauses are introduced)
    t1.join();
    t2.join();
}

the join method is the first example of synchronization: we are controlling the order of the thread termination

the above code does not cause any error messages, because we are terminating the threads explicitly, so no chance that threads are still running when main thread finishes

typical example of race condition:

#include<iostream>
#include<thread>

int glob{ 0 };

void Foo(std::string name) {
    for (int i{0}; i < 100'000; i++)
        glob++;
}

int main() {
    std::thread t1{ Foo };
    std::thread t2{ Foo };

    t1.join();
    t2.join();

    // less than 200k due to race condition
    std::cout << glob << std::endl;
}

global variable is shared between multiple threads, because it is in the data section

issue in above code: multiple threads accessing and modifying same variable

race condition: "data race" between two or more threads

# assume addr in t0
lw $s0, 0($t0)
addi $s0, $s0, 1
sw $s0, 0($t0)

so, definition of race condition: bad scenario where we corrupt shared resource/data because of simultaneous access by multiple threads or processes, when at least one tries to modify a shared resource

(however, if only read-only access, then not an issue - only an issue when modification occurs)

"nastiest kind of errors" because they are not reliably reproducible

if only one thread modifying and other reading, can still cause issues:

simple way to avoid race condition: "locks"

example: mutex locking:

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

int glob{ 0 };
std::mutex m;

void Foo(std::string name) {
    for (int i{0}; i < 100'000; i++) {
        // acquire the mutex m
        // scenario 1: if lock is free, close it
        //  and proceed further
        // scenario 2: if lock is not free,
        //  freeze and do not proceed further until
        //  lock is released (and when it is released,
        //  lock again)
        m.lock();
        // critical section of code
        glob++;
        // once finished, open the lock to allow others
        // to use the variable
        m.unlock();
    }
}

int main() {
    std::thread t1{ Foo };
    std::thread t2{ Foo };

    t1.join();
    t2.join();

    // should be 200k due to avoiding race condition
    std::cout << glob << std::endl;
}

above is not an efficient solution, but is a solution nonetheless

the above negates the benefit of threads because all work is done in series and not parallel