C++_Multithreading_thread_mutex - RicoJia/notes GitHub Wiki

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

Concurrency

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

  1. 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
  2. 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_));
```
  1. 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

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

    ```cpp
    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** ⚠️