多线程的探究
1.基础概念
- 它是cpu的最小调度实体
- 进程
- 线程组
- 进程组
- 作业
- 由线程ID、一组寄存器、栈起始地址、函数入口等构成
2.线程控制 API
linux API
/*
/*
- 线程有三种终止方式:
- 线程可以从启动例程中返回,返回值是线程的退出码;
- 线程可以被同一进程的其它线程取消;
- 线程调用pthread_exit;
/*
- 还可以为线程设置退出回调函数,就像atexit给进程设置退出函数一样;
#include
- 清理函数执行的条件
- 调用pthread_exit时;
- 响应取消请求时;
- 用非零execute参数调用pthread_cleanup_pop时;
- 进程与线程有相似之处
进程原语线程原语描述forkpthread_create创建新的控制流exitpthread_exit从现有的控制流退出waitpidpthread_join从控制流中得到退出状态atexitpthread_cancle_push注册在退出控制流时调用的函数getpidpthread_self获取控制流的IDabortpthread_cancel请求控制流的非正常退出
- 关于对pthread_detach的原理:https://blog.csdn.net/qq_33883085/article/details/89425933
/*
windows API
- 《windows核心编程》与《unix高级环境编程 》的代码风格不一样
- windows函数名都统一驼峰命名,且首个单词首字母大写;而unix是匈牙利命名风格
- 变量名都把基础类型信息给带上了------个人认为没必要,尤其是成员变量在携带基础类型很冗余;
/*
- 终止线程的运行
- 线程函数返回(最好使用这种方式)
- ExitThread函数自行撤销
- 进程调用TerminateThread函数
- 杀死进程
- 线程终止时的操作
- 撤销窗口,卸载挂钩
- 线程的退出码从STILL_ACTIVE改为传递给它的退出码
- 线程内核对象的状态已经通知
- 最好一个线程退出进程会终止
- 线程内核对象的引用计数-1
//功能介绍:自行撤销线程;最好使用_endthreadex
- 以上线程接口来自于kernel32.dll
- 书中告诫:绝对不要使用线程控制的系统API,应该使用C/C++运行库的函数,如下:
unsigned
- windows 标准库的命名更高与unix有点像了,说明微软内部有点混乱
void
- 不应该调用C/C++标准库的beginthread/endthread
- windows API真的很混乱,珍爱生命原理windows开发
//获取自身线程或者进程ID
- 共享资源的线程最好在同一个CPU核心上,因为NUMA内存模型决定CPU访问同一个节点的内存效率更高
- 从下面的函数可以看出,在windows内核中进程与线程是不同的;linux内核中线程与进程无区别
/*
C11 API C++官网
- 编写跨平台程序最好使用C++标准库的线程
- 它是一个对象,而linux和windows的线程api都是c接口
- 有点:使用起来简单快捷
- 缺点:堆栈大小、默认属性等无法修改
class thread;
example
// thread example
java 多线程 API
实现Runnable接口
pulic
线程类Thread
public
线程池工厂Excutor
- CachedThreadPool将会为每个任务创建一条线程
- FixedThreadPool使用有限的线程集合来执行所有任务
- SingleThreadPool使用一个线程来服务所有的任务,内部维护了一个任务队列,一个任务执行完后才能处理其他任务
import
- 休眠:Thread.sleep()
- 放弃cpu:Thread.yield()
3.线程同步
3.1 linux API
- 使用场景
- 多线程共同读写资源时;
- 同步与不同步的效果如下图:
互斥量
- 可以使用malloc分配pthread_mutex_t结构体内存,释放之前必须pthread_mutext_destroy清理其它资源;
- 可以用PTHREAD_MUTEX_INITIALIZER静态分配互斥量;
- 不管用何种方式分配互斥量,都需要使用下面的函数进行初始化之后才能使用
- 把参数attr设置为NULL就能使用默认的参数初始化;
/*
- 解决死锁问题的方法
- 等待超时机制
- 等待图
/*
读写锁
- 与互斥量相比,读写锁有更高的并发性;
- 互斥量只有两种状态:加锁或者不加锁;
- 读写锁有三种状态:读、写、不加锁
- 与互斥量类似,使用之前必须初始化,释放前必须先销毁相关资源
/*
条件变量
- 它必须与互斥量一起使用
- pthread_cond_wait先锁定互斥量,然后把传递给函数,函数把线程放在等待列表上,释放互斥量
- 这样条件变量就不会错过任何变化
- 当有信号来时,从等待列表上唤醒一个线程
- 一般用在发布订阅模式中
#include
自旋锁
- 自旋锁不会是CPU进入休眠,是一种忙等;
- 一般作为锁原语实现其他类型的锁;
- 互斥量自旋次数超过一定限制后就会进入内核休眠
- 所持有时间极短的场景
#include
屏障
- 适用于多线程算法:分治法
- pthread_join也是一种屏障
#include
3.2 windows API
3.2.1 用户层同步
- 具有速度快的优点,同时具备浪费CPU的风险
互锁函数
- 互锁函数的原理
- 打开CPU中的一个特殊标志位,并注明被访问的内存
- 将内存的值读入寄存器
- 修改该寄存器的值
- 如果CPU的特殊位是关闭的则转入第二步,否则特殊标志位任然打开,并将寄存器的值写回内存
- 如果系统中的 另一个C P U试图修改同一个内存地址,那么它就能够关闭 C P U的特殊位标志,从而导致互锁函 数返回第二步
- 互锁函数都是忙等,等待时间过长会浪费很多CPU资源
LONG
高速缓存行工作原理
- CPU1读取一个字节,使该字节和它的相邻字节被读入C P U 1的高速缓存行
- CPU2读取同一个字节,使得第一步中的相同的各个字节读入C P U 2的高速缓存行。
- CPU1修改内存中的该字节,使得该字节被写入C P U 1的高速缓存行。但是该信息尚未写 入R A M。
- CPU2再次读取同一个字节。由于该字节已经放入C P U 2的高速缓存行,因此它不必访问 内存。但是C P U 2将看不到内存中该字节的新值。
- 当一个C P U修改高速缓存行中的字节时,计算机中的其他 C P U会被 告知这个情况,它们的高速缓存行将变为无效。因此,在上面的情况下, C P U 2的高速缓存在 C P U 1修改字节的值时变为无效。在第 4步中,C P U 1必须将它的高速缓存内容迅速转入内存, C P U 2必须再次访问内存,重新将数据填入它的高速缓存行。
- 从高速缓存行推出的编程技巧
- 数据结构对齐方式应该与缓存行边界对齐,保证集中读写的数据都在同一个缓存行
- 只读数据与读写数据分开
临界区
- 使用场景:保护一段关键代码
- 工作原理
- 在应用层进行等待循环计数
- 如果在计数满后还没得到锁才进入内核休眠状态
//初始化关键段
3.2.2 内核层同步-----内核同步对象
- 内核对象
- 进程、线程、作业、控制台输入、信标、互斥对象
- 文件修改通知、事件、可等待定时器、文件
等待事件
//功能:等待当个内核对象,第二参数为INFINITE时为一直等
事件内核对象
//创建内核对象
等待定时器内核对象
//创建定时器
信号量
//创建信号量
互斥量
HANDLE
3.3 C11 API
- 支持的锁类型
- 原子操作
- 条件变量
- 互斥量
- 模板函数
- call_once
- lock
- try_lock
mutex
example
// mutex example
- lock_guard
- 类似mutex的只能指针
// lock_guard example
3.4 java API
- 使用sychronized关键字
- 优点:代码简单
- 缺点:锁粒度大,性能低
- 显示使用Lock对象
- 优点:可以最小化锁粒度,提高性能;更加灵活
- 缺点:需要手动锁定和解锁
- 显示使用Condition
- await()挂起线程
- signal()激活挂起的线程
- 原子变量
- 临界区
- 在其他对象上同步
使用关键字sychronized
sychronized
显示使用Lock对象
import
临界区
sychronized
4.线程控制
4.1 线程属性
#include
- 线程太多需要减小线程栈大小,默认是每个线程8M
- 虚拟地址空间不够了,可以使用malloc或者mmap映射堆空间为栈;(默认在最顶部固定范围)
- 某些处理器结构可能返回的地址不是开始位置,而是结尾位置(向上增长)
#include
- 为了避免栈溢出扩展内存的空间浪费,可以取消或者减小它,默认是1page
#include
4.2同步属性
互斥量属性
- 值得注意的三个属性
- 进程共享
- 健壮性
- 类型属性
#include
- 如果把互斥量属性设置为进程共享,接口就会把同一个互斥量内存映射到不同进程进行进程共享
#include
- 多进程共享互斥量的时候可能另一个进程崩溃,而没有释放锁;这时候可以通过设置健壮性解决
- PTHEAD_MUTEX_STALLED:持有锁进程终止时,等待进程会被拖住(死锁),行为未定;
- PTHREAD_MUTEX_ROBUST:持有锁进程终止不释放时,等待锁的进程会获得锁,返回EOWNERDEAD而不是0
#include
- 持有锁进程终止,或者其它线程解锁互斥量时可能导致互斥量不可用,所以必须设置“一致性”
#include
- 互斥量还有以下属性
- 可以用下面的接口操作这些属性
#include
读写锁属性
- 支持进程共享属性
#include
条件变量
- 支持进程共享和时钟属性
屏障
- 支持进程共享属性
5.线程池
- linux和C11需要自己实现线程池
- windows有内置线程池
- java线城池
6.线程调度《深度linux内核架构》
参考《深入深入linux内核架构》(豆瓣8.8分) 第二章 进程管理和调度
- 在linux内核中,进程与线程本质是一个数据结构task_struct实例
- 创建进程的系统调用fork,创建线程的系统调用clone都会在内核调用do_work创建task_struct,然后把task_struct->entity注册到cpu调度器上
- 每个cpu上有一个核心调度器,核心调度器会根据task_struct->entity的相关属性选择合适的调度器
-
- 完全公平调度器
- 将线程就绪线程放在一颗红黑树上,红黑树按照等待时间的相反数排序
- 等待时间最长的是红黑树的最左边节点
- 每次从红黑树上取出一个线程执行
- 执行时间片满,或者线程自动放弃cpu时间片,可以将线程放回这块红黑树
- 完全公平调度器
-
- 周期性调度器
- 内存回收线程
- 电源休眠检测线程
- 周期性调度器
-
- 实时调度器
- 任务根据不同的优先级放到一个散列表上;散列的键是优先级,键值是线程实例,线程实例按照注册顺序双链表排列
- 调度顺序从高优先级依次往后调度,每个线程可以执行任意长时间
- 实时调度器
7.多线程算法(算法导论3)
- 突破串行算法的极限时间复杂度
- 充分利用多核CPU的每一个核
- 静态线程与动态线程
- 静态是指线程创建之需要保存线程环境上下文,就是我们通常理解的多线程;使用麻烦;
- 直接使用并行语法即可,简化线程调度、通信、管理;
- 示例算法
- 计算第n个斐波那契数
- 矩阵乘法
- 归并排序
- 将多线程运行指令集依赖关系看成是一个有向无环图,可以帮助理解
- 串联的链必须串行计算
- 并联的链可以并行计算,并以最长链为最终时间复杂度
- 性能度量
- 工作量:所有计算指令的总时间;即有向无环图的顶点数;单处理器运行时间;
- 持续时间:有限无环图中的关键路径顶点数;无穷多个处理器运行时间;
- 工作量定律
- T p >= T1/p #Tp是每个处理器运行的平均总时间
-
- 持续时间定律
- Tp >= T
- 持续时间定律
-
- 加速比:T1/Tp;如果 T1/Tp = theta(p)为线性加速比;T1/Tp = P为完美线性加速比
- 超过完美加速比增加CPU核心数已经没有意义;只能改进算法本身的并行度;
- 加速比:T1/Tp;如果 T1/Tp = theta(p)为线性加速比;T1/Tp = P为完美线性加速比
-
- 并行度
- 贪心调度器
- 完全步:有大于核心数任务调度
- 非完全步:少于CPU核心数任务可调度
8.函数可重入
- 仅有局部变量的函数可重入
- 有些系统调用和标准库函数值得注意