| Previous Lecture | Lecture 15 | Next Lecture | 
Lecture 15, Mon 03/11
Threads
Threads
- A decent tutorial covering a lot of details on C++ threads: https://thispointer.com/c-11-multithreading-part-1-three-different-ways-to-create-threads/
Recall our discussion on processes
- A computer program in execution.
- A process contains the memory and state of the program being run on a computer.
- Threads are different than processes such that:
    - A program unit that is executed independently of other parts of your program.
- A process may create many threads
        - An OS manages threads similar to a process, BUT
            - threads shares the memory space of the main process.
 
 
- An OS manages threads similar to a process, BUT
            
- A C++ application starts with a single main thread.
 
Threads and hardware
- For multi-core processors, you can design your program such that several parts of it work concurrently.
- Each processor (core) runs instructions in parallel.
    - For single-core architectures, it provides the illusion that something is working concurrently, but the processor is shared among all programs.
- A a single thread can only be executed on a single core at any given time.
 
Concurrency
- Concurrency is important in computer science.
    - For example, working on “parts of a problem” concurrently can improve performance if the parts can be done independently.
- Many applications use concurrency for performance reasons, for example:
        - Web servers process requests concurrently
- We browsers fetch / render images concurrently
 
- Embarrassingly Parallel problems are usually a good fit for concurrent computing
        - Monte-carlo simulations are embarrassingly parallel – the result is the average (expected) scenario based on independent executions.
 
 
Example of creating a thread
#include <iostream>
#include <thread>
#include <unistd.h>
#include <string>
using namespace std;
void callback1(string val, int num) {
	cout << "----" << endl;
	cout << "Thread with str val: " << val << endl;
	cout << "num: " << num << endl;
	cout << "Thread id: " << this_thread::get_id() << endl;
	sleep(5);
	cout << "----" << endl;
}
int main() {
	std::thread t1(callback1, "one", 1); // add args to function here
	cout << "t1 id: " << t1.get_id() << endl;
	cout << "Done." << endl;
	return 0;
}
- The above program terminates abnormally because when a thread is destructed, it must either be joined back to the parent thread, or the parent thread needs to detach it.
- By default, the main thread does not wait for any thread to finish execution.
- A thread may wait for another thread to terminate execution before continuing its own execution by calling .join() on the thread.
- A thread may also call .detach on a thread to allow it to run independently.
    - In the example above, main terminates before t1 finishes
        - Since main terminates and t1 is destructed from the stack, the process terminates (std::terminate) because t1 was not joined back to the main thread, or t1 was not detached from the main thread.
 
 
- In the example above, main terminates before t1 finishes
        
- The code below shows how to either join or detach the thread in order to avoid abnormal termination:
int main() {
	std::thread t1(callback1, "one", 1); // add args to function here
	cout << "t1 id: " << t1.get_id() << endl;
	// main thread waits for t1 to finish execution before resuming
	// its own execution
	//t1.join();
	
	// or main thread detaches t1 and does not wait for t1 to finish.
	// Since t1 is not detached, program will not terminate abnormally.
	//t1.detach();
	cout << "Done." << endl;
	return 0;
}
Example of a thead creating a thread
- See if you can trace the output and understand the order of execution:
void callback2(bool create) {
	cout << "***" << endl;
	cout << "Starting thread: " << this_thread::get_id() << endl;
	sleep(5);
	if (create) {
		thread subt1(callback2, false);
		cout << "Creating thread: " << subt1.get_id() << endl;
		subt1.join();
	}
	cout << "Finishing thread: " << this_thread::get_id() << endl;
	cout << "***" << endl;
}
int main() {
	thread t2(callback2, true);
	cout << "main::t2 id: " << t2.get_id() << endl;
	t2.join();
	cout << "Done." << endl;
	return 0;
}
Race Conditions
- Careful programming has to be taken into consideration when dealing with multiple threads
- If threads share memory with each other (for example, an object), read / write order conflicts can happen.
- A high-level scenario:
    - A multi-threaded bank software program depends on atomic transactions
        - Atomic transaction means either everything is executed or nothing is.
 
- Imagine a joint bank account contains a $100 balance
        - Person x deposits $10 and Person y withdraws $10 at the same time.
- Person y obtains the current balance of $100 before withdrawing $10.
- The OS switches to Person x after Person y obtains the current balance of $100.
- Person x reads the current balance of $100 and deposits $10 (current balance = $110)
- The OS switches back to Person y and finishes withdrawing $10 using Person y’s current balance ($100), and updates the current balance to $90.
- Customer loses money … :(
 
 
- A multi-threaded bank software program depends on atomic transactions
        
Example of a Race Condition
# Makefile
bank:
	g++ -o main -std=c++11 main.cpp bank.cpp
// Bank.h
class Bank {
public:
	Bank();
	void deposit(double amount);
	double getBalance();
private:
	double totalBalance;
};
// Bank.cpp
#include "Bank.h"
Bank::Bank() {
	totalBalance = 0;
}
void Bank::deposit(double amount) {
	totalBalance += amount;
}
double Bank::getBalance() {
	return totalBalance;
}
#include <iostream>
#include <thread>
#include "Bank.h"
using namespace std;
void run(Bank* b) {
	for (int i = 0; i < 10000; i++) { b->deposit(10); }
}
int main() {
	Bank* bank = new Bank();
	thread t1(run, bank);
	thread t2(run, bank);
	t1.join();
	t2.join();
	cout << "---" << endl;
	cout << bank->getBalance() << endl;
	return 0;
}
- Expected balance is $200000 by the end of execution, but generally is not (unless we get lucky…).
Mutex (mutual exclusion)
- a multi-threaded locking mechanism that only allows a single thread to execute code.
    - Only one thread may acquire the lock at any given time.
- If a thread obtains a lock on a mutex, no other thread can access the code until the lock is released (unlocked).
        - One way to provide atomic transactions and avoid race conditions between threads.
- Another common way is to use Semaphores, but we won’t cover this construct in this class.
 
 
Updated code using a mutex object
// Bank.h
#include <mutex>
class Bank {
public:
	Bank();
	void deposit(double amount);
	double getBalance();
private:
	double totalBalance;
	std::mutex mutex;
};
// Bank.cpp
#include "Bank.h"
Bank::Bank() {
	totalBalance = 0;
}
void Bank::deposit(double amount) {
	mutex.lock();
	totalBalance += amount;
	mutex.unlock();
}
double Bank::getBalance() {
	return totalBalance;
}
#include <iostream>
#include <thread>
#include "Bank.h"
using namespace std;
void run(Bank* b) {
	for (int i = 0; i < 10000; i++) { b->deposit(10); }
}
int main() {
	Bank* bank = new Bank();
	thread t1(run, bank);
	thread t2(run, bank);
	t1.join();
	t2.join();
	cout << "---" << endl;
	cout << bank->getBalance() << endl;
	return 0;
}
- Using the mutex locking object, we now consistently have an ending balance of $200000.
Deadlock
- A situation where two (or more) threads are waiting on each other to release a resource, where each of the threads have a lock on the dependent resources.
- Execution is halted and cannot move forward in this situation.
