一:补充一些知识点
<1>虚假唤醒
class MA
{
public:
int i = 0;
std::unique_lock<std::mutex> rtn_unique_gurad()
{
std::unique_lock <std::mutex> tmpgurad(my_mutex_1);
return tmpgurad; //从函数返回一个局部的unique_lock对象是可以的,三章十四届讲过移动构造函数
//返回这种局部对象会导致系统生成临时unique_lock对象,并调用unique_lock的移动构造函数
}
//把收到的消息(玩家命令)放入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
std::unique_lock<std::mutex> sbguard_1 = rtn_unique_gurad();
msgRecvQueue.push_back(i); //假设这个数字i就是我们收到的命令,直接弄到消息队列中
//假如outMsgRecvQueue()正在处理一个事务,需要一段时间,而不是正卡在wait()那里等待唤醒;
//那么此时这个notify_one()这个调用也许就没效果。
my_cond.notify_one(); //我们尝试把wait()线程唤醒,执行完这行,那么outMsgRecvQueue()里边的wait()就会被唤醒
}
cout << "i = " << i << endl;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
while (true)
{
//wait()用来等待一个东西
//如果第二个参数lambda表达式返回值是true,那wait()直接返回;
//如果第二个参数lambda表达式返回值是false,那么wait()将解锁互斥量,并堵塞到本行
//那堵塞到什么时候为止呢?堵塞到其他某个线程调用notify_one()成员函数为止;
//如果wait()没有第二个参数:my_cond.wait(sbguard1); 那么就跟第二个参数lambda表达式返回false效果一样,堵塞本行
//wait()将解锁互斥量,并阻塞到本行,阻塞到其他某个线程调用欧冠notify_one()成员函数为止;
//当其他线程用notify_one()将本wait()(原来是睡着/阻塞)的状态唤醒后,wait就开始恢复干活了,恢复后wait干什么活?
//a> wait()不断的尝试重新获取互斥量锁,如果获取不到,那么流程就卡在wait()这这里等着获取,如果获取到了锁(等于加了锁),那么wait()就继续执行b;
//b>
//b.1> 如果wait()有第二个参数(lambda),就判断这个lambda表达式,如果lambda表达式为false,那wait()又对互斥量解锁,然后又休眠这里等待再次被notify_one唤醒;
//b.2> 如果lambda表达式为true, 则wait返回,流程走下来(此时互斥锁被锁着);
//b.3> 如果wait()没有第二个参数,则wait()返回,流程走下来。
std::unique_lock<std::mutex> sbguard_1(my_mutex_1);
my_cond.wait(sbguard_1, [this] { //一个lambda就是一个可调用对象(函数)
if (!msgRecvQueue.empty())
return true;
return false;
});
//流程只要能走到这里来,这个互斥锁一定是锁着的,同时msgRecvQueue至少是有一条数据的
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
sbguard_1.unlock(); //因为unique_lock的灵活性,所以我们可以随时的unlock解锁,以免锁住太长时间。
cout << "outMsgRecvQueue()执行,取出一个元素 " << command << " " << std::this_thread::get_id() << endl;
} //end while
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家发送的命令
mutex my_mutex_1; //创建了一个互斥量 (一把锁头)
std::condition_variable my_cond; //生成一个条件变量对象
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
虚假唤醒:wait中要有第二个参数(lambda)并且这个lambda中要正确判断要处理的公共数据是否存在;
wait()、notify_one()、notify_all()。
<2>atomic
class MA
{
public:
MA()
{
atm = 0;
}
public:
void inMsgRecv()
{
for (int i = 0; i < 10000000; ++i)
{
//atm += 1;
atm = atm + 1; //读atm是个原子操作,但是整个这一行代码并不是个原子操作
//auto atm2 = atm; //这种定义时初始化操作不允许,显示“尝试引用已删除的函数”编译器内部肯定把拷贝构造函数给干掉了用 =delete,因为编译器不好操作
//atomic<int> atm2 = atm; //也不允许
//atomic<int> atm2;
//atm2 = atm //尝试引用已删除的函数,拷贝赋值运算符也不让用
//load():以原子方式读atomic对象的值
atomic<int> atm2(atm.load()); //读
auto atm3(atm.load());
//store()以原子方式写入内容
atm2.store(12);
atm2 = 12;
}
}
void outMsgRecv()
{
int command = 0;
while (true)
{
cout << "atm = " << atm << endl;
} //end while
cout << "end" << endl;
}
private:
atomic<int> atm;
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecv, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecv, &myobj);
std::thread myInMsgObj2(&MA::inMsgRecv, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
myInMsgObj2.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
二:浅谈线程池
<1>场景设想
服务器程序 -> 客户端,每来一个客户端,就创建一个新线程为该客户提供服务。
(1)网络游戏,2万玩家不可能给每个玩家创建个新线程,此程序写法在这种场景下不通;
(2)程序稳定性问题:编写的代码中,偶尔创建一个线程这种代码,这种写法,就让人感到不安;
线程池:把一堆线程弄到一起,统一管理。这种统一管理调试,循环利用线程方式,就叫线程池。
<2>实现方式:
在程序启动时,一次性的创建好一定数量的线程。10,8,100-200,更让人放心,觉得程序代码更稳定。
三:线程创建数量谈
<1>线程开的数量极限问题,2000个线程基本就是极限,再创建线程就崩溃。
<2>线程创建数量建议
(1)采用某些技术开发程序;api接口提供商建议你创建线程数量 = CPU数量、CPU * 2、CPU * 2 + 2,遵照专业建议和指示来;专业意见确保程序高效执行;
(2)创建多线程完成业务;一个线程等于一条通路;100要堵塞充值,我们这里开110个线程,那是很合适的;
(3)1800个线程,建议线程数量尽量不要超过500个,能控制在200以内。
四:c++11多线程总结