C++大量线程等待与唤醒

一、线程唤醒方法

  • C++11之后提供了thread线程类,可以很方便的编写多线程程序。线程的等待和唤醒使用条件变量condition_variable和锁mutex结合实现,其中条件变量提供了wait(), notify(), notifyAll()等方法。
  • wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前条件变量上的等待线程;notify()是随机唤醒单个等待的线程,而notifyAll()是唤醒所有等待的线程。

二、线程唤醒方式比较

线程唤醒需要依靠锁和条件变量实现,那么是每个线程拥有自己独立的锁和条件变量性能高,还是共用锁和条件变量性能高呢?下面对其进行测试。

1.测试环境

  • 操作系统:windows 10 64bit
  • CPU:2个内核,4个逻辑处理器
  • 编译器:MinGW 7.3.0 64-bit
  • 测试线程数:3000个(每个线程唤醒立刻又进入等待状态)

2.测试结果

  1. 每个线程拥有独立的锁、独立的条件变量,即代码中的方式一。使用notify_one唤醒线程。
    在这里插入图片描述
  2. 所有线程共用同一个锁,但每个线程拥有独立的条件变量,即代码中的方式二。使用notify_one唤醒线程。
    在这里插入图片描述
  3. 所有的线程共用同一个锁,同一个条件变量,通过bool数组区分需要唤醒的线程。由于同一个条件变量,所以所有等待的线程处于同一等待队列,使用notify_one随机唤醒一个线程会出现无法唤醒的状态,所有使用notify_all唤醒所有等待的线程。然后根据数组区分具体需要唤醒的线程。
    在这里插入图片描述

3.结果分析

从以上三种测试结果看出,方式一和方式二无明显差别,方式三出现CPU负荷高的情况。其原因为方式三中所有等待的线程在同一等待队列,唤醒某个线程时,必须通知所有线程才能确保需要的线程被唤醒,即使用notify_all唤醒线程,此时出现了惊群效应。由于本测试程序频繁唤醒线程,所以CPU占用持续居高。

惊群效应,即当某一资源可用时,多个进程/线程会惊醒,竞争资源,导致n-1个进程/线程做了无效的调度,上下文切换,cpu瞬时增高。

三、测试代码

#include <thread>
#include <mutex>
#include <condition_variable>
#include <sstream>
#include <vector>

#define WAY 2 //设置唤醒线程方式

#if WAY == 1 //方式1,每个线程拥有独立的锁和条件变量
class ThreadCtrl
{
    
    
public:
    ThreadCtrl()
        :m_flag(false)
    {
    
    }
    void wait()
    {
    
    
        std::unique_lock<std::mutex> lock(m_mutex);
        m_cv.wait(lock, [=]{
    
     return m_flag; });
        m_flag = false;
    }
    void wake()
    {
    
    
        std::unique_lock<std::mutex> lock(m_mutex);
        m_flag = true;
        m_cv.notify_one();
    }
private:
    std::mutex m_mutex;
    std::condition_variable m_cv;
    bool m_flag;
};

class ThreadManage
{
    
    
public:
    ThreadManage(){
    
    }
    ~ThreadManage()
    {
    
    
        for (auto ctrl : m_CtrlVec)
            delete ctrl;
        m_CtrlVec.clear();
    }
    ThreadCtrl* createCtrl()
    {
    
    
        auto ctrl = new ThreadCtrl();
        m_mutex.lock();
        m_CtrlVec.push_back(std::move(ctrl));
        m_mutex.unlock();
        return ctrl;
    }
    std::vector<ThreadCtrl*>& getAllCtrl() {
    
     return m_CtrlVec; }
private:
    std::vector<ThreadCtrl*> m_CtrlVec;
    std::mutex m_mutex;
};
#elif WAY == 2 //方式2,所有线程共用同一锁,但每个线程拥有独立的条件变量
class ThreadCtrl
{
    
    
public:
    ThreadCtrl(std::mutex* mutex)
        :m_mutex(mutex), m_flag(false)
    {
    
    }
    void wait()
    {
    
    
        std::unique_lock<std::mutex> lock(*m_mutex);
        m_cv.wait(lock, [=]{
    
     return m_flag; });
        m_flag = false;
    }
    void wake()
    {
    
    
        std::unique_lock<std::mutex> lock(*m_mutex);
        m_flag = true;
        m_cv.notify_one();
    }
private:
    std::mutex* m_mutex;
    std::condition_variable m_cv;
    bool m_flag;
};

class ThreadManage
{
    
    
public:
    ThreadManage(){
    
    }
    ~ThreadManage()
    {
    
    
        for (auto ctrl : m_CtrlVec)
            delete ctrl;
        m_CtrlVec.clear();
    }
    ThreadCtrl* createCtrl()
    {
    
    
        auto ctrl = new ThreadCtrl(&m_wakeMutex);
        m_mutex.lock();
        m_CtrlVec.push_back(std::move(ctrl));
        m_mutex.unlock();
        return ctrl;
    }
    std::vector<ThreadCtrl*>& getAllCtrl() {
    
     return m_CtrlVec; }
private:
    std::vector<ThreadCtrl*> m_CtrlVec;
    std::mutex m_mutex;
    std::mutex m_wakeMutex;
};
#elif WAY == 3 //方式3,所有的线程共用同一锁和同一条件变量
class ThreadCtrl
{
    
    
public:
    ThreadCtrl(std::mutex* mutex, std::condition_variable* cv, bool* flag)
        :m_mutex(mutex), m_cv(cv), m_flag(flag)
    {
    
    }
    void wait()
    {
    
    
        std::unique_lock<std::mutex> lock(*m_mutex);
        m_cv->wait(lock, [=]{
    
     return *m_flag; });
        *m_flag = false;
    }
    void wake()
    {
    
    
        std::unique_lock<std::mutex> lock(*m_mutex);
        *m_flag = true;
        m_cv->notify_all();//所有线程共用条件变量,所以必须通知所有等待的线程
    }
private:
    std::mutex* m_mutex;
    std::condition_variable* m_cv;
    bool* m_flag;
};

class ThreadManage
{
    
    
public:
    ThreadManage(){
    
    }
    ~ThreadManage()
    {
    
    
        for (auto ctrl : m_CtrlVec)
            delete ctrl;
        m_CtrlVec.clear();
    }
    ThreadCtrl* createCtrl()
    {
    
    
        auto flag = new bool(false);
        auto ctrl = new ThreadCtrl(&m_wakeMutex, &m_cv, flag);
        m_mutex.lock();
        m_flagVec.push_back(std::move(flag));
        m_CtrlVec.push_back(std::move(ctrl));
        m_mutex.unlock();
        return ctrl;
    }
    std::vector<ThreadCtrl*>& getAllCtrl() {
    
     return m_CtrlVec; }
private:
    std::vector<ThreadCtrl*> m_CtrlVec;
    std::mutex m_mutex;
    std::mutex m_wakeMutex;
    std::condition_variable m_cv;
    std::vector<bool*> m_flagVec;
};
#endif

//线程ID转为数字
long long threadIdToNumber(const std::thread::id& id)
{
    
    
    std::stringstream oss;
    oss << id;
    return std::stoll(oss.str());
}

//测试线程内执行的函数
void fun(ThreadCtrl* ctrl)
{
    
    
    while (true)
    {
    
    
        ctrl->wait();//睡眠
#if 0
        auto tid = std::this_thread::get_id();
        auto lid = threadIdToNumber(tid);
        printf("Thread ID: %lld\n", lid);
#endif
    }
}

//负责唤醒其它线程
void wakeFun(ThreadManage* manage)
{
    
    
    std::vector<ThreadCtrl*> allCtrl = manage->getAllCtrl();
    const int num = allCtrl.size();
    int id = num - 1;
    while (true)
    {
    
    
        allCtrl[id]->wake();//唤醒
        id--;
        if (id < 0) id = num - 1;
        //等待1ms,尽可能确保所有线程处于等待状态
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
}

int main()
{
    
    
    // 测试线程个数
    constexpr const int count = 3000;
    // 创建线程管理,管理线程唤醒
    auto manage = new ThreadManage;
    // 创建测试线程
    std::vector<std::thread> threadVec(count);
    for (int i = 0; i < count; i++) {
    
    
        auto ctrl = manage->createCtrl();
        threadVec[i] = std::thread(fun, ctrl);
    }
    std::thread wakeThread(wakeFun, manage);
    // 等待子线程
    for (int i = 0; i < count; i++)
        threadVec[i].join();
    wakeThread.join();
    // 释放资源
    delete manage;
}

测试中线程数为3000个,此时必须为64位程序才能正常运行,32位会出现无法正常启动的情况。

猜你喜欢

转载自blog.csdn.net/kpengk/article/details/106875074
今日推荐