C++ std::thread multithreading

catalogue

1. Create thread

There are three different ways to create threads

So what does std::thread accept in the constructor? We can attach a callback to the std::thread object, which will be executed when the new thread starts. These callbacks can be:

  • Function pointer

    void thread_function(){
        for (int i = 0; i < 10000; i++);
        std::cout << "thread function Executing" << std::endl;
    }
    
    // Create thread
    std::thread threadObj(thread_function);
    
  • Function object

    class DisplayThread{
    public:
        void operator()(){
            std::cout << "Display Thread Executing" << std::endl;
        }
    };
    
    int main()
    {
        std::thread tid((DisplayThread()));
        tid.join();
    }
    
  • Class member method (pointer)

    class Producer{
    public:
        Producer(){};
        ~Producer() = default;
    
        void create(){
            cout << "create product: " << this->_no << endl;
            ++_no;
        }
    
    private:
        int _no = 100;
    };
    
    int main(){
        thread tid(&Producer::create, Producer());
        tid.join();
    }
    

    Example 2

    #include <iostream>
    #include <vector>
    #include <thread>
    class Wallet
    {
        int mMoney;
    public:
        Wallet() :mMoney(0) {}
        int getMoney() {
            return mMoney;
        }
        void addMoney(int money)
        {
            for (int i = 0; i < money; ++i)
            {
                mMoney++;
            }
        }
    };
    
    int testMultithreadedWallet()
    {
        Wallet walletObject;
        std::vector<std::thread> threads;
        for (int i = 0; i < 5; ++i)
        {
            threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 100000));
        }
        for (int i = 0; i < threads.size(); i++)
        {
            threads.at(i).join();
        }
        return walletObject.getMoney();
    }
    
    int main()
    {
        int val = 0;
        for (int k = 0; k < 10; k++)
        {
            if ((val = testMultithreadedWallet()) != 500000)
            {
                std::cout << "Error at count = " << k << " Money in Wallet = " << val << std::endl;
            }
        }
        return 0;
    }
    
  • Lambda function

    std::thread threadObj([] {
            for (int i = 0; i < 10; i++)
                std::cout << "Display Thread Executing" << std::endl;
        });
    

1.1. move & bind

Threads created through std::thread cannot be copied, but can be moved.

std::thread t1(threadfunc);
std::thread t2(std::move(t1));

After moving, t1 does not represent any thread, and t2 object represents thread thread func ().

In addition, you can create thread functions through std::bind.

class A {
public:
    void threadfunc(){
        std::cout << "bind thread func" << std::endl;
    }
};
​
A a;
std::thread t1(std::bind(&A::threadfunc,&a));
t1.join();

Create a Class A, and then bind the member function in class A to the thread object t1 in the main function.

1.2. Distinguishing threads

std::thread::get_id()

To get the identifier used by the current thread, i.e

std::this_thread::get_id()

1.3. Parameter transmission

  • Simple parameter transmission

    void threadCallback(int x, std::string str);
    std::thread threadObj(threadCallback, n, str);
    
  • How not to pass parameters to threads in c++11

  • How to pass a reference to std::thread in c++11

    The x in the thread function threadCallback refers to the temporary value copied on the stack of the new thread. How to solve it? Use std::ref(). std::ref is used to wrap values passed by reference.

    void threadCallback(int const & x)
    std::thread threadObj(threadCallback, std::ref(x));
    
  • Assign a pointer to a class member function as a thread function

    class DummyClass {
    public:
        DummyClass()
        {}
        DummyClass(const DummyClass & obj)
        {}
        void sampleMemberFunction(int x)
        {
            std::cout<<"Inside sampleMemberFunction "<<x<<std::endl;
        }
    };
    
    DummyClass dummyObj;
    int x = 10;
    std::thread threadObj(&DummyClass::sampleMemberFunction,&dummyObj, x);
    

1.4. Return value from thread

Many times, we encounter situations where we want the thread to return results. The question now is how to do this? Let's take an example. Suppose that in our application, we create a thread that will compress a given folder, and we want the thread to return a new zip file name and its results. Now, we have two methods:

  1. Use pointers to share data between threads

    Pass the pointer to a new thread, which will set the data in it. Until then, use conditional variables in the main thread to continue waiting. When the new thread sets data and sends a signal to the condition variable, the main thread wakes up and obtains data from the pointer. For simplicity, we use a conditional variable, a mutex, and a pointer (that is, three items) to capture the returned value. Then the problem will become more complex.

    Is there a simple way to return a value from a thread? The answer is yes. Keep looking.

  2. Using std::future

    std::future is a class template whose object stores future values. So what's the use of this future template? In fact, an std::future object internally stores the value to be allocated in the future, and also provides a mechanism to access the value, that is, using the get () member function. However, if someone tries to access this associated value of future before the get() function is available, the get() function blocks until the value is unavailable. std::promise is also a class template whose object promises to set the value in the future. Each std::promise object has an associated std::future object. Once the std::promise object sets the value, it will give the value. An std::promise object shares data with its associated std::future object. Let's take a step-by-step look at creating an std::promise object in Thread1.

    So far, the promise object has no associated value. However, it provides a guarantee that someone will set the value in it. Once the value is set, you can get the value through the associated std::future object. But now suppose thread 1 creates the promise object and passes it to thread 2. Now, how does thread 1 know when thread 2 will set a value in this promise object? The answer is to use the std::future object. Each std::promise object has an associated std::future object through which other objects can obtain the value set by promise. Thread 1 will now pass promiseObj to thread 2. Then, thread 1 will get the value set by thread 2 in std::promise through the get function of std::future.

#include <iostream>
#include <thread>
#include <future>

void initiazer(std::promise<int> * promObj){
    std::cout<<"Inside Thread"<<std::endl;
    promObj->set_value(35);
}

int main(){
    std::promise<int> promiseObj;
    std::future<int> futureObj = promiseObj.get_future();
    std::thread th(initiazer, &promiseObj);
    std::cout<<futureObj.get()<<std::endl;
    th.join();
    return 0;
}

In addition, if you want the thread to return multiple values at different time points, you only need to pass multiple std::promise objects in the thread, and then get multiple return values from the associated multiple std::future objects.

1.5. thread_local

Thread is provided in C++11_ local,thread_ A copy of the variables defined by local is saved in each thread, and they do not interfere with each other. They are automatically destroyed when the thread exits.

#include <iostream>
#include <thread>
#include <chrono>
​
thread_local int g_k = 0;
​
void func1(){
    while (true){
        ++g_k;
    }
}
​
void func2(){
    while (true){
        std::cout << "func2 thread ID is : " << std::this_thread::get_id() << std::endl;
        std::cout << "func2 g_k = " << g_k << std::endl;

        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}
​
void main(){
    std::thread t1(func1);
    std::thread t2(func2);
​
    t1.join();
    t2.join();
}

In func1() for g_k loop plus 1 operation, output g every 1000 milliseconds in func2()_ Value of K:

func2 thread ID is : 15312
func2 g_k = 0
func2 thread ID is : 15312
func2 g_k = 0
func2 thread ID is : 15312
func2 g_k = 0

You can see g in func2()_ K remains constant.

2. Synchronization & mutual exclusion

2.1. std::mutex

#include<mutex>
class Wallet
{
    int mMoney;
    std::mutex mutex;
public:
    Wallet() :mMoney(0) {}
    int getMoney() { return mMoney; }
    void addMoney(int money)
    {
        mutex.lock();  // Lock
        for (int i = 0; i < money; ++i)
        {
            mMoney++;
        }
        mutex.unlock();  // Unlock
    }
};
  • lock()

    The calling thread will lock the mutex.

    The following three situations occur when the thread calls this function:

    1. If the mutex is not currently locked, the calling thread locks the mutex until unlock is called.
    2. If the current mutex is locked by another thread, the current calling thread is blocked.
    3. If the current mutex is locked by the current calling thread, a deadlock (deadlock) is generated.
  • try_lock()

    Try to lock the mutex. If the mutex is occupied by other threads, the current thread will not be blocked.

    The following three situations will also occur when the thread calls this function:

    1. If the current mutex is not occupied by other threads, the thread locks the mutex until the thread calls unlock to release the mutex.
    2. If the current mutex is locked by other threads, the current call thread returns to false, and it will not be blocked.
    3. If the current mutex is locked by the current calling thread, a deadlock (deadlock) is generated.

2.1.1. std::lock_guard

But what if we forget to unlock the mutex at the end of the function? In this case, one thread will exit without releasing the lock, while other threads will remain waiting. This can happen if some exceptions occur after locking the mutex. To avoid this, we should use std::lock_guard .

Lock_Guard is a class template, which implements RAII of mutex. It wraps mutexes in its objects and locks additional mutexes in its constructor. When its destructor is called, it releases the mutex.

void addMoney(int money){
    // In constructor it locks the mutex
    std::lock_guard<std::mutex> lockGuard(mutex);
    for (int i = 0; i < money; ++i){
        // If an exception occurs at this location, the destructor of lockGuard will be called due to stack expansion.
        mMoney++;
    }
    //Once the function exits, the destructor will call the lockGuard object in the destructor, which unlocks the mutex.
}

It is worth noting that lock_ The guard object is not responsible for managing the life cycle of the Mutex object, lock_ The guard object only simplifies the locking and unlocking operations of the Mutex object, which facilitates the thread to lock the Mutex, that is, in a lock_ During the declaration cycle of the guard object, the lock object it manages will remain locked all the time; And lock_ At the end of the guard's life cycle, the lock objects it manages will be unlocked.

#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list; // 1
std::mutex some_mutex; // 2

void add_to_list(int new_value){
    std::lock_guard<std::mutex> guard(some_mutex); // 3
    some_list.push_back(new_value);
}

bool list_contains(int value_to_find){
    std::lock_guard<std::mutex> guard(some_mutex); // 4
    return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
}

In most cases, mutexes are usually placed in the same class as the protected data, rather than being defined as global variables. This is the criterion of object-oriented design: put it in a class, they can be linked together, and the functions of the class can be encapsulated and data protected.

2.1.2. std::unique_lock

But lock_ The biggest disadvantage of guard is that it is simple and does not provide enough flexibility for programmers. Therefore, another class unique related to Mutex RAII is defined in C++11 standard_ Lock, which is similar to lock_ Similar to the guard class, it is also convenient for threads to lock mutexes, but it provides better locking and unlocking control.

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock, std::unique_lock
// std::adopt_lock, std::defer_lock
std::mutex foo, bar;

void task_a() {
     std::lock(foo, bar);         // simultaneous lock (prevents deadlock)
     std::unique_lock<std::mutex> lck1(foo, std::adopt_lock);
     std::unique_lock<std::mutex> lck2(bar, std::adopt_lock);
     std::cout << "task a\n";
     // (unlocked automatically on destruction of lck1 and lck2)
}

void task_b() {
     // foo.lock(); bar.lock(); // replaced by:
     std::unique_lock<std::mutex> lck1, lck2;
     lck1 = std::unique_lock<std::mutex>(bar, std::defer_lock);
     lck2 = std::unique_lock<std::mutex>(foo, std::defer_lock);
     std::lock(lck1, lck2);       // simultaneous lock (prevents deadlock)
     std::cout << "task b\n";
     // (unlocked automatically on destruction of lck1 and lck2)
}

void main() {
     std::thread th1(task_a);
     std::thread th2(task_b);

     th1.join();
     th2.join();
}

2.1.3. Four mutexes: recursion / timeout

Four mutexes are provided in C++11.

std::mutex;                  //Non recursive mutex
std::timed_mutex;            //Non recursive mutex with timeout
std::recursive_mutex;        //Recursive mutex
std::recursive_timed_mutex;  //Recursive mutex with timeout

2.2. Conditional variables

Set the Boolean global variable to the default value of false. Set its value to true in thread 2, thread 1 will continue to check its value in the loop, and once it becomes true, thread 1 will continue to process data. However, because it is a global variable shared by two threads, it needs to be synchronized with the mutex. Let's look at the code.

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

class Application{
    std::mutex m_mutex;
    bool m_bDataLoaded;
public:
    Application(){
        m_bDataLoaded = false;
    }

    void loadData(){
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "Loading Data from XML" << std::endl;
        std::lock_guard<std::mutex> guard(m_mutex);
        m_bDataLoaded = true;
    }

    void mainTask(){
        std::cout << "Do Some Handshaking" << std::endl;
        m_mutex.lock();
        // Check whether the data is loaded
        while (m_bDataLoaded != true){
            m_mutex.unlock();
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            m_mutex.lock();
        }
        m_mutex.unlock();
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};

int main(){
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();
    return 0;
}

This approach has the following disadvantages: the thread will continue to acquire the lock and release it just to check the value, so it will consume CPU cycles and slow thread 1 because it needs to acquire the same lock to update the bool flag. Therefore, we need a better mechanism to achieve this goal. For example, if thread 1 can block by waiting for an event to be signaled, and another thread can send the event through the signal and make thread 1 continue to run, this mechanism can be achieved. This saves many CPU cycles and provides better performance.

We can use conditional variables. A condition variable is an event that signals between two threads. One thread can wait for it to signal, while another thread can signal.

A condition variable is an event that signals between two or more threads. One or more threads can wait for it to signal, while another thread can signal.

  • wait():

    This function blocks the current thread until the condition variable is signaled or a false wake-up occurs. It automatically releases the attached mutex, blocks the current thread, and adds it to the list of threads waiting for the current condition variable object.

  • notify_one():

    Notify if any thread is waiting on the same condition variable object_ One unblocks one of the waiting threads.

  • notify_all():

    Notify if any thread is waiting on the same condition variable object_ All unblocks all waiting threads.

#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>

using namespace std::placeholders;

class Application{
    std::mutex m_mutex;
    std::condition_variable m_condVar;
    bool m_bDataLoaded;
public:
    Application(){
        m_bDataLoaded = false;
    }

    void loadData(){
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "Loading Data from XML" << std::endl;
        std::lock_guard<std::mutex> guard(m_mutex);
        m_bDataLoaded = true;
        m_condVar.notify_one();  // Notification variable
    }

    bool isDataLoaded(){
        return m_bDataLoaded;
    }

    void mainTask(){
        std::cout << "Do Some Handshaking" << std::endl;
        std::unique_lock<std::mutex> mlock(m_mutex);
        // Start waiting for the condition variable to receive the signal. wait() will release the lock internally and block the thread. Once the condition variable gets the signal, the thread will be restored and the lock will be obtained again.
        //  Then check whether the conditions are met. If the conditions are met, continue, otherwise continue to wait.
        m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};

int main(){
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();
    return 0;
}

2.3. Semaphore

3. std::async()

async tutorials and examples

Posted on Thu, 28 Oct 2021 18:44:08 -0400 by hanhao