浏览器的多线程与js引擎的单线程

1. 浏览器的线程与进程

(1) 进程与线程

进程

学术上说,进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。我们这里将进程比喻为工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

线程

在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元。这里把线程比喻一个车间的工人,即一个车间可以允许由多个工人协同完成一个任务。

进程和线程的区别和关系

进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
调度和切换:线程上下文切换比进程上下文切换要快得多。

多进程和多线程

多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

(2) 浏览器的进程与线程

首先打开浏览器,然后打开shift + Esc打开chrome的任务管理器

此时只有三个进程:
浏览器进程(Browser进程): 浏览器的主进程(负责协调、主控),只有一个。作用有
    负责浏览器界面显示,与用户交互。如前进,后退等
    负责各个页面的管理,创建和销毁其他进程
    将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
    网络资源的管理,下载等
GPU进程:用于3D绘制等(可禁止掉,而且这个与页面渲染过程的Composite Layers 有关系,后面性能优化相关文章学习到再来研究一下GPU)
浏览器渲染进程(Renderer进程,内部是多线程的)每一个标签页的打开都会创建一个浏览器渲染进程(浏览器内核)。默认每个Tab页面一个进程,互不影响。主要作用为页面渲染,脚本执行,事件处理等

2. 浏览器为什么要多进程?

在浏览器刚被设计出来的时候,那时的网页非常的简单,每个网页的资源占有率是非常低的,因此一个进程处理多个网页时可行的。然后在今天,大量网页变得日益复杂。把所有网页都放进一个进程的浏览器面临在健壮性,响应速度,安全性方面的挑战。因为如果浏览器中的一个tab网页崩溃的话,将会导致其他被打开的网页应用。另外相对于线程,进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。

3. Browser进程与Render进程,GPU进程之间的如何合作?

在How browser work为文章中看到过这样一幅图:




这里的Browser engine我想对应的就是Browser进程,Rendering engine对应的就是Render进程。
针对与用户打开一个标签页,可以看到首先控制的还是Browser进程。然后我们再看一下chromium多线程模型:



基本工作方式如下
Browser进程收到用户的请求,首先由UI线程处理,而且将相应的任务转给IO线程,他随机将该任务传递给Render进程;
Render进程的IO线程经过简单解释后交给渲染线程,渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染,最后Render进程将结果由IO线程传递给Browser进程;
Browser进程接收到结果并将结果绘制出来;

4. 览器渲染Render进程(浏览器内核)有哪些线程?

GUI渲染线程

负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

JS引擎线程

也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
JS引擎线程负责解析Javascript脚本,运行代码。
JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

事件触发线程

归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

定时触发器线程

传说中的setInterval与setTimeout所在线程
浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

异步http请求线程

在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

5. JS引擎线程的相关介绍

(1) 为什么JavaScript是单线程?

首先在明确一个概念,JS引擎线程生存在Render进程(浏览器渲染进程)。其实从前面的进程,线程之间的介绍已经明白,线程之间资源共享,相互影响。假设javascript的运行存在两个线程,彼此操作了同一个资源,这样会造成同步问题,修改到底以谁为标准。
所以,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

(2) WebWorker会造成js多线程吗?

首先举一个例子,可以查看web worker
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webWorker</title>
</head>
<body>
  <div>
    <a href='./webworker/index.html'>web worker</a>
  </div>
</body>
</html>
//worker.js
onmessage = function (count) {
  console.log("web worker start")

    var i = 0;

    while(i < count.data) {
      i ++;
    }

    postMessage("web worker finish");
};
然后打开performence面板查看:

图中蓝色框是浏览器下载完wokder.js文件。紧接着我们我们可以看到红色框DedicatedWorker Thread,在workder thread的时间内去执行worker.js,然后将计算好的结果返回给main thread,最后执行到蓝色框中去。

这样看起来好像是创建了一个新的线程。如果我没有使用web worker的情况下,DedicatedWorker Thread根本就不存在。
这好像看起来并没有什么说服性。我们再看一个例子,在本地查看这个例子的时候,具体的数字是我调的,是想达到下面图示的效果。

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webWorker</title>
</head>
<body>
    <script>
        var worker = new Worker("worker.js");
        worker.postMessage(123456);


        worker.onmessage = function (e) {
            console.log(e.data)
        };

        setTimeout(function() {
            var i = 0;

            while(i < 9234156) {
                i ++;
            }
        }, 180);
    </script>
</body>
</html>
//worker.js
onmessage = function (count) {
  console.log("web worker start")

    var i = 0;

    while(i < count.data) {
      i ++;
    }

    postMessage("web worker finish");
};

 
 

如图所示,setTimeout的代码执行的时候,worker.js的代码也在执行。这里我得到的结果是webworker可以在js引擎执行代码的时候去执行另外的代码。

这里我们需要在明确开始的问题了,是造成js执行的多线程还是js引擎的多线程。这里是两个概念。上面的图示中main的栏目中是浏览器渲染Render进程(本人猜测),因为在这个过程中我们可以看到js代码的执行,也有GUI渲染线程进行html代码的解析。现在我看到的是DedicatedWorker Thread是与览器渲染Render进程同一级的(当然这些都是performence展现出来给我们看的)。

我在MDN上看到一句话:Worker接口会生成真正的操作系统级别的线程。所以这里的webworker不是一个新的js引擎线程。而是操作系统级别的线程。线程的执行不会影响到原有的js引擎的执行,也不会影响到浏览器渲染Render进程。至于其内部实现,本人就望尘莫及了。但是,人家webworker确实实现了js代码执行的多线程(当然这些都是本人的基于看到的结果猜测的,没有找到实际的论证资料,如果有知道的可以告知,谢谢了)。

所以我目前得到的结论是: webworker是可以造成js代码的多线程执行,但不是js引擎多线程的执行。webwoker的生命周期是由js引擎线程控制的,因为webweoker提供了一系列的api供我们操作。

然后我们再说一下webweoker中的一些不能操作的内容:也是出于安全考虑,如果不太小心,那么并发(concurrency)会对你的代码产生有趣的影响。然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。所以webworker也自己做了限制(下面的内容是在网上找到的,因为我没有这么使用过webworker):

1、不能访问DOM和BOM对象的,Location和navigator的只读访问,并且navigator封装成了WorkerNavigator对象,更改部分属性。无法读取本地文件系统

2、子线程和父级线程的通讯是通过值拷贝,子线程对通信内容的修改,不会影响到主线程。在通讯过程中值过大也会影响到性能(解决这个问题可以用transferable objects)

3、并非真的多线程,多线程是因为浏览器的功能

4、兼容性

5 因为线程是通过importScripts引入外部的js,并且直接执行,其实是不安全的,很容易被外部注入一些恶意代码

6、条数限制,大多浏览器能创建webworker线程的条数是有限制的,虽然可以手动去拓展,但是如果不设置的话,基本上都在20条以内,每条线程大概5M左右,需要手动关掉一些不用的线程才能够创建新的线程(相关解决方案)

7、js存在真的线程的东西,比如SharedArrayBuffer

(3) js代码的执行(Event Loop)与其他线程之间的合作

JavaScript 引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是Web 浏览器。提供了一种机制来处理程序中多个块(这里的块可以理解成多个回掉函数)的执行,且执行每块时调用JavaScript 引擎,这种机制被称为事件循环。换句话说,JavaScript 引擎本身并没有时间的概念,只是一个按需执行JavaScript 任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。这个调度是由事件触发线程调度的。

举例来说,如果你的JavaScript 程序发出一个Ajax 请求,从服务器获取一些数据,那你就在一个函数(通常称为回调函数)中设置好响应代码,然后JavaScript 引擎会通知宿主环境(事件触发线程):“嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个函数。”然后浏览器就会设置侦听来自网络的响应,拿到要给你的数据之后,就会把回调函数插入到事件循环,以此实现对这个回调的调度执行。

关于具体的事件循环的内容可以访问JavaScript之异步 - 基本概念

请看下图对于一个页面的请求以及js的执行过程中,上面的进程/线程之间的合作。

由于图片比较大,详情可以访问(https://www.processon.com/view/link/593fc61fe4b0848d3ecf1d56)。

6. Promise的出现

其实关于Promise的内容我之前也有看过,具体内容可以查看文章现在这里我只是将Promise基于任务队列的内容用代码的形式展示出来。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webWorker</title>
</head>
<body>
    <script>
      setTimeout(function() {
        console.log('setTimeout run');
      }, 0);

        new Promise(function(resolve, reject) {
        resolve();
        })
        .then(function(){  
        console.log('promise run');
    } );  
    </script>
</body>
</html>
这里的输出结果相信大家猜测的出来:
promise run
setTimeout run

这里的原因不在多说,因为首先js引擎要先执行主线程js的代码(会先执行完,因为一个js的加载,从上往下会执行完成之后,js引擎才会有时间去从事件循环队列中拿出代码块执行),至于setTimeout,间隔时间尽管为0ms,其实真正执行的时候是4ms。而且回掉函数是放在事件循环队列里的。那么Promise呢?好比我们现在执行的是js主线程,执行完成之后,js引擎不会立即去事件循环队列里取代码块执行,而是说当前主线程还有一点事情没有做完,那就是promise,在之前的文章中也谈过。二者事件的粒度不同,promsie是事件循环队列之上的任务队列。


参考:
https://blog.csdn.net/Steward2011/article/details/51319298
https://segmentfault.com/a/1190000012925872
http://www.imweb.io/topic/58e3bfa845e5c13468f567d5
http://www.ruanyifeng.com/blog/2014/10/event-loop.html#comment-text
https://segmentfault.com/a/1190000009313491

猜你喜欢

转载自blog.csdn.net/it_rod/article/details/79880745