关于线程和进程
- 核心理论:
CPU: 计算机的核心是,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
单个CPU一次
只能运行一个任务
进程(Process):操作系统分配资源和调度任务的基本单位,
它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
线程(Thread):建立在进程上的一次程序运行单位, 多个线程组合成一个进程任务
-
基本理论
- 以
多进程形式
,允许多个任务同时运行
; - 以
多线程
形式,允许单个任务分成不同的部分
运行; - 提供协调机制(一个是互斥锁 Mutex,一个是信号量),一方面防止进程之间和线程之间
产生冲突
,另一方面允许进程之间和线程之间共享资源
。
- 以
多线程 跟 多进程的部分场景下的优劣选择
对比维度 | 多进程 | 多线程 | 总结 |
---|---|---|---|
数据共享、同步 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU利用率低 | 占用内存少,切换简单,CPU利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 线程占优 |
编程、调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会互相影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程占优 |
Event Loop
-
堆(heap)
堆(heap)
是指程序运行时申请的动态内存,在JS运行时用来存放对象。 -
栈(stack)
栈(stack)遵循的原则是“先进后出”
栈内存1
存放 JS中的基本数据类型 与 指向对象的地址
栈内存2(简称:执行栈)
执行 JS主线程
-
队列(queue)
队列(queue)遵循的原则是“先进先出”
JS中除了主线程之外还存在一个“任务队列”(其实有两个,后面再详细说明)。
浏览器中的Event Loop
详细流程描述
- 所有同步任务都在
主线程上执行
,形成一个执行栈
和堆 - 主线程之外,还存在一个任务队列。只要
异步任务
有了运行结果,就在任务队列之中放置一个事件。 - 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中
依次执行
- 主线程从任务队列中读取事件,这个过程是循环不断的
case 如下:
例如利用setTimeout 实现的这个case
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3)
setTimeout(function(){
console.log(6);
})
},0)
setTimeout(function(){
console.log(4);
setTimeout(function(){
console.log(7);
})
},0)
console.log(5)
代码中的setTimeout的时间给得0,相当于4ms,也有可能大于4ms(不重要)。我们要注意的是代码输出的顺序。我们把任务以其输出的数字命名。
先执行的一定是同步代码,先输出1,2,5,而3任务,4任务这时会依次进入“任务队列中”。同步代码执行完毕,队列中的3会进入执行栈执行,4到了队列的最前端,3执行完后,内部的setTimeout将6的任务放入队列尾部。开始执行4任务……
最终我们得到的输出为1,2,5,3,4,6,7。
处理步骤为:
- 主线程执行,形成执行栈
- 执行栈优先执行同步代码,优先输出1,2,5
-
异步任务
推到主线程之外的任务队列(callback queue)
- 同步代码执行完毕, 任务队列中的3会进入执行栈执行, 4到了队列的
最前端
- 3执行完后,内部的setTimeout将6的任务放入
队列尾部
。开始执行4任务 - 依次执行.
Node 中的 Event Loop
术语阐述
-
事件驱动(event-driven)
是nodejs中的
第二大特性
。何为事件驱动呢?简单来说,就是
通过监听事件的状态变化来做出相应的操作
。比如读取一个文件,文件读取完毕,或者文件读取错误,那么就触发对应的状态,然后调用对应的回掉函数来进行处理。
-
线程驱动
是当收到一个请求的时候,将会为该请求开一个
新的线程来处理请求
。一般存在一个线程池,线程池中有空闲的线程,会从线程池中拿取线程来进行处理,如果线程池中没有空闲的线程,新来的请求将会进入队列排队,直到线程池中空闲线程
-
nodejs 单线程(single thread)
nodejs是单线程运行的,通过一个事件循环(event-loop)来循环取出消息队列(event-queue)中的消息进行处理,处理过程基本上就是去调用该消息对应的回调函数。 消息队列就是当一个事件状态发生变化时,就将一个消息压入队列中。
nodejs的时间驱动模型一般要注意下面几个点:
因为是单线程的,所以当顺序执行js文件中的代码的时候,事件循环是被暂停的。
当js文件执行完以后,事件循环开始运行,并从消息队列中取出消息,开始执行回调函数
因为是单线程的,所以当回调函数被执行的时候,事件循环是被暂停的
当涉及到I/O操作的时候,nodejs会开一个独立的线程来进行异步I/O操作,操作结束以后将消息压入消息队列。
实现异步IO
异步IO(asynchronous I/O)
首先来理解几个容易混淆的概念,阻塞IO(blocking I/O)和非阻塞IO(non-blocking I/O),同步IO(synchronous I/O)和异步IO(synchronous I/O)。
阻塞I/O 和 非阻塞I/O
简单来说,阻塞I/O
当用户发一个读取文件描述符的操作的时候,进程就会被阻塞,直到要读取的数据全部准备好返回给用户,这时候进程才会解除block的状态。
那非阻塞I/O呢,就与上面的情况相反
当用户发起一个读取文件描述符操作的时,函数立即返回,不作任何等待,进程继续执行。但是程序如何知道要读取的数据已经准备好了呢?最简单的方法就是轮询。
除此之外,还有一种叫做IO多路复用的模式,就是用一个阻塞函数同时监听多个文件描述符,当其中有一个文件描述符准备好了,就马上返回,在linux下,select,poll,epoll都提供了IO多路复用的功能。
同步I/O 和 异步I/O
那么同步I/O和异步I/O又有什么区别么?
是不是只要做到非阻塞IO就可以实现异步I/O呢?
其实不然。
同步I/O(synchronous I/O)做I/O operation的时候会将process阻塞,所以阻塞I/O,非阻塞I/O,IO多路复用I/O都是同步I/O。
异步I/O(asynchronous I/O)做I/O opertaion的时候将不会造成任何的阻塞。
非阻塞I/O都不阻塞了为什么不是异步I/O呢?其实当非阻塞I/O准备好数据以后还是要阻塞住进程去内核拿数据的。所以算不上异步I/O。
宏任务与微任务
任务队列中的所有任务都是会乖乖排队的吗?
答案是否定的,任务也是有区别的,总是有任务会有一些特权(比如插队),就是任务中的vip--微任务(micro-task),那些没有特权的--宏任务(macro-task)。
我们看一段代码:
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise')
})
})
setTimeout(function(){
console.log(3);
})
按照“队列理论”,结果应该为1,2,3,promise。
可是实际结果事与愿违输出的是1,2,promise,3。
明明是3先进入的队列 ,为什么promise会排在前面输出?
这是因为promise有特权是微任务,当主线程任务执行完毕微任务会排在宏任务前面先去执行,不管是不是后来的。
换句话说,就是任务队列实际上有两个,一个是宏任务队列,一个是微任务队列,
当主线程执行完毕,如果微任务队列中有微任务,则会先进入执行栈,
当微任务队列没有任务时,才会执行宏任务的队列。
微任务,如下:
原生Promise(有些实现的promise将then方法放到了宏任务中)
Object.observe(已废弃)
MutationObserver
MessageChannel 等
宏任务, 如下:
setTimeout
setInterval
setImmediate
I/O 等
执行图示
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
流程描述
- js代码会交给
v8引擎
进行处理 - 代码中可能会调用nodeApi,node会交给
libuv库
处理 -
libuv
通过阻塞i/o
和多线程
实现了异步io - 通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。
process.nextTick
process.nextTick方法不在上面的事件环
中,我们可以把它理解为微任务,
它的执行时机是当前"执行栈"的尾部
---- 下一次Event Loop(主线程读取"任务队列")之前
---- 触发回调函数。
也就是说,它指定的任务总是发生在所有异步任务之前。
setImmediate方法则是在当前"任务队列"的尾部添加事件,
也就是说,它指定的任务总是在下一次 Event Loop 时执行
。
上代码:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){
console.log(2);
});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
// 1
// 2
// TIMEOUT FIRED
代码可以看出,不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。
这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。