0.windows gcc
1.概念
1. 多进程/多线程
①.进程并发:重量级,开销大,操作系统隔离更安全,跨机器
②.线程并发:轻量级,少开销,共享内存会有隐患
2. 使用并发的原则
①.分离关注点: 比如将网络包处理和业务代码分离,使得代码的耦合度降低
②.性能提升: io操作很多且cpu成为瓶颈的时候
③.线程的数量:
a.线程过少,对硬件资源使用会不充分
b.线程过多,系统所做的上下文切换就越频繁,做的有用功就越低
c.一般情况线程数是cpu核数的2倍,如果io操作更多可以更高,最好是有压测(qps和线程数的曲线)
2.线程管控
1. thread
thread对象:thread对象是主线程内的局部变量,如果在局部变量销毁之前没有调用jion或detach那么thread的析构函数会调用terminate来终止整个程序
2. 指针传递
①.函数对象: 一个重载了operator()类的实例
②.关于指针传递:
a.仿函数作为线程的入口,新的线程会生成函数对象的副本
b.如果函数对象中有指针或者引用,那副本指针指向的还是主线程的局部变量,会存在并发的隐患,例如主线程先退出局部变量被销毁、主子线程共同操作引发竞争关系
③.锁的目标: 锁的目标可以是指针也可以是指针指向的内存,(如果用接口封装了被锁数据可以让代码低耦合,但是如果将被锁数据的指针传出会使得程序不加锁访问被锁数据)
④.锁指针:
a.在读指针时加锁,在写指针时先分配新的内存然后加锁改变指针的指向,常用于写少读多的情况
b.例如后台配置,在更新配置的时候重新开辟新的配置内存,最后加锁让指针指向新的配置内存,这样即使封装了方法浅拷贝传出了指针的复印件(在方法内有加锁锁指针),复印的指针指向旧的配置内存,会有读旧内存的并发,但不存在读写的并发,go语言对于同时读不加锁不会出现问题,同时在复印的指针生命周期到了旧配置的内存会被go自动回收,如果是c++可以使用智能指针
c.这里也会有另外一个问题的产生就是新旧配置交替的时候,一部分代码通过旧指针访问,一部分代码通过新指针访问配置内存,可能出现一段业务同时使用了新旧两个配置导致不定义的行为,解决的方法就是在特定的节点更新配置或者封装深拷贝方法使用配置内存让程序使用同一个配置
⑤.锁指针指向的内存:
a.在访问指针指向内存的时候加锁(根据业务加读写锁或者锁),这种更适合大多数的业务场景
b.如果封装方法浅拷贝的传出了指针的复印件,就会出现代码拿到复印指针且不加锁访问指针指向的内存,另外一段代码可能正在执行加锁写这一部分内存的情况,会导致程序不定义的行为
3.join
jionable: 判断线程是否可汇合
join: 调用join后的线程的存储空间会被清除,thread对象也不再关联该线程,也不能被二次jion
4.RAII
定义: 所有try的代码都能通过RAII技术来规避没有调用清理程序使得代码更为清晰,RAII创建一个管理类管理所占用的资源,局部对象生命周期到了的时候编译器自动调用会调用对象的析构函数来处理异常退出时清理工作,如子线程的jion,智能指针指向的堆内存的释放(unique_ptr/shared_ptr/weak_ptr),锁释放(lock_guard/unique_lock)
5.创建线程的函数参数
①.参数会被复制到新线程的存储空间,然后以右值的方式传给新线程的函数,即使入口函数参数声明是引用也会发生复制
②.要使用主线程的变量需要在线程对象构造的时候显示标识引用传递std::ref(data)
③.以上复制包括传递的函数对象
④.使用类成员函数作为线程的入口,第二个参数需为对象的地址
6.线程的归属权
①.std::move 可以移交线程的归属权
②.t1=std::move(t2) (t为线程对象)如果t1有关联的线程那么会调用terminate终止整个程序
7.线程识别
①.thread对象调用get_id()可以得到该线程的ID对象,当前线程使用this_thread::get_id()获得
②.std::thread::id类重载了== > <等运算符,所以可以挂在map上
3.共享数据
1.防止竞争
①.不变量定义:针对某个特定数据的断言,该断言总是成立
②.在不变量被破坏时,中间状态只对执行改动的线程可见
③.方式:
a.由一连串的不可拆分的改动完成数据变更(原子操作/加锁)
b.cas(compare-and-swap)(java的乐观锁)(针对单个变量) (cas的整个过程是一个硬件级原子指令)
c.stm(事务日志存储记录,若别的线程修改了数据,则事务回滚重新开始)(针对多个变量或者结构体)
d.rcu(read-copy-update)(读拷贝更新)(其实就是2.3说的锁指针)
e.消息队列/事件驱动
f.无锁数据结构: 专门设计过的数据结构,能在并发下用cas保证一致性 这里会有一个ABA的问题
ABA问题描述: 有3个线程1个链表有x->y->z 3个节点
1&3线程想删除链表中的y节点 2线程想在x之后插入一个新节点y1
时间线: 1先删除y节点 y节点的内存并不会被系统快速回收
2插入y1节点 使用了之前的y的地址内存
3cas对比了地址正确删除y1节点 但是3想删除的是y节点 并不是y1节点 最后导致业务出错
2.锁
①.定义: 标记访问该数据的所有代码,令各线程在运行时相互排斥,任何时候只有一个线程在运行某一段加锁的代码
②.实现: 将锁和受保护的数据封装成一个类,提供接口访问数据实现能多线程访问的类,但是使用引用或指针把数据传出去保护就会被破坏
③.接口之前的竞争: 线程安全类只能限制接口内的竞争,无法限制接口间的竞争,接口分开会导致竞争,接口合并可能造成的数据丢失,解决方法传出指针传入引用 (见45 47 50页) todo:
④.死锁解决方案: conn.4.6.③ conn.5.2.②
a.按照相同的顺序对互斥量加锁
b.使用c++11的对需要持有的互斥量同时加锁
c.使用层级锁,在线程持有锁时只能对更低级互斥量加锁,实现使用了一个static thread_local变量(见57页)
⑤.锁的RAII:
a.用lock_guard/unique_lock类的构造析构来管理锁的lock和unlock,编译器自动调用,防止死锁
b.unique_lock更灵活可以不占用锁的情况下与锁关联,转移锁的归属,在生命周期结束时主动解锁
⑥.锁的粒度:
a.所锁的数据量的大小,如果锁的数据很大,那么每次用到数据都会加锁,增加了程序串行的时间
b.在持有锁的期间作的操作,应尽量避免耗时的操作例如io
c.如果使用的锁很多那么锁的粒度会变小,但是锁本身占用系统资源的开销就很高,需要找到一个平衡点
3.单例
①.定义: 需要在初始化中避免竞争,保护的共享数据
②.懒汉模式: todo:
a.如果只有一个判断会导致所有线程都执行if外面的加锁操作,所以在外层在套一个判断可以减少会循环加锁的线程数 (见65 66页代码)
b.可以使用std::once_flag,std::call_once来确保代码只会被执行一次
c.有可能某个单例创建的占用的资源开销不菲(比如内存),所以使用懒汉模式
②.饿汉模式:
a.c++标准规定只要控制流第一次遇到静态数据声明语句,变量即初始化,这个时候会产生竞争,导致多个线程进行初始化,而且可能有进程尝试使用未被初始化完成的数据
b.c++11规定初始化只会在一个线程中发生,而且其他线程在初始化完成之前,不会越过静态数据的声明而继续执行
4.读写锁
①.在读操作占用锁时,写操作试图获取锁会阻塞,直到所有的持有读锁的线程释放锁
②.在写操作占用锁时,读操作试图获得锁会阻塞,直到持有写锁线程释放锁
③.(见unix环境高级编程11.线程.读写锁)
5.递归锁
①.一个线程可以对所持有的递归锁重复加锁,unlock的次数等于lock的次数时锁被解开
②.不建议使用这种锁,因为不变量被破坏在不同函数之间 (把不变量的破环控制在一个函数里面,使得代码清晰降低耦合)
4.同步
1.条件变量
①.demo
②.条件变量与锁配合使用,传入一个锁住的互斥量
③.wait的时候解锁阻塞等待条件变量(这个时候生产者线程可以来抢占锁然后添加任务发送条件变量) (每个消费者线程都会执行①②两个步骤,所以不存在一个锁被解两次的情况)
④.在条件变量到达时消费者线程占用锁,看lambda是否成立,a.如果成立wait返回,操作队列把任务传出然后解锁 b.如果不成立线程解锁阻塞继续等条件变量
⑤.条件变量只有在有worker在wait的时候才有唤醒 没有worker在wait的时候唤醒消息会被丢弃。woker在处理完任务的时候回去拉下一个任务,所以也不会出现任务堆积的情况
⑥.惊群: 发送条件变量时会唤醒所有在等待的线程来抢占锁,这样会浪费不必要cpu的资源(适用业务共享数据更新)
notify_all所有等待条件变量的线程都被唤醒,常用共享数据更新
notify_one确保最少有一个线程去检查判断函数是否成立,常用任务消费
2.伪唤醒
定义:在wait等待的线程被唤醒的通知不是直接相应的线程发的
注: 所以wait中的判断函数(lambda函数)不要做判断以外的事情,不然会受伪唤醒的影响
3.future
①.todo:demo
②.future对象:接收一次性事件的发生结果,一次性事件可以在另外一个线程异步运行std::launch::deferred,也可以在当前线程同步运行std::launch::async
③.使用方法std::future
future
④.std::packaged_task
⑤.promise对象,一个future和一个promise对应,可以通过promise设置future值,一个线程可以有多个promise,promise可以和数据包挂钩,从而实现一个线程管控多个fd
