关于异步任务的一点疑问,有没有老哥帮忙解答下

2023-01-11 10:11:21 +08:00
 yezheyu

假设我需要使用 XMLHttpRequest 下载一张图片,然后展示在网页上。

因为下载图片比较耗时,会阻塞主线程,因此会被放到网络线程中执行。

当网络线程下载完图片后,再把图片对象 img 以参数的形式传到回调函数 showPic(img)中,再把回调函数 showPic 封装为任务放到任务队列中,等待主线程空闲后执行,把图片加载到网页上

我想问下,在异步编程中,对于这种单独起一个线程执行耗时任务,为啥耗时任务执行完的后续收尾代码( showPic )非要放到任务队列中让主线程执行,为啥不干脆在自己的线程中执行。

反正对耗时任务的结果产生依赖的代码都在回调函数中,主线程中代码的执行又不依赖耗时任务的结果

而且万一主线程执行很慢,等图片下载完还没执行结束,那加载图片( showPic )不就被动延后。 而把 showPic 让网络进程执行,不就能在图片下载完后立即被加载出来,也不用管主线程是否执行完

3916 次点击
所在节点    程序员
43 条回复
elonmask
2023-01-11 10:16:32 +08:00
因为主线程才能 UI 吧,你就算在别的线程中还得切换到主线程来执行图片加载,干脆搞个队列让主线程去取。
yezheyu
2023-01-11 10:22:02 +08:00
@elonmask 有道理,但如果不是 UI 相关,收尾代码纯粹 console.log 或者保存图片等等,另一个线程也能做到,是不是就没必要放到任务队列中等主线程执行呢?
Mutoo
2023-01-11 10:28:19 +08:00
主线程的任务顺序严格可控,只取决于任务加入队列的先后。但如果在别的线程中执行,顺序就不受控了,会出现更多的 race condition ,代码非常难维护。
bruce0
2023-01-11 10:30:06 +08:00
在 Android 里,只有主线程才能更新 ui (surface 除外), 如果在子线程更新 ui,会直接抛出异常. 如果多线程更新 ui 可能会导致一些错乱和不可预知的问题, 比如 两个线程同时更新一张图片, 如果加锁,性能上有损失, 不如强制在主线程更新 ui 了
DingJZ
2023-01-11 10:40:00 +08:00
主要就是因为 UI 相关,保存完图片要不要在 UI 上提升用户保存成功,如果单纯的后台任务确实不需要回到主队列
MakHoCheung
2023-01-11 10:40:08 +08:00
@yezheyu #2 是的
unco020511
2023-01-11 10:55:56 +08:00
关键词:「主线程更新 UI 」
yezheyu
2023-01-11 10:56:09 +08:00
@Mutoo 多谢老哥,明白了
peterlitszo
2023-01-11 11:20:04 +08:00
请问上下文应该是前端吧?如果是前端的话,我很好奇 network thread 到底是什么?

按照我所学的,我的一点粗浅的理解是:

- 浏览器不支持多 thread ,除非上 worker: https://developer.mozilla.org/zh-CN/docs/Web/API/Worker
- 浏览器的异步支持事实上是单 thread ,加上事件循环,而多个协程共同协作,没有发生抢占。堵塞协程会及时让出。
okakuyang
2023-01-11 11:25:52 +08:00
UI 绘制本质上是一个线程负责的循环,当有 UI 操作的时候都要将操作加到这个循环之中。
cyndihuifei
2023-01-11 11:26:55 +08:00
我也表示疑问,上下文应该是前端吧?
yezheyu
2023-01-11 12:35:01 +08:00
@cyndihuifei @peterlitszo

个人理解,可能有错,欢迎指出



js 之所以是单线程,主要为了避免资源竞争问题。

但实际上 js 不可能是单线程,就比如 xhr 图片加载,xhr 在下载时没有阻塞主线程,那图片的下载必定不是在主线程执行,那必定有一个线程帮你执行下载工作,就是网络线程。

所以我认为 js 单线程是指你在 js 代码中无法像其它语言那样使用多线程模块创建一个线程执行一个代码块。只能使用预留的 API 调用现有的线程,如浏览器的 xhr Web Api ,node 的 fs 模块

那像 xhr 中在网络线程中要渲染图片,涉及到操作 dom ,就会出现资源竞争问题,所以必须放到任务队列中让主线程来进行



那你在代码中的耗时任务该怎么办呢? js 又没有多线程让你开辟一个线程去处理,而浏览器也没有一个特定的 WEB API 帮你执行。

所以为了不阻塞后续代码只能延期执行,让其它代码先执行,执行完后再执行耗时任务和对其结果的依赖代码,即微任务。

每个宏任务都有一个微任务队列,里面放的就是这些延期代码。微任务的执行时机就是当前宏任务执行完,下一个宏任务执行前。这样就可以完美解决。

而 Promise 就是这样的实现
jybox
2023-01-11 13:10:17 +08:00
@yezheyu JS 就是单线程的,你说的的其他线程是引擎的实现细节,用来提供「事件循环」的语义,但你无法接触到这些线程(无法在其中执行你的 JS 代码)。

>那你在代码中的耗时任务该怎么办呢?

浏览器和 Node.js 分别有 Web Workers 和 Worker threads ,但这种线程其实相当于是一个独立的 JS 运行环境,不能直接操作主线程的内存,和主程序的通讯非常受限(其实和单独的进程差别并不是很大)。
MozzieW
2023-01-11 13:49:31 +08:00
1. 要先知道为什么有主线程( UI 线程),这个前提下再讨论其他问题
2. “对于这种单独起一个线程执行耗时任务,为啥耗时任务执行完的后续收尾代码( showPic )非要放到任务队列中让主线程执行,为啥不干脆在自己的线程中执行” --》把图片下载放到单独线程,就是把耗时任务从主线程摘除来。但是因为只能主线程更新 UI ,所以最后还要回到主线程刷新界面。这里经常遇到的一个问题是:下载后图片解析成 Bitmap 也是耗时的,经常遇到在主线程再解析,更好的方式是在下载线程解析。
3. “万一主线程执行很慢”--》这个万一出现了,就是要解决的问题。主线程要定时刷新页面,60 帧下刷新间隔是 16 毫秒,而且还有 90 帧、120 帧,时间要求更短,执行慢一点就掉帧。如果主线程卡一下,可能不是问题;一直卡,就是要解决的问题。正确情况下,图片的显示间隔不差这么几毫秒,要优化也是下载时间、解析时间。
h0099
2023-01-11 14:03:41 +08:00
> js 之所以是单线程,主要为了避免资源竞争问题。

js 是单线程+无栈异步( promise )+回调异步(传统的 callback hell 就像 IndexedDB )

> 但实际上 js 不可能是单线程,就比如 xhr 图片加载,xhr 在下载时没有阻塞主线程,那图片的下载必定不是在主线程执行,那必定有一个线程帮你执行下载工作,就是网络线程。

负责下载图片等网络请求的是 chrome 的 network thread ,其跟 js 无关

> 所以我认为 js 单线程是指你在 js 代码中无法像其它语言那样使用多线程模块创建一个线程执行一个代码块。

您可以用 worker 创建新的 js thread

> 只能使用预留的 API 调用现有的线程,如浏览器的 xhr Web Api ,node 的 fs 模块

因此 xhr/fetch 本质上是浏览器暴露的一套 api ,其标准化于 https://xhr.spec.whatwg.org https://fetch.spec.whatwg.org 他们存在的目的是允许您在 js thread 中与 network thread 通信,以派发您的下载图片的任务

> 那像 xhr 中在网络线程中要渲染图片,涉及到操作 dom ,就会出现资源竞争问题,所以必须放到任务队列中让主线程来进行

xhr/fetch 没有渲染图片,他们只负责网络请求相关的 payload (实际上跟 os socket 打交道处理网络栈已经够复杂了)
xhr/fetch 最终会给您一坨 byte[]也就是您的 http request 的 http response body ,以及相关的 response 元数据如 http header ( fetch 标准是都放在 https://developer.mozilla.org/en-US/docs/Web/API/Response 里)
要么您的 js thread 去取回这些 byte[](例如通过 https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer
要么是其他浏览器 thread 消费,比如有一个`<img src="url">`,那么 network thread 下载完了`url`后就会把 byte[]传给负责图片渲染和 css layout 的 thread ,让他们绘制这个<img>,这里同样没有 js 的事(即便您禁用了浏览器 js 也不会妨碍<img src>正常工作)
要么是没有任何人消费这些 byte[],那就直接丢弃
h0099
2023-01-11 14:09:34 +08:00
> 那你在代码中的耗时任务该怎么办呢? js 又没有多线程让你开辟一个线程去处理,而浏览器也没有一个特定的 WEB API 帮你执行。

现在您可以使用 web worker 标准的 api https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers 来创建新的 js/wasm thread 来跑您的 cpu 密集耗时任务

> 所以为了不阻塞后续代码只能延期执行,让其它代码先执行,执行完后再执行耗时任务和对其结果的依赖代码,即微任务。

这就是异步

> 每个宏任务都有一个微任务队列,里面放的就是这些延期代码。微任务的执行时机就是当前宏任务执行完,下一个宏任务执行前。这样就可以完美解决。

mirco/macrotask 是 html spec 中标准化了的对 js thread 上的 event loop 和异步调度的优先级实现: https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context

> 而 Promise 就是这样的实现

promise 是无栈协程,也就是说他本质上跟回调地狱异步没有区别(这也是为什么 es6 spec 标准化 promise 之前有 jq promise https://api.jquery.com/category/deferred-object/ 、promises/a+ https://promisesaplus.com/ 等各种民间标准,但他们的本质都是对回调地狱的封装使其更好读)
wangritian
2023-01-11 14:14:59 +08:00
可能用操作系统的异步 IO 接口吧,单线程应用也不会阻塞
yezheyu
2023-01-11 14:23:25 +08:00
@h0099
多谢指正

所以 xhr 只是负责和浏览器的网络线程通信,下发任务
而网络线程就只管从服务器下载资源,对于下载好的资源,你怎么处理,就算只是打印一下,也必须让主线程处理。网络线程只负责 socket 通信。

所以我上面的例子,对于结果不管是更新 UI ,还是打印结果,都只能是主线程处理,对吗?
biguokang
2023-01-11 14:30:07 +08:00
@yezheyu js 是单线程的,但是 chrome 是 c++写的,c++是多线程的。

所以本质上,是 js 代码通过调用浏览器 web api 从而调用 c++的过程,表面上你写的是 js 代码,但最后实际上干活的是 c++,这个道理放在 nodejs 也同理。

你看到的网络线程,不是 js 的网络线程,而是 chrome 的 c++网络线程。

包括图片渲染、dom 渲染,到了最后还是 c++干的活,js 只是指挥 c++干活的。

所以 js 回调函数,其实就是浏览器把活干好了(比如网络加载、计时等等),踢了一下 js 引擎,说我活干完了,你可以执行任务等待队列里的对应回调函数了。
autoxbc
2023-01-11 14:35:02 +08:00
OP 混淆了 JS 线程和 JS 引擎线程,JS 是单线程,引擎执行完下载,后续代码自然需要回到 JS 线程继续。JS 编码应该保持 JS 视角,只观察引擎提供的接口,不关心引擎实现的细节

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/908083

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX