process
context
thread
threads(of the same process) run in a shared memory space, while processes run in separate memory spaces.
task level and data level parallelism
#include
#include
// 1. normal function
void foo(){
printf("Hello from foo \n");
}
class callable_class {
public:
// 2. function call operator
void operator()(){
printf("Hello from class with function call operator \n");
}
}
/*The main function act as the entry point for our program. Every program or
process in C++ is going to have this main function and the thread which runs
the main function we refer to as the main thread.*/
void run(){
std::thread thread1(foo);
callable_class obj;
std::thread thread2(obj);
// 3.lambda function
std::thread thread3([]()->void{
printf("Hellp from lambda \n");
});
/*Join function will force main thread to wait until
the thread that it call upon finish its execution.*/
thread1.join();
thread2.join();
thread3.join();
printf("Hello from main \n");
}
int main(){
run();
return 0;
}
To launch a thread, we have to provide a callable object to thread class constructor. Callable objects are normal function, lambda expressions and class with function call operator.
// check if the thread is joinable
std::thread thread1(func);
thread1.joinable();
thread1.join();
To construct a thread properly we have to pass callable object as the argument to the thread class constructor and if that callable object takes parameters you have to pass those parameters properly as well.
launch thread2 from thread 1 in the middle of thread1's execution.
class thread_guard{
private:
std::thread & t;
public:
explicit thread_guard(std::thread & _t): t(_t){}
// avoid unsafe program
~thread_guard(){
if(t.joinable()){
t.join();
}
}
// we don't mean to copy thread_guard type object from one to another.
// deleted the copy constructor and copy assignment operator
thread_guard(thread_guard & const) = delete;
thread_guard & operator= (thread_guard & const) = delete;
};
void foo(){
}
void other_operations(){
std::cout << "This is other operation /n";
// throw an exception and main thread will go out of scope.
// objects which are created inside this function will be destructed.
throw std::runtime_error("this is a runtime error");
}
void run(){
std::thread foo_thread(foo);
thread_guard tg(foo_thread);
try{
other_operations();
} catch (){
}
}
RAII
resource acquisition is initialization (Constructor acquire resources, destructor releases resources).
thread class has 4 constructors
use initialization constructor. can pass arguments after the function name to this constructor.
template
explicit thread (Fn&& fn, Args&&... args);
to pass the variable by reference, use std::ref(var). this is because thread object itself take its argument by value;
problem may occur when detaching threads after pass reference to a variable to that thread as detach thread can outlive the lifetime of the objects in the thread that it detach from.
use move constructor, this is because thread object are not copy constructable nor copy assignable.
std::thread thread_1(foo);
std::thread thread_2 = std::move(thread_1);
// implicit move call, RHS is a temp object
thread_1 = std::thread(bar);
But, cannot transfer ownership when left side variable of this operation owning a thread.
get_id()
sleep_for()
std::this_thread::yield()
std::thread::hardware_concurrency()
T accumulate(InputIt first, InputIt last, T init);
T accumulate(InputIt first, InputIt last, T init, BinaryOperation op)
#include
#define MIN_BLOCK_SIZE 1000
template
void accumulate(iterator start, iterator end, T &ref){
ref = std::accumulate(start,end,0);
}
template
T parallel_accumulate(iterator start, iterator end, T &ref){
int input_size = std::distance(start,end);
int allowed_threads_by_elements = (input_size) / MIN_BLOCK_SIZE;
int allowed_threads_by_hardware = std::thread::hardware_concurrency();
int num_threads = std::min(allowed_threads_by_elements, allowed_threads_by_hardware);
int block_size = (input_size + 1)/num_threads;
std::vector results(num_threads);
std::vector threads(num_threads-1);
iterator last;
for(int i = 0; i < num_threads-1;i++){
last = start;
std::advance(last, block_size);
threads[i] = std::thread(accumulate,start,last, std::ref(results[i]));
start = last;
}
results[num_threads-1] = std::accumulate(start,end,0);
std::for_each(threads.begin(),threads.end(),std::mem_fn(&std::thread::join));
return std::accumulate(results.begin(),results.end(),ref);
}
the mutex class is a synchronization primitive that can be used to protect shared data from being simultaneous access by my mutiple thread. simply, the mutex provide mutually exclusive access of shared data for multiple threads.
Note about list
3 functions
#include
#include
#include
#include
std::list my_list;
std::mutex m;
// use same mutex for both of the functions, the access to both of these functions are mutually exclusive.
void add_to_list(int const &x){
m.lock();
my_list.push_front(x);
m.unlock();
}
void size(){
m.lock();
int size = my_list.size();
m.unlock();
std::cout << "size of the list is :" << size << std::endl;
}
void run_code(){
std::thread thread_1(add_to_list, 4);
std::thread thread_2(add_to_list, 11);
thread_1.join();
thread_2.join();
}
std::mutex m;
std::lock_guard lg(m);
When using mutex, it can be unsafe when
neither of the threads allowed to proceed because they're waiting for others to finish.
the cause of the deadlock is
The difference unique_locks and lock_guard is that
std::unique_lock
.std::lock_guard
will be locked only once on construction and unlocked on destruction.
#include
#include
#include
#include
#include
#include
bool have_i_arrived = false;
int total_distance = 10;
int distance_covered = 0;
std::condition_variable cv;
std::mutex m;
void keep_moving(){
while(true){
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
distance_covered++;
//notify the waiting threads if the event occurs
if(distance_covered == total_distance)
//wake up the condition variable
cv.notify_one();
}
}
void ask_driver_to_wake_u_up_at_right_time(){
std::unique_lock ul(m);
// conditional check after wake up
// reach the wait statement in the first time, it will get ownership of the mutex associate with the unique lock
// check if the condition in lambda function is true
// if not true, unlock the mutex and make passenger sleep
cv.wait(ul, []{return distance_covered == total_distance;});
}
void run_code(){
std::thread driver_thread(keep_moving);
std::thread passenger_thread(ask_driver_to_wake_u_up_at_right_time);
passenger_thread.join();
driver_thread.join();
}
In c++ asynchronous operation can be created via
std::async
async(std::launch policy, Function&& f, Args&&... args);
// launch policy has
std::launch::async // run function in a separate thread
std::launch::deferred // task run in creator thread in the future get called
// can do, let compiler decide
std::launch::async | std::launch::deferred
#include
int func(){
}
std::future the_answer_future = std::async(func);
the_answer_future.get();
template
int parallel_accumulate(iterator begin, iterator end){
long length = std::distance(begin,end);
if(length <= MIN_ELEMENT_COUNT){
std::cout << std::this_thread::get_id() << std::endl;
return std::accumulate(begin, end, 0);
}
iterator mid = begin;
std::advance(mid, (length+1)/2);
//recursive all to parallel_accumulate
std::future f1 = std::async(std::launch::deferred | std::launch::async, parallel_accumulate, mid,end);
auto sum = parallel_accumulate(begin,mid);
return sum + f1.get();
}
std::packaged_task task(callable object)
void task_thread(){
std::packaged_task task_1(add);
std::future future_1 = task_1.get_future();
std::thread thread_1(std::move(task_1),5,6);
thread_1.detach();
std::cout << "task thread - " << future_1.get() << "\n";
}
int add(int x,int y);
void print_int(std::future &fut){
std::cout << "waiting for value from print thread \n";
std::cout << "value: " << fut.get() << "\n";
}
void run_code(){
std::promise prom;
std::future fut = prom.get_future();
std::thread print_thread(print_int, std::ref(fut));
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
std::cout << "setting the value in main thread \n";
prom.set_value(10);
print_thread.join();
}
// future has to wait for promise to set the value.
Now think of a scenario where one thread is waiting for a future and the other thread sets its value. So if the exception thrown in the other thread the ideal scenario would be to propagate that exception to the waiting thread so it can read through the exception.
use promise.set_exception()
more than one thread wait for the same future object.
future will go invalid after the future.get().
#include
std::atomic_int x;
std::atomic y;
atomic<*> (all atomic type)
functions on atomic bool
functions addition to the above 6
By using this memory ordering options with the atomic operator, we can create synchronization point in the code.
Category of operations
with this property we can synchronize two threads without having any direct release and aquire mechanism between them.
after a release operation A is performed on an atomic object M, the longest continuous subsequence of the modification on M that consists of
is known as release sequence headed by A.
Provide rules to logically deducted synchronization.
then the initial store operation and final load operation will be synchronized.
algorithms and data structures that use mutexes, condition variables and futures to synchronize the data are called blocking data structures and algorithms.
On most systems, it is impractical to have separate thread for every task that can potentially be done with other tasks, but you would still like to take advantage of the available concurrency where possible. A thread pool allows you to accomplish this.