无论是协程还是线程应该都是占用内存用来维护函数调用栈帧,
如果是同步 IO 系统阻塞调用的话,
线程无非是切换栈帧跟当前寄存器,
协程同样是切换栈帧跟当前寄存器,
1
q397064399 OP 另外一个问题,如果大量的线程调用阻塞 IO 会引起 cpu 大量的空转吗?
golang 的协程采用的是将阻塞 IO 用 epoll/select 等多路 IO 复用技术包装了一下, 说白了就是用操作系统注册的硬件中断来判断哪些阻塞式的 IO 是可以返回的,然后切回协程。 |
2
binux 2017-05-14 08:04:34 +08:00
|
3
q397064399 OP @binux 找了很多知乎 google 的回答,,看得主要还是懵逼啊,,
一个代码的线性逻辑流,无非是调用的栈帧跟当前寄存器 在这个上面,线程跟协程应该没有本质区别, 无非是 golang 的协程,使用了 select epoll 包装了同步 IO,这样在语言层面上可以切换协程, 而线程通常采用的是阻塞 IO 使用的是系统的调度,两者调度存在的区别是 select/epoll 是多路 IO 复用技术, 传统的阻塞 IO 是等待系统调用返回 |
4
q397064399 OP @binux 我觉得我这个问题,完全不是通过 google 就能解释的清楚的
|
5
zmj1316 2017-05-14 08:50:06 +08:00
线程是操作系统实现的,所以切换线程时需要保存和重建栈和寄存器的状态
协程一般是编程语言实现的,“协程同样是切换栈帧跟当前寄存器”这句话是不对的,我的理解是协程是变成语言的语法糖,简化了复杂的状态机,实际上还是在同一个线程和地址空间里面执行的。 你 google 解决不了估计是操作系统知识忘了吧... |
6
limhiaoing 2017-05-14 08:52:29 +08:00 via iPhone
goroutine 运行时栈初始是 2kb,而线程一般是几 MB,当创建几千个得时候,goroutine 的内存开销远小于线程。
goroutine 的调度不需要进入内核,也比线程的开销要小。 |
7
q397064399 OP @zmj1316 寄存器应该是要切换的,,一个协程对应一个函数,函数里面有局部变量 等计算的临时结果,如果是一个 for 循环,golang 协程要当前这个循环停下来,然后去执行另外一个协程,,肯定要保存,
|
8
q397064399 OP @limhiaoing 如果是这样的话,那应该好理解一点,,但是 Linux 使用线程池,或者使用 ulimt 是可以调节初始栈的大小的,
|
9
q397064399 OP @zmj1316 栈肯定是要切换的,一个协程对应是一个函数调用的栈帧,,
|
10
q397064399 OP @limhiaoing 不用进入内核是一个依据,毕竟从用户态切换到内核态,也是有开销的
|
11
wwqgtxx 2017-05-14 09:04:10 +08:00 via iPhone
@q397064399 个人建议你看看操作系统原理方面的书,一个线程的切换他还是要经过操作系统本身庞大的调度器,然后修改 pcb 块,重排等待队列等等过程
而协程基本上只是压栈和改寄存器,其他步骤少了很多 |
12
limhiaoing 2017-05-14 09:04:12 +08:00 via iPhone 1
goroutine 现在的实现也不是严格的协程了,协程是非抢占的,goroutine 的调度是抢占的。
|
13
q397064399 OP @wwqgtxx 多谢,,因为我个人对线程跟协程 理解的还是很浅显的,,
我对协程的理解是 其调度是语言级别的,无非是在使用阻塞等 IO 的时候 优化一下,, 让当前协程停下来,去跑其它的协程,当然一些耗时的循环 我不知道 golang 的协程是怎么中断 然后调度其它协程的 @limhiaoing 调度方法上的 抢占跟非抢占是什么区别?类似加锁的公平锁跟非公平锁么? |
14
kier 2017-05-14 09:18:54 +08:00
@q397064399 golang 也是多线程的啊,只不过 1 个线程可能对于多个协程,所以耗时的循环,也就阻塞一个线程,不会导致整个程序阻塞
|
15
zmj1316 2017-05-14 10:03:42 +08:00
@q397064399 我只知道,C#里面的 enumerator 虽然写的时候是一个函数,其实是作为一个数据结构(类)存在的,看起来函数里面的局部变量是在栈里面存在的,其实是这个类里面的成员,你写 yield 的时候,语言就是给类多加了一个状态。
这个有很明显的问题就是在 enumerator 的 for 循环里面 capture 循环里面的局部变量和在普通函数里面不同,因为 capture 到的可能实际上是一个成员变量。没写过 go,不知道 go 是怎么实现的... |
16
zmj1316 2017-05-14 10:22:55 +08:00
@q397064399 #13
线程的抢占由操作系统完成,因为操作系统可以在保存完整的栈和寄存器等信息,因此在任何时候都可以抢占正在执行的线程,之后再还原回去,开销大; 我所理解的协程的调度不会保存完整的栈和寄存器信息,所以只能在预先设定好的位置调度出去(类似存档点?),但是开销小,并且可以由应用程序控制调度; |
17
wwqgtxx 2017-05-14 10:25:23 +08:00 via iPhone 1
@q397064399 go 的耗时循环的退出机制其实是在编译的时候往里面插代码,执行一段时间就自行退出
|
18
wwqgtxx 2017-05-14 10:28:00 +08:00 via iPhone
@zmj1316 goroutine 的切换代码依然是用 asm 实现的,这个在 golang 的源代码中有,要不然你没办法保存执行到了函数中的第几句,也没法保存过程中的局部变量的值呀
|
19
kindjeff 2017-05-14 10:37:24 +08:00
相比操作系统线程,协程维护的信息更少;
协程调度机制更简单; 协程调度的时候始终在用户态,不用从用户态切换到内核态。 |
20
kindjeff 2017-05-14 10:53:36 +08:00
我还有一个不知道对不对的类比:
可以把“函数调用” “回调” “协程”编程模型拿来比较一下。 函数调用是你在代码里显式的写出来,然后代码运行到这里就会进入调用的函数,设置相关的栈上下文等信息; 回调的方法是在程序运行过程中知道发生了某个事件,当前顺序执行的代码让出执行权,然后进入回调的函数设置相关上下文; 而如 Python 的协程是在你显式的写了让出( yield/await )之后,当前正在顺序执行的代码切换到另一个协程的上下文,一个协程和回调函数一样,只是协程切入切除的入口和出口不是唯一的。所以协程非常容易实现,但是操作系统线程还要维护线程的优先级 /线程的缓存,在多核的时候还要考虑负载均衡,切换的时候 /同步原语 /锁的时候还要回到内核态,开销要更大。 |
21
BiuBiuBiuX 2017-05-14 11:34:45 +08:00
@binux 这个是怎么做到的啊
|
22
zonyitoo 2017-05-14 12:23:16 +08:00 1
@zmj1316 显然是需要保存栈和寄存器信息,如果是共享栈模式,在每次切换的时候还要把栈的内容复制一份存起来。
你说的那些保存很少的,是类似于 C#那样的 stackless coroutine,本质是一个状态机,和手写状态机没什么两样,开销与函数调用一致。但显然 Goroutine 不是这样的东西,它是 stackful 的。 |
23
willm 2017-05-14 12:42:01 +08:00 via Android
线程涉及到用户态和内核态的切换,成本很高;协程是纯用户态实现的,成本很低
|
24
SlipStupig 2017-05-14 12:42:31 +08:00
go coroutine 是用的 M:N 的并发模型,M 个线程调度 N 个 coroutine,coroutine 在运行的时候是用户态去切换,而线程切换是内核到用户态通信,然后用户态收到消息后作出响应的操作,所以线程更重一些,而 golang 的 coroutine 的是保存在 TLS 里面当要用的时候进行创建和销毁
|
26
itfanr 2017-05-14 14:28:32 +08:00 via Android
线程切换太重量级 而且各种锁乱飞
|
27
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 |
28
noli 2017-05-14 17:56:03 +08:00 1
先澄清几个问题:
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 合理地用满。 两种实现方式都需要额外都内存来存储上下文,只不过编译器魔法保存上下文的内存使用概率可能高一点(因为明确知道上下文都数据大小)但是会丧失调用栈上下文的信息,而平台魔法的上下文数据通常是要预先分配(通常会过量分配)。 |
29
binux 2017-05-14 18:12:03 +08:00
|
30
rrfeng 2017-05-14 18:53:00 +08:00
我的理解
线程本身是操作系统概念,为了解决进程切换的高代价来实现的。 后来发现线程切换也不是很完美,那么就有了『用户态线程』以及『协程』,这两个我不是很区分有什么不同,但是确定的是都是在用户态直接切换的,比系统线程轻量,不需要进入内核等。 而通常意义上的协程例如 lua,切换是手动的或者确定的,你必须用 yield/await 来控制。但是 golang 里,你只需要把 goroutine 扔给 golang 的调度器,它自然帮你干好这些事情。实际上调度器开启了 N 个线程来分配 goroutine,也就是通常说的 M:N,比起手动控制,只简化为一种方式:后台运行,通过 channel 沟通,大大简化了我等程序员的逻辑负担…… |
31
dawniii 2017-05-14 19:20:29 +08:00
@wwqgtxx 原来是这么实现类似抢占式调度的。。。 好像 go 比较早的版本里 如果有一个很大的 for 循环不会自动让出,后来就是这么插代码做的啊
|
32
bigpigeon 2017-05-14 19:32:40 +08:00
操作系统是不知道携程的,携程是用户态的线程
可以这么去理解 |
34
ghostheaven 2017-05-14 22:03:32 +08:00
@bigpigeon 没用过 go。记得如果是用户态线程的话,不同的线程可能是调度在同一个内核线程上的,他们的内存空间是完全共享的,不知道这样会不会给 go 带来安全隐患。
|
35
cloud107202 2017-05-14 22:15:00 +08:00
@dawniii 1.2 之后实现的,这种方式弊端是方法不能被内联,否则还是不能让出时间片。
|
36
wwqgtxx 2017-05-14 22:34:51 +08:00 via iPhone
@ghostheaven 并不会呀,系统态线程本来就是内存空间完全共享呀,不共享的只有寄存器状态
|
37
smallHao 2017-05-14 22:40:15 +08:00
如果你想理解他们的区别,那你最好知道它们的实现方式,thread 可以看 pthread,coroutine 的话可以去看看 lambda calculus 里的 CPS
|
38
araraloren 2017-05-15 09:15:44 +08:00
|
39
woshixiaohao1982 2017-05-15 11:51:20 +08:00
@araraloren 即使是 函数调用也有上下文跟栈帧的.. 线程也没多多少东西
|
40
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 事小,数据紊乱事大 |