Multithreading Learning Guide

Recently, I saw some readers asking for the C++ multithreading learning method in the official account. I have summarized it here, hoping to help you.

catalogue

  • What is multithreading?

  • Why use multithreading?

  • How do I create a thread?

  • joinable()?

  • Multi thread parameter passing mode

  • lock

  • Atomic variable

  • Conditional variable

  • async

  • Multithreaded peripheral

  • Some suggestions on Multithreading

What is multithreading?

No introduction, basic knowledge, just look at Wikipedia: https://zh.wikipedia.org/wiki/%E5%A4%9A%E7%BA%BF%E7%A8%8B

Why multithreading?

No introduction, basics, and above in a link.

C + + multithreading knowledge points

How do I create a thread?

There are many ways to create multithreads: pthread, std::thread, std::jthread.

Here I recommend learning std::thread introduced by C++11, which is more convenient than pthread and more commonly used in C + +.

As for std::jthread, don't worry. After learning std::thread, you will naturally learn std::jthread.

You can see the detailed introduction of multithreading in C++11 All knowledge points related to threads of new features of c++11

Using std::thread to create a thread is very simple. You can directly use its constructor:

void func() {
    xxxx;
}

int main() {
    std::thread t(func);
    if (t.joinable()) {
        t.join();
    }
    return 0;
}

Notice the above code. I used a joinable() and a join(). Why?

If not, the program will crash at the end of the thread life cycle. The reason is to look directly at the destructor of thread:

~thread()
{
    if (joinable())
    std::terminate();
}

join() and detach()?

The above describes that if you do not call join, the program will crash. In fact, you can also call detach to avoid program crash. What is the difference between the two?

join() means that the blocking waits for the execution of the child thread to end, and the child thread will continue to execute after the child thread ends.

detach() means that it is separated from the current object. No matter what the sub thread does or whether the execution ends, it has nothing to do with me. Whatever it likes, it finally depends on the operating system to recover relevant resources.

What is joinable()?

In the above code, joinable() appears, which can be simply understood as returning true if join() or detach() is not called. If one of them is called, joinable() returns false. It is mainly used with join() and detach().

Parameter transfer problem

Multithreading is actually to start a thread and run a function. The above example is a parameterless function. How to run a parameterless function? How to pass parameters in? In fact, there are several methods to pass parameters. I prefer to use lambda expressions, which encapsulate parametric functions + parameters into nonparametric functions, and then call them in multiple threads.

Example code:

#include <iostream>
#include <thread>

void func(int a, int b) { std::cout << "a + b = " << a + b << std::endl; }

int main() {
    auto lambda = []() { func(1, 2); };
    std::thread t(lambda);
    if (t.joinable()) {
        t.join();
    }
    return 0;
}

I have written an article on lambda expressions before. You can see here:

Get rid of the new features of c++11 std::function and lambda expression

How does the compiler implement lambda expressions?

Member function problem

Many people may still have questions. If multithreading runs the member function of class object, the same method as above can be used here. lambda expression:

#include <iostream>
#include <memory>
#include <thread>

struct A {
    void Print() { std::cout << "A\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    auto func = [a]() { a->Print(); };
    std::thread t(func);
    if (t.joinable()) {
        t.join();
    }
    return 0;
}

Little knowledge

There are two common methods to create thread objects:

std::thread a(func);

std::thread *a = new thread(func);
delete a;

Someone has asked the difference between the two methods in the technical exchange group. I believe you should know the answer after reading the above content carefully!

Here's a hint: the two objects, one on the heap and the other on the stack, have different life cycles, that is, the time of calling the destructor of thread is different. Then you can think about it in combination with the implementation of ~ thread() introduced above.

Why do I need a lock?

Because multi-threaded data reading and writing may have thread safety problems, one way to ensure thread safety is to use locks.

About thread safety, you can find it on any website, such as Wikipedia, Baidu Encyclopedia, etc.

https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8

There are four types of mutex:

  • std::mutex: exclusive mutex, which cannot be used recursively, without timeout function

  • std::recursive_mutex: recursive mutex, reentrant, without timeout function

  • std::timed_mutex: mutex with timeout, cannot be recursive

  • std::recursive_timed_mutex: mutex with timeout, which can be used recursively

There are three ways to add and unlock:

  • std::lock_guard: it can be locked in RAII mode

  • std::unique_lock: better than lock_ The guard has multiple functions of manual adding and unlocking

  • std::scoped_lock: a lock that prevents deadlock caused by multiple lock sequence problems

Example code:

std::mutex mutex;
 
void func() {
    std::lock_guard<std::mutex> lock(mutex);
    xxxxxxx
}

Atomic operation

As described above, using locks can solve thread safety problems. In fact, atomic operations can be used for simple variables, such as integer variables. Atomic operations of C++11 are all in < atomic >.

Example code:

std::atomic<int> count;

int get() {
    count.load();
}

void set(int c) {
    count.store(c);
}

The above two functions can be called arbitrarily in multiple threads without thread safety problems.

Conditional variable

Conditional variable is a synchronization mechanism that can block one or more threads until other threads notify them. This notification and blocking requires conditional variables.

Example code:

class CountDownLatch {
   public:
    explicit CountDownLatch(uint32_t count) : count_(count);

    void CountDown() {
        std::unique_lock<std::mutex> lock(mutex_);
        --count_;
        if (count_ == 0) {
            cv_.notify_all();
        }
    }

    void Await(uint32_t time_ms = 0) {
        std::unique_lock<std::mutex> lock(mutex_);
        while (count_ > 0) {
            if (time_ms > 0) {
                cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
            } else {
                cv_.wait(lock);
            }
        }
    }

    uint32_t GetCount() const {
        std::unique_lock<std::mutex> lock(mutex_);
      return count_;
    }

   private:
    std::condition_variable cv_;
    mutable std::mutex mutex_;
    uint32_t count_ = 0;
};

In fact, there are two pits related to conditional variables that need to be noted. Move here: Do you know how to use conditional variables

Task based concurrency

I think we only need to understand async. Through async, we can not only achieve the purpose of concurrency, but also get the results after concurrent execution.

Example code:

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

using namespace std;

int func(int in) { return in + 1; }

int main() {
    auto res = std::async(func, 5);
    cout << res.get() << endl; // Block until function returns
    return 0;
}

See the following for details: All knowledge points related to threads of new features of c++11

You can also see the thread pool I write in this way: C++11 thread pool

other

How do I hibernate a thread?

You can use std::this_thread and chrono, the combination of which makes it convenient for threads to sleep, and the sleep time is also very clear. Unlike C-language sleep, every time I use C-language sleep, I will deliberately search. Is the unit second or millisecond

std::this_thread::sleep_for(std::chrono::milliseconds(10));

Number of threads

Many people worry about the number of threads in the thread pool with the highest efficiency. Assuming that the number of CPUs is N, some materials will introduce that N threads have the highest efficiency, and some materials will introduce that 2N threads have the highest efficiency. In < thread >, the number of CPUs can be obtained through the following functions:

static unsigned hardware_concurrency() noexcept;

As for how many threads need to be opened, I think it needs to be actually tested according to the personalized needs. You can open as many threads as you measure the highest performance.

deadlock

The definition of deadlock can be found directly on Wikipedia: https://zh.wikipedia.org/wiki/%E6%AD%BB%E9%94%81

As for how to solve deadlock, you can see: How to use gdb to accurately locate deadlock in multithreading

I have some suggestions on multithreading. I recommend you to see this:

Finally, in the process of learning multithreading, I found a blog that introduces C++11 multithreading in great detail. I also recommend you to have a look.

Blog link: https://www.cnblogs.com/haippy/p/3284540.html

Keywords: C++ Linux Multithreading

Added by pornophobic on Mon, 08 Nov 2021 10:00:51 +0200