golang 的协程比线程轻量级 轻量级在哪里,谢谢

2017-05-14 07:53:38 +08:00
 q397064399

无论是协程还是线程应该都是占用内存用来维护函数调用栈帧,

如果是同步 IO 系统阻塞调用的话,

线程无非是切换栈帧跟当前寄存器,

协程同样是切换栈帧跟当前寄存器,

3983 次点击
所在节点    Go 编程语言
40 条回复
q397064399
2017-05-14 07:56:50 +08:00
另外一个问题,如果大量的线程调用阻塞 IO 会引起 cpu 大量的空转吗?

golang 的协程采用的是将阻塞 IO 用 epoll/select 等多路 IO 复用技术包装了一下,
说白了就是用操作系统注册的硬件中断来判断哪些阻塞式的 IO 是可以返回的,然后切回协程。
binux
2017-05-14 08:04:34 +08:00
q397064399
2017-05-14 08:30:07 +08:00
@binux 找了很多知乎 google 的回答,,看得主要还是懵逼啊,,
一个代码的线性逻辑流,无非是调用的栈帧跟当前寄存器 在这个上面,线程跟协程应该没有本质区别,
无非是 golang 的协程,使用了 select epoll 包装了同步 IO,这样在语言层面上可以切换协程,
而线程通常采用的是阻塞 IO 使用的是系统的调度,两者调度存在的区别是 select/epoll 是多路 IO 复用技术,
传统的阻塞 IO 是等待系统调用返回
q397064399
2017-05-14 08:34:13 +08:00
@binux 我觉得我这个问题,完全不是通过 google 就能解释的清楚的
zmj1316
2017-05-14 08:50:06 +08:00
线程是操作系统实现的,所以切换线程时需要保存和重建栈和寄存器的状态
协程一般是编程语言实现的,“协程同样是切换栈帧跟当前寄存器”这句话是不对的,我的理解是协程是变成语言的语法糖,简化了复杂的状态机,实际上还是在同一个线程和地址空间里面执行的。

你 google 解决不了估计是操作系统知识忘了吧...
limhiaoing
2017-05-14 08:52:29 +08:00
goroutine 运行时栈初始是 2kb,而线程一般是几 MB,当创建几千个得时候,goroutine 的内存开销远小于线程。
goroutine 的调度不需要进入内核,也比线程的开销要小。
q397064399
2017-05-14 08:55:32 +08:00
@zmj1316 寄存器应该是要切换的,,一个协程对应一个函数,函数里面有局部变量 等计算的临时结果,如果是一个 for 循环,golang 协程要当前这个循环停下来,然后去执行另外一个协程,,肯定要保存,
q397064399
2017-05-14 08:56:18 +08:00
@limhiaoing 如果是这样的话,那应该好理解一点,,但是 Linux 使用线程池,或者使用 ulimt 是可以调节初始栈的大小的,
q397064399
2017-05-14 08:57:04 +08:00
@zmj1316 栈肯定是要切换的,一个协程对应是一个函数调用的栈帧,,
q397064399
2017-05-14 08:58:34 +08:00
@limhiaoing 不用进入内核是一个依据,毕竟从用户态切换到内核态,也是有开销的
wwqgtxx
2017-05-14 09:04:10 +08:00
@q397064399 个人建议你看看操作系统原理方面的书,一个线程的切换他还是要经过操作系统本身庞大的调度器,然后修改 pcb 块,重排等待队列等等过程
而协程基本上只是压栈和改寄存器,其他步骤少了很多
limhiaoing
2017-05-14 09:04:12 +08:00
goroutine 现在的实现也不是严格的协程了,协程是非抢占的,goroutine 的调度是抢占的。
q397064399
2017-05-14 09:09:53 +08:00
@wwqgtxx 多谢,,因为我个人对线程跟协程 理解的还是很浅显的,,
我对协程的理解是 其调度是语言级别的,无非是在使用阻塞等 IO 的时候 优化一下,,
让当前协程停下来,去跑其它的协程,当然一些耗时的循环 我不知道 golang 的协程是怎么中断 然后调度其它协程的


@limhiaoing 调度方法上的 抢占跟非抢占是什么区别?类似加锁的公平锁跟非公平锁么?
kier
2017-05-14 09:18:54 +08:00
@q397064399 golang 也是多线程的啊,只不过 1 个线程可能对于多个协程,所以耗时的循环,也就阻塞一个线程,不会导致整个程序阻塞
zmj1316
2017-05-14 10:03:42 +08:00
@q397064399 我只知道,C#里面的 enumerator 虽然写的时候是一个函数,其实是作为一个数据结构(类)存在的,看起来函数里面的局部变量是在栈里面存在的,其实是这个类里面的成员,你写 yield 的时候,语言就是给类多加了一个状态。
这个有很明显的问题就是在 enumerator 的 for 循环里面 capture 循环里面的局部变量和在普通函数里面不同,因为 capture 到的可能实际上是一个成员变量。没写过 go,不知道 go 是怎么实现的...
zmj1316
2017-05-14 10:22:55 +08:00
@q397064399 #13

线程的抢占由操作系统完成,因为操作系统可以在保存完整的栈和寄存器等信息,因此在任何时候都可以抢占正在执行的线程,之后再还原回去,开销大;
我所理解的协程的调度不会保存完整的栈和寄存器信息,所以只能在预先设定好的位置调度出去(类似存档点?),但是开销小,并且可以由应用程序控制调度;
wwqgtxx
2017-05-14 10:25:23 +08:00
@q397064399 go 的耗时循环的退出机制其实是在编译的时候往里面插代码,执行一段时间就自行退出
wwqgtxx
2017-05-14 10:28:00 +08:00
@zmj1316 goroutine 的切换代码依然是用 asm 实现的,这个在 golang 的源代码中有,要不然你没办法保存执行到了函数中的第几句,也没法保存过程中的局部变量的值呀
kindjeff
2017-05-14 10:37:24 +08:00
相比操作系统线程,协程维护的信息更少;
协程调度机制更简单;
协程调度的时候始终在用户态,不用从用户态切换到内核态。
kindjeff
2017-05-14 10:53:36 +08:00
我还有一个不知道对不对的类比:
可以把“函数调用” “回调” “协程”编程模型拿来比较一下。
函数调用是你在代码里显式的写出来,然后代码运行到这里就会进入调用的函数,设置相关的栈上下文等信息;
回调的方法是在程序运行过程中知道发生了某个事件,当前顺序执行的代码让出执行权,然后进入回调的函数设置相关上下文;
而如 Python 的协程是在你显式的写了让出( yield/await )之后,当前正在顺序执行的代码切换到另一个协程的上下文,一个协程和回调函数一样,只是协程切入切除的入口和出口不是唯一的。所以协程非常容易实现,但是操作系统线程还要维护线程的优先级 /线程的缓存,在多核的时候还要考虑负载均衡,切换的时候 /同步原语 /锁的时候还要回到内核态,开销要更大。

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

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

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

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

© 2021 V2EX