golang并发编程-02多线程编程-01线程概述
@
1. 概念
1.1 线程
- 线程:可以被看作是在某个进程中的一个控制流。
- 主线程:一个进程的第一个线程会随着它的启动而被创建,该线程被称为主线程
1.2 线程和进程
- 一个进程至少会包含多个线程(至少一个),线程必须属于一个进程。
- 进程中的所有线程都拥有自己的线程栈,并以此存储自己的私有数据。
- 进程中被线程共享的资源:
虚拟内存地址中存储的代码段、数据段、堆、信号处理函数,以及当前进程所持有的文件描述符,等等。
1.3 线程的标识
- 线程ID在系统范围内可以
不是
唯一的 - 在其所属进程的范围内必须
是
唯一的。
1.4 多线程和多进程
交换数据
- 多进程
通过 管道、消息队列、信号灯和共享内存区等手段完成
开发成本高 - 多线程
数据可共享
竞争
- 多线程
可能发生竞态条件而不得不使用一些同步工具(比如互斥量和条件变量)加以保护
会增加程序的复杂度,甚至可能造成死锁。 - 多进程
无
2. 线程控制
2.1 线程间控制
2.1.1 创建线程
主线程在其所属进程启动的时候被创建。因此它的创建并不在此论述的范围之内。
创建过程:
- 调用线程创建新线程
任何线程都可以通过调用系统调用pthread_create来创建新的线程。我们把调用 系统调用 或函数 的线程简称为调用线程
- 在新线程中执行 start函数
调用线程需要给定新线程将要执行的函数以及传入该函数的参数值。由于代表该函数的参数被命名为start,因此我们通常称这个函数为start函数。
- 返回线程ID
如果新线程创建成功,调用线程会得到新线程的ID。
2.1.2 终止线程
调用系统调用pthread_cancel。其过程如下:
pthread_cancel函数的作用是取消掉给定的线程ID代表的线程。
- 向目标线程发出一个请求,要求它立即终止执行。
请求不会被线程立刻执行,而是由线程判断执行时机。
- 目标线程接受线程取消请求,但等到时机相应该请求。
2.1.3 连接已终止的线程
由系统调用pthread_join代表。
过程
- 操作系统调用pthread_join 函数
该函数会一直等待(或者说阻塞)直到给定线程ID对应的线程的终止。
- 将给定ID的线程 的start函数的返回值给调用线程
- 调用线程会接过流程控制权并继续执行pthread_join函数调用之后的代码
如果一个线程是可被连接的,那么在它终止之时就必须被连接,否则它就会变成一个僵尸线程。
2.1.4分离线程
分离操作由系统调用pthread_detach代表。
- 作用:
使一个线程不可被连接。
让操作系统内核在目标线程终止时自动进行清理和销毁工作。
注意
- 分离操作是不可逆的
- 一个已处于分离状态的线程执行终止操作仍然会起作用。
2.2 线程自我控制
2.2.1 终止
有如下几种方式:
- 在线程执行的start函数中执行return语句会使该线程随着start函数的执行结束而终止。
如果在主线程中执行了return语句,那么当前进程中的所有线程都会被终止。
- 显式地调用系统调用pthread_exit
如果在主线程中调用了pthread_exit函数,那么只有主线程自己会被终止
2.1.2 分离
同样调用pthread_detach函数。
区别仅在于调用线程传递给该函数的线程ID是自己的ID。
3 线程状态
上图说明:
前缀“【另】”代表描述的操作是由当前进程中的其他线程执行的。
前缀“【自】”代表描述的操作是由当前线程执行的。
前缀“【主】”则代表描述的操作是由主线程执行的。
4 线程的调度
上图说明:
激活的优先级阵列:用于存放正在等待运行的线程
过期的优先级阵列:用于存放已经运行过但还未完成的线程
调度器的工作流程:
- 调度器将要使用CPU的线程放入
激活的优先级阵列
末尾 - 长时间占用CPU的线程 被
激活的优先级阵列
中的线程从CPU的运行队列中换下,放入过期优先级阵列
。 - 激活的优先级阵列中没有待运行的线程的时,调度器就会把这两个优先级阵列的身份互换。
即
过期的优先级阵列
变为激活的优先级阵列
,之前空的激活的优先级阵列
变为新的过期的优先级阵列
- 被阻塞而进入睡眠状态的线程从运行队列中被移除
- 等待队列中的线程会随即进入睡眠状态。条件触发时会被内核唤醒,从等待队列移至相应的运行队列。
- 调度器会尽量使一个线程在一个特定的CPU上运行。
5 线程实现模型
三个模型:用户级线程模型、内核级线程模型、两级线程模型
5.1 用户级线程模型
该模型特点:
此模型下的线程是由用户级别的线程库全权管理的。
线程库并不是内核的一部分,只被存储在进程的用户空间之中。进程中的线程的存在对于内核来说是无法感知的。
应用程序在对线程进行创建、终止、切换或同步等操作的时候,并不需要让CPU从用户态切换到内核态。
因此在调度器的眼里,进程是一个无法再被分割的调度单元。
优势:
资源消耗低
(模型特点导致的)缺陷:
- 导致在此模型下的多线程并不能够被真正地并发运行。
例如,如果线程在I/O操作过程中被阻塞,那么其所属进程也会被阻塞。
- 即使计算机上存在多个CPU,进程中的多个线程也无法被分配给不同的CPU运行。
5.2 内核级线程模型
该模型下的线程是由内核负责管理的。
它们是内核的一部分。应用程序对线程的创建、终止和同步都必须通过内核提供的系统调用来完成。
内核可以分别对每一个线程进行调度。
优势:
可以真正实现线程的并发运行。
因为这些线程完全是由内核来管理和调度的。正如前文所述,内核可以在不同的时间片内让CPU运行不同的线程。
内核对线程的全权接管使操作系统在库级别几乎无需为线程管理做什么事情。
缺点:
内核线程的管理成本显然要比用户级线程高出很多。
线程的创建会使用到更多的内核资源。并且,像创建线程、切换线程,同步线程这类操作所花费的时间也会更多。
如果一个进程包含了大量的线程,那么它会给内核的调度器造成非常大的负担,甚至会影响到操作系统的整体性能。
5.3 两级线程模型
它也被称为多对多(M:N)的线程实现。
一个进程可以与多个KSE(内核调度实体)相关联。
首先,已被加载到进程的虚拟内存中的实现两级线程模型的线程库会通过操作系统内核创建多个内核级线程。
然后,它再通过这些内核级线程对应用程序线程进行调度。
大多数此类线程库都可以为实际运行的应用程序线程动态地分配若干个内核级线程。
优缺点:
虽然加大了管理工作的复杂度,但内存消耗大大降低,提高了线程管理操作的效能。
GO的协程
因为两级线程模型的实现的复杂性,它往往不被操作系统内核的开发者采纳。
但却可以很好地在编程语言层面上实现并发挥出其应有的作用。
就拿Go语言来说,其并发编程模型就与两级线程模型在理念上非常类似
其不受操作系统内核管理的独立控制流被称为Goroutine(也可以被称为Go程或协程)。