Open findxc opened 2 years ago
每个电脑都会有一个 CPU ,全称 Central Processing Unit ,中文翻译为中央处理器。
现在的 CPU 一般是多核的。比如你的电脑是双核,这个双核是指硬件/物理层面 CPU 包含两个核心。
终端输入 sysctl hw.physicalcpu 来查看物理核心数,也是显示的 2 。
sysctl hw.physicalcpu
终端输入 sysctl hw.logicalcpu 来查看逻辑核心数,会发现显示的是 4 。因为英特尔的超线程技术,使得从软件层面来看,变成 4 个核心了。
sysctl hw.logicalcpu
在活动监视器中,双击底部的 CPU 负载区域,会展示 CPU 历史记录,也会发现是 4 个核心。
进程是指运行中的某个程序。
当我们启动一个软件后,进程列表中就会出现这个软件,并且可能出现多个这个软件相关的进程(说明这个软件是多个进程配合来工作的)。
我们可以在 Chrome 自己的任务管理器中看到这些进程具体是啥。点击 Chrome 右上角竖着的三个点,然后选择「更多工具」,再选择「任务管理器」。
Chrome 任务管理器的进程和活动监视器中的进程是一致的。(活动监视器中多了一个 chrome_crashpad_handler ,是操作系统启动的)
我们可以发现 Chrome 一共有以下进程:
为什么 Chrome 要启动这么多进程呢?
由于 Chrome 需要加载第三方插件、第三方的网页,进程之间天然的隔离性会让 Chrome 更加安全。进程是资源分配的最小单位。不同进程之间无法读写私有数据。
先看一下维基上的解释。
线程(thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。 同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈,自己的寄存器环境,自己的线程本地存储。
线程(thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈,自己的寄存器环境,自己的线程本地存储。
我们可以把进程理解为一个工厂,线程理解为工厂里的流水线。一个工厂里至少会有一条流水线,也允许有多条流水线,这些流水线是共用工厂的资源的。
一个逻辑核心就是一个机器人。操作系统会调度这个机器人为所有流水线服务。
当只有一个逻辑核心时,那它就会按一定规则去轮流服务流水线 m-1 、 m-2 、 n-1 、 n-2 等等。看起来这些流水线就像在“同时”运行,其实同一时刻只有一条流水线在运行,毕竟只有一个机器人。这种在一段时间内多个任务能一起推进的场景我们叫做并发执行。
如果只能等流水线 a 整体运行结束,再开始处理流水线 b ,这种我们就叫做不支持并发。
当你有多个逻辑核心时,那就相应有那么多个机器人,这些机器人可以同时工作,同一时刻多个任务一起推进的场景叫做并行执行。
干活的人多了,活干得会更快,所以并行是会增大并发的能力的。
就是字面意思, JS 的执行是单线程的。
想一下现在有两个逻辑核心,但是只有一条流水线,这时候两个逻辑核心会轮流为这一条流水线服务,但是两个逻辑核心其实都没有充分利用,因为核心 1 工作的时候核心 2 就在休息,核心 2 工作的时候核心 1 就在休息,毕竟只有一条流水线需要服务。
一般显示器的刷新率是 60Hz ,也就是一秒钟刷新 60 次,时间间隔约为 16.7ms 。
如果用户点了一个按钮,里面执行了 JS 代码,然后界面要更新,这个整体时间应尽量不超过 16.7ms ,否则显示器显示的就还是旧的界面。如果这个时间再长点,用户就会觉得界面卡住了。
比如下面这个例子, 在 while 里等待了 1s ,那么这 1s 内界面不会再响应用户其它交互了。
<input id="nameInput" /><br /> <div>hello, <span id="name"></span></div> <script> const nameInput = document.getElementById('nameInput') const name = document.getElementById('name') nameInput.addEventListener('input', e => { const d = new Date() // 会等待 1s,这 1s 内界面不再响应用户的交互 while (new Date() - d < 1000) {} name.innerText = e.target.value }) </script>
由于一般来说用户交互时需要执行的 JS 代码不会很多,所以 JS 的单线程其实也不影响。
但总还是有一些 web 应用会需要做大量计算,那咋整呢?
一个方案是使用 Web Workers , worker 会启动新的线程,所以不会阻塞用户界面。并且多个 worker 会启动多个线程,这样能充分利用多核。你把计算工作放 worker ,等 worker 计算完了可以通知你。
至于具体启动几个 worker ,可以参考 navigator.hardwareConcurrency 返回的逻辑核心数,详见 Navigator.hardwareConcurrency - Web APIs | MDN 。
navigator.hardwareConcurrency
我用下面的代码测试了一下 worker 对多核的利用。
index.html 内容如下:
index.html
<button id="logBtn">log</button> <script> // worker 工作时点击按钮是有响应的 const logBtn = document.getElementById('logBtn') logBtn.addEventListener('click', () => { console.log('in') }) const workerCount = 8 new Array(workerCount).fill('').forEach((x, index) => { // 实例化一个 worker var myWorker = new Worker('worker.js') // 给 worker 发信息,worker 接受信息后做相应处理 myWorker.postMessage(index + 1) // worker 处理完之后再发回信息 myWorker.onmessage = function (e) { console.log(`worker${index + 1} 发来信息:`, e.data) } }) </script>
worker.js 内容如下:
worker.js
onmessage = function (e) { console.log('worker 接到任务:', e.data) // 用循环来模拟大量计算 for (let i = 0; i < 900000; i++) { for (let j = 0; j < 900000; j++) {} } console.log('worker 完成任务') postMessage('end') }
当启动 2 个 worker 时:
当启动 4 个 worker 时:
当启动 8 个 worker 时:
启动 8 个 worker 时 CPU 占用并没有达到 800% ,依然是接近 400% 。其实从启动 2 个 worker 时就有端倪了,当时就只有 4 个核心在工作,另外 4 个没有。呃,为啥不是 8 个一起平分工作呢?猜测和超线程有关系,具体是啥原因还没弄清楚。
只能说从我的这次测试来看, worker 确实利用了多核,但又没完全利用 =.=
再补充一点,如果确实会去使用多个 worker ,那么多线程场景可能涉及到的锁(多个线程想修改同一资源时需要加锁)、死锁问题等就需要在编程时多多注意了。
WebAssembly 是一种新的编码方式,其它语言比如 C++ , Go, Rust 等可以编译为 .wasm 二进制文件在 JS 中使用。
呃,我原本想吹一波 WebAssembly 性能的,然后想说用 WebAssembly 的方式计算会快很多,然后我本地用 Go 测试了一波,发现也不一定,我就突然不知道写啥了 ...
呃,如果对 WebAssembly 性能感兴趣,可以去看这个: 一个白学家眼里的 WebAssembly - 知乎 。
如果是真的想在项目中使用,最好写个小 demo 先对比一下,万一用了 WebAssembly 发现并没有变快就人傻了 =.=
WebAssembly 本身是无法多线程的,可以通过结合 Web Workers 来实现多线程,详见 Using WebAssembly threads from C, C++ and Rust 。
就这样吧,可能得遇到相应的场景才能感受到 WebAssembly 的魅力。
如果是使用 Node.js 来写后端服务,由于 JS 是单线程的,如果某个请求中需要进行大量同步的计算,当收到这个请求后,会无法响应其它请求。
比如下面这个例子,启动服务后,先访问 /slow 然后马上访问 /xxx , /xxx 会被 /slow 中的同步代码阻塞,等到 /slow 返回后才会继续处理 /xxx 。
/slow
/xxx
const express = require('express') const app = express() const port = 4000 app.get('/slow', (req, res) => { for (let i = 0; i < 5000000000; i++) {} res.send('Hello World!') }) app.get('/xxx', (req, res) => { res.send('Hello World!') }) app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) })
需要说明,其实 node 服务本身是包含多个线程的。
但是这和 JS 是单线程的并不冲突。可以这样去理解,在 node 服务中,它的主逻辑是单线程的。
就像我们自己写代码时,主函数定义为 main 一样,然后 main 函数里面去调用这调用那的。当 node 服务接收到 /slow 请求后, main 函数就转去处理这个请求了,然后会一直处理里面的同步代码,这个过程中如果接收到别的请求,别的请求就只有排队。
更多细节见 不要阻塞你的事件循环(或是工作线程池) | Node.js 。
我用 Go 写了一个同样的服务,由于 Go 是多线程的,所以 /xxx 不会被 /slow 阻塞。
package main import ( "net/http" "github.com/gin-gonic/gin" ) func getXXX(c *gin.Context) { c.String(http.StatusOK, "Hello World!") } func getSlow(c *gin.Context) { for i := 0; i <= 5000000000; i++ { } c.String(http.StatusOK, "Hello World!") } func main() { router := gin.Default() router.GET("/xxx", getXXX) router.GET("/slow", getSlow) router.Run("localhost:3001") }
我本地测试的这个电脑物理核心是 4 逻辑核心是 8 ,然后试了下同时发 4 个 /slow ,请求时间和发 1 个是差不多的, 8 条流水线处理 4 个工作绰绰有余,这就是并行的好处了。
但是如果是同时发 8 个,时间就长很多了。和我预期不太一样,因为逻辑核心是 8 个,那么 8 个线程一起处理,即使时间会更长点,也没想到会长这么多。呃,不太清楚具体是为什么。
然后我把接口里面的 for 循环弄得比较久,来测一下 CPU 利用率。
同时发 4 个请求时大概就是 400% 。注意这里看起来像只利用了 4 个核心。不知道为啥不是 8 个平分工作。
同时发 8 个请求是能达到大概 800% 的,说明确实所有逻辑核心都有用上。
遗留问题:超线程技术使得 1 个物理核心可以表现为 2 个逻辑核心,但是在 web worker 多线程测试中,多出来的逻辑核心并没有利用上。同时在测试 Go 多线程时, 4 个物理核心 8 个逻辑核心同时发 4 个请求,看起来也只有 4 个逻辑核心在工作,如果是同时发大于 4 个请求比如 5 个,就确实是 8 个逻辑核心一起工作了。逻辑核心的具体调度细节还不太清楚。
写的真好👍
👍
物理核心数和逻辑核心数
每个电脑都会有一个 CPU ,全称 Central Processing Unit ,中文翻译为中央处理器。
现在的 CPU 一般是多核的。比如你的电脑是双核,这个双核是指硬件/物理层面 CPU 包含两个核心。
终端输入
sysctl hw.physicalcpu
来查看物理核心数,也是显示的 2 。终端输入
sysctl hw.logicalcpu
来查看逻辑核心数,会发现显示的是 4 。因为英特尔的超线程技术,使得从软件层面来看,变成 4 个核心了。在活动监视器中,双击底部的 CPU 负载区域,会展示 CPU 历史记录,也会发现是 4 个核心。
进程
进程是指运行中的某个程序。
当我们启动一个软件后,进程列表中就会出现这个软件,并且可能出现多个这个软件相关的进程(说明这个软件是多个进程配合来工作的)。
我们可以在 Chrome 自己的任务管理器中看到这些进程具体是啥。点击 Chrome 右上角竖着的三个点,然后选择「更多工具」,再选择「任务管理器」。
Chrome 任务管理器的进程和活动监视器中的进程是一致的。(活动监视器中多了一个 chrome_crashpad_handler ,是操作系统启动的)
我们可以发现 Chrome 一共有以下进程:
为什么 Chrome 要启动这么多进程呢?
由于 Chrome 需要加载第三方插件、第三方的网页,进程之间天然的隔离性会让 Chrome 更加安全。进程是资源分配的最小单位。不同进程之间无法读写私有数据。
线程
先看一下维基上的解释。
我们可以把进程理解为一个工厂,线程理解为工厂里的流水线。一个工厂里至少会有一条流水线,也允许有多条流水线,这些流水线是共用工厂的资源的。
一个逻辑核心就是一个机器人。操作系统会调度这个机器人为所有流水线服务。
当只有一个逻辑核心时,那它就会按一定规则去轮流服务流水线 m-1 、 m-2 、 n-1 、 n-2 等等。看起来这些流水线就像在“同时”运行,其实同一时刻只有一条流水线在运行,毕竟只有一个机器人。这种在一段时间内多个任务能一起推进的场景我们叫做并发执行。
如果只能等流水线 a 整体运行结束,再开始处理流水线 b ,这种我们就叫做不支持并发。
当你有多个逻辑核心时,那就相应有那么多个机器人,这些机器人可以同时工作,同一时刻多个任务一起推进的场景叫做并行执行。
干活的人多了,活干得会更快,所以并行是会增大并发的能力的。
JS 是单线程的是指什么
就是字面意思, JS 的执行是单线程的。
想一下现在有两个逻辑核心,但是只有一条流水线,这时候两个逻辑核心会轮流为这一条流水线服务,但是两个逻辑核心其实都没有充分利用,因为核心 1 工作的时候核心 2 就在休息,核心 2 工作的时候核心 1 就在休息,毕竟只有一条流水线需要服务。
浏览器环境
一般显示器的刷新率是 60Hz ,也就是一秒钟刷新 60 次,时间间隔约为 16.7ms 。
如果用户点了一个按钮,里面执行了 JS 代码,然后界面要更新,这个整体时间应尽量不超过 16.7ms ,否则显示器显示的就还是旧的界面。如果这个时间再长点,用户就会觉得界面卡住了。
比如下面这个例子, 在 while 里等待了 1s ,那么这 1s 内界面不会再响应用户其它交互了。
由于一般来说用户交互时需要执行的 JS 代码不会很多,所以 JS 的单线程其实也不影响。
但总还是有一些 web 应用会需要做大量计算,那咋整呢?
Web Workers
一个方案是使用 Web Workers , worker 会启动新的线程,所以不会阻塞用户界面。并且多个 worker 会启动多个线程,这样能充分利用多核。你把计算工作放 worker ,等 worker 计算完了可以通知你。
至于具体启动几个 worker ,可以参考
navigator.hardwareConcurrency
返回的逻辑核心数,详见 Navigator.hardwareConcurrency - Web APIs | MDN 。我用下面的代码测试了一下 worker 对多核的利用。
index.html
内容如下:worker.js
内容如下:当启动 2 个 worker 时:
当启动 4 个 worker 时:
当启动 8 个 worker 时:
启动 8 个 worker 时 CPU 占用并没有达到 800% ,依然是接近 400% 。其实从启动 2 个 worker 时就有端倪了,当时就只有 4 个核心在工作,另外 4 个没有。呃,为啥不是 8 个一起平分工作呢?猜测和超线程有关系,具体是啥原因还没弄清楚。
只能说从我的这次测试来看, worker 确实利用了多核,但又没完全利用 =.=
再补充一点,如果确实会去使用多个 worker ,那么多线程场景可能涉及到的锁(多个线程想修改同一资源时需要加锁)、死锁问题等就需要在编程时多多注意了。
WebAssembly
WebAssembly 是一种新的编码方式,其它语言比如 C++ , Go, Rust 等可以编译为 .wasm 二进制文件在 JS 中使用。
呃,我原本想吹一波 WebAssembly 性能的,然后想说用 WebAssembly 的方式计算会快很多,然后我本地用 Go 测试了一波,发现也不一定,我就突然不知道写啥了 ...
呃,如果对 WebAssembly 性能感兴趣,可以去看这个: 一个白学家眼里的 WebAssembly - 知乎 。
如果是真的想在项目中使用,最好写个小 demo 先对比一下,万一用了 WebAssembly 发现并没有变快就人傻了 =.=
WebAssembly 本身是无法多线程的,可以通过结合 Web Workers 来实现多线程,详见 Using WebAssembly threads from C, C++ and Rust 。
就这样吧,可能得遇到相应的场景才能感受到 WebAssembly 的魅力。
Node.js 中
如果是使用 Node.js 来写后端服务,由于 JS 是单线程的,如果某个请求中需要进行大量同步的计算,当收到这个请求后,会无法响应其它请求。
比如下面这个例子,启动服务后,先访问
/slow
然后马上访问/xxx
,/xxx
会被/slow
中的同步代码阻塞,等到/slow
返回后才会继续处理/xxx
。需要说明,其实 node 服务本身是包含多个线程的。
但是这和 JS 是单线程的并不冲突。可以这样去理解,在 node 服务中,它的主逻辑是单线程的。
就像我们自己写代码时,主函数定义为 main 一样,然后 main 函数里面去调用这调用那的。当 node 服务接收到
/slow
请求后, main 函数就转去处理这个请求了,然后会一直处理里面的同步代码,这个过程中如果接收到别的请求,别的请求就只有排队。更多细节见 不要阻塞你的事件循环(或是工作线程池) | Node.js 。
感受一下 Go 的多线程
我用 Go 写了一个同样的服务,由于 Go 是多线程的,所以
/xxx
不会被/slow
阻塞。我本地测试的这个电脑物理核心是 4 逻辑核心是 8 ,然后试了下同时发 4 个
/slow
,请求时间和发 1 个是差不多的, 8 条流水线处理 4 个工作绰绰有余,这就是并行的好处了。但是如果是同时发 8 个,时间就长很多了。和我预期不太一样,因为逻辑核心是 8 个,那么 8 个线程一起处理,即使时间会更长点,也没想到会长这么多。呃,不太清楚具体是为什么。
然后我把接口里面的 for 循环弄得比较久,来测一下 CPU 利用率。
同时发 4 个请求时大概就是 400% 。注意这里看起来像只利用了 4 个核心。不知道为啥不是 8 个平分工作。
同时发 8 个请求是能达到大概 800% 的,说明确实所有逻辑核心都有用上。
总结
遗留问题:超线程技术使得 1 个物理核心可以表现为 2 个逻辑核心,但是在 web worker 多线程测试中,多出来的逻辑核心并没有利用上。同时在测试 Go 多线程时, 4 个物理核心 8 个逻辑核心同时发 4 个请求,看起来也只有 4 个逻辑核心在工作,如果是同时发大于 4 个请求比如 5 个,就确实是 8 个逻辑核心一起工作了。逻辑核心的具体调度细节还不太清楚。