C++ New Classic | C++ Detecting and Filling Lapse (Concurrency and Multithreading)

Table of contents

1. Threads, processes and concurrency

1. Thread

2. Concurrency

(1) Multi-process concurrency

(2) Multi-thread concurrency

(3) Summary

2. Thread starting, ending and creating threads

1.Thread class

(1)join

(2)detach

(3)joinable

(4) Thread id

2. Thread entry function parameters

3. Multi-threaded data sharing

1. Protect shared data

(1) Mutex mutex

(2) std::lock_guard class template

2. Deadlock

(1) std::lock function template

3.unique_lock class template

(1)std::defer_lock

(2) try_to_lock parameter

(3) Member function lock                

(4) Member function unlock        

(5) Member function try_lock

(6) Member function release

(7)unique_lock ownership

4. Lock granularity

5.Double locking of singleton mode

6.std::call_once

(1)std::once_flag

7. Condition variable std::condition_variable

4. std::async and std::future, std::shared_future

1.Member functions of std::future

(1)get()

(2)wait()

2.std::shared_future

3.std::async extra parameters

(1)std::launch::deferred

(2)std::launch::async

4.std::thread VS std::async

5. Atomic operation std::atomic

6. Thread number issue

1. The problem of limiting the number of threads created

2. Recommended number of threads to create


1. Threads, processes and concurrency

1. Thread

        The more threads, the better. Each thread requires an independent stack space (consuming memory, for example, one thread occupies 1MB stack space), and switching between threads also requires saving a lot of intermediate states, etc., which also involves the above mentioned A context switch has been reached. Therefore, if there are too many threads, context switching will be frequent, and context switching is a necessary but worthless and meaningless extra work, which will consume the time that should belong to the running of the program.

        The maximum number of threads created is generally not recommended to exceed 200~300. As for the appropriate number, it must be continuously adjusted and optimized in actual projects. Sometimes the efficiency will decrease if there are too many threads.

        The main thread is automatically started, so there is at least one thread (main thread) in a process.

2. Concurrency

        There are two ways to achieve concurrency:

  • Concurrency is achieved through multiple processes, each doing one thing. The process mentioned here refers to a process that contains only one main thread. This method does not require writing any thread-related code in the program code.
  • Create multiple threads in a separate process to achieve concurrency. In this case, you have to write code to create other threads besides the main thread (the main thread does not need to be created, as soon as the process is started, the main thread automatically exists and starts running).

(1) Multi-process concurrency

        There are many means of communication between processes. If the communication is between processes on the same computer, it can be achieved using technologies such as pipes, files, message queues, and shared memory. The communication between processes on different computers can be achieved using socket (network socket) and other network communication technologies. Due to data protection issues between processes, communication between processes is quite complicated even on the same computer.

(2) Multi-thread concurrency

        Multithreading is the creation of multiple threads in a single process. Each thread runs independently, but all threads in a process share the address space (shared memory), and global variables, pointers, references, etc., can be passed between threads, so we can draw a Conclusion: The overhead of using multiple threads is much less than that of multiple processes.

(3) Summary

        Generally speaking, it is recommended to give priority to using multi-threading technology to achieve concurrency rather than multiple processes. Compared with multi-process concurrency, the advantages and disadvantages of multi-thread concurrency are as follows:

  • Advantages: Thread startup is faster and more lightweight; system resource overhead is less; execution speed is faster.
  • Disadvantages: It is difficult to use, and you must be careful to deal with data consistency issues.

2. Thread starting, ending and creating threads

void ThreadA()
{
    Sleep(1000);

    std::cout << "ThreadA" << std::endl;
}

void TestThread()
{
    std::thread _thread(ThreadA);

    std::cout << _thread.joinable() << std::endl;    //1

    _thread.join();

    std::cout << _thread.joinable() << std::endl;    //0

    std::cout << "TestThread end" << std::endl;
}
//---结果---
1
ThreadA
0
TestThread end

1.Thread class

    std::thread _thread(ThreadA);

        Thread is a class in the C++ standard library. This class is used to create threads. As you can see, this class is used to generate an object named _thread, which contains a callable object (the callable object here is the function ThreadA) as an actual parameter of the thread constructor to construct the thread object. As soon as this line of code is executed, a new thread is created and immediately begins executing the new thread's initial function ThreadA.

(1)join

        Looking at the literal translation, join means “joining/converging”. In other words, it means "blocking" - the main thread waits for the sub-thread to complete execution, and the execution process finally converges together (after the sub-thread completes execution, the execution process returns to the main thread and executes the main function).

        A well-written program should have the main thread wait for the child thread to complete execution before it can finally exit.

(2)detach

        Detach member function. Translated into Chinese, detach means "separation". The so-called separation means that the main thread does not merge with the sub-thread. The main thread executes the main thread, and the sub-thread executes the sub-thread. The main thread does not have to wait for the sub-thread to finish running, and can finish it first. This does not affect the execution of the sub-thread. Why is a function like detach introduced? The saying is: if many sub-threads are created and the main thread waits for the sub-threads to end one by one, this programming method is not necessarily the best, so the detach method is introduced.

        Once the thread is detached, the thread object associated with this thread will lose its association with this thread (of course, because the thread object is defined in the main thread), at this time the thread will reside in the background and run ( The main thread is equivalent to losing contact with this thread). This newly created thread is equivalent to being taken over by the C++ runtime library. When the thread finishes executing, the runtime library is responsible for cleaning up the resources related to the thread.

        For a thread, once detach is called, join cannot be called again, otherwise the program will run abnormally.

(3)joinable

        Determine whether join or detach can be used successfully (determine whether join or detach has been called for a certain thread). If there is no join or detach, it returns true.

(4) Thread id

        Each thread (regardless of the main thread or the sub-thread) actually corresponds to a number, which is used to uniquely identify the thread. Therefore, the number corresponding to each thread is different. In other words, different threads must have different thread IDs. The thread ID can be obtained using the function std::this_thread::get_id in the C++ standard library.

2. Thread entry function parameters

  • If you pass simple type parameters such as int, it is recommended to use value passing instead of reference types to avoid unnecessary complications.
  • If you pass a class object as a parameter, avoid implicit type conversion (for example, convert a char* to a string, convert an int to a class A object), and construct a temporary object in the thread creation line, and then thread entry Use references as formal parameters in the function's formal parameter position (if you do not use references, it may lead to one more temporary class object being constructed under certain circumstances, which is not only wasteful, but also creates new potential problems). The purpose of this is nothing more than to find a way to prevent the main thread from exiting and causing the child thread to make illegal references to memory.
  • It is recommended not to use detach, but only use join, so that there is no problem of invalid reference of local variables to threads' illegal memory.
  • So if you really want to use detach to create a thread, remember not to pass parameters such as references and pointers to the thread.

        In C++, when you create a thread and pass parameters, these parameters are passed by value by default. That is, a copy is created for each thread regardless of whether they are handled as reference types. However, if you pass a pointer, then the pointer value (i.e., the memory address) is actually copied, not the data pointed to by the pointer. This means that the newly created thread will be able to access and modify the original data, so you need to pay attention to the scope issue.

        The C++ language will only generate temporary objects for const references.

        Temporary objects cannot be used as non-const reference parameters, that is, they must be modified with const. This is due to the semantic limitations of the C++ compiler. If a parameter is passed in as a non-const reference, the C++ compiler has reason to believe that the programmer will modify the contents of the object in the function, and the modified reference will be effective after the function returns. But if a temporary object is passed in as a non-const reference parameter, due to the special nature of the temporary object, the programmer cannot operate the temporary object, and the temporary object may be released at any time. Therefore, generally speaking, it is not necessary to modify a temporary object. Meaningless. Accordingly, the C++ compiler added a semantic restriction that temporary objects cannot be used as non-const references, intended to limit potential errors in this unconventional usage.

void TestCanShu(const int*i)
{
    std::cout << "i:"<<i << std::endl;
}
void main()
{
    int mvar = 10;
    std::cout << "mvar:" << &mvar << std::endl;

    std::thread  thread(TestCanShu, &mvar);            //地址相同
    thread.join();
}
//结果---
mvar:012FF960
i:012FF960
void TestCanShu1(int*i)
{
    std::cout << "i:"<<i << std::endl;
}
void main()
{
    int mvar1 = 20;
    std::cout << "mvar1:" << &mvar1 << std::endl;

    std::thread  thread1(TestCanShu1, &mvar1);               //地址相同
    thread1.join();
}
void TestCanShu2(const int&i)
{
    std::cout << "i:" << &i << std::endl;
}
void main()
{
    int mvar2 = 30;
    std::cout << "mvar2:" << &mvar2 << std::endl;

    std::thread  thread2(TestCanShu2, mvar2);    //拷贝了一份参数,虽然形参是引用类型,但是地址不同
    thread2.join();
}
void TestCanShu3( int&i)
{
    std::cout << "i:"<<&i << std::endl;
}
void main()
{
    int mvar3 = 40;
    std::cout << "mvar3:" << &mvar3 << std::endl;

    std::thread  thread3(TestCanShu3, std::ref(mvar3));   //地址相同
    thread3.join();
}

        For the sake of data security, when passing a class type object as a parameter to the thread entry function, regardless of whether the receiver (formal parameter) is received by reference, the parameter will always be passed by copying the object. If you really need to explicitly tell the compiler to pass a reference that can affect the original parameters (actual parameters), you have to use std::ref.

class TestThread {
public:
    int age;
    TestThread()
    {
        std::cout << "无参构造函数" << std::endl;
    }
    TestThread(int _age) :age(_age) {
        std::cout << "含参构造函数" << std::endl;
    }
};

void TestCanShu4(const TestThread& arg)
{
    std::cout << "arg:"<<&arg << std::endl;
}
void main()
{
    TestThread test(10);
    std::cout << "test:"<<&test << std::endl;
    std::thread  thread(TestCanShu4,test);             //地址不同    
    thread.join();
}
void main()
{
    TestThread test(10);
    std::cout << "test:"<<&test << std::endl;
    std::thread  thread1(TestCanShu4,std::ref(test));  //地址相同         
    thread1.join();
}

3. Multi-threaded data sharing

  • Read-only data: a piece of shared data. If the data is read-only and every thread reads it, it doesn’t matter. What each thread reads must be the same.
  • There is reading and writing: you cannot write when you are reading, you cannot read when you are writing, two (or more) threads cannot write at the same time, and two (or more) threads cannot read at the same time.

1. Protect shared data

        When a thread operates the shared data, it uses some code to lock the shared data. Other threads that want to operate the shared data must wait for the current operation to complete and unlock the shared data before other threads can continue to operate the shared data. Share data. In this way, the shared data will be accessed in order and according to the rules, the shared data will not be destroyed, and the program will not report an exception.

(1) Mutex mutex

        Mutex, translated into English as mutex, is actually a class and can be understood as a lock. At the same time, multiple threads can call the lock member function to try to lock the lock, but only one thread can successfully lock the lock. For other threads that have not successfully locked, the execution process will be stuck at the lock statement line. Try to lock the lock repeatedly, and the execution process will continue until the lock is successful.

        Mutexes need to be used with caution. The principle is to protect the data that needs to be protected, neither more nor less. If the data to be protected is less (for example, there are two lines of code that operate on shared data, but only one line of code is protected), it does not reach the goal. Protection effect, program execution may cause abnormalities. If you protect too much data, it will affect the program running efficiency, because when you operate this protected data, others (other threads) are waiting there, so you must do it as soon as possible after the operation. Unlock the lock so that others can operate this shared data.

#include <mutex>
std::mutex mutex;
void InputData()
{
    for (size_t i = 0; i < 100; i++)
    {
        mutex.lock();
        list.push(i);
        mutex.unlock();
    }
}

bool GetData()
{
    mutex.lock();
    if (!list.empty())
    {
        list.pop();
        mutex.unlock();          //注意
        return true;
    }
    mutex.unlock();              //注意
    return false;
}

        Rules for using lock and unlock: Use in pairs. If there is a lock, there must be an unlock. Every time lock is called, unlock must be called once. It should not and is not allowed to call lock once but call unlock twice, nor is it allowed to call 2 times. Call unlock once once for lock, otherwise the code will be unstable or even crash.

(2) std::lock_guard class template

        The C++ language is very considerate of programmers and has specially introduced a class template called std::lock_guard. This class template is very considerate, and it has a very good function, that is, it doesn't matter even if the developer forgets to unlock, it will unlock it for the developer.

        The std::lock_guard class template can be used directly to replace lock and unlock. Please note that lock_guard replaces the lock and unlock functions at the same time. That is to say, after using lock_guard, you no longer need to use lock and unlock.

        The working principle of std::lock_guard is very simple. It can be understood this way: in the constructor of the lock_guard class template, the lock member function of mutex is called, and in the destructor, the unlock member function of mutex is called.

bool GetData()
{
    std::lock_guard<std::mutex> _mutex(mutex);
    if (!list.empty())
    {
        list.pop();
        return true;
    }
    return false;
}

        ​​​​​_mutex is a random variable name. Only when _mutex goes out of scope or the function returns, the unlock member function of mutex will be called due to the execution of the std::lock_guard<std::mutex> destructor.

2. Deadlock

        The problem of deadlock is caused by at least two locks, that is, two mutexes. Each thread is here waiting for the other thread to unlock the lock. You wait for me and I wait for you.

        As long as you ensure that the two mutexes are locked in the same order, there will be no deadlock. The order of unlocking does not matter much (it is recommended that whoever locks later should unlock first).

(1) std::lock function template

        The std::lock function template can lock two or more mutexes at a time (the number of mutexes is 2 to more, not 1). It does not exist in multiple threads because of the order of the locks. The problem leads to the risk of deadlock.

        If one of these mutexes is not locked, it will be stuck waiting at std::lock. When all mutexes are locked, std::lock can return and the program execution process can continue.

        The characteristic of std::lock locking two mutexes is: either both mutexes (mutexes) are locked; or neither mutex is locked. At this time, std::lock is stuck there and constantly tries to lock the two mutexes. Mutex. Therefore, std::lock only appears when it needs to handle multiple mutexes.

3.unique_lock class template

        ​​​​ unique_lock is a class template. Its function is similar to lock_guard and more flexible than lock_guard, but it takes up memory and is less efficient. unique_lock can completely replace lock_guard.

(1)std::defer_lock

        (The prerequisite for using defer_lock is that programmers cannot lock the mutex themselves first, otherwise an exception will be reported). std::defer_lock means to initialize the mutex, but this option means that the mutex is not locked and an unlocked mutex is initialized.

(2) try_to_lock parameter

        The system will try to lock the mutex with the mutex lock, but if the lock is not successful, it will return immediately and will not be blocked there (the premise of using std::try_to_lock is that the programmer cannot lock the mutex by himself first, because std ::try_to_lock will try to lock. If the programmer locks once first, then it is equivalent to locking again. The result of two locks is that the program is stuck).

void TestLock()
{
    std::unique_lock<std::mutex> temp(mutex, std::defer_lock);
    if (temp.owns_lock())
    {
        std::cout << "defer_lock:已锁" << std::endl;     //不输出
    }
    //temp.lock();                                                 //加锁    方法一
    //temp.try_lock();                                             //加锁    方法二
    std::unique_lock<std::mutex> temp1(mutex, std::try_to_lock);   //加锁    方法三

    if (temp1.owns_lock())                              //输出
    {
        std::cout << "try_to_lock:已锁" << std::endl;
    }
}

(3) Member function lock                

        Lock the mutex. If it cannot be locked, it will block and wait to get the lock.

(4) Member function unlock        

        For a locked mutex, unlock the mutex. It cannot be used for an unlocked mutex, otherwise an exception will be reported. After locking the mutex, you can use this member function to unlock the mutex again at any time. Of course, after unlocking, if you need to operate the shared data, you must re-lock it before you can operate it. Although unique_lock can be unlocked automatically, you can also use this function to unlock it manually. Therefore, this function also reflects the flexibility of unique_lock than lock_guard - it can be unlocked at any time.

(5) Member function try_lock

        Try to lock the mutex. If the lock cannot be obtained, false is returned; if the lock is obtained, true is returned.

(6) Member function release

        Returns the pointer to the mutex object it manages and releases ownership. That is, this unique_lock is no longer related to mutex. Strictly distinguish the difference between the two member functions release and unlock. Unlock only unlocks the mutex managed by the unique_lock rather than disassociating the two.

        Once the association between the unique_lock and the managed mutex is released, if the original mutex object is in a locked state, the programmer is responsible for unlocking it.

(7)unique_lock ownership

        Ownership refers to the mutex owned by unique_lock. Unique_lock can pass the mutex it owns to other unique_locks. Therefore, the unique_lock's ownership of this mutex can be moved but cannot be copied. The transfer of this ownership is very similar to the transfer of ownership of the unique_ptr smart pointer.

        For example: when returning a unique_lock through a function, the bottom layer will call the move constructor.

4. Lock granularity

        ​ ​ ​ Some people also call the amount of code locked with a lock the granularity of the lock. The granularity is generally described in terms of thickness:

  • The less locked code, the finer the granularity, and the higher the program execution efficiency.
  • The more locked code, the coarser the granularity, and the lower the program execution efficiency (because other threads will have to wait longer to access shared data).

5.Double locking of singleton mode

 static Test* GetInstance()
     {
         if (_instance == nullptr)
         {
             std::unique_lock<std::mutex> lock(mutex);
             if (_instance == nullptr) {
                 _instance = new Test();
             }
         }
         return _instance;
     }

        It can be noticed that there are two if (_instance == nullptr). Many materials call this writing method "double lock" or "double check".

6.std::call_once

        This is a function introduced in C++11. The second parameter of this function is some other function name. Suppose there is a function named a. The function of call_once is to ensure that function a is only called once. Readers all know that if two threads call function a, then function a will definitely be called twice. However, with call_once, it can be guaranteed that even under multi-threading, this function a will only be called once.

        So from this perspective, call_once also has the capability of a mutex, and it is said to consume less resources than a mutex in terms of efficiency.

(1)std::once_flag

        std::once_flag, this is a structure, it can be understood as a mark here. call_once uses this mark to determine whether the corresponding function a is executed. After calling call_once successfully, call_once will reverse the state of this mark, so that again After calling call_once, the corresponding function a will not be executed again.

7. Condition variable std::condition_variable

        What is the use of condition variables? Of course, it is also used in threads. For example, it is used in thread A to wait for a condition to be met (such as waiting for data to be processed in the message queue). There is also a thread B (specially throwing data into the message queue). When the condition is met, When (when there is data in the message queue), thread B notifies thread A, then thread A will continue execution from where it is waiting for this condition. (Making a notification when the message queue is not empty can avoid constantly judging whether the message queue is empty.)

        std::condition_variable is a class, a class related to conditions, used to wait for a condition to be reached. This class needs to work with a mutex, and an object of this class must be generated when used.

        The notify_one function will randomly select a waiting thread to wake up, and the notify_all function will wake up all waiting threads.

class Test_condition_variable
{
private:
    std::condition_variable cv;
    std::queue<int> *list;
    static Test_condition_variable* _instance;
public:
     Test_condition_variable()
    {
         list = new std::queue<int>();
    }
     static  Test_condition_variable* GetInstance()
     {
         if (_instance == nullptr)
         {
             std::unique_lock<std::mutex> lock(mutex);
             if (_instance == nullptr) {
                 _instance = new Test_condition_variable();
                 static ReleaseInstance ri;
             }
         }
         return _instance;
     }
     class ReleaseInstance {
     public:
         ~ReleaseInstance()
         {
             if (_instance != nullptr)
             {
                 delete _instance;
                 _instance = nullptr;
             }
         }
     };
    void InputData(int data)
    {
        std::unique_lock<std::mutex> lock(mutex);
        list->push(data);
        if (list->size() > 0)
        {
            cv.notify_all();
        }
    }
    void OutputData()
    {
        std::unique_lock<std::mutex> lock(mutex);

        cv.wait(lock, [&]() {             //wait被InputData线程中的notify_all唤醒了
            if (!list->empty())           //如果没有第二个参数,则默认为一个返回false的谓词,即线程会一直等待直到被notify唤醒。即:如果唤醒之后, wait返回,执行流程走下来(注意现在互斥量是被锁着的)
            {                            //判断非常重要,因为唤醒这件事,存在虚假唤醒
                return true;             //如果唤醒之后,lambda表达式为true,那么wait返回,执行流程走下来(注意现在互斥量是被锁着的)
            }
            return false;                //如果唤醒之后,lambda表达式为false,那么这个wait又对互斥量解锁,然后又堵塞在这里等待被notify_all唤醒
            });
        //如果走到这里,说明list中一定有数据,注意此时互斥量是被锁着的
        auto value = list->front();
        list->pop();
    }
};

4. std::async and std::future, std::shared_future

        When we want the thread to return a result, in addition to assigning the thread execution result to a global variable, we can also use std::async and std::future to achieve this.

        std::async is a function template. It is usually used to start an asynchronous task. That is to say, std::async will automatically create a new thread (sometimes it will not create a new thread) and start executing the corresponding thread entry function. After starting this asynchronous task, it will return a std::future object (std::future is a class template), which contains the return result of the thread entry function. You can get the result by calling the member function get of the future object (a value will be saved in the future and can be obtained at some point in the future.).

#include <future>
int TestThreadReturn()
{
    std::chrono::milliseconds timer(5000);
    std::this_thread::sleep_for(timer);
    return -1;    //等待5秒返回-1
}
void TestFuture()
{
    std::future<int> result = std::async(TestThreadReturn);
    //result.wait();             //wait成员函数只是等待线程返回,但本身不返回结果。
    int value = result.get();    //当主线程执行到result.get()这行时,卡在这里,get成员函数等待线程结束(等待5秒)并返回结果-1。
    std::cout << value << std::endl;
}

1.Member functions of std::future

(1)get()

        Get is a very special function. It will not give up until it gets the value. The program execution flow must be stuck here waiting for the thread to return the value. Therefore, it must be ensured that the content related to std::future must return a value or must give the result value, otherwise the subsequent result.get will always be stuck.

        The get function of the future object is designed with move semantics, so once you call get, it is equivalent to moving the thread result information into the result. Therefore, you cannot call get in the future object again because the result value has been If it is moved, an exception will be reported if it is moved again. (This is the difference between std::future and std::shared_future)

(2)wait()

        This member function just waits for the thread to return, but does not return a result itself.

2.std::shared_future

        std::shared_future, like std::future, is also a class template. The get function of the future object transfers data, and shared_future is a shared future from a literal analysis, so it is not difficult to guess that the get function of shared_future should copy the data (not transfer). In this way, multiple threads can obtain the return results of the thread.

3.std::async extra parameters

        You can provide async with an additional parameter. The type of this additional parameter is std::launch type (an enumeration type) to represent some additional meanings. Take a look at what values ​​this enumeration type can take.

(1)std::launch::deferred

        This parameter indicates that the execution of the thread entry function is delayed until the wait or get function of std::future is called. If wait or get is not called, the thread will not be executed at all.

        If you subsequently call wait or get, you will find that the thread entry function is executed, but you will also be surprised to find that the cognitive async call does not create a new thread at all, but the thread entry function called in the main thread (the main thread). The thread id and the sub-thread id are the same, indicating that no sub-thread is created).

(2)std::launch::async

        This parameter indicates that the thread will be created and executed when the async function is called (forcing this asynchronous task to be executed on a new thread). This means that the system must create a new thread for execution (the main thread ID and the child thread ID are different).

(3)std::launch::async | std::launch::deferred

        If std::async is called without any additional parameters, it is equivalent to using std::launch::async | std::launch::deferred as an additional parameter, which means that the system determines whether to use synchronous (not Run tasks asynchronously (create a new thread) or asynchronously (create a new thread).

4.std::thread VS std::async

        To create a thread, the std::thread method is generally used. However, if too many threads are created in a process and system resources are tight (or if system resources are already tight), continuing to call std::thread may cause thread creation to fail and the program will crash. . Moreover, std::thread is a way of creating threads. If this thread returns a value, it is not easy to get the result.

        std::thread directly creates threads, while std::async is actually called to create an asynchronous task, which means that std::async may or may not create a thread. At the same time, std::async also has a unique advantage: programmers can get the value returned by this asynchronous task directly through the std::future object at a certain time in the future (the thread finishes executing).

        ​​​Due to system resource limitations:

  • If there are too many threads created with std::thread, the creation is likely to fail, and the program will report an exception and crash.
  • If you use std::async, you will generally not report an abnormal crash. If system resources are tight and it is impossible to create a new thread, std::async does not add additional parameters (or the additional parameters are std::launch::async|std::launch ::deferred) will not create a new thread but whoever subsequently calls result.get to request the result, then this asynchronous task will run on the thread where this get statement is executed. In other words, std::async does not guarantee that new threads will be created. If the programmer insists on creating a new thread, he must use the additional parameter std::launch::async. The price to bear for using this additional parameter is: when system resources are tight, if a new thread must be created To perform tasks, the program may generate exceptions and crash.
  • According to experience, the number of threads created in a program (process), if there are really a large number of business needs, is generally 100 to 200, and the maximum should not exceed 500. Because please don’t forget that thread scheduling and switching threads consume system resources and time.

5. Atomic operation std::atomic

        In nature, atoms are very small, and there is no substance smaller than atoms. In the world of computers, atomic operations also have similar meanings, generally referring to "indivisible operations", that is to say, the operation of The status is either completed or not completed, and there will be no semi-completed state (semi-completed: interrupted in the middle of execution).

        Mutexes can achieve the effect of atomic operations. Therefore, atomic operations can be understood as a multi-threaded concurrent programming method that does not require the use of mutex locking (lock-free) technology, or it can be understood that atomic operations are Segments of program execution that are not interrupted by multiple threads. In terms of efficiency, it can also be considered that atomic operations are more efficient. Otherwise, just use a mutex, and who would use atomic operations?

        Another thing to realize is that the locking of a mutex is generally for a code segment (several lines of code), while the atomic operation is for a variable, not a code segment.

        In C++11, std::atomic is introduced to represent atomic operations, which is a class template. This class template contains a type template parameter, so std::atomic is actually used to encapsulate a value of a certain type.

std::atomic<int> value = 0;
void TestAtomic_Thread()
{
    for (size_t i = 0; i < 1000; i++)
    {
        value++;                 //原子操作
        //value += 1;              //原子操作
        //value = value + 1;       //不是原子操作
    }
}

        ​​​​ std::atomic<int>Not all operations are atomic. Generally speaking, operations involving simple operators such as ++, --, +=, -=, &=, |=, ^=, etc. are atomic, while other operations involving more complex expressions may not be atomic. of.

        The applicable occasions for atomic operations are relatively limited. They are generally used for counting (data statistics) and other tasks, such as the cumulative number of data packets sent, the cumulative number of data packets received, etc. Imagine that multiple threads are used for counting. If there is no atomic operation, the statistical numbers will be chaotic as mentioned above. If atomic operations are used, the statistical result data obtained will remain correct.

6. Thread number issue

1. The problem of limiting the number of threads created

        ​​​​​According to tests by relevant people, generally opening about 2,000 threads is the limit. Creating more threads will lead to resource exhaustion or even program crash.

2. Recommended number of threads to create

        When programmers use some unique development technologies to develop programs, such as using IOCP completion port technology to develop network communication programs, they often receive suggestions from development interface providers. For example, it is recommended that the number of communication threads created is equal to the number of CPUs and equal to the number of CPUs. The number of CPUs*2 is equal to the number of CPUs*2+2 and so on. It is recommended to follow these recommendations as they are professional, extensively tested and authoritative.

        First: If there are many threads, the CPU must save and restore a large amount of data when switching between threads, because when the thread switches back, the data used in the thread, such as local variables, must also be restored. Obviously, saving and restoring a large amount of data takes up a lot of CPU time. The CPU spends its time saving and restoring data, but does it still have time to do other things? Therefore, when too many threads are created, you will find that the execution of each thread becomes extremely slow, and the execution efficiency of the entire system decreases instead of increasing.

        Second: Today's operating systems are all multi-tasking operating systems. Although the system virtualizes an application into an independent individual, and it seems that all hardware is used by this independent individual, the system's hardware resources must be limited. If one program takes up more, the other program will inevitably occupy less. When the resources required to run the program exceed the load of the entire computer hardware, the computer's operating efficiency will plummet, and the program execution will become extremely slow.

        It is recommended that the number of threads contained in a process should not exceed 500, and less than 200 is better. Even if it is based on business needs, generally speaking, more than 200 threads are rarely used. If the business is too large to be handled by a single computer, then a cluster solution must be considered, and desperately squeezing the hardware resources of a single computer will eventually come to an end.

Guess you like

Origin blog.csdn.net/weixin_39766005/article/details/133714025