任务切换
硬件并发
多进程并发
将应用程序分为多个独立的进程,它们在同一时刻运行。
独立的进程可以通过进程间常规的通信渠道传递讯息
优点
缺点
多线程并发
在单个进程中运行多个线程
线程
优点
缺点
关注点分离(SOC)
性能
两种方式利用并发提高性能
任务并行
将一个单个任务分成几部分,且各自并行运行,从而降低总运行时间。
一个相当复杂的过程,因为在各个部分之间可能存在着依赖。
数据并行
引入线程库,创建线程时传入要调用的函数,并使用t.join()调用。
提供的函数对象会复制到新线程的存储空间当中,函数对象的执行和调用都在线程的内存空间中进行。
当把函数对象传入到线程构造函数中时,如果传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。
举例
解决办法
使用在前面命名函数对象的方式,或使用多组括号,或使用新统一的初始化语法
使用lambda表达式也能避免这个问题
函数已经结束,局部变量被销毁,但线程依旧访问局部变量。
常规方法
等待线程完成
join()函数
线程分离
在线程启动后,可以调用t.detach()进行分离
父线程异常处理
当倾向于在无异常的情况下使用join()时,需要在异常处理过程中调用join(),从而避免生命周期的问题。
资源获取即初始化
后台运行线程
后台运行的线程需要调用detach()进行线程分离
通常称分离线程为守护线程(daemon threads)
某些导致问题的例子
当线程函数需要传递string对象,但传参是一个字符串指针时,就有可能导致问题。
当线程调用的函数需要传递对象,而线程传递了对象的拷贝时,则会导致线程对对象的修改无法保留。
线程可移动性
执行线程的所有权可以在多个std::thread实例中互相转移,这是依赖于std::thread实例的可移动且不可复制性。
不可复制保性证了在同一时间点,一个std::thread实例只能关联一个执行线程。
可移动性使得程序员可以自己决定,哪个实例拥有实际执行线程的所有权。
转移线程所有权
可能的场景介绍
线程对象即执行线程的所有权可移动,但不可拷贝。
未关联的线程变量能够通过移动操作获得某个线程对象的所有权,此时前一个关联的变量自动失去关联,类似于unique_ptr指针。
对一个已关联的线程变量转移所有权,会导致程序崩溃。
转移所有权时需要调用std::move()函数。
thread的移动特性可用于函数内部传递thread对象、线程管理对象的处理、线程自动化管理等。
一个合理的线程数量计算
确认每个线程执行的最小任务数量,并通过任务总数计算所需的最大任务线程数量。
获取当前系统支持的最大线程数量,并与上面的值取最小值。
通过上面得到的实际可分配线程数量,计算出实际每个线程应分配的任务数量,并开始分配任务及计算。
每个线程和结果应单独存在vector中,因为他们的数量在运行时才可知。
获取硬件支持的同时并发在一个程序中的线程数量。
std::thread::id
获取方式
线程标识类型,如果线程未关联,则返回”没有线程“类型
id对象可以自由拷贝和对比,因为标识符就可以复用。如果id相等,就是同一个线程。
共享数据可能导致的问题
两个线程同时处理数据结构,造成不变量被破坏。
C++标准定义:一种特殊的条件竞争:并发的去修改一个独立对象,数据竞争是(可怕的)未定义行为的起因。
良性竞争:系统不变量保持不变
避免恶性条件竞争的方法
无锁编程
软件事务内存
使用事务的方式去处理数据结构的更新。所需的一些数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交。
当数据结构被另一个线程修改后,或处理已经重启的情况下,提交就会无法进行。
C++未支持
使用互斥量保护共享数据
互斥量
一种最通用的数据保护机制,但不是万能的,可能造成死锁,或是对数据保护的太多(或太少)
保证了所有线程能看到共享数据,而不破坏不变量。
步骤
C++中使用互斥量
原始用法
推荐用法
std::mutex和std::lock_guard都在头文件中声明
精心组织代码来保护共享数据
互斥锁并不是万能的,当代码传递了被保护数据的引用,则会导致互斥锁失效。
正确做法:不将受保护数据的指针或引用传递到互斥锁作用域之外,包括
举例介绍:线程安全的stack如何设计
原本stack存在的问题
stack执行弹栈时,会先获取顶部元素(top()),然后从栈中移除(pop())。这样,在不能安全的将元素拷贝出去的情况下,栈中的这个数据还依旧存在,没有丢失
这样的分割导致了本想避免或消除的条件竞争
可选方法
死锁问题
问题描述
一对线程需要操作自己所有的互斥量,其中每个线程都有一个互斥量,且等待另一个解锁。
避免死锁的一般建议
C++标准库解决死锁的办法
避免死锁的进阶指导
避免嵌套锁
避免在持有锁时调用用户提供的代码
使用固定顺序获取锁
在每个线程上,用固定的顺序获取锁
定义遍历的顺序
使用锁的层次结构
锁的其它操作和设定
C++中更加灵活的锁
std::unique_lock
提供的功能
std::adopt_lock
std::defer_lock
可以被std::unique_lock对象(不是互斥量)的lock()函数所获取,或传递std::unique_lock对象到std::lock()中
会占用比较多的空间,并且比std::lock_guard稍慢一些
允许std::unique_lock实例不带互斥量:信息已被存储,且已被更新
std::unique_lock实例有销毁前释放锁的能力,当锁没有必要持有时,可以在特定的代码分支对其进行选择性的释放
不同域中互斥量所有权的传递
举例:std::unique_lock是可移动,但不可赋值的类型。
锁的粒度
术语介绍
锁的使用建议
一般情况下,执行必要的操作时,尽可能将持有锁的时间缩减到最小
简要介绍