【c++】第五章 并发与多线程

第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个以内比较理想

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值