线程

  • 是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用户定义的信号处理程序处理
  • 每个信号都有内核在处理信号时运行的默认处理程序
    • 信号来了,内核先有一个默认处理方式
    • 程序员可以自己改写其中一部分信号的处理方式
    • 单线程时,信号发给进程,最终就是这个唯一线程来处理

多线程信号传递到哪:

  • 传递到应用该信号的线程
  • 传递给进程中的每个线程
  • 传递给进程中的某些线程
  • 制定一个特定线程来接收进程的所有信号