人人FED

专注于前端技术

JS与多线程

多线程对前端开发人员来说既熟悉又陌生,一方面前端几乎很少写多线程,另一方面多线程又经常会碰到,如你买个电脑它会标明它是四核八线程、四核四线程之类的,它是多核多线程的。什么叫做多核呢?四核四线程和八线程又有什么区别?

先来看一下自己电脑的CPU配置。

1. 查看CPU配置

(1)自己电脑的配置

如在Mac上可以通过查看系统偏好的方式,如下图所示,有一个CPU,并且是四核的:

那怎么看它是四线程还八线程呢?可以运行以下命令:

> sysctl hw.logicalcpu

    hw.logicalcpu: 8

可以看到逻辑核数为8,所以它是八线程的,再来看一下CPU的型号:

> sysctl -n machdep.cpu.brand_string

Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz

(2)服务器配置

如在Linux服务器上面,可以运行以下命令:

> cat /proc/cpuinfo| grep “physical id”| sort| uniq

physical id: 0

physical id: 1

可以得知它有两个物理CPU,也就是说这台服务器插了两个CPU。然后再看下每个CPU的物理核数:

> cat /proc/cpuinfo| grep “cpu cores”| uniq

cpu cores: 6

物理核为数6,总的逻辑核数为12:

> cat /proc/cpuinfo | grep “processor” | wc –l

12

也就是说每个CPU为六核六线程,总共有两个CPU,所以是12核12线程。我们还可以看下它的内存:

> cat /proc/meminfo

MemTotal:  16322520 kB MemFree: 1065508kB

总内存为16G,当前可用内存为1G,并且这个数据是实时,同理上面CPU的数据也是实时,虽然它是cat了一个文件。

2. 什么是四核四线程?

一个CPU有几个核它就可以跑多少个线程,四核四线程就说明这个CPU同一时间最多能够运行4个线程,四核八线程是使用了超线程技术,使得单个核像有两个核一样,速度比四核四线程有所提升。但是当你看你电脑的任务管理器,你会发现实际上会运行几千个线程,如下图当前OS运行了1917个线程,376个进程(进程是线程的容器,每个进程至少有一个主线程)。

由于四核四线程的CPU同一时间只能运行四个线程,所以有些线程会处于运行状态,而大部份的线程会处理于中断、堵塞、睡眠的状态,所以这里就涉及到操作系统的任务调度。

3. OS的任务调度

以Linux为例,先来看下Linux操作系统进程的分类:

(1)Linux进程分类

可分为三种:

a) 交互式进程

需要有大量的交互,如vi编辑器,大部分时间处于休眠状态,但是要求响应要快

b)批处理进程

运行在后台,如编译程序,需要占用大量的系统资源,但是可以慢点

c) 实时进程

需要立即响应并执行,如视频播放器软件,它的优先级最高

根据线程的优先级进行任务调度。

(2)任务调度方式

常用的有以下两种:

a)SCHED_FIFO

实时进程或者说它的实时线程的优先级最高,先来先运行,直到执行完了,才执行下一个实时进程

b)SCHED_RR

对于普通线程使用时间片轮询,每个线程分配一个时间片,当前线程用完这个时间片还没执行完的,就排到当前优先级一样的线程队列的队尾

虽然操作系统运行了这么多个线程,但是它的CPU使用率还是比较低的,如下图所示:

理解了多线程的概念后,我们可以来说JS的多线程web worker了

4. Webworker

HTML5引入了webworker,让JS支持线程。我们用webworker做一个斐波那契计算,首先写一个fibonacci函数,如下所示:

把这个函数写到worker.js里面,webworker有一个全局的函数叫onmessage,在这个回调里面监听接收主线程的数据:

计算完结果后,再把结果postMessage给主线程。

主线程先启动一个worker子线程,把数据发给它,同时监听onmessage,取到子线程给它传递的计算结果,如下main.js:

然后在页面引入这个main.js的script就行了:

运行结果如下图所示:

最后主线程打印出子线程计算的结果,可以看到JS如果发生了整型溢出会自动转换成双精度浮点数。

需要注意的是,JS的多线程是系统级别的。

5. JS的多线程是OS级别的

也就是说JS的多线程是真的多线程,如下,一口气创建500个线程:

然后观察操作系统的线程数变化,如下图所示:

你会发现操作系统一下子多了500个线程,也就是说JS的多线程是调的系统API创建的多线程。还有一种多线程是用户级别的多线程,这种多线程并不会产生实际的系统线程,它是应用程序自已控制任务切换,如Ruby的Fiber。

我们一下子开了500个线程,有点任性,如果一下子开5000个呢?首先一个进程最多能有多少个线程,一般操作系统是有限制的,再者你开太多,Chrome会把你这个页面杀了,如下,跑着跑着页面就挂了:

假设WebWorker可以操作DOM。

6. 线程同步

由于web worker是不可以操作DOM的,那如果能够操作DOM会发生什么事情?必须要限制同一个DOM结点只能有一个线程操作,不允许同个变量或者同一块内存被同时写入。

线程同步主要是靠锁来实现,锁可以分成三种。

(1)互斥锁

如下代码所示,假设有一个互斥锁的类,叫Mutex:

在改变某个DOM元素的高度时,先把这块代码的执行给锁住了,只有执行完了才能释放这把锁,其它线程运行到这里的时候也要去申请那把锁,但是由于这把锁没有被释放,所以它就堵塞在那里了,只有等到锁被释放了,它才能拿到这把锁再继续加锁。

互斥锁使用太多会导致性能下降,因为线程堵塞在哪里它要不断地查那个锁能不能用了,所以要占用CPU。

第二种锁叫读写锁。

(2)读写锁

如下,假设读写锁用ReadWriteRock表示:

在第二个函数getHeight获取高度的时候可以给它加一个读锁,这样其它线程如果想读是可以同时读的,但是不允许有一个线程进行写入,如调第一个函数的线程将会堵塞。同理只要有一个线程在写,另外的线程就不能读。

第三种锁叫条件变量。

(3)条件变量

条件变量是为了解决生产者和消费者的问题,由用互斥锁和读写锁会导致线程一直堵塞在那里占用CPU,而使用信号通知的方式可以先让堵塞的线程进入睡眠方式,等生产者生产出东西后通知消费者,唤醒它进行消费。

不同编程语言锁的实现不一样,但是总体上可以分为上面那三种。

 

7. 多线程操作DOM的问题

上面只是做到了不允许多个线程同时执行:

   $(“#my-div”)[0].style.height = height + “px”;

但是另外的函数也可以用选择器去获取那个DOM结点然后去设置它的高度,所以无法避免多线程同时写的问题。

如果真的发生了同时写的情况,那么不仅仅是页面崩了,而是整个浏览器都崩了。所以这个就比较危险了,假设那边我有一个页面打了1万个字还没保存,但是因为不小了打开了你的页面,导致整个浏览器挂了还没保存就有点悲摧了。这里有个问题,为什么浏览器会挂呢?因为操作系统检测到异常,它要把你杀了,如果不把你杀了,它自己就要脆了,它一脆你也脆了。为什么以前的windows系统会蓝屏,因为它没有检测到应用程序的异常,任由应用程序胡作非为,结果为了保护硬件它必须得挂,它一挂应用程序也得跟着挂。

所以JS不给程序员犯错的机会。

8. JS没有线程同步的概念

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

所以会web worker基本上出不了什么错。

然后我们再来看一下JS的单线程模型。

9. JS的单线程模型

如下图所示,应该可以很清楚地表示:

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

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

再来看下Chrome的多线程模型。

10. Chrome的多线程模型

每开一个tab,Chrome就会创建一个进程,进程是线程的容器,如下Chrome的任务管理器:

我们从click事件来看一下Chrome的线程模式是怎么样的,如下图表所示:

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

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

这个是从浏览器线程到页面线程的过程,反过来从页面线程到浏览器线程的例子如下图表所示(在代码里面改变光标形状):

看完Chrome的,再来看Node.js的线程模型。

11. Node.js的单线程模型

我们知道Node.js也是单线程的,但是单线程如何处理高并发呢?传统的web服务是多线程的,它们通常是先初始化一个线程池,来一个连接就从线程池里取出一个空闲的线程处理,而用Node.js如果有一个连接处理时间过长,那么其它请求将会被堵塞。

但是由于数据库连接本来是就是多线程,调用操作系统的IO文件读取也是多线程,所以Node.js的异步是借助于数据库和IO多线程。

这样的好处是不需要启动新的线程,不需要开辟新线程的空间,不需要进行线程的上下文切换,所以当服务应用不是计算类型的,使用Node.js可能反而会更快,同时由于是单线程的所以写代码更容易。缺点是不能够提供很耗CPU的服务,如图形渲染,复杂的算法计算等。

可以在同一台多核的服务器上开多几个Node服务弥补单线程的缺陷,然后用nginx均匀地分发请求。

我们出来转了一圈之后,再回到webworker

12. 内联webworker

当new很多个worker.js的时候,浏览器会从缓存里面取worker.js:

有时候并不想多管理一个JS文件,想要把它搞内联的。这个时候可以用HTML5的新的数据类型Blob,如下代码所示:

Blob还经常被用于分割大文件。

 

最后,JS的设计是单线程,后来HTML5又引入webworker,它只能用于计算,因为它不能改DOM,无法造成视觉上的效果。然后它不能共享内存,没有线程同步的概念,所以可以认为JS还是单线程,可以把webworker当成另外的一种回调机制。

本文并不是要介绍webworker怎么用,重点还是介绍一下多线程的一些概念,例如什么叫多线程,它和CPU又有什么关系,什么叫线程同步,为什么要进行线程同步,还讨论了JS/Chrome/Node的线程模型,相信看了本文对多线程应该会有更好的理解。

目录: 基础技术

10 回复

  1. 提醒一下,线程、进程、协程是不一样的概念
    文章里面提到的“线程”都应该是进程,不知道作者是不是记错了
    到目前为止js都还没有多线程,所以并不会遇到这种锁的情况

  2. 你知道你的网站返回顶部的按钮有bug吗?是定时器没取消还是什么原因,点击后就不能往下滚动了,往下滚动一段距离就会自动滚回顶部。

  3. “由用互斥锁和读写锁会导致线程一直堵塞在那里占用CPU”
    使用互斥锁和读写锁,另一个线程进来发现不可用,是被挂起吧,不占用cpu

  4. 感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/uytvvb 欢迎点赞支持!
    欢迎订阅《前端疯魔院》https://toutiao.io/subject/104480

Trackbacks

  1. WebAssembly与代码的编译 – 人人网FED博客
  2. 我接触过的前端数据结构与算法 – 人人网FED博客
  3. 使用Service Worker发送Push推送 – 人人网FED博客

杨高飞进行回复 取消回复

邮箱地址不会被公开。 必填项已用*标注