1. 1. 进程(process)和线程(thread)
    1. 1.1. 什么是进程
  2. 2. 线程
  3. 3. JavaScript 单线程到底指什么
  4. 4. 浏览器是多线程的
  5. 5. 浏览器内核
  6. 6. JavaScript Runtime 和 EventLoop
  7. 7. 参考资料
Table of Contents ▼

解剖 JavaScript 单线程

人有九窍,窍窍缘心,九窍全通,遍体通泰,明心见性。

单线程是 JavaScript 的精髓之一,但是在初学以及大部分开发过程中,不懂单线程,貌似也不会遇到什么困难。这类原理性的东西,不懂也是能写代码的,但是懂了心里才有底,不至于写了上万行代码还是畏手畏脚。JavaScript 单线程涉及进程、线程的基本概念,浏览器架构、事件循环、异步机制…

进程(process)和线程(thread)

什么是进程

进程(process)是程序的一次执行,是系统进行资源分配和调度的一个独立单元。

理解进程的核心在于清楚进程所解决的问题:

操作系统是管理计算机硬件资源和软件资源的计算机软件,可以把操作系统想象为资源的统一抽象表示,资源包括内存、网络接口、文件系统等,可以被应用程序请求和访问,一旦操作系统为应用程序创建了这些资源的抽象表示,就必须管理它们的使用,例如一个操作系统可以允许资源共享和资源保护。 ——参考自《操作系统:精髓与设计原理》第3章

为有效地管理程序的执行,进程的概念被提出来,从而实现:

  • 资源对多个应用程序是可用的
  • 物理处理器在多个应用程序间切换以保证所有程序都在执行中
  • 处理器和 I/O 设备能得到充分利用

进程的出现是为了解决程序执行中,资源的共享和充分利用。一个进程的组成包括了:

  • 程序代码
  • 和代码像关联的数据集
  • PC中的值,用来指示下一条将运行的指令
  • 一组通用的寄存器的当前值,堆、栈;,
  • 一组系统资源

一个 CPU 任何时间内仅能运营一个进程,但即使你是单核计算机你会发现你依然能同时跑多个程序,这就涉及到进程控制,进程控制的主要任务是创建、撤销进程,以及实现进程的状态转换,所以我们会说进程是动态的,因创建而生,因调度而执行,因无资源而暂停,因撤销而消亡。

线程

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

进程和线程都是 CPU 时间段的描述,只不过进程的颗粒读更大。

以一个音乐播放器为例,我们启动程序:加载上下文环境(资源分配)➡ 执行。这就是我们所说的创建-执行一个进程。但是这个程序里边,实际包含了多个模块:(a) read 从音频文件读取数据;(b) decompress 对数据进行压缩;(c) play 播放音频数据。这三者同样也是加载上下文执行保存上下文调入下一个任务的上下文,但是在执行过程中,共享包含它们的进程的上下文。

学术性的对比:

  • 一个进程可以拥有多个线程,而一个线程同时只能被一个进程所拥有。
  • 进程是资源分配的基本单位,线程是处理机调度的基本单位,所有的线程共享其所属进程的所有资源与代码。
  • 线程执行过程之中很容易进行协作同步,而进程需要通过消息通信进行同步。
  • 线程的划分尺度更小,并发性更高。
  • 线程共享进程的数据的同时,有自己私有的的堆栈。
  • 线程不能单独执行,但是每一个线程都有程序的入口
  • 执行序列以及程序出口。它必须组成进程才能被执行。

JavaScript 单线程到底指什么

JavaScript 单线程说白一点,是指在任一时间内只能执行一条指令,这个线程被称为 主线程, HTML5 提供的 web worker 虽然可以让浏览器新开一个线程,用于执行复杂耗时的计算任务,与主线程独立执行,通过 postMessageonMessage 进行通信,但是它不能操作 DOM 节点。本质上来讲,JavaScript 仍是单线程的。

我们说多线程的出现是为了充分调度资源提升效率,然而你会发现不管 Node 还是浏览器都保持 JavaScript 单线程的特点。(个人认为 JavaScript 语言本身并不包含线程的机制,准确的表述是执行环境 Runtime 的机制,此处我如果理解有错误,希望大家不吝指证)。

采用单线程我认为最主要原因还是为了简单,对 DOM 节点的操作能保证不会存在歧义。

写到这儿,想到鸡生蛋和蛋生鸡的问题,JavaScript 和其宿主环境竟也是如此相似,JavaScript 最初随 Netscape Navigator 2.0 一起发布(当时还叫 LiveScript),伴随着浏览器的发展,JavaScript 也不断演进,v8 引擎的优化和 JavaScript 在事件处理上的成熟迎来了服务端的 Node。不禁让人感叹任何事物从低级到高级进化的耐心和伟大,人的可怕之处,可能正是这种不断吸取、发展的能力。(附上“Node与浏览器以及W3C组织、CommonJS组织、ECMAScript之间的关系”。 )
来源: 朴灵. 《深入浅出Node.js》

浏览器是多线程的

浏览器一般有以下7个常驻线程:

  • GUI 渲染线程

    GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。GUI 渲染线程与 JavaScript 引擎线程互斥,在 JavaScript引擎运行脚本期间,GUI渲染线程被挂起,GUI 更新被保存在一个队列中等待JavaScript 引擎线程空闲时立即被执行。正是这样,JavaScript 的运行会阻塞页面的加载。

    试想,如果二者不是互斥的,当页面正在渲染时,JavaScript 恰巧更新了正在渲染的 DOM 元素,那就存在冲突了。要么制定一个逻辑策略,要么采用目前这种更为单一和稳健的方式。(简单并不一定是为了偷懒,更多的情况是保证稳健,我们写代码也是一样,需要学会将复杂逻辑拆分成简单的逻辑)

  • JavaScript 引擎线程

    也称 JavaScript 内核,负责解析和执行 JavaScript 代码。例如我们熟悉的 V8 引擎。

  • 定时触发器线程

    浏览器定时计数器 (setTimeout, setInterval, setImmediate) 并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

    这里可以理解为何我们的计数器会有延迟:当到达指定时间(通过事件循环不断检测事件实现),产生一个 timeout 事件, 定时器就会将相应的回调函数插入任务队列尾部,真正的执行需要等到所有同步任务完成且任务队列之前的任务都被执行。

      (function testSetTimeout() {
        const label = 'setTimeout';
        console.time(label);
        setTimeout(() => {
            console.timeEnd(label);
        }, 10);
        for(let i = 0; i < 100000000; i++) {}
      })();
    

    输出结果:setTimeout: 70.490966796875ms

  • 异步 http 请求线程

    XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到任务队列中等待处理。

  • 事件触发线程

    当一个事件被触发时该线程会把事件添加到事件队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

此节内容参考自浏览器进程?线程?傻傻分不清楚!

浏览器内核

平时我们说的浏览器内核到底是指什么,JS 引擎还是视图渲染?

网上关于浏览器内核的解释大多不严谨,需要理解透彻可能需要去看一本转本将 Webkit 之类的书,这先埋个坑,等读完再进行梳理。下面是一个目前我最为赞同的说法:

浏览器有一个重要的模块,它主要的作用是将页面变成可视(听)化的图形、音频结果,这就是浏览器内核。不同浏览器有不同内核,常用的有Trident(IE)、Gecko(Firefox)、Blink(Chrome)、Webkit(Safari)

浏览器内核又可以分成两部分:渲染引擎和 JS 引擎。

最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指 渲染引擎

JavaScript Runtime 和 EventLoop

JavaScript 是通过 Event loop (事件循环)实现异步。主线程一运行就会创建 StackHeapQueueStack 中是主线程正在执行的任务,后进先出 LIFO。Queue 是等待处理的任务队列,先入先出FIFO),依次执行 stack 中的任务,遇到异步任务,就丢到任务队列中,当 stack 为空,就从 Queue 中读取任务执行。这是一个不断循环的过程,而且我们的任务队列实际是一个事件队列;对于一个事件驱动的系统,一切事物都被抽象为事件,代码的执行、鼠标的操作、网络请求状态的变化以及定时器到达指定事件等等都是事件,理解了这些你就懂了为什么叫 Event loop。

整个代码执行的机制和流程推荐观看 Philip Roberts: Help, I’m stuck in an event-loop.

最后简单说下 watcher 观察者,watcher 是事件驱动系统中很重要的概念,我们刚一直所有个事件队列 Queue,然而并不是说所有的事件都放在一个队列中。不同的事件放在不同的队列中,并设置相应的 watcher,对于没有的事件,自然就不会有watcher。事件循环实际上就是调用 watcher,检查每个 watcher 的事件队列,处理完相应事件后,再进入下一个 watcher。盗图一张:

参考资料

  1. 【朴灵评注】JavaScript 运行机制详解:再谈Event Loop
  2. 【转向Javascript系列】从setTimeout说事件循环模型
  3. chrome浏览器页面渲染工作原理浅析
  4. Concurrency model and Event Loop