先撸清楚:并发/并行、单线程/多线程、同步/异步

前言

在编码的过程中经常会遇到并发/并行、同步/异步、单线程/多线程等术语,在分析JS setTimeout/Promise之前先把这些概念厘清。

通过本篇文章,你将了解:

  1. 并发/并行的概念及区别
  2. 同步/异步的概念及区别
  3. 单线程/多线程的概念及区别
  4. 主线程和子线程的概念及区别
  5. JS执行引擎是单线程吗?

1. 并发/并行的概念及区别

1.1 并发

1.1.1 无并发时代

在远古时代(其实就是几十年前),计算机硬件基础比较落后,刚开始只有一个CPU核心。

计算机操作员需要把待完成的事项编排为一个个的任务,依次交给CPU去执行。

可以看出,任务是依次执行的,上一个任务未执行完(未释放CPU),下个任务是不能执行(获取CPU)的。

1.1.2 单核并发

假设任务3执行时间需要4s,任务4执行时间需要6s,执行完任务3和任务4总共需要10s的时间。

任务3执行的过程中需要等待读取磁盘(磁盘读取需要2s),此时任务3是不怎么占用CPU的,它完全可以先释放CPU专注等待磁盘读取,而此刻任务4就可以有机会得到CPU的执行,任务3磁盘读取返回后继续申请CPU执行未完成的项。

此时任务3、4执行完成总共需要2+6=8s时间。

任务间的执行流程大致如下:

在一段时间内,任务3和任务4是共存的(任务3执行的过程中,任务4可以插进来),同样的,任务3、任务4执行的过程中也可以插入其它任务5、6...n,此种设计方式可以在一定程度上提高CPU的利用率。

因此,我们简单归纳一下:

  1. 若一个系统支持同时存在多个任务(任务交替运行),则称该系统支持并发
  2. 在单核心的CPU场景下,同一时间只会有一个任务得到执行,CPU将时间分给多个任务执行,CPU执行任务的速度非常快,给人的感觉是多个任务是同时处理完毕的,实际上是分享了CPU的时间片

1.1.3 多核并发

随着硬件的发展,CPU从单核变成多核,每个核心都能独立处理各自的任务,此种场景下,并发的功能依然存在,如下图:

很容易理解,单核心能够并发,多核心更不在话下了。

1.2 并行

从多核并发的图里,我们知道CPU1的任务队列和CPU2的任务队列可以同时进行。

如图,同一时刻,任务3在CPU1里执行,任务8在CPU2里执行,通过增加CPU核心数,处理任务的速度更快了。

因此,我们简单归纳如下:

  1. 若一个系统支持同时运行多个任务,则称该系统支持并行。
  2. 并行从字面上意思就是两人在一起肩并肩并排行走。

2. 同步/异步的概念及区别

2.1 量血压的例子

先看一种场景: 小明去医院体检,他决定量血压,此时量血压的人都排成一队了,他排在了队伍的末尾。 如果把医生当做线程,队伍里的每个人都是一个代码行(方法/函数),每个人都按序等待量血压(代码行等待线程执行),前一个人量好了再到下一个人。

我们称这些代码行是按时间先后顺序同步地执行。

再看另一种场景:

小明去医院体检,他起得比较晚,急匆匆跑到医院的时候,他决定先量血压。此时量血压的人都排成一队了,他排在了队伍的末尾。

轮到他测量血压的时候,医生发现他心跳比较快,于是建议他先休息冷静一会儿再来测,于是他坐到了一旁冷静平复心情。

当他休息的差不多的时候,他又去排队,最终他量好了血压。

小明确实是排了队,医生那会儿没给他量血压,告诉他满足条件后再来,当满足条件后再重新排队。

我们称此时小明量血压的过程是异步地执行。

2.2 转为TS代码

同步:

ts 复制代码
//ts 代码
function testBPSync(name:String) {
    console.log(name, 'test bp')
}

testBPSync('体检者1')
testBPSync('体检者2')
testBPSync('小明')
testBPSync('体检者3')

可以看到打印是依次执行的。

异步:

ts 复制代码
//ts 代码
function testBPSync(name:String) {
    console.log(name, 'test bp')
}

function testBPAsync(name:String) {
    setTimeout(()=>{
        console.log(name, 'test bp')
    }, 2000)
}

testBPSync('体检者1')
testBPSync('体检者2')
testBPAsync('小明')
testBPSync('体检者3')

可以看到,虽然小明排在体检者3之前,但是他并没有在体检者3前被量血压,它是过一会才被量,是异步执行的。

2.3 异步==另一个线程?

小明量血压是异步的,但他最终还是同一个医生给他量的,我们把医生比作执行在CPU上的线程,此时虽然是异步的,但并不等于在另一个线程里执行。

上图使用的setTimeout()最终回调还是在主线程里执行。

小明平复了心情后,另一个体检医生让他过去体检,此时他就不在原来的医生那体检了,此种场景对应在另一个线程里执行:

kotlin 复制代码
//Kotlin 代码
fun testBPsync(name:String) {
    println("${name} test bp")
}

fun testBPAsync(name:String) {
    thread {
        //在另一个线程里执行
        println("${name} test bp")
    }
}

fun main() {
    testBPSync('体检者1')
    testBPSync('体检者2')
    testBPAsync('小明')
    testBPSync('体检者3')
}

总结来说:

  1. 同步:表示在一个线程里按照时间的先后顺序执行代码
  2. 异步:同步反义词,异步执行的代码可能在同一个线程执行,也可能在另一个线程执行,取决于具体的实现逻辑,异步通常表现为回调的形式。

3. 单线程/多线程的概念及区别

在最开始我们有提到过任务的并发执行,先执行任务的一部分,再执行其余部分,这种"部分"在CPU上是如何调度的呢?

答案是线程。

先了解一个老生常谈的知识:
进程是资源分配的基本单位,线程是CPU任务调度和执行的基本单位

CPU上执行的是线程,不同进程(任务)的线程、同一进程里的线程交替在CPU上执行,达到了并发的效果。

多线程能够充分利用CPU的能力,比如某个进程里开了两个线程分别去获取网络数据和进行磁盘I/O读写,这俩线程可以同时执行(并行),也可以并发,提升了效率。

但多线程也带来了挑战:

  1. 线程之间的互斥,多线程修改同一内存变量的一致性,通常加锁解决
  2. 线程之间的同步,某个线程修改同一内存变量后通知其它线程变量已经修改,通常等待-通知机制解决

因此多线程编程永恒的话题:线程间的同步和互斥

单线程永远不会担心同步和互斥,单线程通常伴随着事件循环。

有关线程和锁相关请移步系列文章:线程和锁系列

4. 主线程和子线程的概念及区别

承担着进程入口的线程,通常称为主线程。

一般来说我们编写Hello World会有个Main入口,执行此处代码的就是主线程。

实际上主线程和子线程本质上是没有区别的,都是通过构造线程对象并执行代码。

在Android、iOS设计里,把一个线程选作主线程,那么就会在更新UI的时候做一些校验,判断是否是主线程(UI线程),如果不是那么不允许操作界面元素。并且主线程主要做一些UI布局计算相关的操作(不是渲染线程),同时响应其它线程的事件,比如触摸事件等,因此它一定要比较流畅,不能被同步阻塞,轻则界面卡顿,重则App Crash,因此阻塞的事情交给其它线程(子线程)去做。

5. JS执行引擎是单线程吗?

答案是的。

至于为什么选择单线程,前面也有提到一些。

  1. 单线程不用考虑多线程里的同步互斥问题,无需上锁,无需来回切换线程,简单纯粹
  2. 主线程通常用来更新UI元素,保证唯一的修改源可以减少很多莫名其妙的问题

了解了以上概念,将助力我们进入下篇的Promise/SetTimeout的世界。

相关推荐
Nan_Shu_6141 天前
学习: Threejs (2)
前端·javascript·学习
G_G#1 天前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界1 天前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 天前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 天前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 天前
React面向组件编程
开发语言·前端·javascript
学历真的很重要1 天前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
持续升级打怪中1 天前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 天前
GDAL 实现矢量合并
前端
hxjhnct1 天前
React useContext的缺陷
前端·react.js·前端框架