前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >准确理解阻塞、非阻塞、同步、异步

准确理解阻塞、非阻塞、同步、异步

作者头像
用户6901603
发布2024-03-11 21:08:41
1050
发布2024-03-11 21:08:41
举报
文章被收录于专栏:不知非攻

在计算机领域,这几个概念其实很基础。

但是你还真别小看这几个基础概念,由于网上的说法五花八门,众说纷纭,导致了不同的人对这几个概念的理解差异还有点大,错误理解和云里雾里的人大有人在。有的人还会把阻塞/非阻塞同步/异步混合起来理解,认为阻塞=同步、非阻塞=异步。

那么到底是不是这样呢?这篇文章,我们就从本质上跟大家分享一下这几个概念,到底应该怎么样去准确理解他们。

0

阻塞/非阻塞

首先我们要明确的第一个概念是:阻塞/非阻塞是一种现象,这是一个相对概念

例如我们去医院看病。当你挂号之后,发现前面还有人在排队等待。那么前面的人,对于你而言,都是阻塞任务。但是如果这家医院生意不是很好,你挂号之后发现前面一个病人都没有,你可以直接得到主治医生的诊断,那么你就没有被阻塞。

因此我们说阻塞/非阻塞,就跟堵车是一样的,他是一种现象,或者说是一种结果描述,他并不是某一个任务自身携带的特性,我们有的时候都不能提前说这是一个阻塞任务。

代码语言:javascript
复制
foo();
bar();

例如在这个案例中,对于 bar 而言,只有当 foo 执行完了,bar 才可以执行。那么我们就可以说:foo 阻塞了 bar 的执行。相对于 bar 而言,foo 是一个阻塞任务。但是我们不能直接说:foo 就是一个阻塞任务。

代码语言:javascript
复制
const task1 = () => {
  console.log('task1')
  var now = performance.now();
  while(performance.now() - now < 2000) {
    // todo
  }
  console.log('task1 finish')
}

const task2 = () => {
  console.log('task2')
}

const job = () => {
  console.log('job')
}

setTimeout(task1, 2000)
setTimeout(task2, 2000)
job()

又例如这个例子。我们定义了三个任务。两个异步任务 task1task2,一个同步任务 job。job 会最先执行。2000ms 之后 task1 开始执行,2000ms 之后执行结束,此时 task2 执行。

从结果上来说,我们可以说:task1、task2 都没有阻塞 job 的执行。

我们可以说:setTimeout 本身阻塞了 job 的执行。

我们可以说:task1 阻塞了 task2 的执行。

但是实际上,这种结论的总结除了增加新手的学习成本,没多大实际意义,因为按照事件循环约定的执行顺序,这是必然的结果。

在实现的过程中,我们约定的任务执行顺序、任务优先级、当前的任务数量等综合因素,造成了阻塞、非阻塞的结果。

因此,我们学习的时候,重点不应该放在阻塞/非阻塞的概念上,而是应该去关注任务执行顺序和任务优先级的约定。阻塞/非阻塞只是在描述一个现象,一个结果。

例如当你在学习操作系统时,进程之间通信方式上,有的书或者文章,会给你灌输这样一个概念:blocking send:阻塞式发送,表示发送方进程一直被阻塞,直到消息被接受。nonblocking send:非阻塞式发送,发送方进程调用 send() 之后,立即可以进行其他操作。

那在我们前端,也有这样一个类似的场景,在请求接口时,我们看一下下面这两种写法:

阻塞式

代码语言:javascript
复制
const res = await api()
// 阻塞式:等待 api 调用成功并有返回结果之后,才能执行后面的任务
console.log('xxx')
foo();

非阻塞式

代码语言:javascript
复制
api().then(res => {
  console.log('xxx')
})
// 非阻塞式:不用等待 api 请求成功,直接可以执行后续的任务
foo();

熟悉前端的都知道,我们利用了一个小小的语法糖,然后在写法上稍作调整,在结果上就会出现两种不同的表达。

因此,结果的表象描述,不是我们学习的重点,我们也没必要使用阻塞/非阻塞去标注任务特性,或者解释什么专业术语,这个概念本身就是相对的,结果是动态的。

1

同步与异步

事实上,中文互联网环境里,有大量的文章和观点对同步的理解是如下图这种的模式。

这个例子中,老板交给你任务之后,就一直等待,什么都不做,等你做完之后才继续下一个任务。许多人把这种情况理解为同步。

而把下图这种情况,理解为异步。因为老板可以在给你发了指令之后,可以做别的事情。

很显然,我并不认同这样的说法。为什么呢?因为这里有一个非常明显的逻辑冲突,如果我在同步模式中,老板的四次刷剧任务在哪里执行呢?是的,消失了。在这个图的同步模式里,他不是不执行别的任务,而是压根就没有别的任务。

如果上图中的理论成立,其实在代码表现上,也有这样的逻辑悖论。

例如,在异步模式下,我有一个任务 foo 需要执行。我们这样去写代码。

代码语言:javascript
复制
api().then(res => {
  console.log('xxx')
})

foo();

然后呢,在同步模式下,我应该怎么办呢?很简单,我们可以借助 await 来模拟。

代码语言:javascript
复制
const res = await api()
console.log('xxx')
foo();

但是这里问题就出现了,为什么呢,因为这两段代码其实并不一样,上面这个例子他其实等同于

代码语言:javascript
复制
api().then(res => {
  console.log('xxx')
  foo()
})

.then 的回调函数需要依赖 api 的请求成功才会执行。我们依然会称该回调函数是一个异步任务。而任务 foo 成为了该回调任务的一部分。意思就是说,除了这个回调任务,整个代码里已经没有别的任务了。

因为别的任务被迫成为了异步回调任务的一部分

因此在上图的同步逻辑的悖论就出现了,老板并不是不做别的事情,而是没有别的事情可做了... 只是说,在代码的写法上,看上去好像 foo 还是独立的,其实并不是。他的本质和执行顺序已经发生了根本的变化。因此我们常常称 await 为语法糖,写法是同步的写法,但任务,还是异步任务。

如果这种在国内大行其道的理解都是错的,那么正确的理解应该是怎么样的呢?好在我又翻阅了许多资料,在国外的论坛中,看到了我比较认可的解释。

同步是单线程的。因此同一时刻只能运行一个任务。

异步是多线程的。这意味着任务可以并行运行。

事实上,这种理解非常符合实情。在我们浏览器环境中,凡是需要别的线程参与的任务,我们都把他们称之为异步任务。

例如,定时器

代码语言:javascript
复制
setTimeout(task, 2000)

我们会说,setTimeout 发布了一个异步任务 task。这个任务的处理权会首先交给别的线程,然后 2000 ms 之后再交换给主线程。

这里经常会有面试题去问大家为什么定时器的时间是不一定准确的,因为这里的 2000 ms,代表的是到了时间交还处理权给主线程,而不是直接执行 task。主线程得到 task 的处理权之后,具体什么时候执行,还要结合自己的规则来判断。

这里的规则,就是事件循环的执行顺序

例如,点击事件

代码语言:javascript
复制
xxx.onclick = task

此时我们给点击事件绑定了一个任务 task,但是 task 什么时候执行,主线程做不了主。我们需要等到 I/O 线程监听到了对应的输入之后,再将 task 的执行权交还给主线程。这也是一个异步任务。

因此当主线程任务执行时间过长时,我们的点击可能得不到及时的响应,给使用者的感觉很卡。因为这个时候,I/O 只是告诉主线程这个任务可以执行了,但是具体什么时候执行,得主线程自己安排,如果主线程忙不过来,那么该任务就会长时间得不到执行,给用户的感受就是,点了没反应。

因此在实践中,当我们预感到一个任务耗时比较严重时,就需要对任务进行拆分,让点击事件的异步任务有机会得到优先执行。从而避免让用户感知到卡顿,以达到性能优化的目的。

而同步任务的理解其实就比较简单了,如下所示,在同一个线程中,都可以称之为同步执行。

代码语言:javascript
复制
foo()
bar()

foo 执行完之后,bar 才能执行。这是单线程内部的任务执行顺序。

或者

代码语言:javascript
复制
function p() {
  _bar();
  _xyz();
}

p()

2

串行

有的时候,我们会定义多个异步任务,并且让这些异步任务依次执行:上一个异步任务执行完之后,再执行下一个异步任务。这种情况,我们称之为串行

在实践中,我曾经利用串行解决过一个性能问题。在一个大型活动页面中,由于要加载大量的图片,如果不做控制的话,大量的请求会导致页面卡顿明显。因此为了解决这个问题,我们在页面首页设计了一个进度条功能展示资源的加载进度,并以串行的方式加载图片

每一张图片的加载是一个异步任务,上一张图片加载完之后,再加载下一张图片,这样可以极大减小浏览器的压力。

代码实现如下:

代码语言:javascript
复制
images.keys().reduce((cachePromise, path) => cachePromise.then(() => {
  return new Promise(resolve => {
    const image = new Image();
    const complete = () => {
      clearTimeout(timer);
      resolve();
    }
    const timer = setTimeout(complete, 1000);  // 单张图片最多加载1s
    image.src = images(path);
    image.onload = image.onerror = complete;
  })
}), Promise.resolve());
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-03-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 这波能反杀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0
  • 1
  • 2
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档