一、简介
计算机的核心:
中央处理器CPU、内存、I/O控制芯片
计算机软件体系结构:
Applications: Web Browser Video Player Word Processor Email Client Image Viewer ... | Development Tools: C/C++ Compiler Assembler Library Tools Debug Tools Development Libraries ... |
Runtime Library |
Operating System Kernel |
Hardware |
每个层次之间都须要相互通信,所以需要有一个通信的协议,称之为接口(Interface)
接口的下面那层是接口的提供者,由它定义接口;
接口的上面那层是接口的使用者,它使用该接口来实现所需要的功能。
应用程序编程接口(Application Programming Interface),其提供者是运行库。
运行库使用操作系统提供的系统调用接口(System Call Interface)。
操作系统的一个功能是提供抽象的接口,另一个主要功能是管理硬件资源。
物理地址空间(Physical Address Space)
虚拟地址空间(Virtual Address Space)
分段(Segmentation)
分页(Paging)
线程(Thread),
有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
一个标准的线程由
线程ID、当前指令指针(PC)、寄存器集合
和
堆栈
组成。
一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码、数据段、堆等)及一些进程级的资源(如打开文件和信号)。
线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址),但实际运用中线程也拥有自己的私有存储空间,包括以下几个方面:
- 栈(并非完全无法被其他线程访问,一般情况下可以认为是私有数据)
- 线程局部存储(Thread Local Storage, TLS)。某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。
- 寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有。
线程私有 | 线程之间共享(进程所有) |
|
|
多处理器上,线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。
单处理器上,多线程轮流执行,称之为
线程调度
(Thread Schedule)。
线程调度中,线程通常有至少三种状态,分别是:
- 运行(Running):此时线程正在运行
- 就绪(Ready):此时线程可以立刻运行,但CPU已经被占用。
- 等待(Waiting):此时线程正在等待某一事件(通常是I/O或同步)发生,无法执行。
处于运行中线程拥有一段可以执行的时间,这段时间称为
时间片
(Time Slice)
如图,线程状态切换
优先级调度(Priority Schedule)
轮转法(Round Robin)
一般把频繁等待的线程称之为
IO密集型线程
(IO Bound Thread),
很少等待的线程称为
CPU密集型线程
(CPU
Bound Thread
)。
IO密集型线程 总是比 CPU密集型线程 容易得到优先级的提升。
在优先级调度的环境下,
线程的优先级改变
一般有三种方式:
- 用户指定优先级
- 根据进入等待状态的频繁程度提升或降低优先级
- 长时间得不到执行而被提升优先级
抢占(Preemption)
在不可抢占线程中,线程主动放弃执行,
- 当线程试图等待某事件时(I/O等)
- 线程主动放弃时间片
Linux将所有的执行实体(无论是线程还是进程)都称为
任务(Task)
,每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。
系统调用 | 作用 |
fork | 复制当前进程 |
exec | 使用新的可执行映像覆盖当前可执行映像 |
clone | 创建子进程并从指定位置开始执行 |
fork产生新任务的速度非常快。
因为fork并不复制原任务的内存空间,而是和原任务一起共享一个 写时复制(Copy on Write,COW) 的内存空间。
所谓写时复制,指的是两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其他的任务使用。
使用clone可以产生一个新任务,从指定的位置开始执行,并且(可选的)共享当前进程的内存空间和文件等。如此就可以在实际效果上产生一个线程。
线程安全
自增(++)
操作在多线程环境下会出现错误是因为这个操作被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码。
我们把单指令的操作称为
原子的(Atomic)
,因为无论如何,单条指令的执行是不会被打断的。
同步与锁
所谓
同步(Synchronization)
,指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
锁(Lock)
、
获取(Acquire)锁
、
释放(Release)锁
1、二元信号量(Binary Semaphore)
是最简单的一种锁。它只有两种状态:占用与非占用。
多元信号量简称为
信号量(Semaphore)
。一个初始值为N的信号量允许N个线程并发访问。
线程访问资源的时候首先获取信号量。进行如下操作:
- 将信号量的值减1。
- 如果信号量的值小于0,则进入等待状态,否则继续执行。
访问完资源之后,线程释放信号量,进行如下操作:
- 将信号量的值加1。
- 如果信号量的值小于1,唤醒一个等待中的线程。
2、互斥量(Mutex)
3、临界区(Critical Section)
获取临界区的锁称为
进入临界区
;释放临界区的锁称为
离开临界区
临界区和互斥量和信号量的
区别
:
- 互斥量和信号量在系统的任何进程里都是可见的。(一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的)
- 临界区的作用范围仅限于本进程,其他进程无法获取该锁。
4、读写锁(Read-Write Lock)
对于同一个锁,读写锁有两种获取方式:共享的(Shared
)或独占的(Exclusive)
5、条件变量(Condition Variable)
使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。
可重入(Reentrant)与线程安全
一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。
一个函数要被重入,只有两种情况:
- 多个线程同时执行这个函数。
- 函数自身(可能经过多层调用之后)调用自身。
可重入的:表明这个函数被重入之后不会产生任何不良后果。
一个函数要称为可重入的,必须具有如下几个特点:
- 不使用任何(局部)静态或全局的非const变量。
- 不返回任何(局部)静态或全局的非const变量的指针。
- 仅依赖于调用方提供的参数。
- 不依赖任何单个资源的锁(mutex等)
- 不调用任何不可重入的函数。
volatile关键字
volatile关键字试图阻止过度优化,volatile基本上可以做到两件事情:
- 阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
- 阻止编译器调整volatile变量的指令顺序。
即使volatile能够阻止编译器调整顺序,也无法阻止CPU动态调度换序。
一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后。
多线程内部情况
三种线程模型
1、一对一模型
一个用户使用的线程就唯一对应一个内核使用的线程。(反过来却不一定,一个内核里的线程在用户态不一定有对应的线程存在)
优点:
线程之间的并发是真正的并发,一个线程的阻塞,其他线程的执行不会受到影响。
(一般直接使用API或系统调用创建的线程均为一对一线程。)
缺点:
- 由于许多操作系统限制了内核线程的数量,因此一对一线程会让用户的线程数量受到限制。
- 许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。
2、多对一模型
多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行。
优点:
高效的上下文切换和几乎无限制的线程数量。
缺点:
如果其中一个用户线程阻塞,那么所有的线程都将无法执行,因为此时内核的线程也随之阻塞了。
3、多对多模型
将多个用户线程映射到少数但不止一个内核线程上。
多对多模型对用户线程的数量也没什么限制。
性能也能得到提升,不过提升幅度不如一对一模型高。