C++_Multithreading_thread_mutex - RicoJia/notes GitHub Wiki

========================================================================

Concurrency

========================================================================

  1. Motivation: Greatest thing of C++ is concurrency in STL, which provides a standardized behavior

    • C++11 is when the nice API came abou. in C++98, no formal memory-model was proposed. So you have to use C-APIs or compiler specific API
    • C++ include: std::thread, std::unique_lock, std::atomic
    • Based on Boost implementation. Then boost started accomodating to STL as well
    • RAII is also quite important here.
    • we have a memory-model, and raw atomic operations for manipulating bits, bytes, and synchronization
    • You don't pay for what you don't get, most of the time
  2. Asynchronous programming vs multi-threaded programming: they're two different models. multi-threading is about Workers vs asynchronous programming is about tasks.

    1. Asynchronous means: you can do something else while waiting. Synchronous means: you have to wait there while something is being executed.
    2. Analogy:
      1. Synchronous: you set a timer, cook an egg, wait, then set a timer, boil some water
      2. Asynchronous one thread: you set a timer, cook an egg, and while waiting, you set a timer and boil some water, then wait.
      3. asynchrouns multi-threaded: one cook set a timer cook an egg, then do something else; another cook boil some water, then do something else.
  3. Context switching

    • Single core machine can do task switching (or context switch) on a core for having a "quasi" multi-threaded behavior
      • Multi-core machines need this as well,
        • so you have many background processes running, e.g, word, web browser.
  4. A process or a thread?

    1. process:
    • Cons
      1. interprocess communication is tricky
      • added protection from OS, to prevent changing data of another process
      1. info sharing may generate overhead
      2. overhead on starting and managing multiple processes.
    • pros:
      1. safer concurrent code.
      • Erlang's concurrency is based on this, and is very successful
      1. Run on different machines
    1. Threads:
    • Pros:
      1. Threads have the same address space
      • meaning memory could be shared
      1. communication is much easier, without all the data protection
      2. C++ doesn't have intrinsic suport for interprocess communcation
      3. Now the free lunch is over
      • Before, we just need to have more and more cores on a processor, and more processes can be run at the same time
    • Cons:
      1. Too many threads will take up too much stack-space
      • quite common, one thread has 1MB stack space, on 4GB RAM
      • thread pool can help, for handling say too many TCP connections.
      1. Code is more complicated
      • DON'T USE MULTI_PROCESS AT THE BEGINNING, unless the gains are larger
  5. Invariant is a statement that's always true about a data structure.

    • Like "A node in linked list always points to a different node, and that node will point back to the original node.
  6. race condition means: the outcome of a variable in a thread depends on the ordering of two threads.

    • Difficult to debug cuz they can totally disappear.
    • Will cause undefined behavior
    • One potential problem is: passing a pointer to mutex-protected variables out to caller functions
      • Problematic cuz other functions can modify them later without mutexes
  7. Mutex and lock free methods. There's also "transactions" to modify data, just like a transaction in data base, which will restart if a trasaction is failed. ========================================================================

threads and join

========================================================================

  1. Basics: create a thread, put a callable in there -> the thread is "joinable" -> join the thread if you want to wait for it to finish, or detach it if you want it to run in the background
    std::thread t;
    std::thread t2 {func, arg};   //a new "thread of execution"
    t = std::move(t2);
  2. ways to create thread
    	// 1. functor
    	class Functor{public: operator(){}}; 
    	std::thread th1((Functor()));	//pay attention to the () here
    std::thread th2(Func());    // ERROR: This is most-vexing parse:
    std::thread th3{Func()};    // Works :)
    
    	// 2. function
    	void func1(){}
    	std::thread th2(func2); obj
    	std::thread(&Class::func, &Obj) // Note: needs pointer, **not smart pointer**
    
    	// lambda expression
    	std:;thread obj([]{cout<<"haha"; });
    
    //Create a thread, running another member object's function:  
    ws_th_ = std::thread (&util::Webserver::start, &(ws_));
  3. join()
    • clears up any storage allocated to the thread
    • can only be joined once, so **this is important: **: if(th.joinable())
      std::thread th1(func); 
      std::thread th2 = std::move(th1); 
      std::thread th3; 
      std::thread th4(func); 
      th1.join();		// bad: here no thread is associated with the thread object, so join() again will cause the program to terminate. 
      th2.join();     // this is joinable
      th3.join();   // not joinable
      // You haven't join th4 yet. If you want to destroy an obj, it has to be joinable!!
    • Check joinability
      //good: check if the thread is joinable; 
      if (th1.joinable()) th1.join()
    • If you don't call thread.join() on an obj, thread's destructor will be called at return. And An error for std::terminate will be called
  4. detach()
    • after detach, the thread object is not joinable any more.
    • Daemon/Background threads: these thread objects are no longer associated with their objects.
      if (th.joinable()) //same thing with the daemon thread. 
        th.detach(); 
    • Be very careful with detach: child function may live longer than the main function
      void func(){
        std::string s{"C++11"};
        std::thread t([&s]{ std::cout << s << std::endl;});   // undefined behavior
        t.detach();
      }
      int main(){
        func();
      }
    • One app is logger, or you sure something will be done very fast.
      • Or a file editor, which starts a thread for a window. the window can terminate on its own. ========================================================================

Common Errors & Cautions

========================================================================

  1. when referencing data from the outside: smart pointer or ref: make sure the object is stored as a class variable, or at least persists beyond the lifetime of the thread
  2. std::thread will copy a variable, cuz its ctor has the same mechanism w std::bind
  std::thread(some_func, std::ref(data))
1. pass in argument by reference:
  ```cpp
  func(int& v);
  int v = 1;
  std::thread t1(func, std::ref(v));
  ```
  1. std::thread might have no functions to execute
  • So it's an null handle
  • Could be: - default-constructed - Moved thread - joined threads - detached threads
  • Reasons why STL does not implicitly make t join(), or detach()
    • we can't join() the thread unconditionally, because the thread may be in an infinitely that never completes
  • This happens when you "double-joining" a thread
    terminate called after throwing an instance of 'std::system_error' what():  Invalid argument
  1. Cannot copy std::thread, movable only
    std::thread ret_th(){
        std::thread t3(func);
        return t3;      // func may/may not be done. 
    }
    
    int main(){
        // Case 1
        std::thread t1(func);
        std::thread t2 = std::move(t1);     // func is done, won't see func executed again.
        t2.join();          //t1 is not joinable
    
        //case 2 return a thread
        auto t3 = ret_th();
        t3.join();
    
        //case 4 asssigning a thread to a joinable thread -> termination
        t1 = std::thread(func);
        t1 = ret_th();              // error: termination!
        t1.join();
        return 0;
    }
  • Error "terminate called without an active exception"
    1. Happens when thread is terminated while still in a joinable state
    2. A thread is not joinable when:
    3. remember, when a thread is done executing, it's in a JOINABLE state!
  1. Be careful with exceptions, conditions that keep the thread still joinable
    int main(){
      std::thread t1([]{
          do_work(); 
        });
        if (Some_Condition) t.join();   // if false, t will still be joinable, that will lead to std::terminate
      }
  2. overhead of creating std::threads? - system dependent. So use thread_pool if you can.
  3. throw error resource temporarily not available?? Must be you're trying to create too many threads!!
  4. dangerous scenario: after detach, local variable is no more valid
    void oops(int some_param) {
      char buffer[1024];
      sprintf(buffer, "%i",some_param);
      std::thread t(f,3,buffer); //you first have to convert char const* of char[] into std::string, which might finish after the function goes out of scope (undefined behavior)
      t.detach();
      }
  5. unjoinable_thread.join() returns error invalid_argument if joinable() is false ========================================================================

RAII_THREAD

========================================================================

  1. Solution to not joining a thread at the end of the current function scope: RAII
    class RAII_THREAD{
      public:
        RAII_THREAD(int dummy_param, std::thread&& th)th_(std::move(th)){}      // Notice that dummy_param comes before th. That's because th might start immediately after they're initialized, and it is dependent on other params!! We want to make sure the other params are already there. 
        ~RAII_THREAD(){if(th.joinable()) th_.join(); }      
        std::thread& get(){return th_;}     //analogous to smart pointer
    
        RAII_THREAD(RAII_THREAD&&) = default;     // rvlaue ref, yes go have it. copy ctor and assignment don't make sense. 
        RAII_THREAD& operator=(RAII_THREAD&&) = default; 
    
      private:
        std::thread th_; 
    };
    • through get, two other threads might be able to get access to thread and do join(), which might be a race
    • Simultaneous calls are usually safe only with const member functions
    • The key is to make sure we join the thread when function goes out of scope
  2. using RAII, so t1 will be automatically joined
    void detect(){
      ThreadRAII tr(
      []{
        p.get_future().wait();    //by default, this is std::future, not std::shared_future
        react(); 
      }); 
      ...     // Here you may get an exceptions
      p.set_value(); 
    }
  3. Cautions:
    1. For threadRAII, You might see multiple destructor msgs, if you're in a dynamically allocated container.

========================================================================

Thread Related

========================================================================

std::hardware_concurrency

  1. There's hardware & software threads:
  • hardware threads: usually more than one per core.
  • Software threads: operating system manages this.
    • Possible to have more software threads than hardware threads
    • schedulers will schedule. When one thread is blocked, another unblocked thread will be executed.
    • std::thread is Cpp API for accessing lower level software threads (pthreads, Windows' threads)
  1. int i = std::thread::hardware_concurrency(); returns the max available threads.
    • maybe num of cores on a multi-core machines. And this will be 0 if the info is not available.

Uses of std::this_thread::get_id()

  1. shared thread_local storage: remember only a static member function / global function can be passed into C as a callback? You can store some thread_local data there:
    std::map<int, double> storage; 
    ...
    storage.at(std::this_thread::get_id()) = ...
  2. Make different thread do different work
    // thread 1: 
    std::thread::id master_thread_id = std::this_thread::get_id(); 
    // All functions: 
    void func(){
      if (std::this_thread::get_thread_id() == master_thread_id){
        do_master_work(); 
      }
      else{
        do_common_work(); 
      }
    }

std::thread::native_handle

  1. returns the handle to the thread, defined by the compiler, working with pthread functions.

========================================================================

Mutexes and Lock

========================================================================

  1. do std::lock_guard<T> calls .lock() and .unlock() methods from a mutex. It was designed for mutex.

  2. scoped_lock: lock two mutexes in a deadlock-free way

    class Employee{
      public:
        std::mutex mtx_;
        int id;
    };
    
    int main(){
      Employee e1, e2;
      std::scoped_lock lock(e1.mtx_, e2.mtx_);
      cout<<e1.id<<e2.id;
    }
    • deadlock avoidance algorithm is automatically given!
    • non-copyable, RAII
  3. spinlock vs mutex: mutex will have thread scheduling, when going out of a quantum, other threads will execute on the core. spin lock: always waits in a loop, good when wait time is shorter than the quantum

  4. std::timed_mutex and std::timed_recursive_mutex supports try_lock_for() and try_lock_until()

    • similarly, std::unique_lock has these:
      • std::unique_lock::try_lock_for(duration), std::try_lock_until(time_point)
      • std::unique_lock(TimeLockable, duration), std::unique_lock(TimeLockable, time_point)
  5. shared_mutex:

    • std::shared_lock calls lock_shared, try_lock_shared. shared_mutex has these too. They also have lock, try_lock
      std::shared_timed_mutex m;
      std::shared_lock<std::shared_timed_mutex> slk(m);
    • std::shared_timed_mutex: have try_lock_for()
⚠️ **GitHub.com Fallback** ⚠️