1 进程、线程和协程
(1)进程
进程可以简单理解成正在执行的一个程序,比如你电脑上运行的QQ、360杀毒软件等就是进程。
进程是程序资源管理的最小单位。
进程会管理那些资源呢?通常包括内存资源、IO资源、信号处理等。
这里对内存资源简单介绍下。
操作系统采用虚拟内存技术,把进程虚拟地址空间划分成用户空间和内核空间。内核空间是操作系统内核访问的区域,独立于普通的应用程序,是受保护的内存空间。
用户空间是普通应用程序可访问的内存区域。用户空间又分成5个不同的内存区域,分别是:
栈、堆、数据段、BSS段和代码段。
(2)线程
线程是操作操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位,一个进程内可以包含多个线程,线程是资源调度的最小单位,CPU上真正运行的是线程。
同一进程中的多条线程共享该进程中的全部系统资源,如虚拟地址空间,文件描述符文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈、寄存器环境、线程本地存储等信息。
线程创建的开销主要是线程堆栈的建立,分配内存的开销。这些开销并不大,最大的开销发生在线程上下文切换的时候。
线程是被内核所调度,线程被调度切换到另一个线程上下文的时候,需要用户态到内核态转换,开销比较多。
(3)协程
协程 (Coroutines) 是一种比线程更加轻量级的微线程。类比一个进程可以拥有多个线程,一个线程也可以拥有多个协程,因此协程又称微线程和纤程。
协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈,协程调度切换时,直接操作用户空间栈,完全没有内核切换的开销。
2 并发(concurrency)和并行(parallelism)
并发在CPU层面其实是交替执行的,比如有2个任务A和B,CPU一会儿执行A,一会儿又执行B,不停地在2个任务之间切换。因为CPU执行的速度很快,切换间隔非常短,所以在我们看来任务A和B在同时进行。而并行是真正的同时执行。
3 临界区
临界区用来表示一种共享资源。它可以被多个线程使用,但同一时刻只能被一个线程使用。一旦临界区资源被某个线程占用,其他线程就必须等待。
4 阻塞(blocking)和非阻塞(non-blocking)
阻塞和非阻塞用来形容多线程间的相互影响。
(1)阻塞
比如一个线程占用了临界区资源,那么其他需要这个资源的线程就必须等待,等待会导致线程挂起,这种情况就是阻塞。
(2)非阻塞
非阻塞指线程间相互不影响,所有线程都在尝试不断向前执行。
5 同步(synchronous)和异步(asynchronous)
同步和异步用来形容方法的一次调用。
(1)同步
同步方法调用一旦开始,调用者必须等待调用返回后,才能继续执行后面的任务。
(2)异步
而异步方法调用无需等待,在调用后直接返回,调用者可以继续做其他的任务。
6 死锁(deadlock)、饥饿(starvation)和活锁(livelock)
死锁、饥饿和活锁属于多线程的活跃性问题。
(1)死锁
死锁是最糟糕的一种情况,比如线程A持有了线程B的锁,而线程B持有了线程A的锁,而双方又同时在等待对方持有的锁。
(2)饥饿
饥饿指某个线程可能因为优先级比较低等原因,导致它一直无法得到资源,所以没法执行。
(3)活锁
活锁就好像是两个人走路碰面了,这个时候相互谦让让路,结果又碰上了。这样就会导致资源不断在两个线程之间来回跳动,而没有一个线程能同时拿到所有资源而正常执行。
7 原子性(atomicity)、可见性(visibility)和有序性(ordering)
(1)原子性
原子性指一个操作是不可中断的。假如有一个变量a,两个线程同时对它赋值,比如线程A给它赋1,线程B给它赋2,那么不管是怎样的情况,变量a的值要么是1,要么是2,不会是其他值。这就是原子性的一个特点。
(2)可见性
可见性指当线程A修改了某个变量的值后,线程B是否能够马上看到修改后的值。
在串行执行时,当前面修改了变量的值后,后面的代码肯定能马上得到修改后的值。但在并发环境中,线程B读到的变量的值,不一定是线程A刚写进去的值。
(3)有序性
为了优化程序性能,编译器和处理器可能会对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的。比如下面的2条指令:
int a = 1;
bool flag = true;
可能会被重排序成这样:
bool flag = true;
int a = 1;
重排序不会使串行执行的程序逻辑发生变化,也就是说在一个线程中重复执行被重排序的代码后,得到的结果永远是相同的;但重排序可能会导致多线程程序出现内存可见性问题。