某些语言的协程机制,其作用是什么,是否会造成额外的开销

2020-08-31 09:36:30 +08:00
 maxxfire
一般情况下线程就够用了,线程也是系统的最小调度单位。
有些语言又提供了协程机制,那么在运行的时候不是还要多开一个线程(调度器或虚拟机)来调度这些协程?这不是一种资源的浪费吗。当然有些简单的协程可能直接用 siglongjmp 堆栈还原来实现了。
像传统 C/C++,直接编译机器码,直接就跑了,简单粗暴,还整什么调度器。
6188 次点击
所在节点    程序员
51 条回复
sagaxu
2020-08-31 11:08:56 +08:00
当然有额外开销,不过起 100 万个协程很轻松,换成 100 万个线程就呵呵了
lbp0200
2020-08-31 11:17:03 +08:00
楼主说的非常对,协程只是用来解决异步 IO 的,所以没事别用协程。
shuax
2020-08-31 11:17:31 +08:00
原因很简单,线程资源消耗太大。
SaKuLa
2020-08-31 11:22:16 +08:00
这个看不同语言的实现吧,Kotlin 的协程和线程开销差不多,但是能够将异步回调处理成同步的方法
InkStone
2020-08-31 11:24:10 +08:00
且不说内核用户的切换开销,光是一个线程几 M 的内存,就已经吃不消了。
mmggll
2020-08-31 11:25:06 +08:00
@ysc3839 这只是编译器部分的支持,实现要自己写。真正到 std 标准库提供,估计还得好久。。。
lewis89
2020-08-31 11:32:10 +08:00
@RudyC #20

是多协程工作的时候,多个协程的 IO 操作让 golang 给接管了,golang 应该有一个机制 将 IO 调用单独用一个线程来处理 派发 响应,像 epoll 调用就可以使用水平触发来 监听多个 fd,这样语言层面上,只要一个线程就能接管跟系统调用的操作,这样其它 golang 的线程就不用频繁进入内核态了,进入内核态首先要切换 MMU 的页表,L1 L2 cache 可能还会被 invalid 掉,单次内核态切换开销小,但是如果上千个线程的内核态频繁切换开销就大了,切换少了,很容易把线程给饿死,不频繁切换是不可能的。但是像 Java 这种线程池模型,你只能拿线程池死扛,没有其它好办法,因为语言层面上就没做协程,后续的版本可能会推出。

另外协程也有自己的问题,就是公平调度的问题,万一一个协程长期跑着,不退让 CPU,这样可能其它协程就饿死了,在调度算法上面还需要做很多处理,至于协程的中断,可以参考 Java 的 GC safePoint 实现,应该是使用 Linux 的 mprotect 系统调用,在特定的汇编地址下,插入一些 nop 代码,当操作系统检测到 CPU 运行到这个地址的时候,就会触发软中断跳转到 mprotect 事先设置的回调调用

有兴趣的朋友最好了解一下 epoll 跟 mprotect 调用,这两个函数读完说明书,基本上就了解协程是如何实现的了
lewis89
2020-08-31 11:34:57 +08:00
@RudyC #20 这也是我称 golang 是虚拟机的原因,因为其协程本身就是虚拟了一套操作系统的调度功能,并且会在编译后的特定的汇编代码处插入特定指令,此时会触发一些 golang 协程系统内部机制的一些调用来完成调度功能
misaka19000
2020-08-31 11:38:40 +08:00
RudyC
2020-08-31 11:40:49 +08:00
@lewis89 是的,我记得在学习 golang 时看到有一个叫 netpoller 的东西,大概就是 epoll 的功能,只是 golang 利用 netpoller 来接管了网络 io 操作

golang 用得不多,所以也不确定自己理解得对不对,感觉协程主要发挥空间是在 IO 密集场景 lol
lewis89
2020-08-31 11:42:56 +08:00
另外协程这一套机制等于把协程的栈空间 全部放在 golang 整个进程的堆空间,如果频繁向操作系统申请释放内存空间,会造成内存抖动,因为现代操作系统有一套复杂的堆内存管理机制,需要整理碎片大小的内存空间,如果 golang 进程频繁申请释放内存空间,就会频繁触发操作系统的内存整理,而传统的线程,其栈空间都是预设好的,会随着调用增加,会随着返回减少,操作系统有特定的机制整理栈空间。
RudyC
2020-08-31 11:42:56 +08:00
@lewis89 感谢评论,看完你的评论之后理解更多了
lewis89
2020-08-31 11:44:35 +08:00
@RudyC #30 是的,协程本身就是应付互联网的 IO 密集型场景,计算密集型,其实没有多线程的必要,很多算法并不能并行。
lewis89
2020-08-31 11:49:09 +08:00
@RudyC #32 其实简单来说,就是接管了调用,让 epoll 去完成多路 IO 复用,这样当 epoll 告诉你可以读这个 fd 的时候,就让协程回到运行态,协程应该算是被发明出来对抗回调地狱用的,毕竟大部分人习惯线性思维,而不是 callback hell
Nugine0
2020-08-31 13:06:28 +08:00
所有语言的异步 IO 都需要操作系统的非阻塞支持,比如 epoll 。
应用注册 IO,当 IO 完成后,系统会通知应用处理对应事件,这样一个线程就能同时进行多个 IO 操作,不用被阻塞调用卡住。

拿 js 举例,无栈异步语法分为回调、Promise 、async/await 三种,第一种是回调地狱,第二种链式调用开火车,第三种用同步格式写异步,最人性化。

再拿 go 举例,有栈异步语法与同步一致,调度器会在进行 IO 时自动把协程切走。py 在没有 async 语法时用的是 gevent 有栈协程,把同步操作自动换成异步,无需修改代码。

js 本身就是事件循环,无栈协程是加糖解决回调地狱。

py 有两种协程,gevent 的有栈协程,asyncio 的无栈协程,都是为了提高 IO 效率。

go 是有栈协程,m:n 调度,多个线程上可以运行多个协程,卡住时其他线程还会偷走多余的任务。本来一个线程只能进行一个 IO 操作,现在可以同时进行多个,提高 IO 效率。

c++20 的是无栈协程,但可能有隐式分配。

Rust 的是无栈协程,没有隐式分配,调度器要自己选择第三方库,基本上都有工作窃取算法。其中的无栈协程可以不用分配直接放栈上执行,也可以交给调度器作为顶层 future 执行。而且 Rust 没有 GC,实时性可以有保证。

个人认为,在各种语言中,Rust 的无栈协程是最轻量的。
owenliang
2020-08-31 13:23:21 +08:00
那点内存资源和 CPU 开销都不是重点吧,更好的编程模型让开发者更爽的表达自己的想法,这才是背后的意图。
charlie21
2020-08-31 13:34:08 +08:00
你的感觉是对的

它 不是因为替你种菜而优秀,而是把种菜这件辛苦事做成了一个 robust 的 layer,一个健壮的层。这是它的意义所在。它方便了那些想种菜而能力不足的人。

本身就不想种菜,你不需要 它
本身就在想种菜的时候自己种得非常好( robust ),那么你也不需要 它

当你关注的问题是 菜是不是 robust,那么 谁种的菜也无所谓

自己种菜的人是大有人在的,在它出现之前的日子里。

(它的出现令种菜大师暗淡无光,令无名小卒洋洋得意:看 我是垃圾,但我也能有菜吃,它种的。我不会种,我会调用它种,就 OK 。你可想而知吹它的都是什么菜)

-
guanhui07
2020-08-31 14:41:18 +08:00
协程 占用资源更少,能创建协程更多
no1xsyzy
2020-08-31 14:46:30 +08:00
@Nugine0 #35 还有 Lisp 系,基本没什么库,直接拿 call/cc 手撸就成(
@charlie21 #37 然而楼主的问题是:“这种出来的菜能吃?”
NakeSnail
2020-08-31 14:59:52 +08:00
感觉楼主的思考更具有启发意义,上面几位告诉楼主线程和协程优缺点的貌似没有楼主理解的深入

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

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

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

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

© 2021 V2EX