参考文章 参考书: go语言并发编程
1. 什么是并发?
2. 串行、并发、并行的区别?
3. 为什么要使用并发编程?
4. 并发编程是怎么实现的?
5. 并发编程有什么风险?
6. 什么时候适合使用并发编程?
7. 操作系统怎么实现并发?
8. 什么是进程?
9. 什么是线程?
10. 进程和程序的区别
11. 操作系统(3种程实现模型)
11.1. 用户级线程模型
11.2. 内核级线程模型
11.3. 两级线程模型
多个任务同时执行从而使资源得到能充分的利用以便能够更快的得到结果.并发是一种思维方式,并不限定于特定的领域,对应编程来说也不限于特定的编程语言
在日常生活中,并发的例子也是比比皆是.
比如我们用烧水壶烧水的时候,我们可以去看看书或者做其他的事,
等到烧水壶响铃(通知我们)的时候证明水已经烧开了,这个时候我们就可以喝水了
串行: 一段时间内,执行一个任务同时不能执行其他任务,只能等到一个任务完成才能进行下一个任务
并发: 在一段时间内,执行两个或多个任务(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)
例如: 单核cpu交替运行多个任务
并行: 在一段时间内,同时执行两个或多个任务
例如: 多核cpu同时在不同的cpu上,执行两个或者多个任务
提高计算机硬件的利用率,同时减少任务完成的总时间. 提高程序的运行速度和计算机硬件资源利用率
1. 提高计算机硬件的利用率
比如打印机打印的时候, 我们可以听音乐、逛论坛看贴吧等
2. 减少任务完成的总时间
比如我们需要烧水喝水需要10分钟、看书也需要10分钟
1. 我们先烧好水喝水10分钟,然后在去看书10分钟, 总共花掉20分钟
2. 如果我们打开开关烧水然后就去看书, 水烧好了我们在回来喝水,这个时候我们可能11分钟就完了
况且没有那个人会傻傻的在哪里等水烧开了,在去做其他事
3. 让程序变的简短清晰
每个任务单独编写程序并发编程是怎么实现的比把所有任务编写混杂在一起,要简单的多.
- 并发编程实现方式一种是多进程并发, 一种是多线程并发.
- 从操作系统的角度来看, 并发是启动多个程序交替执行(cpu不停的切换程序,执行程序).
- 从操作系统的角度来看,进程是资源分配的基本单位,线程是任务调度的基本单位,线程是轻量级的进程但它不能脱离进程存在, 也就是说线程使用的资源都是从宿主进程获得的.
并发编程相对于一般的串行编程来说存在很大的风险,如果把我们之前在单线程中运行的程序不加改造直接拿到多线程中运行, 很有可能不会有正确的结果.导致结果不正确的原因就是并发编程中的风险,也是我们需要格外注意的地方
1. 安全性
安全性的核心是正确性, 它要能保证并发编程不会出现不符合预期结果的情况.
2. 不确定性
串行程序中所有代码的先后顺序是固定的, 而并发程序中只有部分代码是有序的.
并发程序中一些代码的执行顺序并没有明确的指定,这一特性被称为不确定性, 这
导致并发程序每次运行的代码执行路径都是不同的. 即便是在输入数据相同的前提下,也是如此.
3. 性能
并发编程不是"空手套白狼",它也是有开销的.不论是多进程还是多线的并发,
在它们切换的时候会执行上下文的切换,寄存器中变量的更新等操作,这都会消耗一定的资源和时间,
所以并发编程一定要预估好并发编程节省的资源和时间是不是足以弥补线程间切换的开销.
4. 复杂性
单线程中所有变量的值可以从本地线程对应的栈、寄存器和进程公共的堆拿到,执行时也是孤军奋战,所以不需要和别人打交道,
而多线程之间一旦需要协同就要在线程之间传递信息,再加上前面所说的安全性、活跃性等要求,这一整套多线程的并发会大大提高程序的复杂性,
这也是并发编程的难度所在.
并发编程最大的优势就是提高程序的运行速度和资源利用率. 如果串行程序在这两方面并不受到限制的话就没有必要使用并发编程.
1. 任务会堵塞线程导致之后的代码不能执行: 比如一边从文件读取, 一边进行大量计算
2. 任务执行时间过长,可以划分为分工明确的子任务: 比如下载大文件
3. 任务本身需要协作执行: 比如生产者消费者问题
系统并发是启动多个程序交替执行(cpu不停的切换程序,执行程序)
进程就是运行(进行)中的程序,它是程序的一次执行过程,也是操作系统进行资源分配和调度的基本单位
线程就是进程中的执行路径,或者说是进程中的一个执行单元,也是操作系统调度的基本单位 线程是轻量级的线程,一个进程可以包含多个线程(即一个进程中可以并发执行多个线程)
进程是动态的,程序是静态的,进程是运行中的程序,而程序是一些保存在硬盘上的可执行代码
11.1.用户级线程模型
11.2.内核级线程模型
11.3.两级线程模型
在用户级线程模型下的线程是由应用程序完成的,也可说是由用户级别的线程库全权管理的,线程库不是是内核的一部分,
而是存储在进程的用户空间中,这些线程的存在对于内核来说无法感知的.显然这些线程也不是内核调度器的调度对象.
对于线程的各种管理和协调完全是用户级别程序的自主行为,与内核无关.应用程序在对线程进行创建、终止、切换或同步等操作的时候,
并不需要让cpu从用户态切换到内核态,所以存在速度上的优势.由于对线程的管理不需要要内核参与,所以使得程序的移植性更强一些.
这一特点导致此模型下的多线程不能真正的并发运行.例如,如果某个线程在I/O操作过程中堵塞,那么所属的进程也会被堵塞.
这正是由于线程无法被内核调度造成的.在调度器眼里,进程是一个无法再被分割的调度单元.无论其中存在多少个线程.另外,
即使计算机上存在多个cpu,进程中的多个线程也无法被分配给不同的cpu运行.对于cpu的负载均衡来说,进程的粒度太粗了.
而且同一个进程中所有线程的优先级只能由该进程的优先级来体现.同时线程库对线程的调度完全不受内核控制的影响.
所以线程的优先级与内核为进程设定的优先级是没有关系的.所以现代操作系统都不使用这种模型来实现线程.但是在早期,
这种模型作为线程实现方式的案例确实存在.由于包含了多个用户级线程的进程只与一个KSE相对应,因此这种线程实现模型
又称为多对一(M:1)的线程实现
特点:
1. 可移植性强(可以在不支持线程的操作系统中实现)
2. 速度快(创建和销毁线程、线程切换、线程管理的代价比内核线程少得多,因为保存线程状态的过程和调用程序都只是本地过程,所以速度快)
3. 控制简单(线程的调度不需要内核直接参与)
4. 线程管理比较灵活(允许每个进程定制自己的调度算法,这就必须自己写管理程序,与内核线程的区别)
5. 线程发生I/O堵塞时(调用堵塞时系统调用,由于内核无法感知线程存在,从而会堵塞整个进程,导致所有线程会堵塞,因此同一个进程中只能同时有一个线程在运行)
6. 同一个进程中只能同时有一个线程在运行
在内核级线程模型下线程是由内核负责管理的,它们是内核的一部分.应用程序对线程的创建、终止、同步都必须通过内核提供的
系统调用来完成.进程中的每一个线程与一个KSE相对应.也就是说,内核可以分别对每一个线程进行调度.所以内核级线程模型
又称为一对一(1:1)的线程实现.一对一线程消除了多对一线程实现的很多弊端,因为线程都是由内核来管理和调度,可以真正的
实现线程的并发运行.内核可以在不同的时间片内让cpu运行不同的线程,内核在极短的时间内快速切换和运行各个线程,使得它们
看起来就像正在同时运行.即使进程中的一个线程由于某种原因进入到堵塞状态,其他线程也不会受到影响,这也使得内核在多个cpu
上进程负载均衡变得容易和有效.但是内核线程的管理成本显然要比用户级线程高出很多.线程的创建会用到更多的内核资源.
并且像线程的创建、切换、同步这类操作所花费的时间也会更多.如果一个进程包含了大量的线程,那么它会给内核的调度器
造成非常大的负担,甚至会影响到操作系统的整体性能.因此,采用内核级线程模型的操作系统对一个进程中可以创建的线程的
数量都有直接和间接的限制.尽管内核级线程模型有资源消耗较大、调度较慢等缺点,但是与用户级线程的实现相比,它还是
有较大的优势.很对现代操作系统都是由内核级线程模型实现的线程,包括Linux操作系统.实际上Linux操作系统的最新线程库
实现(NPTL)为最小化内核级线程模型的劣势付出了巨大的努力,这也使得在Linux操作系统中使用线程更加高效.
特点:
1. 并发运行(如果进程中的某个线程被堵塞,能够切换到进程内的其他线程继续执行, 注意这是用户级线程的缺点,用户级线程模型的线程就会堵塞)
2. 并行运行(多处理器(cpu)中,内核能够并行执行多个线程)
3. 所有的堵塞都是调用系统调用实现的
4. 信号是发送给进程而不是线程的,当一个信号到达时,应该由哪一个线程处理它?线程可以注册它们感兴趣的信号
5. 如果一个进程有大量的线程,那么它会给调度器操作非常大的负担
6. 一个进程中可以创建的线程的数量都有直接和间接的限制
7. 像线程的创建、切换、同步这类操作所花费的时间也会用户级线程更多
两级线程模型的目的是取前两种模型的精华,并去二者的劣势,也成为多对多(M:N)的线程实现.与其他模型相比,
两级线程模型提供了更多的灵活性.在此模型下,一个进程可以与多个KSE想关联,这与内核级线程模型相似.
但与内核级线程不同的是,进程中的线程(以下称为应用程序线程)并不与KSE一一对应,这些应用程序线程可以映射到
同一个已关联的KSE上.首先,实现了两级线程模型的线程库会通过操作系统内核创建多个内核级线程.然后,它会通过
这些内核级线程对应用程序线程进行调度.大多数此类线程库都可以将这些应用程序线程动态地与内核级线程关联.
这样的设计显然使用线程的管理工作更加复杂,因为这需要内核级和线程库的共同努力和协作才能正确、有效地进行,
但是,也是由于这样的设计,内核资源的消耗才得以大大减少,同时也使线程管理操作的效率提高不少.因为两级线程模型
实现的复杂性,它往往不会被操作系统内核的开发者所采用.但是,这样的模型却可以很好地在编程语言层面上实现并充分
的发挥出其应有的作用.就拿Go语言来说,它的并发编程模型就与两级线程模型在理念上非常类似,只不过它的具体实现方式
更加高级和优雅一些.在Go的并发编程模型中,不受操作系统内核管理的独立控制流并不叫作应用程序线程或者线程,
而称为goroutine
操作系统3种线程模型如下图