线程
- 是CPU调度的基本单元
- 只包含运行时状态
- 静态部分由进程提供
- 包括了执行所需的最小状态
- 包含线程ID、PC、Register、Stack
- 与其他线程共享代码段、数据段、OS资源(没有自己独立的代码空间)
- 线程不能独立存在,需要依靠进程
- 一个进程的多线程可在不同处理器上同时执行
- 多线程指OS在单个进程内支持多个并发执行路径的能力
- 每个线程都有状态
- 上下文切换的单位变为线程
- 每个线程拥有自己的栈
- 内核中也有为线程准备的内核栈(这里的线程属于内核进程)
线程在内存中的分布
代码部分
线程共享代码,但每个线程从不同入口函数开始执行
- 所有线程共享同一份代码(每个线程执行程序的某个函数)
- 每个线程都会从一个指定函数开始执行。(叫做线程函数)
- 线程函数不能回到调用者
动态分配的内存
所有线程共享同一全局变量、Heap
- 全局变量位于进程的静态存储区,被共享
- 动态分配的内存属于堆,被共享
局部变量
线程共享进程资源,但不共享栈;局部变量在线程栈中,因此对每个线程都是私有的。
- 代码段共享、全局变量共享、Heap共享,但局部变量(即Stack)不共享
- 栈是线程私有的
动机
- 进程创建很重载,而线程创建很轻量;可简化代码提高效率
- 内核通常是多线程的,多个线程在内核中运行,每个线程执行一个特定任务
- 三段论:
- 大多数现代应用程序是多线程的
- 线程在应用程序中运行
- 应用程序的多个任务可由单独的线程实现,如Web浏览器
时延与吞吐量
-
时延(latency) 说的是:一个请求从开始到完成要多久。
-
吞吐量(throughput) 说的是:单位时间内系统能完成多少个请求。
吞吐量不等于时延的倒数,因为时延针对单个请求,吞吐量针对单位时间内所有完成请求;只有在单任务串行、无并发的理想情况下,二者才近似互为倒数。
多线程优点
- 响应性:如果部分线程被阻塞,仍可继续执行
- 资源共享:共享Process资源,比共享内存、消息传递更容易,允许一个应用程序在同一地址空间由多个不同活跃线程
- 经济:比进程创建更便宜,线程切换比上下文切换开销低
- 可扩展性:可利用多核体系结构
并发与并行
并发(交替执行)
多个任务在一段时间内交替推进
- 同一时间段内,多个任务都在推进,但同一时刻不一定同时执行。
- 比如单核系统上并发执行,同一时间只能执行一个任务,但是OS快速切换,看起来好像在同时执行
并行
多个任务在同一时刻真正同时执行
- 有多核,同一时刻可有多个任务一起运行
并行的类型
- 数据并行性:将相同数据的子集分布在多个core上,每个核上的操作相同
- 任务并行性:将任务而不是数据分配到多个计算核心,每个线程执行唯一操作,不同线程可以操作相同的数据,也可操作不同数据
- 大多数情况混合执行
区别
并发是指在同一时间段内多个任务交替执行,使系统表现为多个任务同时推进;并行是指在同一时刻多个任务真正同时执行,通常需要多核或多处理器支持。
Amdahl公式
线程类型
用户空间线程
- 管理线程的所有工作由应用程序完成
- 内核意识不到线程的存在
- 采用第三方库如
pthreads来创建 - 线程状态与进程状态解耦,线程的状态不用与其进程的状态相同
优势
- 不需要内核介入,切换不需要陷入内核
- 可根据不同应用程序来调度
- 可以跑在任何OS上,与其解耦
劣势
- ULT执行系统调用时,会阻塞线程,以及进程中的所有线程
- 纯ULT策略中,多线程应用不能利用多处理技术,内核一次只把一个进程分配给一个处理器,一个进程中只有一个线程可以执行
- 解决方法
- 套管:把一个产生阻塞的系统调用转化为一个非阻塞的系统调用
- 把应用程序写成一个多进程程序而非多线程程序,每次切换就变成进程间切换而非线程间切换
内核空间线程
- 由内核完成管理工作
- 应用级没有线程管理代码,只有内核线程设施的API,比如Windows
优势
- 可把同一进程的多个线程调度到多个处理器
- 进程中一个线程阻塞,内核可调用同一进程中的另一个线程
- 内核线程本身也可是多线程
劣势
- 把控制权从一个线程传送到同一个进程的另一个线程时,需要切换内核模式
混合方法
- 线程创建完全在用户空间中完成
- 线程调度和同步也在饮用中进行
- 多个用户线程会映射到一些内核线程上
线程模型(描述用户线程和内核线程的关系)
多对一
一对一
多对多
线程库
- 提供创建和管理线程的API
- 两种方式
- 在用户空间提供一个没有内核支持的库
- 实现OS支持的内核级库
- POSIX、Windows、Java
创建多线程
异步线程
- 父线程创建子线程后继续执行
- 父子进程并发执行
- 每个线程相对独立
- 数据共享较少
- 各做各的任务,依赖少,共享数据少,竞态少
同步线程
- 父进程等待子线程结束
- 数据共享显著:协同完成同一个大任务
- fork-join模式
- 分叉:创建多个子线程
- 汇合:等待它们完成再合并结果
Pthread线程创建
for(int i=0;i<5;i++)
{
pthread_create(&tid[i],NULL,do_your_job,&i);
}
//这样一共会创建5个线程,再包括主线程,最终一共6个线程
for(int i=0;i<5;i++)
{
pthread_join(tid[i],NULL);
}
//第二个循环执行时,线程总数不会增加,而是主线程依次回收5个线程Java线程
- 线程由JVM管理
- 使用底层OS提供的线程模型实现
隐式多线程
- 由编译器和运行时库创建管理线程而不由程序员创建,被称为隐式多线程
- 有以下几种方式
线程池
在池中创建多个线程,在其中等待工作
优势:
- 比创建新线程快
- 允许将应用程序中的线程数限制到池的大小
- 将要执行的任务和创建任务的机制分离,使用不同策略来运行任务
Fork-join
- 多个线程被分叉,然后合并

OpenMP
通过标识并行区域来——并行运行的代码块
为共享内存环节中的并行编程提供支持
大中央调度
Intel TBB
多线程问题
fork()和exec()的语义
大致理解:fork是复制,exec是替换
fork() 创建子进程。
在多线程进程中,通常只复制调用 fork 的线程到子进程,而不是复制所有线程。
exec() 不创建新进程,而是用新程序替换当前进程的地址空间和执行内容;原进程中的所有线程都被新程序取代。
信号处理
信号用于通知进程某个特定事件已发生
信号处理器用于处理信号:
- 信号由特定事件产生
- 信号被传送到进程
- 信号由缺省的信号处理程序or用户定义的信号处理程序处理
- 每个信号都有内核在处理信号时运行的默认处理程序
- 信号来了,内核先有一个默认处理方式
- 程序员可以自己改写其中一部分信号的处理方式
- 单线程时,信号发给进程,最终就是这个唯一线程来处理
多线程信号传递到哪:
- 传递到应用该信号的线程
- 传递给进程中的每个线程
- 传递给进程中的某些线程
- 制定一个特定线程来接收进程的所有信号