Detailed explanation of multithreading of C++ features

1. Multithreading

Traditional C++ (before C++11) did not introduce the concept of thread. Before C++11 came out, if we want to implement multithreading in C++, we need to use the API provided by the operating system platform, such as Linux's < pthread.h>, or <windows.h> under windows.

C++11 provides multithreading at the language level, which is included in the header file <thread>. It solves the cross-platform problem and provides 管理线程、保护共享数据、线程间同步操作、原子操作等类. The new C++11 standard introduces five header files to support multi-threaded programming, as shown in the following figure:
insert image description here

1.1 Multi-process and multi-thread

  • Multi-process concurrency

The use of multi-process concurrency is to divide an application into multiple independent processes (each process has only one thread), and these independent processes can communicate with each other to complete tasks together. Since the operating system provides a large number of protection mechanisms for the process, 以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码. But this also creates two disadvantages of multi-process concurrency:

  1. In interprocess communication, whether using signals, sockets, files, pipes, etc., the use is either, or, or 比较复杂both 速度较慢.
  2. To run multiple threads 开销很大, the operating system needs to 分配很多的资源来对这些进程进行管理.

When multiple processes complete the same task concurrently, it is unavoidable that: 操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择.

  • multi-thread concurrency

Multithreaded concurrency refers to the execution of multiple threads in the same process.

Advantages :

Those who have knowledge about the operating system should know that 线程是轻量级的进程each thread can independently run different instruction sequences, but the thread does not own resources independently, but depends on the process that created it. That is to say, 同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递. In this way, multiple threads in the same process can easily share data and communicate, which is more suitable for concurrent operations than processes.

Disadvantages :

Due to the lack of the protection mechanism provided by the operating system, when multi-threads share data and communicate, it is necessary to 程序员做更多的工作ensure that the operations on the shared data segment are carried out in the expected order of operations, and to avoid deadlock (deadlock) as much as possible.

Excerpted from "C++ Concurrent Programming"

1.2 Multithreading understanding

  • Multiple threads on a single CPU core.

One time slice runs the code of one thread, which is not true parallel computing.
insert image description here

  • Multiple CPUs or multiple cores

Real parallel computing can be done.
insert image description here

1.3 Create thread

Creating a thread is very simple, just add the function to the thread.

  • Form 1:
std::thread myThread ( thread_fun);//函数形式为void thread_fun()
myThread.join();
//同一个函数可以代码复用,创建多个线程
  • Form 2:
std::thread myThread ( thread_fun(100));
myThread.join();
//函数形式为void thread_fun(int x)
//同一个函数可以代码复用,创建多个线程
  • Form 3:
std::thread (thread_fun,1).detach();//直接创建线程,没有名字
//函数形式为void thread_fun(int x)
  • code example
#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
    
    
    cout<<"子线程1"<<endl;
}
void thread_2(int x)
{
    
    
    cout<<"x:"<<x<<endl;
  cout<<"子线程2"<<endl;
}
int main()
{
    
    
  thread first ( thread_1);     // 开启线程,调用:thread_1()
  thread second (thread_2,100);  // 开启线程,调用:thread_2(100)
  //thread third(thread_2,3);//开启第3个线程,共享thread_2函数。
  std::cout << "主线程\n";

  first.join(); //必须说明添加线程的方式            
  second.join(); 
  std::cout << "子线程结束.\n";//必须join完成
  return 0;
}

1.4 join and detach methods

After the thread is started, it is necessary to determine how to wait for the end of thread execution before the thread associated with the thread is destroyed . Such as the join in the above example.

  • In the detach mode, the started thread runs independently in the background, and the current code continues to execute without waiting for the end of the new thread.
  • The join method waits for the started thread to complete before continuing to execute.

You can use joinable to determine whether it is join mode or detach mode.

if (myThread.joinable()) foo.join();

(1) join example

In the following code, the code after join will not be executed unless the child thread ends.

#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
    
    
    while(1)
    {
    
    
        //cout<<"子线程1111"<<endl;
    }
}
void thread_2(int x)
{
    
    
    while(1)
    {
    
    
        //cout<<"子线程2222"<<endl;
    }
}
int main()
{
    
    
    thread first ( thread_1);     // 开启线程,调用:thread_1()
    thread second (thread_2,100);  // 开启线程,调用:thread_2(100)

    first.join();                // pauses until first finishes 这个操作完了之后才能destroyed
    second.join();               // pauses until second finishes//join完了之后,才能往下执行。
    while(1)
    {
    
    
        std::cout << "主线程\n";
    }
    return 0;
}

(2) Example of detach

In the following code, the main thread does not wait for the child thread to finish. If the main thread finishes running, the program ends.

#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
    
    
    while(1)
    {
    
    
        cout<<"子线程1111"<<endl;
    }
}
void thread_2(int x)
{
    
    
    while(1)
    {
    
    
        cout<<"子线程2222"<<endl;
    }
}
int main()
{
    
    
    thread first ( thread_1);     // 开启线程,调用:thread_1()
    thread second (thread_2,100);  // 开启线程,调用:thread_2(100)

    first.detach();                
    second.detach();            
    for(int i = 0; i < 10; i++)
    {
    
    
        std::cout << "主线程\n";
    }
    return 0;
}

this_thread is a class with 4 functional functions, as follows:

function use illustrate
get_id std::this_thread::get_id() get thread id
yield std::this_thread::yield() Abandon thread execution and return to ready state
sleep_for std::this_thread::sleep_for(std::chrono::seconds(1)); Pause for 1 second
sleep_until as follows Execute after one minute, as follows
using std::chrono::system_clock;
std::time_t tt = system_clock::to_time_t(system_clock::now());

struct std::tm * ptm = std::localtime(&tt);
cout << "Waiting for the next minute to begin...\n";
++ptm->tm_min; //加一分钟
ptm->tm_sec = 0; //秒数设置为0
//暂停执行,到下一整分执行
this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));

2. mutex

The mutex header file mainly declares classes related to mutexes. mutex provides 4 types of mutual exclusion, as shown in the following table.

type illustrate
std::mutex The most basic Mutex class.
std::recursive_mutex Recursive Mutex class.
std::time_mutex Timed Mutex class.
std::recursive_timed_mutex A timed recursive Mutex class.

std::mutex is the most basic mutex in C++11. The std::mutex object provides the characteristics of exclusive ownership - that is, it does not support recursive locking of the std::mutex object, while std::recursive_lock does Mutex objects can be locked recursively.

2.1 lock and unlock

Common operations of mutex:

  • lock(): resource lock
  • unlock(): unlock resource
  • trylock(): Check whether it is locked, it has the following three types:

(1) If it is not locked, return false and lock it;
(2) If other threads have locked it, return true;
(3) If the same thread has already locked it, a deadlock will occur.

Deadlock : It refers to a phenomenon in which two or more processes are blocked due to competition for resources or communication with each other during execution . If there is no external force, they will not be able to advance. At this time, the system is said to be in a deadlock state or a deadlock has occurred in the system, and these processes that are always waiting for each other are called deadlock processes.

The following describes lock and unlock with examples.

After the same mutex variable is locked, only one thread is allowed to access it within a period of time. For example:

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
    
    
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx.lock();
  for (int i=0; i<n; ++i) {
    
     std::cout << c; }
  std::cout << '\n';
  mtx.unlock();
}

int main ()
{
    
    
  std::thread th1 (print_block,50,'*');//线程1:打印*
  std::thread th2 (print_block,50,'$');//线程2:打印$

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

  return 0;
}

output:

**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

If they are different mutex variables , because they do not involve competition for the same resource, the following codes may be printed alternately, or another thread may modify the common global variable! ! ! :

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx_1;           // mutex for critical section
std::mutex mtx_2;           // mutex for critical section

int test_num = 1;

void print_block_1 (int n, char c) {
    
    
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx_1.lock();
  for (int i=0; i<n; ++i) {
    
    
      //std::cout << c;
      test_num = 1;
      std::cout<<test_num<<std::endl;
  }
  std::cout << '\n';
  mtx_1.unlock();
}
void print_block_2 (int n, char c) {
    
    
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx_2.lock();
  test_num = 2;
  for (int i=0; i<n; ++i) {
    
    
      //std::cout << c;
      test_num = 2;
      std::cout<<test_num<<std::endl;
  }
  mtx_2.unlock();
}

int main ()
{
    
    
  std::thread th1 (print_block_1,10000,'*');
  std::thread th2 (print_block_2,10000,'$');

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

  return 0;
}

2.2 lock_guard

When a lock_guard object is created, it attempts to take ownership of the mutex provided to it. When the control flow leaves the scope of the lock_guard object, the lock_guard destroys and releases the mutex.

Features of lock_guard:

  • Locking upon creation, automatically destructing and unlocking when the scope ends , without manual unlocking
  • Cannot be unlocked halfway , you must wait for the scope to end before unlocking
  • cannot copy

code example

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

int g_i = 0;
std::mutex g_i_mutex;  // protects g_i,用来保护g_i

void safe_increment()
{
    
    
    const std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
    std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
    // g_i_mutex自动解锁
}

int main()
{
    
    
	std::cout << "main id: " <<std::this_thread::get_id()<<std::endl;
    std::cout << "main: " << g_i << '\n';

    std::thread t1(safe_increment);
    std::thread t2(safe_increment);

    t1.join();
    t2.join();

    std::cout << "main: " << g_i << '\n';
}

illustrate:

  1. The function of this program is to add 1 to g_i every time a thread passes through.
  2. Because the common resource g_i is involved, a common mutex is required: g_i_mutex.
  3. The id of the main thread is 1, so the next thread id will increase by 1 in turn.

2.3 unique_lock

Simply put, unique_lock is an upgraded and enhanced version of lock_guard. It has all the functions of lock_guard and has many other methods. It is more flexible and convenient to use, and can meet more complex locking needs.

Unique_lock features:

  • It can be created without locking (by specifying the second parameter as std::defer_lock), and then locked when needed
  • Can be locked and unlocked at any time
  • The scope rules are the same as lock_grard, and the lock is automatically released when it is destructed
  • Can't be copied, can be moved
  • The condition variable requires a lock of this type as a parameter (unique_lock must be used in this case)

All lock_guard can do, can use unique_lock to do, and vice versa. So when to use lock_guard? Very simple, when you need to use locks, first consider using lock_guard, because lock_guard is the simplest lock.

Here is a code example:

#include <mutex>
#include <thread>
#include <iostream>
struct Box {
    
    
    explicit Box(int num) : num_things{
    
    num} {
    
    }

    int num_things;
    std::mutex m;
};

void transfer(Box &from, Box &to, int num)
{
    
    
    // defer_lock表示暂时unlock,默认自动加锁
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);

    //两个同时加锁
    std::lock(lock1, lock2);//或者使用lock1.lock()

    from.num_things -= num;
    to.num_things += num;
    //作用域结束自动解锁,也可以使用lock1.unlock()手动解锁
}

int main()
{
    
    
    Box acc1(100);
    Box acc2(50);

    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);

    t1.join();
    t2.join();
    std::cout << "acc1 num_things: " << acc1.num_things << std::endl;
    std::cout << "acc2 num_things: " << acc2.num_things << std::endl;
}

illustrate:

  1. The function of this function is to subtract a num from a variable in one structure and load it into a variable in another structure.
  2. std::mutex m; In the structure, the mutex is not shared. But it can be locked with only one lock, because after the reference is passed, the same lock is passed to two functions.
  3. cout needs to be performed after join, otherwise the result of cout is not necessarily the final result.
  4. std::ref is used to wrap values ​​passed by reference.
  5. std::cref is used to wrap values ​​passed by const reference.

3. condition_variable

The header file of condition_variable has two variable classes, one is condition_variable and the other is condition_variable_any. condition_variable must be used in conjunction with unique_lock. condition_variable_any can use any lock. The following takes condition_variable as an example.

The condition_variable condition variable can block (wait, wait_for, wait_until) the calling thread until the notification is resumed using (notify_one or notify_all) . condition_variable is a class. This class has both a constructor and a destructor. When using it, you need to construct the corresponding condition_variable object and call the corresponding function of the object to realize the above functions.

type illustrate
condition_variable build object
destruct delete
wait Wait until notified
wait_for Wait for timeout or until notified
wait_until Wait until notified or time point
notify_one Unlocks one thread, if there are more than one, it is unknown which thread executes
notify_all unlock all threads
cv_status This is a class that represents the state of a variable as shown below
enum class cv_status {
    
     no_timeout, timeout };

3.1 wait

The current thread will be blocked after calling wait() (at this time the current thread should have acquired the lock (mutex), you might as well set the lock lck), until another thread calls notify_* to wake up the current thread.

When a thread is blocked, this function will automatically call lck.unlock() to release the lock, so that other threads that are blocked on the lock competition can continue to execute . In addition, once the current thread is notified (notified, usually another thread calls notify_* to wake up the current thread), the wait() function also automatically calls lck.lock(), so that the state of lck is the same as when the wait function is called.

Code example:

#include <iostream>           // std::cout
#include <thread>             // std::thread, std::this_thread::yield
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;

int cargo = 0;
bool shipment_available() {
    
    return cargo!=0;}

void consume (int n) {
    
    
    for (int i=0; i<n; ++i) {
    
    
        std::unique_lock<std::mutex> lck(mtx);//自动上锁
        //第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源
        cv.wait(lck,shipment_available);
        // consume:
        std::cout << cargo << '\n';
        cargo=0;
    }
}

int main ()
{
    
    
    std::thread consumer_thread (consume,10);

    for (int i=0; i<10; ++i) {
    
    
        //每次cargo每次为0才运行。
        while (shipment_available()) std::this_thread::yield();
        std::unique_lock<std::mutex> lck(mtx);
        cargo = i+1;
        cv.notify_one();
    }

    consumer_thread.join();,
    return 0;
}

illustrate:

  1. The while in the main thread runs only when cargo=0.
  2. Every time cargo is set to 0, the child thread will be notified to unblock (non-blocking), that is, the child thread can continue to execute.
  3. After the cargo in the child thread is set to 0, wait starts waiting again. That is to say, if shipment_available is false, wait.

3.2 wait_for

Similar to std::condition_variable::wait(), but wait_for can specify a time period, and the thread will be blocked until the current thread receives the notification or the specified time rel_time times out. Once timeout or notification from other threads is received, wait_for returns, and the remaining processing steps are similar to wait().

template <class Rep, class Period>
  cv_status wait_for (unique_lock<mutex>& lck,
                      const chrono::duration<Rep,Period>& rel_time);

In addition, the last parameter pred of the overloaded version of wait_for represents the prediction condition of wait_for, and only when the pred condition is false, calling wait() will block the current thread, and after receiving notifications from other threads, only when pred is true will be unblocked.

template <class Rep, class Period, class Predicate>
       bool wait_for (unique_lock<mutex>& lck,
                      const chrono::duration<Rep,Period>& rel_time, Predicate pred);

Code example:

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <chrono>             // std::chrono::seconds
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable, std::cv_status

std::condition_variable cv;

int value;

void read_value() {
    
    
  std::cin >> value;
  cv.notify_one();
}

int main ()
{
    
    
  std::cout << "Please, enter an integer (I'll be printing dots): \n";
  std::thread th (read_value);

  std::mutex mtx;
  std::unique_lock<std::mutex> lck(mtx);
  while (cv.wait_for(lck,std::chrono::seconds(1))==std::cv_status::timeout) {
    
    
    std::cout << '.' << std::endl;
  }
  std::cout << "You entered: " << value << '\n';

  th.join();

  return 0;
}

  1. Notifications or timeouts are unlocked, so the main thread keeps printing.
  2. In the example, as long as one second passes, it will continue to print.

4. Thread pool

4.1 Concept

In a program, if we need to use threads multiple times, this means that threads need to be created and destroyed multiple times. The process of creating and destroying threads will inevitably consume memory, and too many threads will bring about the overhead of mobilization, which in turn affects cache locality and overall performance.

The creation and destruction of threads has the following disadvantages:

  • Creating too many threads will waste certain resources, and some threads will not be fully used.
  • Destroying too many threads will waste time creating them again later.
  • Creating threads too slowly will result in long waits and poor performance.
  • Destroying threads is too slow, starving other threads of resources.

The thread pool maintains multiple threads, which avoids the cost of creating and destroying threads when processing short-term tasks.

4.2 Implementation of thread pool

Because it is time-consuming to create threads while the program is running, we adopt the idea of ​​pooling: create multiple threads before the program starts running, so that when the program is running, it only needs to be used from the thread pool .Greatly improved the operating efficiency of the program.

A general thread pool will consist of the following parts:

  1. Thread Pool Manager (ThreadPoolManager): used to create and manage thread pools, that is, thread pool classes
  2. Work Thread (WorkThread): thread in the thread pool
  3. Task queue task: used to store unprocessed tasks. Provides a buffering mechanism.
  4. append: interface for adding tasks

Thread pool implementation code:

#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include <vector>
#include <queue>
#include <thread>
#include <iostream>
#include <stdexcept>
#include <condition_variable>
#include <memory> //unique_ptr
#include<assert.h>

const int MAX_THREADS = 1000; //最大线程数目

template <typename T>
class threadPool
{
    
    
public:
    threadPool(int number = 1);//默认开一个线程
    ~threadPool();
    std::queue<T *> tasks_queue;		   //任务队列

    bool append(T *request);//往请求队列<task_queue>中添加任务<T *>

private:
    //工作线程需要运行的函数,不断的从任务队列中取出并执行
    static void *worker(void *arg);
    void run();

private:
    std::vector<std::thread> work_threads; //工作线程

    std::mutex queue_mutex;
    std::condition_variable condition;  //必须与unique_lock配合使用
    bool stop;
};//end class

//构造函数,创建线程
template <typename T>
threadPool<T>::threadPool(int number) : stop(false)
{
    
    
    if (number <= 0 || number > MAX_THREADS)
        throw std::exception();
    for (int i = 0; i < number; i++)
    {
    
    
        std::cout << "created Thread num is : " << i <<std::endl;
        work_threads.emplace_back(worker, this);//添加线程
        //直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
    }
}
template <typename T>
inline threadPool<T>::~threadPool()
{
    
    

    std::unique_lock<std::mutex> lock(queue_mutex);
    stop = true;

    condition.notify_all();
    for (auto &ww : work_threads)
        ww.join();//可以在析构函数中join
}
//添加任务
template <typename T>
bool threadPool<T>::append(T *request)
{
    
    
    /*操作工作队列时一定要加锁,因为他被所有线程共享*/
    queue_mutex.lock();//同一个类的锁
    tasks_queue.push(request);
    queue_mutex.unlock();
    condition.notify_one(); //线程池添加进去了任务,自然要通知等待的线程
    return true;
}
//单个线程
template <typename T>
void *threadPool<T>::worker(void *arg)
{
    
    
    threadPool *pool = (threadPool *)arg;
    pool->run();//线程运行
    return pool;
}
template <typename T>
void threadPool<T>::run()
{
    
    
    while (!stop)
    {
    
    
        std::unique_lock<std::mutex> lk(this->queue_mutex);
        /* unique_lock() 出作用域会自动解锁 */
        this->condition.wait(lk, [this] {
    
     return !this->tasks_queue.empty(); });
        //如果任务为空,则wait,就停下来等待唤醒
        //需要有任务,才启动该线程,不然就休眠
        if (this->tasks_queue.empty())//任务为空,双重保障
        {
    
    
            assert(0&&"断了");//实际上不会运行到这一步,因为任务为空,wait就休眠了。
            continue;
        }
        else
        {
    
    
            T *request = tasks_queue.front();
            tasks_queue.pop();
            if (request)//来任务了,开始执行
                request->process();
        }
    }
}
#endif

illustrate:

  • The number of threads required for constructor creation
  • A thread corresponds to a task, the task may be completed at any time, and the thread may sleep, so the task is implemented with a queue (the number of threads is limited), and the thread uses the wait mechanism.
  • Tasks are constantly being added, which may be greater than the number of threads, and the task at the head of the queue is executed first.
  • Only after the task (append) is added, the thread condition.notify_one() is started.
  • wait means that when the task is empty, the thread sleeps, waiting for the addition of new tasks.
  • Locks need to be added when adding tasks because of shared resources.

Test code:

#include "mythread.h"
#include<string>
#include<math.h>
using namespace std;
class Task
{
    
    
    public:
    void process()
    {
    
    
        //cout << "run........." << endl;
        //测试任务数量
        long i=1000000;
        while(i!=0)
        {
    
    
            int j = sqrt(i);
            i--;
        }
    }
};
int main(void)
{
    
    
    threadPool<Task> pool(6);//6个线程,vector
    std::string str;
    while (1)
    {
    
    
            Task *tt = new Task();
            //使用智能指针
            pool.append(tt);//不停的添加任务,任务是队列queue,因为只有固定的线程数
            cout<<"添加的任务数量: "<<pool.tasks_queue.size()<<endl;;
            delete tt;
    }
}

reference:

C++11 thread pool implements github: https://github.com/progschj/ThreadPool

Two implementations of C++11 thread pool: https://blog.csdn.net/liushengxi_root/article/details/83932654

Atomic operations (similar to mutual exclusion, but closer to the underlying implementation, more efficient. Not yet updated)

Guess you like

Origin blog.csdn.net/yohnyang/article/details/131499421