C++ multi-thread implementation

C++ multi-thread implementation

C++11 supports language-level multi-threaded programming, can run across platforms, and supports windows/linux/mac, etc.

Mainly involved:

  1. thread/mutex/condition_variable
  2. lock_quard/unique_lock
  3. automatic: atomic type, atomic type based on CAS operation, thread-safe
  4. sleep_for

C++'s thread essentially calls functions supported by the system, such as windows (createThread) and linux (pthread_create) for multi-threading.

Getting to know multithreading

  1. How to create and start a thread? threadTo create a thread object, the thread function and parameters required by the thread are required; the thread is automatically opened.
  2. How does the child thread end? After the sub-thread function runs, the thread ends.
  3. How does the main thread handle child threads? joinand detachmethods.
#include <iostream>
#include <string>
#include<thread>

void threadHandle1() 
{
    
    
    // 让子线程睡眠两秒,this_thread获取当前线程,chrono计时的函数
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "hello,thread1" << std::endl;
}

void threadHandle2(int time)
{
    
    
    // 让子线程睡眠两秒,this_thread获取当前线程,chrono计时的函数
    std::this_thread::sleep_for(std::chrono::seconds(time));
    std::cout << "hello,thread2" << std::endl;
}

int main()
{
    
       
    // 创建了一个线程对象t1,传入一个线程函数,新线程就开始运行了
    std::thread t1(threadHandle1);
    // join是子线程等待主线程结束,主线程继续往下执行,
    // detach则是分离线程,子线程和主线程无关联,可以独立运行,等主线程结束,整个程序结束,所有子线程都自动结束了
    
    // 传入参数的情况
    std::thread t2(threadHandle2, 2);

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

    std::cout << "main thread hello" << std::endl;

    return 0;
}

mutexes and locks

Race conditions may appear in multi-threaded programs: the execution results of multi-threaded programs are consistent, and different running results will not be produced according to the different calling sequences of threads by the CPU. So it is necessary to introduce a mutex to prevent the correctness of resource access between multiple threads.

Get to know mutexthe mutual exclusion lock for the first time, lockand use unlockthe function to complete the process mutual exclusion, which will cause the program to be interrupted and cause the memory release problem of the mutex. Specifically as follows:

// 模拟车站卖票的程序
#include <iostream>
#include <string>
#include<thread>
#include<list>
#include<mutex>

using namespace std;
// 一共有tickCount张票
int tickCount = 10;
// 定义全局互斥锁
mutex mtx;

void sellTicket(int index)
{
    
    
    // mtx.lock(); //1、这样就只会存在一个窗口在卖票,因为while循环只支持一个线程访问
    while(tickCount>0)
    {
    
       
        /*
        直接使用mutex.lock()和mutex.unlock()将会出现一个问题,
        当函数还未unlock时就因为程序中间return或者error结束后,
        导致mutex资源无法释放的问题。
        */
        mtx.lock(); //2、仅在临界区代码段 -> 原子操作 -> 线程间互斥操作 -> mutex
        // 在锁里面加判断是防止当一个进程1面临tickCount=1时,还为-1成功,
        // 另一个线程2进入while循环,只是在mtx.lock阻塞了,等进程1-1成功后,
        // 其实进程2获取到的tickCount已经由1->0,但是还是会进行tickCount--
        // 导致了最终卖出-1张票
        if(tickCount>0)
        {
    
    
            cout << "窗口:" << index << "卖出第" << tickCount << "张票。" << endl;
            tickCount--;
        }
        mtx.unlock(); //2、
        this_thread::sleep_for(chrono::milliseconds(2));
    }
    // mtx.unlock(); // 1、
}
/*
多线程程序:
竞态条件:多线程程序执行的结果是一致的,不会随着CPU对线程不同的调用顺序,而产生不同的运行结果。
*/
int main()
{
    
    
    list<thread> tlist;
    int thread_num = 3;
    for (int i = 0; i < thread_num;i++)
    {
    
    
        tlist.push_back(thread(sellTicket, i));
    }
    for (thread &t : tlist)
    {
    
    
        t.join();
    }

        return 0;
}

Therefore, it is further introduced lock_guard(it cannot be used in the process of passing or returning function parameters, nor can it be used for assignment, it can only be used in critical code segments that are simply locked and unlocked) and ( unique_lockgenerally used for process communication, and condition_variblejoint use) in the role Automatically destructed after the end of the field, similar to smart pointers. As shown in the following code

void sellTicket2(int index)
{
    
    
    while (tickCount > 0)
    {
    
    
        
        // mtx.lock(); 
        {
    
    
        	// lock_guard函数删除了拷贝构造函数和操作符=重载,类似于scoped_ptr,但保留了构造函数
            lock_guard<mutex> lock(mtx); //在这个局部作用域中,程序结束后自动析构,如果中间return了也会析构
            if (tickCount > 0)
            {
    
    
                cout << "窗口:" << index << "卖出第" << tickCount << "张票。" << endl;
                tickCount--;
        }
        }
        // mtx.unlock(); 
        this_thread::sleep_for(chrono::milliseconds(2));
    }
}
void sellTicket3(int index)
{
    
    
    while (tickCount > 0)
    {
    
    

        // mtx.lock();
        {
    
    
        // lock_guard函数删除了拷贝构造函数和操作符=重载,类似于scoped_ptr,但保留了构造函数
        // lock_guard<mutex> lock(mtx); // 在这个局部作用域中,程序结束后自动析构,如果中间return了也会析构
        unique_lock<mutex> temp_lock(mtx); // 类似于unique_ptr,虽然删除了拷贝构造函数和操作符=重载,但是扩展了右值引用
        if (tickCount > 0)
        {
    
    
                cout << "窗口:" << index << "卖出第" << tickCount << "张票。" << endl;
                tickCount--;
        }
        }
        // mtx.unlock();
        this_thread::sleep_for(chrono::milliseconds(2));
    }
}

unique_lockin conjunction with condition_variableusing:

mutex mtx;
condition_variable cv;

unique_lock<mutex> lck(mtx);
cv.wait(lck); //1、wait的作用使线程进入等待状态;2、lck.unlock可以把mtx给释放掉
// 通知cv上等待的线程,条件成立了,可以往下运行了
//其他在cv上等待的线程,收到通知,从等待状态->阻塞状态->获取互斥锁->线程执行
cv.notify_all();

thread synchronous communication

Problems with multithreaded programming:

  1. Mutual exclusion between threads; preventing resource access problems. Race condition -> critical section code segment -> atomic operation -> mutual exclusion lock mutex (lock_guard, unique_lock) / strong two-level lock-free implementation of CAS
  2. Synchronous communication between threads. Producer and consumer threading models.

mutually exclusive

Multi-threaded execution of this code of shared variables may cause a race condition, so we call this code a critical section, which is a code fragment that executes shared resources, and must not be executed by multiple threads at the same time.

So we hope that this code is mutually exclusive (mutual exclusion), that is to say, only one thread can execute the critical section code segment, and other threads are blocked and waiting to achieve the queuing effect.

Mutual exclusion is not only for multi-threaded race conditions, but also for multi-processes to avoid confusion of shared resources.

Synchronize

Mutual exclusion solves the problem of "multiple processes/threads" using critical sections, but it does not solve the problem of "multiple processes/threads" working together

We all know that in multithreading, each thread must be executed sequentially. They are independent and advance at an unpredictable speed, but sometimes we hope that multiple threads can cooperate closely to achieve a common task.

The so-called synchronization means that "multiple processes/threads" may need to wait and communicate with each other at some key points. This kind of mutual restriction of waiting and communicating information is called "process/thread" synchronization.

Producer, consumer thread communication

This involves communication between two threads. The producer and consumer threads inform each other, and lock_guardit is impossible to achieve such complex work as inter-process communication, so using unique_lockand condition_variablematching can achieve inter-process communication. The code looks like this:

#include <iostream>
#include <string>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<queue>

using namespace std;

// 定义互斥锁,用户线程间互斥
mutex mtx;
// 定义条件变量,用于线程间的同步通信
condition_variable cv;

// 最常见的问题就是消费者线程消费的更快,生产者线程还没生产出来就开始消费了
class Queue
{
    
    
    public:
    void put(int val)
    {
    
    
        // lock_guard<mutex> lock(mtx);

        unique_lock<mutex> lck(mtx);
        while(!que.empty())
        {
    
    
            // que不为空,生产者应该通知消费者去消费,消费完了在生产
            // 生产者进程应该进入阻塞状态,并把mtx互斥锁
            cv.wait(lck);
        }
        que.push(val);
        /*
        notify_one:通知另外的一个线程
        notify_all:通知另外的所有线程
        */
        // 通知其他的所有线程,生产了一个物品,可以进行消费了,
        // 其他线程得到该通知就会从 等待状态 -> 阻塞状态 -> 获取互斥锁才能继续执行。
        cv.notify_all(); 
        cout << "生产者 生产:" << val << "号物品" << endl;
    }
    int get()
    {
    
    
        // lock_guard<mutex> lock(mtx);
        unique_lock<mutex> lck(mtx);
        while(que.empty())
        {
    
    
            // 消费者发现que是空的,通知生产者线程生产物品
            // 进入等待状态,把互斥锁mutex进行释放
            cv.wait(lck);
        }
        int val = que.front();
        que.pop();
        cv.notify_all(); //消费完了,通知其他线程进行生产
        cout << "消费者 消费:" << val << "号物品" << endl;
        return val;
    }
    private:
        queue<int> que;
};
void producer(Queue* que)
{
    
    
    for (int i = 0; i <= 10;i++)
    {
    
    
        que->put(i);
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}
void consumer(Queue* que)
{
    
    
    for (int i = 0; i <= 10; i++)
    {
    
    
        que->get();
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}
int main()
{
    
    
    Queue que;
    thread t1(producer, &que);
    thread t2(consumer,&que);

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

    return 0;
}

CAS operation

Mutex locks are relatively heavy. If the critical section code does complicated things, it will be very troublesome to use mutex locks. But it is enough to use CAS to realize the atomic nature of some code operations, and CAS is lock-free. The header file used atomicis, in essence, setting certain types as atomic type variables, resulting in only one thread that can be used independently. As shown in the following example:

#include <iostream>
#include <string>
#include<atomic>
#include<list>
#include<thread>

using namespace std;

/*
使用lock_guard实现临界代码段的互斥访问
lock_guard<mutex> lock(mtx);
Count++;
*/
volatile std::atomic_bool isReady = {
    
    false};
volatile std::atomic_int number = {
    
    0};

void task()
{
    
    
    while(!isReady)
    {
    
    
        // 让线程让出当前的CPU时间片,等待下一次调度
        this_thread::yield();
    }
    for (int i = 0; i < 100;i++)
    {
    
    
        number++;
    }
}
int main()
{
    
    
    list<thread> tlist;
    for (int i = 0; i < 10;i++)
    {
    
    
        tlist.push_back(thread(task));
    }

    // 让主线程睡眠三秒
    this_thread::sleep_for(chrono::seconds(3));
    cout << "number = " << number << endl;
    isReady = true;
    cout << "number = " << number << endl;
    for(thread &t:tlist)
    {
    
    
        t.join();
    }
    cout << "number = " << number << endl;
    return 0;
}
/*output::
number = 0
number = 1000
number = 1000
*/

Guess you like

Origin blog.csdn.net/qq_45041871/article/details/131939910