为什么说异步编程是反人类_-洋耗子
异步编程不反人类,它反的是cpu这种笨蛋,以及因此连累了程序员而已。
最基础的异步怎么做的?
把一个同步工作一刀切两半:先做一点准备工作,然后调用异步api,同时传入一个回调,就完事了。回调呢?等什么时候被激活了,吭哧吭哧干完收尾的活就搞定了——能有多难?
但问题来了:cpu它笨啊——它不会自己去找它需要什么样的数据,只能让程序员不厌其烦的告诉它每条数据都分别在哪。所以,前半段的数据好说,让调用者传参进来就行了。但后半段的回调就麻烦了,这回调又不是我程序员自己调用的,数据从哪来?这事用学术点的术语来说,就是:(在你一刀把工作流切两半时)上下文也同样被切断了——所以你需要做上下文切换和现场保存工作。
所以,为了解决这个问题,大多数异步api都会提供一两个额外的透传参数让程序员有办法重新连接起上下文。
如果你的上下文很小,能完全塞进透传参数里(一般就一两个int),这一切依然非常简单美好。
好,麻烦开始来了:如果上下文在参数里放不下怎么办?
你要借助堆来暂存。然后把这块内存的指针传过去,那边用完了再清掉——看上去也不难。但问题是:你的那个回调函数,一定会被调用吗?如果没回调,这块内存可就泄露了。
这还是有比较通行的方案的:全局弄一个上下文管理器,把这内存指针放过去,在回调里重新从这管理器里重新找到对应的上下文就可以了。那没回调……就定时清理咯。于是,你又要多一个定时器和定时器线程(当然可以和其他定时需求合并共用)。
但有了定时机制还是不够——你说我这超时值该多少呢?或者更明确一个场景:如果它回调时相关的上下文已经被清理了,怎么办?更麻烦的是,在超时清理上下文时,很可能不仅仅是清理那块内存,还可能有其他相关的清理工作——这一切都清理了,然后这时你告诉我这活一直干得好好的,其实不需要清理???!!!
好,退一步说,这我也忍了,大不了就当白干嘛。但事情真没那这么简单:你在异步api里做的事情,有没有副作用的?如果有,这上层看上去工作已经被清理了,但底层实际上工作又做完了,就会出现状态不一致——往下就会有不少逻辑问题出来了。
意识到这麻烦不小了吧?
例如说如果是异步读文件,这事没副作用,大不了就当浪费一次io嘛,没什么大不了的。但如果是写文件呢?如果能覆盖写,还可以尝试重新发起个一样的任务redo一下(这也不一定可靠)。但如果是追加写,怎么办?是让底层undo刚才的操作?是让上层undo刚才的清理工作?无论哪个,都不简单——甚至有些场景可能就根本做不到。
所以,这里的问题已经接近了数据库/存储引擎的那种“事务”的概念了——你的工作设计上,要么有redo能力,要么有undo能力,要不然就自求多福咯。
那最后一个问题是:有多少程序员敢拍胸脯说自己能设计/定制一个健壮的带事务的存储引擎?或者给自己的每个异步工作都设计一套足够健壮能随时redo/undo的机制?
甚至都不用说动手撸代码实现它,光说逻辑设计,我敢说相当一部分顶着“架构师”title的人都实际上搞不定这活。
说完了问题。
就说一下解决方案的本质:就是业务层在设计上,就必须要有一套运转良好的状态机,明确各个状态,以及对应的状态迁移函数。把这玩意在逻辑上吃透了,这异步的问题才算是过关了。
至于说协程这玩意,它确实通过底层架构的设计,简化了上下文连接和对接的工作。简化了撸代码时的心智负担,也减少了各种手贱bug低级错误bug。但是,在逻辑上,上下层状态不一致的可能性并没有消除(甚至在概率上都不见得降低了)。
为了研究协程“用同步方式写异步代码”的作用和价值有多大,可以顺带再思考一个问题:同步api,就一定没有这样的问题了吗?也就是说,你用同步api发起一个操作——假定是写磁盘吧——那在内核里还是会把你的上下文切出去让给其他线程,等到dma,中断,信号,再到内核把你的上下文重新切进来……理论上,上面的流程任何一环出问题,它一样有可能永远卡住你的线程的……本质上还是一样的。
但我们为什么会觉得同步api简单?是因为我们通常相信cpu和外设芯片是没有bug的,操作系统的内核是久经考验的……那如果你真的无比信赖异步库的健壮性,那你同样可以把这事做得非常简单。
这个是否信任问题还带来另一个区别:原本同步模型里,在工作没有确定最终完成之前,所有的“半状态”都可以以局部变量形式保存在调用栈上,等到最终结果才合并提交到全局。这样,就算线程真的被永久卡住了,至少不会污染全局。而我们又相信内核基本上是可靠的,所以我们基本上也相当于实现了最终一致性。但异步,我们之所以要做全局管理器什么的,本质还是不信任它的可靠性和健壮性。如果真的信任它的话,我们照样可以简单的new一段内存出来,然后指针传给回调就完事了。最终一致性?和同步是一样的……
所以,异步编程为什么难?
抛开前面那些理论和术语,最简单的就是:是因为它把原本已经由操作系统解决的(健壮性)问题又再一次赤裸裸的暴露在了写业务的程序员面前。而大多数程序员,并没有足够的经验和能力去驾驭他——这大概就是为了榨取高性能的代价吧。
评论区
RMosaicDCFan: [赞][赞][赞]这讲到了编程的本质,把代码拆开来(异步编程)和放在一起(同步编程)在操作系统和CPU看来本质是一样的,所不同的是,在同步编程的场景下,切换到其他线程上下文和切换回当前线程上下文的事情,CPU和操作系统合起来做掉了。另一方面,所有指令的执行都可能失败,甚至下一个指令地址接续或跳转的执行都可能出错,只是硬件相当可靠的情况下,概率非常小而已。 👍🏽67 💭江苏 🕐2023-09-15 21:13:20
冰箱: 异步链路只要混进个同步调用,直接性能下降几个数量级 👍🏽32 💭安徽 🕐2023-09-19 12:19:38
│ └── 无志少年: 木桶能装多少水,取决于最短的那块木板 👍🏽17 💭广东 🕐2023-09-20 15:06:54
实名用户: CPU:你说的这些和我有关系吗?我只是个搬砖的啊[捂脸] 👍🏽7 💭江苏 🕐2023-10-13 12:23:23
0x72314F60: js的promise和async/await是目前用过最方便的方式[捂嘴] 👍🏽6 💭山东 🕐2023-11-04 15:41:52
│ └── 书痕: 好像用的是微任务队列实现,还不得不用闭包保证上下文 👍🏽2 💭山东 🕐2024-01-31 13:33:02
│ └── momo: Go语言的goroutine是目前用过最方便的方式 👍🏽5 💭浙江 🕐2024-05-13 11:57:41
│ └── CHUANWISE: 不妨试试 kotlin [思考] 👍🏽0 💭湖南 🕐2025-05-02 17:20:09
Fang: 有句话不知当讲不当讲:没有金刚钻.… 👍🏽5 💭上海 🕐2023-09-23 22:42:05
│ └── 云云: 又不是一上来就有金刚钻 👍🏽2 💭江苏 🕐2024-07-25 16:38:55
│ └── 卡酷卡: 行业总是需要一批又一批新程序猴的,但哪有天生长了金刚钻的猴,所以,不当讲。 👍🏽0 💭广东 🕐2025-06-08 01:45:24
GaaS: 呃呃,但是cpu还真知道哪里有数据,因为他有中断,linux这样的操作系统本质上就是一个大循环等中断 👍🏽5 💭北京 🕐2023-09-27 09:21:47
│ └── 洋耗子: 就那1k还是多少的中断向量表,够干啥的?再说了,中断向量表里的数据,照样是软件给写进去的。 👍🏽0 💭广东 🕐2023-09-27 12:19:04
│ └── GaaS: 是有相应的硬件的啊 👍🏽0 💭北京 🕐2023-09-28 08:44:07
不在场证明: 阻塞才是好文明, 唯一的问题是硬件不够快 👍🏽2 💭辽宁 🕐2024-05-13 21:00:11
知乎用户IiMb8N: 异步本身就复杂 复杂性不会消失 👍🏽3 💭四川 🕐2024-06-22 11:08:40
焕元: 这是真懂的 也能说明白的。 25年来看 goroutine已经完美解决了网络io的问题 现在就差异步文件io和cgo还是仍然需要同步卡线程了 👍🏽1 💭北京 🕐2025-05-18 23:38:52
那些追寻叫做宿命: 异步算是面对flow编程,妥妥的进步 👍🏽1 💭河北 🕐2023-09-26 18:54:42
我的昵称: 异步编程其实没啥毛病 蒸饭的时候 炒菜挺好的。真正有毛病的是异步回调陷阱。蒸完饭,凉一夜,然后异步打鸡蛋 切葱 切火腿肠 然后 等打鸡蛋 切葱 切火腿肠 这些都完成 起锅烧油 再把 隔夜饭 鸡蛋液 葱花 火腿肠碎 按照顺序放进去。这个就是回调陷阱。它会把平铺直叙变成压栈然后再回溯。平铺直叙是人的脑子,压栈回溯就是计算机的脑子了。所以这样的代码写的时候爽。给别人看的时候就是火葬场。 👍🏽0 💭辽宁 🕐2025-04-26 21:01:36
李峰: cpu一点都不笨,你以为人脑能异步吗?你左手画圆,同时右手划圈看看。人脑可是彻彻底底的单线程,一点异步能力都没有的。 👍🏽0 💭上海 🕐2025-02-27 12:16:06
│ └── composer.json: 你没打过游戏吗?[大笑][大笑] 👍🏽0 💭河北 🕐2025-03-01 21:31:40
│ │ └── 李峰: 单cpu一样可以多线程,不等于真正异步 👍🏽0 💭上海 🕐2025-03-01 22:20:20
│ └── 咔咔不咔: 你这是多线程,并不完全等同于异步。 如果做类比,你早上起来烧水是一定要等水烧开了再去刷牙吗 还是可以先刷牙 等待水开了水壶报警回调。 正常人类处理长任务逻辑都是异步的,没有人愿意一直阻塞等待 👍🏽1 💭湖北 🕐2025-03-13 19:30:34
mytone: 无限嵌套,看的很累,没有条理型的直观表现,是反人类 👍🏽0 💭江苏 🕐2024-09-25 12:04:53
Wanchope Lee: 可能是切一刀没切好 👍🏽0 💭山东 🕐2024-07-26 18:02:15
蚂蚁: 真厉害的回答啊,受教了[赞] 👍🏽0 💭广东 🕐2024-07-10 10:41:53
Illyasviel: 讲的挺不错的 👍🏽0 💭上海 🕐2024-05-22 22:08:42
鲁卡米纳: 如果完全抛弃状态机的思路,全写成fp呢? 👍🏽0 💭江苏 🕐2024-05-13 18:45:06
│ └── 杨个毛: 但是如果系统带着一个 GC 管不到的数据库,就仍然有无状态的 fp 世界 <=> 有状态的数据库世界之间的接口。如果这个接口只是偶尔用用,那还好说。但是如果这个接口本身也是高并发的,那这个问题仍然存在吧。 👍🏽1 💭北京 🕐2024-06-25 22:44:12
lht: 普通人怎么想多线程,其实和程序专家是很不同的。普通人。妹.do(函数).start线1.do(函数).start线2.do(函数).start师告诉了程序。程序必须会加锁,解锁,队列。计算机专家。平权,平权,平权。买书吗? 👍🏽0 💭辽宁 🕐2024-01-28 16:09:04
红色的红: java里面异步编程(包括普通的线程调用),上下文传递已经非常简单了,工调用了线程,了线程可以访问自己的局部变量一样访问a线程 的变量,唯一的条件就是此类变量是要是final的,或者等效于final的。 没有你想象的那么复杂。同样completablefuture 也一样。 👍🏽0 💭北京 🕐2024-01-26 08:30:16
│ └── 洋耗子: 你说的就是我倒数三四段说的内容。 👍🏽0 💭广东 🕐2024-01-26 08:48:02
孟亮: 所以说,小而快的,就同步调用。简单耗时的就异步调用。复杂耗时的就展示进度条。好像完全没问题。 👍🏽0 💭河南 🕐2023-10-13 11:01:44
新上路的司机: 所以最简单的办法就是提升CPU性能。孩子傻,但孩子力气大,只要我(现代CPU)同步跑得比你(上古8位16位CPU)最优化的异步还快,那异步优化问题在绝大多数场景下就不是问题。 👍🏽0 💭湖北 🕐2023-10-07 20:44:26
│ └── 洋耗子: 很显然,在摩尔定律接近失效的今天,你所谓的“最简单的办法”,反倒是“最难的办法”。 👍🏽8 💭广东 🕐2023-10-07 20:58:23
│ └── 新上路的司机: 摩尔定律虽然说接近失效了,但其实提升空间还有。除非是有极端性能需求的场景,现代CPU已经足够力大砖飞。 👍🏽0 💭湖北 🕐2023-10-07 21:00:22
熊起: 这个事儿不一定操作系统做, 语言的运行时应该也能做 👍🏽0 💭陕西 🕐2023-09-26 14:14:06
│ └── 洋耗子: 运行时、库之类的,都可以做,但是很难做得很完善——毕竟它们没有像内核那样有强行挂起的能力。 👍🏽6 💭广东 🕐2023-09-27 12:20:28