进程(Process)和 线程(Thread)
- 进程:一段程序的执行过程,是操作系统分配给 应用程序的独享资源,因此分配者是由操作系统,且不同进程之间的资源独立不共享,一个应用程序包含一个或多个进程。
- 线程:系统能够 进行运算调度的最小单位。它被包含在进程之中,是 进程的最小运行单位,因此也被称作 “轻量级” 的进程。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,同一进程中的每个线程都可共享进程资源。
- 区别 - 工厂 VS 员工:进程是CPU分配资源的最小单位,线程是CPU调度的最小单位:进程就像一个工厂,线程就像工厂中的员工,工厂与工厂之间相互独立,工厂中的员工相互协作完成任务,每个工厂至少都有一个员工,员工之间共享工厂的空间。这段话对应的是: 1.工厂=进程=cpu分配的一块可独立运行的内存; 2.进程与进程之间相互独立; 3.进程中至少有一个线程; 4.进程中的所有线程共享同一空间; 5.进程中的线程相互协作;
- 单线程:应用程序的运行必定是建立在某个进程的线程之上的,即运行不能脱离线程而存在,如果一个应用程序只有一个进程,且一个进程中也只有一个线程,那么这种程序称之为单线程程序。JavaScript 是一种单线程运行的语言,但使用 Web Workers 可以多线程运行。
- 主线程:应用程序在运行时,会开启一个默认的线程,这个线程称为主线程或UI线程,主线程只会有一个,因为,主线程一般用于浏览器处理UI事件和UI绘制等(比如:点击,滚动,拖拽等事件)。默认情况下,浏览器在一个线程中运行一个页面中的所有 JavaScript 脚本,以及呈现布局,回流,和垃圾回收。这意味着一个长时间运行的 JavaScript 会阻塞线程,因此,别将耗时的操作放到主线程中,耗时操作会卡住主线程,严重影响UI的流畅度,给用户不良体验。
- 多线程:如果应用程序的某个进程有多个线程同时运行,称为多线程程序。多线程能够有效提升CPU的利用率,但是创建多线程也会带来创建开销,并且线程间切换也会存在开销。
- 多进程:一个应用程序同时启动多个实例运行
- 浏览器进程:浏览器一般是多进程多线程的应用程序,包括有渲染进程(页面渲染,脚本执行,事件处理等), 包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程
- Browser进程:主进程,打开浏览器即创建,功能包括:创建销毁其它进程、页面显示、资源管理下载、前进后退、管理页面等
- 插件进程:每种类型的第三方插件只有一个进程,只有当插件运行时才会创建
- GPU进程:最多创建一个,用于3D绘制;
- Renderer进程:浏览器渲染进程,也是每个标签页所拥有的进程,互不影响,负责页面渲染,js执行,事件处理等,每打开一个页面都会创建一个Renderer进程,一般几个空页面会合并成一个Renderer进程以节省资源
- 浏览器线程
- GUI线程:图形用户界面(Graphic User Interface)渲染,功能:构建HTML、CSS、DOM/Renderer树、布局与绘制等,当界面需要重绘(repaint)或由于某种操作引发回流(reflow)时,该线程就会执行;
- JS引擎线程:JS内核,也称js引擎(比如Chrome的V8引擎),负责处理执行js脚本。
- 区别 - GUI线程 VS JS引擎线程:GUI线程负责将DOM渲染到页面上,JS引擎线程涉及到DOM的一些增删改的操作。如果两个线程同时运行,GUI线程渲染DOM的同时,进行一些增删改的操作,必将造成冲突(渲染结果与DOM操作结果不一致)。因此,这两个线程设计为互斥的关系。当GUI线程运行时,JS引擎线程处于冻结状态,同理,JS引擎线程运行时,GUI线程处于冻结状态。当JS执行较长时间时,页面就会出现卡顿现象,所以要尽量避免长时间执行的代码。
- 事件触发线程:当js引擎执行代码块(如setTimeout、如鼠标点击,ajax异步请求等)时,会将对应任务添加到事件线程中,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待js引擎的处理,所有在处理队列的任务都要等待JS引擎空闲时才会执行。
- 定时器触发线程:setTimeout和setInterval所在的线程,定时任务不是在js引擎线程中进行的(会阻塞js的执行),而是另外开一线程进行计时,符合触发条件后,事件触发线程会把事件添加到队列尾(定时器最小时间间隔为4ms)
- 异步http请求线程:在XMLHttpRequest连接后,通过浏览器开一个线程请求,当检测到状态变更时,如设置了回调函数,异步线程就会产生状态变更事件,将这个事件添加到事件队列尾
并行(parallel)和并发(concurrent)
- 并行:如果系统有多个CPU,且由一个CPU执行一个线程,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,实现同时进行,这种方式称之为并行(Parallel)
- 并发:如果系统仅一个CPU,但有多个线程需要处理,那么只有一个CPU的系统不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状,这种方式称之为并发(Concurrent)
- 区别 - 同时执行 VS 分时交替执行:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在 同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是 分时地交替执行,因此,某种程度上可以说 并发是在尽力地模拟并行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行
同步(Synchronous)和异步(Asynchronous)
- 同步:实时(或者尽可能实时)地收取(而且必要的话也处理或者回复)信息的即时沟通方式,即为同步,由于实时这一特点,就会使得代码需要在做出反馈之后,才能往下执行,因此,同步请求可能阻止代码的执行,这会导致屏幕上出现“冻结”和无响应的用户体验。
- 异步:两个或两个以上的对象或事件不同时存在或发生(或多个相关事物的发生无需等待其前一事物的完成)。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。异步通信,是一种在双方或多方之间交换消息的方式。其中每个参与方各自在他们方便或可操作的情况下接收并处理消息,而不是在收到消息后立即进行处理。另外,消息的发送无需等待确认信息,前提是如果出现问题,接收方将请求更正或以其他方式处理该情况,例如Ajax技术。异步软件设计,通过构建代码扩展了异步的概念,按照这种设计编写的代码使得程序能够要求一个任务与先前的一个(或多个)任务一起执行,而无需为了等待它们完成而停止执行。当后来的任务完成时,程序将使用约定好的机制通知先前的任务,以便让它知道任务已经完成,以及如果有结果存在的话,这个结果是可用的。
事件循环机制
执行栈
- 栈内存运行代码,因此也用于执行任务,会区分出同步还是异步任务,同步任务是立即执行的任务,一般会直接进入到主线程中执行;异步任务是异步执行的任务(如Ajax请求、定时器等),会通过任务队列进入栈内执行。
任务队列
- 任务队列:存放JavaScript异步任务的队列,因此遵从的是先进先出的队列机制。任务队列存在两种异步任务类型,一种为microtask queue,另一种为macrotask queue,微任务包含在宏任务之内,一个宏任务执行完毕才执行下一个宏任务,有微则微,无微则宏
- 宏任务(macrotask queue):不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。
- script 整体代码(JavaScript代码整体代码是同步执行的,可以看做成没有异步任务的异步执行)
- setTimeout、setInterval
- I/O
- UI 交互事件
- setImmediate(Node.js 环境)
- 微任务(microtask queue):唯一,整个事件循环当中,仅存在一个;同步执行,同一个事件循环中的microtask会按队列顺序,串行执行完毕。
- Promise then (Promise是JS整体代码的微任务,会直接同步执行,then是异步执行,会被放入任务队列中)
- MutaionObserver
- process.nextTick(Node.js 环境)
事件循环(event loop)
内涵:同步和异步任务分别进入不同的执行环境,同步进入主线程(主执行栈),异步进入任务队列。主线程内的任务执行完毕为空时,会去任务队列读取任务并推入主线程执行,待到该任务执行完毕,会继续在任务队列读取下一任务并推入主执行栈,依次循环直到任务全部执行外币,由于整个读取任务并执行的过程是不断重复的,因此,称之为事件循环(Event Loop)。
循环逻辑
- 主线程执行同步任务:主线程执行JavaScript整体代码,并且将JavaScript整体代码这个宏任务的所有微任务代码执行完毕;
- 异步队列:在主线程执行JavaScript整体代码的过程中,如果遇到异步任务,将异步任务放入任务队列中
- 读取队列:主线程中的同步环境代码执行完毕后,读取异步任务队列中的任务,如果此时异步任务队列有一些任务的条件得到满足(如Ajax返回、DOM事件处罚、Timer到等),将相应的异步任务推入主线程
- 循环事件 - 异步执行宏/微任务:异步任务推入主线程后,执行对应的宏任务中的微任务,等微任务全部执行完毕,继续读取下一个异步任务,循环执行
- 简而言之:同步环境执行 -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个) -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个)...
代码验证 - 宏任务/微任务
//一
console.log(1);
setTimeout(function () {
console.log(2);
});
new Promise(function (resolve, reject) {
console.log(3);
resolve();
}).then(function () {
console.log(4);
setTimeout(function () {
console.log(5)
});
});
console.log(6);
//二
console.log(1);
setTimeout(function(){
console.log(2);
});
new Promise(function(resolve, reject){
console.log(3);
resolve();
setTimeout(function(){
console.log(5)
});
}).then(function(){
console.log(4);
});
//三
Promise.resolve().then(()=>{
console.log(1);
setTimeout(()=>{
console.log(2);
});
});
setTimeout(()=>{
console.log(3);
Promise.resolve().then(()=>{
console.log(4);
});
});
console.log(5);
//四
console.log(1);
new Promise(resolve=>{
console.log(2);
setTimeout(()=>{
resolve(3);
console.log(4);
});
}).then(res=>{
console.log(res);
});
console.log(5);
//一 1 3 6 4 2 5
//二 1 3 4 2 5
//三 5 1 3 4 2
//四 1 2 5 3 4 => resolve放在了异步宏任务内,导致then被阻塞,宏任务内的同步也比异步快