一、线程唤醒方法
- 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.测试结果
- 每个线程拥有独立的锁、独立的条件变量,即代码中的方式一。使用notify_one唤醒线程。
- 所有线程共用同一个锁,但每个线程拥有独立的条件变量,即代码中的方式二。使用notify_one唤醒线程。
- 所有的线程共用同一个锁,同一个条件变量,通过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位会出现无法正常启动的情况。