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

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

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

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

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

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

4092 次点击
所在节点    Go 编程语言
40 条回复
BiuBiuBiuX
2017-05-14 11:34:45 +08:00
@binux 这个是怎么做到的啊
zonyitoo
2017-05-14 12:23:16 +08:00
@zmj1316 显然是需要保存栈和寄存器信息,如果是共享栈模式,在每次切换的时候还要把栈的内容复制一份存起来。

你说的那些保存很少的,是类似于 C#那样的 stackless coroutine,本质是一个状态机,和手写状态机没什么两样,开销与函数调用一致。但显然 Goroutine 不是这样的东西,它是 stackful 的。
willm
2017-05-14 12:42:01 +08:00
线程涉及到用户态和内核态的切换,成本很高;协程是纯用户态实现的,成本很低
SlipStupig
2017-05-14 12:42:31 +08:00
go coroutine 是用的 M:N 的并发模型,M 个线程调度 N 个 coroutine,coroutine 在运行的时候是用户态去切换,而线程切换是内核到用户态通信,然后用户态收到消息后作出响应的操作,所以线程更重一些,而 golang 的 coroutine 的是保存在 TLS 里面当要用的时候进行创建和销毁
zmj1316
2017-05-14 13:17:43 +08:00
@zonyitoo 学习了,感觉 go 的方法和 fiber 有点像
itfanr
2017-05-14 14:28:32 +08:00
线程切换太重量级 而且各种锁乱飞
CRVV
2017-05-14 17:49:15 +08:00
@limhiaoing
“ goroutine 的调度是抢占的”,求这句话的来源

我看到 goroutine 的调度不是抢占的
https://github.com/golang/go/issues/10958
https://github.com/golang/go/issues/11462
noli
2017-05-14 17:56:03 +08:00
先澄清几个问题:

1. 什么是协程 ( Coroutine )?

协程可以主动放弃 CPU 使用权并交给约定的另外一个协程,根据约定方式的差异——明确指定跳转到另一个协程 或者 交还给调用者(另一个协程)——可分为 非对称(两种方式都可以) 和 对称协程(只允许交还 CPU 给调用者) 两种。但这种区分方法并不一定就是业界共识,只是有论文提出过这种概念。

抛开协程的物理实现方式不谈(即不讨论栈帧和寄存器之类的概念),协程必然存在一个执行上下文的概念。协程切换前后,其执行上下文是不变的,就好像这个切换没有发生过一样。这一点和 线程切换是一样的。

从这个概念来看,以我所知,goroutine 并不是 coroutine 协程。

因为实际上程序员并不能自行指定切换到哪一个 goroutine,而是由 gosched 来自行决定下一个要从 suspend 变成 active 的 goroutine。

但 goroutine 也不能说是抢占式的 (preemptive),因为 goroutine 被切换的时机是明确的,就是访问 chan 等等应该 block 的时候。

2. 协程的实现方式及代价

把执行上下文的这个概念,对应到物理实现方式的时候,有很多种实现方式。

C# yield return 搭配 IEnumeratable 语法糖 和 async await 的实现方式是,在用户代码之中插入状态机代码和数据,使得从程序员的角度看来是保持了上下文不变。这是编译器魔法,是编程语言相关的。

Windows Fiber API 以及 boost::fiber boost::coroutine 的实现方式是保存寄存器状态和栈帧数据。这实际上就是通用 内核 实现 进程切换的 技术变种(所以实现方式是平台相关的),可以称为 平台魔法。

这两种魔法跟线程切换的最大区别就是无需系统内核介入( windows fiber 实际上应该不需要深入内核,但是不是真的没有进入内核,我并没有研究)。因此,假设在同一个 OS,同一个 CPU 满负载都用于协程/线程的情况下,支持发生协程切换的最大次数,很大可能是高于线程切换的。

但是这个数据对实践并没有什么指导意义。因为实际生产环境中很少能把 CPU 合理地用满。

两种实现方式都需要额外都内存来存储上下文,只不过编译器魔法保存上下文的内存使用概率可能高一点(因为明确知道上下文都数据大小)但是会丧失调用栈上下文的信息,而平台魔法的上下文数据通常是要预先分配(通常会过量分配)。
binux
2017-05-14 18:12:03 +08:00
@q397064399 #3
协程是在一个线程空间内的,至少对于操作系统是这样的。
你要从寄存器上比较,那就太底层了,不同编译参数对寄存器的使用都是不同的,在这个层面比较没有意义。
rrfeng
2017-05-14 18:53:00 +08:00
我的理解
线程本身是操作系统概念,为了解决进程切换的高代价来实现的。
后来发现线程切换也不是很完美,那么就有了『用户态线程』以及『协程』,这两个我不是很区分有什么不同,但是确定的是都是在用户态直接切换的,比系统线程轻量,不需要进入内核等。
而通常意义上的协程例如 lua,切换是手动的或者确定的,你必须用 yield/await 来控制。但是 golang 里,你只需要把 goroutine 扔给 golang 的调度器,它自然帮你干好这些事情。实际上调度器开启了 N 个线程来分配 goroutine,也就是通常说的 M:N,比起手动控制,只简化为一种方式:后台运行,通过 channel 沟通,大大简化了我等程序员的逻辑负担……
dawniii
2017-05-14 19:20:29 +08:00
@wwqgtxx 原来是这么实现类似抢占式调度的。。。 好像 go 比较早的版本里 如果有一个很大的 for 循环不会自动让出,后来就是这么插代码做的啊
bigpigeon
2017-05-14 19:32:40 +08:00
操作系统是不知道携程的,携程是用户态的线程
可以这么去理解
loqixh
2017-05-14 19:44:53 +08:00
@rrfeng c#默认也是 M:N, 但是也可以自己实现 1:N
ghostheaven
2017-05-14 22:03:32 +08:00
@bigpigeon 没用过 go。记得如果是用户态线程的话,不同的线程可能是调度在同一个内核线程上的,他们的内存空间是完全共享的,不知道这样会不会给 go 带来安全隐患。
cloud107202
2017-05-14 22:15:00 +08:00
@dawniii 1.2 之后实现的,这种方式弊端是方法不能被内联,否则还是不能让出时间片。
wwqgtxx
2017-05-14 22:34:51 +08:00
@ghostheaven 并不会呀,系统态线程本来就是内存空间完全共享呀,不共享的只有寄存器状态
smallHao
2017-05-14 22:40:15 +08:00
如果你想理解他们的区别,那你最好知道它们的实现方式,thread 可以看 pthread,coroutine 的话可以去看看 lambda calculus 里的 CPS
araraloren
2017-05-15 09:15:44 +08:00
@q397064399
协程的本质是函数调用的切换,和线程那么重量级的东西不是一回事
函数切换快不快?所以协程可以达到那么大的并发量
可以说协程之于线程 其实类似与 线程 之于进程
woshixiaohao1982
2017-05-15 11:51:20 +08:00
@araraloren 即使是 函数调用也有上下文跟栈帧的.. 线程也没多多少东西
VYSE
2017-05-15 15:26:55 +08:00
Go 里有两种调度, goroutine 和系统 thread 的.
runtime.GOMAXPROCS(1)下所有 goroutine 在一个 thread 下根据类似 greenlet 方法进行调度,在某些 call 里会 yield 才会切换 cpu 资源给下一个 goroutine.
但 runtime.GOMAXPROCS(n)下不同 goroutine 会跑在不同 thread 下,就存在同一个时间多个 goroutine 同时运行,这时你就得按传统多任务编程的方法去写代码,不然 crash 事小,数据紊乱事大

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

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

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

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

© 2021 V2EX