Javascript异步任务(如promise)操作全局变量是否需要加锁?

各位大佬,本人接触过后端,前端小白请教一个问题。javascript的异步任务可以直接操作全局变量或者dom,这会不会和主线程中的操作发生冲突呢?后端…
关注者
39
被浏览
32,190
登录后你可以
不限量看优质回答 私信答主深度交流 精彩内容一键收藏

【给某些傲慢的「多线程」教条主义者】

「锁」到底是什么?

锁是锁线程的还是锁共享资源的?

什么是「并发」?

并发一定要多线程吗?

JS中的异步(无论回调、promise或async/await)是否可以并发?

并发执行的代码访问共享资源会不会需要锁?


不爱看正文也罢,送你个文档吧:

docs.python.org/3/libra

对,不是JS的,是Python的。因为某人的个人简介是「大数据算法/Python」,饭直接给喂到嘴里了,自己嚼两口总会吧?

【/给某些傲慢的「多线程」教条主义者】


要不要加锁,还是要看你的程序逻辑。必要时就加锁。


JS(除了 SharedArrayBuffer 和相关设施之外)固然是「单线程」的,但异步操作既然是「异步」的,多个异步操作当然是可能交错访问相同的共享变量的。(全局变量是共享变量的特例——全局可访问,天然可共享。不像局部变量,不主动传递引用,就不会共享。)多段逻辑或同一段逻辑有多个实例并行执行,访问相同的共享变量,自然是有可能发生冲突的。

JS和「多线程」的编程语言的区别是什么呢?是不确定性的范围。

对于直接有多线程的语言来说,除了少量原子操作,任何操作默认都是「多线程不安全」的。比如

if (x == null) x = calc(); print(x)

这么简单的代码,如果这段代码是多线程执行的, 都不能确定输出是啥。

JS因为具有「run to completion」语义,当然就没有这种不确定性。但如果代码是

if (x == null) x = await calc(); print(x)

并有多个异步任务执行呢?在await完成后,x可能已经不是null了。

当然即使如此,这个代码的不确定性也没有之前的高,因为之前的代码如果对应到JS的异步大概是这样:

if (x == null) x = await calc(); await tick; print(x)

也就是print的也未必是刚刚计算出来的x。

正常代码中不存在 await tick 这样的东西,也就是并不会任意中断。所以只有真正需要await的地方并且在await前后访问了共享变量才有冲突风险。

这就是显式异步和一般多线程的差别。多线程默认没有锁,有需求就加锁;而显式异步,你大体可以倒过来理解为:默认就是有锁的,只有显式标记异步处是没锁的。或者说,是从显式的程序暂停点(await或yield等指令)到下一个显式的暂停点之间是有锁的。

可见,在这种异步模型下,已经大大减小了加锁的必要性。

其次,如果对共享变量访问的代码的调用链路上均是以await方式调用,则根本不会产生多个并行的异步任务。这相当于我知道某个代码是多线程不安全,但确信其只会被单线程调用,那么就可以不加锁。而且就前端编程来说,很多时候,即使存在理论上的并行异步可能(比如点击一个按钮触发一段异步操作,在异步操作未完前,又点击了按钮一次),实践中也基本不会发生(比如第一次点击后已经将按钮的状态改为不可点击)。发生了,可能也不至于造成重大问题——比方说,只是重复计算和渲染,浪费了客户端算力,不够低碳。(不像服务器端,客户端耗能也不是你掏钱,就let it go啦。)

当然,像上面这段代码,如果确实可能触发多个异步任务,并且你的逻辑是 x null 时开始计算直到算完并打印,期间不允许并发修改 x ,那么确实就要在这段代码上加锁。只不过要case by case看,很多时候锁可能直接加在 calc 上的,那么从 calc 的调用方角度看也不用加锁。

以上。

【评论区交流lift上来】

@Makoto Ruu 评论说:

标准库里没有现成的锁设施……尽管用户自己实现或诉诸第三方也没多难,但毕竟具有严重的基础设施色彩,各自造轮子一是麻烦,二是缺乏统一风格也是无谓徒增各方面的心智成本,三是隐形广泛传达着错误的理念,让那么多人真的以为单线程就完全不需要锁了。

作为TC39代表我讲一下。

第一,TC39在这类设施的标准化方面是比较保守的,尤其JS标准库的成本是由多方如浏览器厂商所承担,门槛较高;

第二,异步编程本身在JS里还存在很多更棘手的、userland无法自行解决的问题,比如AsyncLocal(类似于ThreadLocal,node.js已有API,希望能推进入JS标准,不过很困难,目前champion是阿里的代表),所以这种你们自己可以搞定的事情优先级就比较低;

第三,实际上异步锁本身在社区里貌似也不是很流行。除了我正文里解释的,锁的需求不像真·多线程语言中那么高,可能也有Makoto Ruu说的认知上的问题,并导致鸡生蛋蛋生鸡问题——因为认知不足,所以用的少;因为用的少,所以不流行;因为不流行,所以欠缺认知……

当然,最关键的是,这个东西没有人写提案和推进。有人强力推进的话,上面几条也不是不可克服。

假设有人按照Python的async lock复刻一把,我觉得至少进stage 1还是应该有可能的。有兴趣的同志可以尝试写个提案,然后找阿里、腾讯、字节、华为、360等公司的代表做champion。(我本人是invited expert,虽然也可以做champion,不过手头提案已经很多了,精力不够。)