第1节 并发基本概念及实现、进程、线程基本概念
一、并发、进程、线程的基本概念和综述
1、并发:一个程序同时执行多个独立的任务
2、可执行程序:.exe rwxrwxrwx(x执行权限)
3、进程:运行(双击或./文件名)一个可执行程序
4、线程:每个进程都有唯一一个主线程,主线程随进程启动而自动启动,与进程唇齿相依
自己创建的线程不走main函数道路,有自己的执行通路;每个线程要有自己的堆栈空间(1M)左右。
二、并发的实现方法
1、多个进程实现并发
比如:账号服务器和游戏逻辑服务器之间的进程间通信,同一台电脑上可以用管道、文件、消息队列、共享内存通信,不同电脑可以用socket进行通信。
2、多线程实现并发
一个进程中所有线程共享内存,即可以有全局变量、指针等。
线程优点:启动速度快,执行速度快,更轻量级;系统资源开销少。
线程缺点:数据一致性问题(比如同时写东西)。
三、c++11新标准线程库(c++本身增加了对多线程的支持,可移植性大大提高)
以往的多线程代码不能跨平台(除了pthread),要配置,不方便!
第2节 线程启动、结束、创建线程方法、join、detach
一、线程的开始与结束
自己创建的线程也要从一个函数开始运行,函数运行完,线程结束;
一般情况下,若主线程执行完毕,子线程还没有执行完毕,子线程会被操作系统强行终止。
1、包含头文件:#include<thread>
2、初始函数要写:myprint()
3、main函数中
thread mytobj(myprint); //创建线程,线程执行起点为myprint,然后myprint执行
mytobj.join(); //join表示阻塞,等待myprint执行完再汇合
4、detach():主线程不用等待子线程,子线程被c++运行时库接管,在后台执行(不显示)
5、joinable():判断是否可以成功使用join和detach,线程对象join或detach后变为false。
二、其他创建线程手法
1、用类对象:入口函数为operator()
2、用lambda表达式
第3节 线程传参详解、detach大坑、成员函数做线程入口函数
一、传递临时对象作为线程参数
要避免的陷阱:反对往线程函数中传引用和指针
thread mytobj(myprint,mvar,string(mybuf));
//这种写法mybuf到string的转换是在main函数结束后进行的
thread mytobj(myprint,mvar,string(mybuf));
//使用临时对象,main之前已经构造完成了
总结:
1、传递int这种简单类型参数,都用值传递(不用引用);
2、传递类对象,要避免隐式类型转换,自己创建临时对象,入口函数的形参用引用来接。
二、传递临时对象作为线程参数继续
1、线程id概念:每个线程都对应一个唯一数字
std::this_thread::get_id()
2、临时对象构造时机捕获
经调试,用临时对象作为形参可以使对象在主线程结束前构造出来,不怕被detach
三、传递类对象、智能指针做线程入口函数参数
1、传递类对象:在线程中修改成员变量值不会影响到main中的对象,因为线程把const A&t这个形参当做拷贝!
所以要用std::ref函数:
thread mytobj(myprint2,ref(myobj));
2、传智能指针:
thread mytobj(myprint2,std::move(p));
四、用成员函数指针做线程参数
thread mytobj(&A::thread_work,myobj,15);
//用&myobj等价于用std::move(myobj),但是这样就有了拷贝构造函数执行,不能用detach
五、总结
1、想用detach就得在调用时用临时对象
2、想改对象的值就用ref函数,但是这样就不能用detach了
第4节 创建多个线程、数据共享问题分析、案例分析
一、创建和等待多个线程
执行多个线程是乱的,这跟操作系统内部的调度机制有关(用容器和迭代器更加方便!)
创建10个线程范例:
vector<thread> mythreads;
for(int i=0; i<10; i++){
mythreads.push_back(thread(myprint,i));
}
for(vector<thread>::iterator iter=mythreads.begin(); iter!=mythreads.end();iter++){
iter.join();
}
二、数据共享问题:不能同时读、同时写
三、案例
网络游戏服务器:一个线程往队列里写,另一个线程从队列里读(list容器在频繁按顺序插入和删除数据的时候效率高)
c++解决多线程保护共享数据问题的第一个概念:互斥量
第5节 互斥量概念、用法、死锁演示及解决详解
一、互斥量(mutex)基本概念
互斥量是个类对象,多个线程用lock函数加锁,加锁成功后操作,函数返回,若没锁成功则一直尝试加锁。
二、互斥量的用法
1、步骤:lock() → 操作共享数据 → unlock()(切记成对使用!)
2、std::lock_guard类模板可以直接取代lock()和unlock()
lock_guard<mutex> guard(my_mutex);
原理:lock_guard的构造函数中进行了lock(),析构函数中进行了unlock()
三、死锁(至少两个互斥量才存在死锁)
1、例:线程A锁上金锁后去锁银锁,线程B锁上银锁后去锁金锁
2、一般解决方案:保证两个互斥量加锁顺序一致!
3、std::lock()模板函数:一次锁住两个或两个以上的互斥量,若同时锁不完,则先释放锁住的!
std::lock(my_mutex1,my_mutex2)
4、lock_guard的adopt参数
目的:使lock_guard结合std::lock(),通知guard对象已经锁上了,要不就重复加锁了,但是这样处理完数据就不用unlock了。
std::lock(my_mutex1,my_mutex2)
lock_guard<mutex> guard1(my_mutex1,adopt_lock);
lock_guard<mutex> guard2(my_mutex2,adopt_lock);
四、总结
std::lock()虽然可以一次锁多个互斥量,但并不建议使用,因为每一次加锁中往往有额外的代码。
第6节 unique_lock详解
一、unique_lock可直接取代lock_guard( 用lock_guard一般足够了)
二、unique_lock的第二个参数
【前提:锁】1、adopt_lock:表示该互斥量已经锁上了,通知unique的构造函数中无需执行lock()
【前提:不锁】2、try_to_lock:尝试去锁,若没有锁成功,立即返回
unique_lock<mutex> guard1(my_mutex1,try_to_lock);
if(guard1.owns_lock()){
//操作共享数据
}else{
//do something else
}
这个参数的优点时当这个锁锁住时不用一直等待,可以干点别的事!
【前提:不锁】3、defer_lock:初始化一个不加锁的my_mutex,后面代码用unique_lock的成员函数操作,如下。
三、unique_lock成员函数
lock()、unlock():最后不用unlock但是,这样可以先解锁对共享数据的处理,干点别的,再锁上,高效!
try_lock():尝试给互斥量加上,拿不到锁,返回false,不阻塞;
release():返回它所管理的mutex对象指针,并释放所有权(也就是说unique_lock和mutex无关了)
unique_lock<mutex> guard2(my_mutex2,defer_lock);
mutex *ptx = guard2.release();
//如果原来是加锁的,就要自己unlock了
ptx->unlock();
四、unique_lock所有权的传递
所有权:guard1拥有my_mutex1的所有权,guard2拥有my_mutex2的所有权,不能复制!
方法1:用std::move,方法二:从函数返回一个局部的unique_lock对象。
五、额外说明
1、锁住的代码越少越好,少叫粒度细,多叫粒度粗;
2、休息时长(20s)实例代码
std::chrono::milliseconds dura(20000);
std::this_thread::sleep_for(dura);
第7节 单例设计模式共享数据分析、解决、call_once
一、设计模式大概谈
“设计模式”:代码的一些写法(跟常规不一样),程序灵活但代码晦涩,别人一般看不懂。
外国传入时是先有开发需求,后有理论总结和整理。
二、单例设计模式(使用频率高)
整个项目中有某个或某些特殊的类,属于该类的对象只能创建一个!
单例类写法:利用一个if每次都返回同一个对象!
if (m_instance == NULL) {
m_instance = new MyCAS();
static huishou hs;
//在程序结束时必然会调用该类的析构函数,在huishou的析构函数中delete m_instance
}
return m_instance;
三、单例设计模式共享数据问题的分析与解决
1、单例类初始化尽量在主线程的一开始进行!
2、若在自己的线程中创建MyCAS类对象,就需要对GetInstance中的代码做互斥处理了!
利用双重检查提高效率:
if (m_instance == NULL) {
unique_lock<mutex> mymutex(resource_mymutex); //单用这个效率很低!
if (m_instance == NULL) {
m_instance = new MyCAS(); //这行必须互斥!!!
static huishou hs;
}
}
return m_instance;
四、std::call_once():c++11函数,第二个参数为函数名a
功能:保证函数a只被调用一次!
这个函数需要与一个标记结合使用,这个标记的类型是once_flag,调用一次CreateInstance后,g_flag被置为一种状态,下次就不会调用CreateInstance函数了,相当于取代了双重检查的所有代码。
once_flag g_flag;
//在线程中
call_once(g_flag,CreateInstance);
【问题】每次线程都会调用一次call_once,效率也不一定高!
第8节 condition_variable、wait、notify_one、notify_all
一、条件变量std::condition_variable、wait()、notify_one()
1、condition_variable是等待一个条件达成的类,需要和互斥量配合工作,如创建全局对象my_cond;
2、wait与notify_one的配合使用:my_cond.wait()用在对一个互斥量加锁后,若第二个参数返回true,函数继续执行;若第二个参数返回false(没有第二个参数等同于false),解锁该互斥量并堵塞到本行,一直堵塞到其他某个线程调用了my_cond.notify_one()后,该堵塞状态被唤醒。这之后,首先不断尝试拿锁,若获取不到会卡在这里(此时是醒着的),然后判断第二个参数,同上,但若无参时相当于true。
unique_lock<mutex> guard1(my_mutex1);
my_cond.wait(guard1, [this] {
if (!msgRecvQueue.empty())
return true;
return false;
});
二、notify_all():唤醒所有wait()
第9节 async、future、packaged_task、promise
需求:希望线程给我返回一个值!
一、std::async、std::future创建后台任务并返回值
async用来启动一个异步任务(自动创建一个线程,并开始执行入口函数),这之后返回一个future对象(这是一个类模板)
【解释】future对象中有线程的返回结果,用get()获得,这个结果可能无法马上得到,得等线程结束。
future<int> result = async(mythread,tmp);
result.get()用来获得mythread函数的返回值
get()函数只会调用一次,若没有收到结果会一直卡在这里,和wait()的作用一样,wait不返回值!
二、std::package_task类模板
模板参数是各种可调用对象,包装起来方便作为线程的入口函数。
std::packaged_task<int(int)> mypt(mythread);
thread t1(ref(mypt), 1); //只要是对象的往往要加ref
t1.join();
//为的就是下面这行
future<int> reult = mypt.get_future();
三、std::promise类模板
能够在某个线程中赋值,在其他线程中取出,可以保存一个值(set_value),将来通过future绑定到myprom上,用get得到值!
void mythread2(promise<int>& tmp,int tmp2) {
chrono::milliseconds dura(5000);
this_thread::sleep_for(dura); //休眠5s
int result = tmp2;
tmp.set_value(result); //往promise对象中传值
}
//main中
promise<int> myprom;
thread t1(mythread2, ref(myprom), 100);
t1.join();
future<int> fu = myprom.get_future();
auto result = fu.get();
cout << result << endl;
第10节 future其他成员函数、shared_future、atomic
一、future其他成员函数
future<int> result = async(mythread);
std::future_status status = result.wait_for(std::chrono::seconds(3));
if(status == future_status::timeout){
//这说明3s后没有收到线程的返回
}else if(status == future_status::ready){
//这说明3s后成功收到线程的返回
}
//若async的第一个参数为deferred才与这个有关
if(status == future_status::deferred){
//没创建子线程,遇到get()才调用入口函数
}
二、std::shared_future类模板
解决多个线程get()的问题(因为get的内部其实是一个移动语义)
shared_future<int> result_s(mypt.get_future());
三、原子操作std::atomic
1、引出
即使是atomvalue = 6这样简单的代码其实也是几行汇编代码写成的,所以程序执行过程中即使加锁了也可能出错。
原子操作是不需要用到互斥量的多线程并发编程方式,是不会被打断的程序执行片段。
※ 互斥量针对一段代码,原子操作适用于单个变量的读写操作
2、使用方法:如下基本当int使用
atomic<int> g_mycout = 0;
3、心得:原子操作一般用于计数和统计作用,如累计发送了多少个数据包等。
第11节 std::atomic续谈、std::async深入谈
一、原子操作atomic续谈
一般atomic支持++、--、+=等原子操作,其他的如a = a +1 则不支持。
二、std::async深入谈
1、std::async参数详述
async和thread的最大区别是async可能不创建新线程!
thread必然创建线程,若因为内存不够等原因创建失败,则程序崩溃;
async是创建一个异步任务,可能创建可能不创建,相比thread更容易拿到线程入口函数的返回值。
1)延迟到get()调用thread,不创建线程
future<int> result = async(launch::deferred,mythread);
2)强制mythread在新线程上进行
future<int> result = async(launch::async,mythread);
3)系统自动评估使用哪个参数
future<int> result = async(mythread);
//等价于
future<int> result = async(launch::async | launch::deferred,mythread);
2、系统如何决定是异步(创建新线程)还是同步(不创建新线程)呢?
系统紧张时,会自动选择deferred参数,调用get()时,mythread就运行在主线程上了。
3、std::async不确定问题的解决
【技巧】利用wait_for等0s,此时,若status等于deferred,证明系统自动紧张,系统已经自动采用launch::deferred策略了,调用get来获取返回值;若status不等于deferred,则证明系统已经创建了线程,再判断是timeout还是ready。
第12节 windows临界区、其他各种mutex互斥量
一、windows临界区
#include<windows.h>
#define __WINDOWSJQ_ //开关!
#ifdef __WINDOWSJQ_
//windows代码
#else
//c++代码
#endif
二、多次进入临界区试验
【windows】在同一个线程中允许2次加锁相同的临界区变量(EnterCriticalSection函数),但是加了几次就得释放几次;
【c++11】不允许连续lock()两次,会报异常。
三、自动析构技术(lock_guard)
windows下可以自己写一个CWinLock类,构造函数中加锁,析构函数时解锁,这种类被称为RAII类,即Resource Acquisition Is Initialization(资源获取即初始化)。
四、recursive_mutex递归的独占互斥量
test1()里调用test2(),lock()可能会进行两次,用recursive_mutex可以避免这种问题。
五、带超时的互斥量time_mutex和recursive_time_mutex
chrono::milliseconds timeout(5000);
if(mytimemutex.try_lock_for(timeout)) //若拿到了锁继续执行,没拿到也可以写一些代码
if(mytimemutex.try_lock_until(chrono::steady_lock::now() + timeout))
第13节 补充知识、线程池浅谈、数量谈、总结
一、补充一些知识点
1、虚假唤醒:第8节wait中第二个参数的判断可以避免虚假唤醒
2、atomic:这种变量想复制只能用 auto atm2(atm.load()),写数据用atm2.store(12)
二、浅谈线程池
1、场景设想:2万个玩家2万个线程不可行;代码偶尔创建一个线程也不安全。
2、线程池的思路是程序启动时一次性创建好一定数量的线程,循环利用线程,用完了放回去。
三、线程创建数量谈(一般2000个是极限)
1、采用某些技术开发程序:按照api接口提供商建议的数量
2、创建多线程完成业务:控制在200个以内比较理想