如何理解JS的多线程

基础知识

首先我们回顾下操作系统的知识,即:

  • 为什么会出现进程和线程的概念,他们的出现是为了解决什么问题
  • 进程和线程的概念与定义
  • 线程的调度
  • 线程的同步与互斥
  • 死锁问题

进程与线程的由来

要解释这个问题,要从操作系统的分类和发展历程来讲,我们平时接触比较多的计算机都是基于时间片进行调度的分时系统,但其实操作系统还有其他很多种类型,分时操作系统并不是一开始就有的,同样进程和线程的概念也不是一开始就有的。
操作系统的大致发展历程如下,我们简单了解下就好:

  • 手工操作阶段(此阶段无操作系统)
  • 批处理操作系统
    • 单道批处理系统
    • 多道批处理系统
  • 分时操作系统
  • 实时操作系统
    • 硬实时操作系统
    • 软实时操作系统
  • 网络操作系统和分布式操作系统

上面不同的操作系统的资源调度算法都不尽相同,比如人硬实时系统就不是基于时间片调度的,他是基于优先级的。我们主要关注单道操作系统到多道操作系统的变换。

对于单道操作系统来讲,没必要出现进程和线程的概念,它就是按次序执行作业,不存在一段时间内多个作业并行的情况。

当然我们我们也能看出单道操作系统的问题,就是各种计算机资源利用效率很低,比如一个作业在等待I/O的时候,CPU是空闲的。

为了提高各种资源的利用效率,提出了多道批处理系统,这也是操作系统复杂度的一个很重要的来源:

  • 因为要并发,就存在程序的切换,切换之前要从备选项中选择一个进行运行,这个选择的过程就是调度,有多种不同的调度算法,比如FCFS,SJF,时间片等等。
  • 因为并发带来了异步性,各个程序的执行顺序是不确定的,所有可能会对临界资源产生争抢,为了避免这种争抢,我们提出了互斥锁;另一种情况是我们需要某些程序按一定的顺序执行,所以我们提出了同步的概念。
  • 因为互斥锁的提出,导致可能出现死锁的问题,比如A程序申请了P资源,等待Q资源,而B程序申请了Q资源但是在等待P资源,A和B之间就形成了循环依赖且二者都不愿意放弃到手资源,那么A和B程序之间就形成了死锁。

为了描述上面的调度,互斥,同步,死锁问题,我们需要引入一个新的概念描述程序执行过程中状态,并且在程序切换时保存当时执行的上下文,这个概念就是进程。

然后后面我们发现相比于其他资源,CPU资源的调度非常频繁,所以我们单独为CPU调度抽取出一个概念,叫做线程。

进程与线程的概念

到现在,我们有这么几个概念:

  • 作业:可以理解为我们硬盘上的一个个还没执行的程序
  • 进程:出了CPU之外的资源调度基本单位。当我们选择执行一个程序时,就是为该作业申请内存等资源,然后进程默认会启动一个线程,用于申请CPU资源
  • 线程:CPU调度的基本单位。同一个进程的线程共享同一套资源,比如同一个内存空间,也就是说同一个进程的线程之间的数据是可以直接修改的

线程又分为内核级线程(KLT)和用户级线程(ULT),内核级线程意思就是说,操作系统中真的存在多个线程,线程的切换需要用到操作系统的原语进行上下文的切换,寄存器等的保存与装载等等,我们确实切换了线程;而用户级线程的意思则是说,操作系统本身完全没有感知到线程切换,我们是在线程内部自己模拟了寄存器等的切换,二者各有各的好处,后者有时候也被称为协程,它的好处就是切换消耗少,不需要用到操作系统的中断处理来进行上下文切换,缺点就是并不能发挥多核处理器优势,因为不管你有多少个线程,在操作系统看起来都是一个。

线程的调度算法

  • FCFS,先来先服务算法,属于不可剥夺算法,不适用于分时系统和实时系统。表面上对所有作业是公平的,但是如果一个长作业先到了系统,就会是后面的短作业等待很长时间
  • SJF,短作业优先算法。对长作业不利,而且可能会导致长作业“饥饿”
  • 优先级调度算法,按照高优先级作业到达是否中断当前作业分为抢占式和非抢占式,按照优先级是否可以在运行时改变分为静态优先级和动态优先级
  • 高响应比优先算法。响应比=(等待时间+要求服务时间)/ 要求服务时间
  • 时间片轮转调度算法。
  • 多级队列调度算法。针对不同的CPU设置多个队列,每个队列采取不同的调度算法
  • 多级反馈队列调度算法

同步与互斥

同步是直接制约关系,比如A就是要比B先执行,而互斥是间接制约关系,比如A和B都要使用唯一的打印机资源,A占用了,B就只能阻塞。
临界区实现互斥的基本方法

软件方法

  1. 单标志法
  2. 双标志法
  3. 双标志后检查法
  4. Peterson’s Algorithm

硬件实现方法

  1. 中断屏蔽算法:关中断,因为CPU只有在中断时可以进行进程切换,关中断可以直接阻止一切的进程切换
  2. 硬件指令方法:TestAndSet

互斥锁(mutex lock)

一个进程在进入临界区时获得锁,在退出临界区时释放锁

信号量

信号量机制是一种功能较强的机制,可以用来解决互斥与同步问题,只能被两个标准的原语wait和signal访问,也可记录为P操作和V操作,分为两种:

  • 整型信号量
1
2
3
4
5
6
7
8
wait(S) {
while(S <=0);
S = S - 1;
}

signal(S) {
S = S + 1;
}
  • 记录型信号量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wait(semaphore S) {
S.value--;
if (S.value < 0) {
add this process to S.L;
block(S.L)
}
}

signal(semaphore S) {
S.value++;
if (S.value <= 0) {
remove a process P from S.L;
wakeup(P)
}
}

四核八线程是什么意思

我们总是会提到一个计算机是四核八线程,这是个什么概念呢?

我们刚才说到的,无论是单道还是多道操作系统,它的并发都是基于只有一个CPU核心的基础上的,也就是说它在同一时刻,只能有一个程序在执行,只有一个程序计数器。

我们要实现真正意义上的多个程序在同一时刻执行,方法就是多几个CPU核心,也就是这里说的四核。

理论上四个核心就只能有四个线程并行,之所以出现四核八线程,其实还是一个核上模拟了两个核心,让上层操作系统以为它有八个核心而已,实际上还是4个核心。这是计算机虚拟技术的一种实现。

理解JS的多线程

Web Worker

理解了多线程的概念后,我们可以来说JS的多线程Web Workers 了。

HTML5引入了Web Workers,让JS支持线程。

JS的多线程是OS级别的 ,也就是说JS的多线程是真的多线程,也就是上面说的内核级线程 。

JS没有线程同步的概念

JS的多线程无法操作DOM,没有window对象,每个线程的数据都 是独立的。主线程传给子线程的数据是通过拷贝复制,同样的子线程 给主线程的数据也是通过拷贝复制,而不是共享同一块内存区域。

从这一点来看,JS的多线程并不属于同一个进程,或者说是内部有什么隔离机制

所以Web Workers基本上出不了什么错。

在主逻辑里面fun1和fun2的调用是连在一起的,它是一个执行单 元,要么还没执行,要么得一口气执行完。执行完之后,再执行 setTimout append到后面的。然后由于已经超过了setInterval定的20ms, 所以又马上执行setInterval的函数。这里可以看出setTimeout的计时是从 逻辑单元执行完了才开始计时,而setInterval是执行到这一行的时候就 开始计时了。

单线程里面的特例如异步回调,异步回调是Chrome自己的IO线程 处理的,每发一个请求必须要有一个线程跟着,Chrome限制了同一个 域最多同时只能发6个请求

Chrome的多线程模型

我们从click事件来看一下Chrome的线程模式是怎么样的

每开一个tab,Chrome就会创建一个进程,进程是线程的容器 ,同一个域名的Tab是同一个进程

首先用户单击了鼠标,浏览器的UI线程收到之后,把这个消息数 据封装成一个鼠标事件发送给IO线程,IO线程再分配给具体页面的渲 染线程。其中IO线程和UI线程是浏览器的线程,而渲染线程是每个页 面自己的线程。

如果在执行一段很耗时的JS代码,渲染线程里的render线程将会被 堵塞,而main线程继续接收IO线程发过来的消息并排队,等待render线 程处理。也就是说当页面卡住的时候,不断地单击鼠标,等页面空闲 了,单击的事件会再继续触发。