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

本文探讨了C++11中线程等待与唤醒的实现,通过条件变量和锁结合的方式。文章对比了三种不同的唤醒策略:每个线程独立的锁和条件变量、共享锁和独立条件变量、以及共享锁和条件变量并使用数组区分。测试结果显示,共享锁和条件变量策略在频繁唤醒时可能导致CPU负荷过高,原因是惊群效应。测试代码展示了3000个线程的场景,32位系统可能无法正常运行。
摘要由CSDN通过智能技术生成

一、线程唤醒方法

  • 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位会出现无法正常启动的情况。

  • 8
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值