进程与线程
我们都知道,CPU是计算机的核心,承担所有的计算任务; 官网说法:进程就是CPU资源分配的最小单位。 我理解为就是进行中的程序,一个拥有自己的资源空间并且可以独立运行的程序。 进程包括运行中的程序和程序所使用到的内存和系统资源,CPU可以有很多的线程:当我们的电脑打开一个软件就会产生一个或多个进程,因此打开多个软件时就会使电脑变卡,是因为CPU在给每个进程分配资源空间,但CPU的内存空间是有限的,进程越多,电脑越卡。
什么是线程(thread)?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
进程与线程的区别
做个比喻: 进程 == 火车 ; 线程 == 车厢 。
- 线程在进程下行进(只有车厢无法运行)
- 一个进程可以包含多个线程(一列火车可以拥有多节车厢)
- 不同进程间数据很难共享(一列火车的车厢很难换到另一列火车上去)
- 同一进程下不同线程间数据很容易共享(车厢间传递)
- 进程间不会相互影响,一个线程挂掉将会导致整个进程挂掉(一列火车不会影响到另一列火车,但是同一列火车上的车厢发生事故,将影响整列火车)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一列火车无法开在不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(火车上的洗手间只能按顺序使用) --互斥锁 ·进程使用的内存地址可以限定使用量(火车上的餐厅,最多允许多少人进入,如果满了则需要在外等候,等有人出来才能进去) --信号量
多进程和多线程
打个比喻:
- 单进程单线程:一个人在一张桌子上吃饭;
- 单进程多线程:多个人在一张桌子上吃饭;
- 多进程单线程:多个人每个人在自己桌子上吃饭;
多进程:多进程指的是在同一时间内,同一个计算机系统中多个进程同时处于运行状态。 多线程:就是指一个进程中同时有多个线程正在执行。 多线程的问题就是多个人同时在一张桌子上吃饭时容易发生争抢,就是资源共享时就会发生冲突争抢。 多线程的缺点:
- 使用太多的线程,是很耗费系统资源的,因为线程需要开辟内存(用空间换取时间)
- 影响系统性能,因为操作系统需要在线程之间来回切换。
- 需要考虑线程操作对程序的影响,如线程挂起,中止等操作对程序的影响。
- 线程使用不当会发生很多问题。
多线程是异步的,但这不代表多线程真的是几个线程同时进行,实际上是系统不停的在各个线程间来回切换,因为系统切换的速度很快,所以导致我们感觉像是同时运行的错觉。
js为什么是单线程
js的单线程与它的用途有关。作为浏览器脚本语言,js主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题:假定js同时有两个线程,一个线程操作DOM节点添加内容,一个线程删除这个DOM节点,这时浏览器该如何执行? 还有人说js有worker线程;是的,在H5新标准中为了利用多核CPU的计算能力,提出了Web Worker标准,允许js脚本创建多个线程,但是这些创建的子线程都是受主线程控制的,并且不能操作DOM,所以这个标准并没有改变js是单线程的本质。 了解了进程和线程之后,接下来看看浏览器解析,浏览器之间也是有些许差距的,不过大致差不多,因此就按Chrome为例。
浏览器
浏览器是多线程的。 作为前端,免不了和浏览器打交道,浏览器是多线程的,拿Chrome来说,每创建一个Tab页就会产生一个新的进程,我们打开多个标签不关闭,就会让电脑创建多个进程,会使电脑越来越卡,非常耗费CPU。
浏览器包含哪些进程
- Browser进程 浏览器的主进程(负责协调,主控),该进程只有一个。 负责浏览器界面显示,用户交互,控制页面前进后退等。 负责各个页面的管理,创建和销毁其他进程。 将渲染(Renderer)进程得到的内存中的Bitmap(位图),绘制到用户界面上。 网络资源的管理,下载等。
- 第三方插件进程 每种类型的插件都对应一个进程,当使用该插件时才创建。
- GPU进程 该进程也只有一个,用于3D绘制等。
- 渲染进程(重点) 浏览器内核(Renderer进程,内部是多线程)。 每个页面都有一个独立的渲染线程,互不影响。 主要作用是为了渲染页面,脚本执行,事件处理等。
为什么浏览器要多线程
我们假设浏览器是单线程,那么某个Tab页面奔溃了,就影响了整个浏览器,用户体验非常差。 同理如果某个插件奔溃了也会影响整个浏览器。 浏览器进程很多,每个进程有包含了多个线程,占用很大的内存,意味着要消耗很多资源,有点空间换时间的意思。 到此可不只是为了理解为何Chrome运行时间长了电脑会卡,第一个重点来了: 浏览器的内核是多线程的,他们在内核控制下互相配合以保持同步,一个浏览器至少实现三个常驻线程: javascript引擎线程,GUI渲染线程,浏览器事件触发线程;
渲染进程Renderer
浏览器中所有选项卡内部的逻辑,都由渲染进程处理。在渲染进程中,主线程处理了绝大部分的网页代码。由Worker或Service Worker 注册的js代码会有单独的Worker线程处理。Compositor(合成器)和Raster(光栅)线程确保了页面快速平滑的呈现。 渲染进程最重要的任务就是将html,css,js代码转换为用户交互的界面。
以下就是浏览器渲染过程:
- 构建DOM 当渲染进程接受到导航栏的确认信息后,并开始接受响应数据(HTML data)时,主线程就会开始解析数据(HTML data),生成DOM(document object model)对象。 DOM是浏览器对页面和数据结构的表示,通过浏览器提供的API,用户可以获取到这些DOM节点。 浏览器根据HTML标准来解析html文档。你可能注意到了,浏览器解析HTML文档从来不会报错,即使你少写了一个标签,浏览器也会自动帮你补上;这是因为html规范优雅的处理了这些错误。如果你对这个过程好奇,可以通过阅读 html.spec.whatwg.org/multipage/p…来了解。
- 加载子资源
一个网页生成时通常会使用额外的资源,比如图片,css,js;这些资源文件都会通过网络加载(或通过缓存)。当主线程在解析数据并构建DOM树时,会逐一找出这些文件并加载。为了加速页面的显示,预加载扫描(preload scanner)会同时在后台运行。如果页面上有img或者link标签需要加载的资源,当解析器生成相应的标签时,预加载器就会通知浏览器进程中的网线线程去加载资源。
- 阻塞解析 当解析器碰到script标签时,就会停止继续解析剩余的html文档,加载相应的js代码并执行。 为什么浏览器会这样做? 前面提到js能够改变文档结构,比如document.write()就会改变整个文档结构。这就是为什么html解析器需要等待js代码执行结束才能继续解析html的原因。
- 提示浏览器如何加载资源 web开发者有很多种方式让浏览器更加智能的加载资源,如果js代码没有使用document.write(),开发者可以在script标签上添加async或者defer属性,告诉浏览器可以通过异步的方式来加载对应的资源,并且不会阻塞解析器的执行。还可以通过来告诉浏览器,该资源在当前的导航中也会被用到,需要尽快加载。
- 样式计算
仅仅有DOM树结构,浏览器呈现的页面毫无美观可言,因此我们可以通过css来为元素设置更加丰富的样式。
主线程会解析css并通过css选择器来确定出每个DOM节点的样式信息。你可以通过DevTools来查看DOM节点的样式信息:
即使页面没有使用任何的css,每个DOM节点也会有默认的样式信息。比如H2比H3要大;每个元素都有不同的margin和padding。
- 布局 在渲染进程知道了文档的DOM树和节点样式信息后,它仍然不能将页面呈现在显示屏上。试想一下:如果你通过口述向你的同事描述一幅画,这有一个红色的方块,那有一个彩色的菱形;在你的同时脑海中呈现的未必是你所说出来的模样,因为他不知道固体的尺寸位置。 确定DOM树节点的几何信息这个过程叫做布局。主线程会遍历所有DOM节点,根据样式信息计算并创建布局树(布局树上的节点拥有坐标信息和几何信息),但是DOM上一些不呈现的节点将不会出现在布局树上。比如当一个元素拥有display:none;就不属于布局树上的节点。但是某个元素使用了伪元素比如p::before{content:'Hi'},虽然伪节点并不在DOM树上,但却是属于布局树上的节点。
- 绘画 拥有DOM树,样式信息和已经生成的布局树仍不能渲染出一个页面。还需要知道渲染的先后顺序,因为后渲染的元素会覆盖前面的元素,呈现页面就是如此。
- 更新消耗 需要关注一点:由于从DOM&CSS合成布局树,到布局树生成绘画记录是一系列的过程,在这个过程中,每一步都需要前一步来生成数据。如果DOM或者CSS结构发生改变,那么需要通过以上步骤生成受影响部分的绘制记录。
js引擎线程
- js引擎线程就是js内核,负责处理js脚本程序(例如V8引擎)
- js引擎线程负责解析js脚本,运行代码
- js引擎一直等待任务队列中任务的到来,然后加以处理 浏览器同时只能有一个js引擎线程在运行js程序,所以js是单线程运行的 一个Tab页(renderer进程)中无论什么时候都只有一个js线程在运行js程序
- GUI渲染线程与js引擎线程是互斥的,js引擎线程会阻塞GUI渲染线程 就是我们常遇见的js执行时间过长时,造成页面的渲染不连贯,导致页面渲染加载阻塞(加载慢) 例如浏览器渲染的时候遇到script标签,就会停止GUI渲染,然后js引擎线程开始工作,执行里面的js代码,等js代码执行完毕后,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况。
事件触发线程
- 属于浏览器而不是js引擎,它是用来控制事件循环,并管理一个事件队列(task queue)
- 当js执行碰到事件绑定和一些异步操作,比如setTimeout,或者说鼠标点击,ajax异步请求等,会通过事件触发线程将对应的事件添加到对应的线程中,比如定时器线程加到定时器线程,等异步事件有了结果,便把我们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待js引擎的处理。
- 因为JS是单线程,所以这些等待处理队列中的事件都得排队等待js引擎处理。
定时器触发线程
- setInterval与setTimeout所在线程
- 浏览器定时计算器并不是由js引擎来计数的,因为js引擎是单线程的,如果js处于阻塞状态就会影响计时准确性
- 通过单独线程来计时并触发定时器(计时完毕后,添加到事件触发线程的事件队列中,等待js引擎空闲后执行),这个线程就是定时器触发线程,也叫定时器线程
- W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
异步HTTP请求线程
- 在XMLHttpRequest在连接后通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置由回调函数,异步线程就会产生状态变更事件,将这个回调再放入事件队列中交由js引擎执行
- 简单说就是当遇到一个异步http请求时,就把异步请求事件添加到异步请求线程,等到http的状态发生变化时,再把回到函数添加到事件队列,等待js引擎线程来执行
事件循环(Event Loop)
所有的任务都可以分为同步和异步任务,同步任务,顾名思义就是立即执行的任务,同步任务一般会直接进入到主线程执行,而异步任务,比如ajax网络请求,定时器函数都属于异步任务,会通过任务队列的机制(先进先出的原则)来进行协调。用图来表示就是:
同步和异步任务会进入不同的执行环境,同步进入主线程,异步进入任务队列。主线程内的任务执行完毕之后,会去任务队列读取已经完成的任务,推入主线程执行。上述过程不断重复执行,就是我们所说的Event Loop(事件循环)。
在事件循环中,每执行一次循环操作称为tick,通过阅读规范可知,每一次tick的任务处理模型是比较复杂的,关键的步骤如下总结:
- 在此次tick中选择最先进入队列的任务(oldest task),如果有则执行一次
- 检查是否存在Microtasks,如果存在则不停的执行,直至清空Microtask Queue
- 更新Render
- 主线程重复执行上述步骤
那么,什么是microtasks?规范中规定:task分为两大类,分别是Macro Task(宏任务)和Micro Task(微任务),并且每个宏任务结束后,都要清空所有的微任务,这里的Macro Task也是我们常说的task,后面提到的task皆看作Macro Task(宏任务)。
什么是宏任务,什么是微任务:
- 宏任务主要包含:script(整体代码),setTimeout,setInterval,I/O,UI交互事件,setImmediate;
- 微任务珠岙包含:Promise,MutaionObserver,process.nextTick;
我们来看一段简单的代码:
let setTimeoutCallback=function(){
console.log('我是定时器回调')
}
let httpCallback=function(){
console.log('我是http请求回调')
}
console.log('我是同步任务')
setTimeout(setTimeoutCallback,2000)
ajax.get('/info',httpCallback)
console.log('我是同步任务2')
以上js代码按照顺序自上而下,可以理解为这段代码的执行环境就是主线程,也就是当前执行栈。
- 首先执行console.log('我是同步任务')
- 接着执行到setTimeout()时,发现是一个定时器,便移交给定时器线程,通知定时器线程2秒后将setTimeoutCallback这个回调交给事件触发线程,2s后事件触发线程得到这个回调并加入事件队列等待执行。
- 接着执行http请求,移交给http请求线程执行,请求响应后将httpCallback这个回调函数通过事件触发线程加入到事件队列中等待执行。
- 在执行console.log('我是同步任务2')
- 至此主线程执行栈中执行任务完毕,js引擎空闲,开始向事件触发线程发起轮询,询问事件队列中是否有需要执行的回调函数,如果有则将事件队列中的回调函数添加到执行栈中开始执行,如果没有回调则会一直发起询问,直到有为之。这样反反复复的过程就是所谓的事件循环。
我们发现浏览器上的所有线程工作都是单一且独立,非常符合单一原则。 最后记住一点,js是一门单线程语言,所谓的异步操作只不过是放在事件循环队列中,等待主线程来执行的,并没有专门的异步执行线程。