没有努力过就不要说自己运气不好 点个关注防止迷路! 据说 点赞 + 收藏 == 学会
除了协程,通过本文你将对进程、线程、并发有一定的了解...
进程
定义
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 狭义定义:进程就是一段程序的执行过程例如启动的某个app。
- 广义定义:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它是
操作系统动态执行的基本单元
,在传统的操作系统中,进程即是基本的分配单元,也是基本的执行单元。
1.进程(process)最小的资源管理单元[操作系统]
2.应用程序的启动实例,进程拥有代码和打开的文件资源、数据资源、独立的内存空间
进程的生命周期基本由操作系统内核进行支配,即进程的创建、切换、销毁等操作都将使会陷入内核,进行系统调用。该操作消耗较大。当进行进程的销毁时,包含但不仅于内存地址空间、内核态堆栈和硬件上下文(CPU寄存器)的切换甚至在内存资源较少的情况下会将已存入内存的数据写入磁盘交换区,代价较大。因此,我们可以发现当系统运行的进程越多,系统越是卡顿。
特征
- 每个进程都有自己的地址空间,一般情况下,包含文本区域、数据区域、堆栈
- 进程是执行中的程序,程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称之为进程
- 进程本身不会运行,是线程的容器。线程不能单独执行,必须组成进程
- 一个程序至少有一个进程,一个进程至少有一个线程
- 对于操作系统来讲,一个任务就是一个进程,比如开一个浏览器就是启动一个浏览器进程。打开一款app就是打开一个进程。
- 有些进程还不止同时做一件事情。在一个进程内部,可以同时做多件事情,比如边看视频可以边发弹幕。
进程状态:(三状态)
- 就绪:获取CPU外的所有资源、只要处理器分配资源就可以马上执行
- 运行:获得处理器分配的资源,程序开始执行
- 阻塞:当程序条件不够的时候,需要等待提交满足的时候才能执行。
状态详解
- 创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,
完成资源分配
。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态 - 就绪状态:进程已经准备好,
已分配到所需资源,只要分配到CPU就能够立即运行
- 执行状态:进程
处于就绪状态被调度后,进程进入执行状态
- 阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,
进程受到阻塞
。在满足请求时进入就绪状态等待系统调用 - 终止状态:
进程结束,或出现错误,或被系统终止
,进入终止状态。无法再执行
线程
定义
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
1.线程(thread)最小的执行单元[操作系统]
2.一个进程包含多个线程(一主多从),拥有自己的栈空间
特征
- 一个进程中至少有一个线程,不然就没有存在的意义
- 在一个进程内部,要同时干多件事情,就需要同时运行多个子任务,我们把进程内的这些子任务叫做线程
- 多线程就是为了同步完成多项任务(在单个程序中同时运行多个线程完成不同的任务和工作),是为了提高资源使用效率来提高系统的效率,而不是为了提高运行效率。
- 一个简单的比喻,多线程就像是给车保养的工人有洗车工有维修有美容的,而进程就是待被清洗维护美容店车
- 线程是程序执行流的最小单元。一个标准的线程由当前的线程ID、当前指令指针、寄存器和堆栈组成
- 同一个进程中的多个线程之间可以并发执行
线程状态:
- 就绪:指线程具备运行的所有条件,逻辑上可以运行,在等待处理机
- 运行:指线程占用处理机正在运行
- 阻塞:线程在等待一个事件,逻辑上不可执行
进程与线程对比
-
进程是操作系统资源分配的
基本单位
,而线程是任务调度和执行的基本单位线程和进程的实现在操作系统之间有所不同,但在大多数情况下,线程是进程的一个组件。进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。在操作系统中能同时运行多个进程;而在同一个进程中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)。
-
进程拥有独立的
内存空间
,线程则共享所在进程中的内存空间进程是系统中独立存在的实体,它可以拥有自己独立的资源,系统在运行的时候会为每个进程分配不同的内存空间,所以每一个进程都拥有自己私有的内存空间。在没有经过进程本身允许的情况下,一个用户的进程不可以直接访问其它进程的内存空间。而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),一个进程中的多个线程之间只能共享进程的资源。而不同的进程不共享这些资源。
-
进程之间切换开销较大,而线程间切换
开销
较小每个进程都有独立的数据空间(程序上下文),进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程的切换的消耗随略小于进程,较少进行内存和磁盘的交换,但是仍然会有堆栈的映射和切换。
-
程序是一个静态
指令的集合
,而进程是一个正在系统中活动的指令集合进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这写概念在程序中是不具备的。
协程
定义
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制(进程和线程都是由cpu 内核进行调度)。
1.协程(Coroutines)特殊的函数[程序控制]
2.一个线程可以拥有多个协程
3.可以暂停执行(暂停的表达式称为暂停点)
4.可以从挂起点恢复(保留其原始参数和局部变量)
5.事件循环是异步编程的底层基石
从名字可以看出,协程的粒度比线程更小,并且是用户管理和控制的,多个协程可以运行在一个线程上面。那么协程出现的背景又是什么呢,先来看一下目前线程中影响性能的特性:
- 使用锁机制
- 线程间的上下文切换
- 线程运行和阻塞状态的切换
以上任意一点都是很消耗cpu性能的。相对来说协程是由程序自身控制,没有线程切换的开销,且不需要锁机制,因为在同一个线程中运行,不存在同时写变量冲突,在协程中操作共享资源不加锁,只需要判断状态就行了,所以执行效率比线程高的多。
特征
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
-
对于 协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。
-
对于 进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度(有多种调度算法)
协程的优点:
- 无需线程上下文切换的开销,goroutine(协程) 切换调度开销方面远比线程小。
- 无需原子操作锁定及同步的开销
- 方便切换控制流,简化编程模型
- 每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。goroutine:2KB(官方),线程:8MB(参考网络)
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
协程的缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
CPU密集型代码(各种循环处理、计算等等):使用多进程。IO密集型代码(文件处理、网络爬虫等):使用多线程
并发与并行
并发
并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。
当有多个线程时,如果系统只有一个CPU,那么CPU不可能真正同时进行多个线程,CPU的运行时间会被划分成若干个时间段,每个时间段分配给各个线程去执行,一个时间段里某个线程运行时,其他线程处于挂起状态,这就是并发。并发解决了程序排队等待的问题,如果一个程序发生阻塞,其他程序仍然可以正常执行。
并行
并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。
并发与并行的区别
- 并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行。
- 在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。
知乎上高赞例子:
- 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
- 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
- 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是 『同时』。
JavaScript中的协程
JavaScript 协程的发展
- 同步代码
- 异步JavaScript: callback hell(回调地狱)
- ES6引入 Promise/a+, 生成器Generators(语法 function foo(){}* 可以赋予函数执行暂停/保存上下文/恢复执行状态的功能), 新关键词yield使生成器函数暂停.
- ES7引入 async函数/await语法糖,async可以声明一个异步函数(将Generator函数和自动执行器,包装在一个函数里),此函数需要返回一个 Promise 对象。await 可以等待一个 Promise 对象 resolve,并拿到结果,
Promise中也利用了回调函数。在then和catch方法中都传入了一个回调函数,分别在Promise被满足和被拒绝时执行, 这样就就能让它能够被链接起来完成一系列任务。总之就是把层层嵌套的 callback 变成 .then().then()...,从而使代码编写和阅读更直观
生成器Generator的底层实现机制是协程Coroutine。
function* foo() {
console.log("foo start")
a = yield 1;
console.log("foo a", a)
yield 2;
yield 3;
console.log("foo end")
}
const gen = foo();
console.log(gen.next().value); // 1
// gen.send("a") // http://www.voidcn.com/article/p-syzbwqht-bvv.html SpiderMonkey引擎支持 send 语法
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(foo().next().value); // 1
console.log(foo().next().value); // 1
/*
foo start
1
foo a undefined
2
3
foo start
1
foo start
1
*/
JavaScript 协程成熟体
Promise继续使用
Promise 本质是一个状态机,用于表示一个异步操作的最终完成 (或失败), 及其结果值。它有三个状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
最终 Promise 会有两种状态,一种成功,一种失败,当 pending 变化的时候,Promise 对象会根据最终的状态调用不同的处理函数。
async、await语法糖
async、await 是对 Generator 和 Promise 组合的封装, 使原先的异步代码在形式上更接近同步代码的写法,并且对错误处理/条件分支/异常堆栈/调试等操作更友好。Async、Await 实现了 Generator 的自动迭代,正因为 Async、Await 是对 Generator 和 Promise 组合的封装,所以 Async 和 Await 基本上就只能用来实现异步和并发了,而不具有协程的其他作用。
JavaScript 异步执行的运行机制
- 所有任务都在主线程上执行,形成一个执行栈。\
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。\
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
遇到同步任务直接执行,遇到异步任务分类为宏任务(macro-task)和微任务(micro-task)。
当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
var sleep = function (time) {
console.log("sleep start")
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve();
}, time);
});
};
async function exec() {
await sleep(2000);
console.log("sleep end")
}
async function go() {
console.log(Date.now())
c1 = exec()
console.log("-------1")
c2 = exec()
console.log(c1, c2)
await c1;
console.log("-------2")
await c2;
console.log(c1, c2)
console.log(Date.now())
}
go();
event loop将任务划分:
- 主线程循环从"任务队列"中读取事件
- 宏队列(macro task)js同步执行的代码块,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等, 本质是参与了事件循环的任务.
- 微队列(micro task)Promise、process.nextTick(node环境)、Object.observe, MutationObserver等,本质是直接在 Javascript 引擎中的执行的没有参与事件循环的任务.
最佳实践
- 线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
- 对于CPU密集型的任务,则需要多个进程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。
- 所以大并发下的最佳实践就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
总结:进程、线程和协程的设计,都是为了并发任务能够更好的利用CPU资源,他们最大的区别即在于对CPU的使用上(任务调度):如前文所述,进程和线程的任务调度由内核控制,是抢占式的;而协程的任务调度在用户态完成,需要在代码里显式的把CPU交给其他协程,是协作式的。
由于我们可以在用户态调度协程任务,所以,我们可以把一组互相依赖的任务设计成协程。这样,当一个协程任务完成之后,可以手动进行任务调度,把自己挂起(yield),切换到另外一个协程执行。这样,由于我们可以控制程序主动让出资源,很多情况下将不需要对资源加锁。
╭╮╱╭┳━━━┳╮╱╭╮
┃┃╱┃┃╭━╮┃┃╱┃┃
┃╰━╯┃┃┃┃┃╰━╯┃
╰━━╮┃┃┃┃┣━━╮┃
╱╱╱┃┃╰━╯┃╱╱┃┃
来都来了还不点个赞再走, 据说点赞 + 收藏 == 学会