Golang Go语言中协程比线程轻量级 轻量级在哪里

发布于 1周前 作者 phonegap100 来自 Go语言

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

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

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

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


Golang Go语言中协程比线程轻量级 轻量级在哪里
41 回复

另外一个问题,如果大量的线程调用阻塞 IO 会引起 cpu 大量的空转吗?

golang 的协程采用的是将阻塞 IO 用 epoll/select 等多路 IO 复用技术包装了一下,
说白了就是用操作系统注册的硬件中断来判断哪些阻塞式的 IO 是可以返回的,然后切回协程。

更多关于Golang Go语言中协程比线程轻量级 轻量级在哪里的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


找了很多知乎 google 的回答,,看得主要还是懵逼啊,,
一个代码的线性逻辑流,无非是调用的栈帧跟当前寄存器 在这个上面,线程跟协程应该没有本质区别,
无非是 golang 的协程,使用了 select epoll 包装了同步 IO,这样在语言层面上可以切换协程,
而线程通常采用的是阻塞 IO 使用的是系统的调度,两者调度存在的区别是 select/epoll 是多路 IO 复用技术,
传统的阻塞 IO 是等待系统调用返回

我觉得我这个问题,完全不是通过 google 就能解释的清楚的

线程是操作系统实现的,所以切换线程时需要保存和重建栈和寄存器的状态
协程一般是编程语言实现的,“协程同样是切换栈帧跟当前寄存器”这句话是不对的,我的理解是协程是变成语言的语法糖,简化了复杂的状态机,实际上还是在同一个线程和地址空间里面执行的。

你 google 解决不了估计是操作系统知识忘了吧…

goroutine 运行时栈初始是 2kb,而线程一般是几 MB,当创建几千个得时候,goroutine 的内存开销远小于线程。
goroutine 的调度不需要进入内核,也比线程的开销要小。

寄存器应该是要切换的,,一个协程对应一个函数,函数里面有局部变量 等计算的临时结果,如果是一个 for 循环,golang 协程要当前这个循环停下来,然后去执行另外一个协程,,肯定要保存,

如果是这样的话,那应该好理解一点,,但是 Linux 使用线程池,或者使用 ulimt 是可以调节初始栈的大小的,

栈肯定是要切换的,一个协程对应是一个函数调用的栈帧,,

不用进入内核是一个依据,毕竟从用户态切换到内核态,也是有开销的

个人建议你看看操作系统原理方面的书,一个线程的切换他还是要经过操作系统本身庞大的调度器,然后修改 pcb 块,重排等待队列等等过程
而协程基本上只是压栈和改寄存器,其他步骤少了很多

goroutine 现在的实现也不是严格的协程了,协程是非抢占的,goroutine 的调度是抢占的。

多谢,,因为我个人对线程跟协程 理解的还是很浅显的,,
我对协程的理解是 其调度是语言级别的,无非是在使用阻塞等 IO 的时候 优化一下,,
让当前协程停下来,去跑其它的协程,当然一些耗时的循环 我不知道 golang 的协程是怎么中断 然后调度其它协程的


调度方法上的 抢占跟非抢占是什么区别?类似加锁的公平锁跟非公平锁么?

golang 也是多线程的啊,只不过 1 个线程可能对于多个协程,所以耗时的循环,也就阻塞一个线程,不会导致整个程序阻塞

我只知道,C#里面的 enumerator 虽然写的时候是一个函数,其实是作为一个数据结构(类)存在的,看起来函数里面的局部变量是在栈里面存在的,其实是这个类里面的成员,你写 yield 的时候,语言就是给类多加了一个状态。
这个有很明显的问题就是在 enumerator 的 for 循环里面 capture 循环里面的局部变量和在普通函数里面不同,因为 capture 到的可能实际上是一个成员变量。没写过 go,不知道 go 是怎么实现的…

#13

线程的抢占由操作系统完成,因为操作系统可以在保存完整的栈和寄存器等信息,因此在任何时候都可以抢占正在执行的线程,之后再还原回去,开销大;
我所理解的协程的调度不会保存完整的栈和寄存器信息,所以只能在预先设定好的位置调度出去(类似存档点?),但是开销小,并且可以由应用程序控制调度;

go 的耗时循环的退出机制其实是在编译的时候往里面插代码,执行一段时间就自行退出

goroutine 的切换代码依然是用 asm 实现的,这个在 golang 的源代码中有,要不然你没办法保存执行到了函数中的第几句,也没法保存过程中的局部变量的值呀

相比操作系统线程,协程维护的信息更少;
协程调度机制更简单;
协程调度的时候始终在用户态,不用从用户态切换到内核态。

我还有一个不知道对不对的类比:
可以把“函数调用” “回调” “协程”编程模型拿来比较一下。
函数调用是你在代码里显式的写出来,然后代码运行到这里就会进入调用的函数,设置相关的栈上下文等信息;
回调的方法是在程序运行过程中知道发生了某个事件,当前顺序执行的代码让出执行权,然后进入回调的函数设置相关上下文;
而如 Python 的协程是在你显式的写了让出( yield/await )之后,当前正在顺序执行的代码切换到另一个协程的上下文,一个协程和回调函数一样,只是协程切入切除的入口和出口不是唯一的。所以协程非常容易实现,但是操作系统线程还要维护线程的优先级 /线程的缓存,在多核的时候还要考虑负载均衡,切换的时候 /同步原语 /锁的时候还要回到内核态,开销要更大。

这个是怎么做到的啊

显然是需要保存栈和寄存器信息,如果是共享栈模式,在每次切换的时候还要把栈的内容复制一份存起来。

你说的那些保存很少的,是类似于 C#那样的 stackless coroutine,本质是一个状态机,和手写状态机没什么两样,开销与函数调用一致。但显然 Goroutine 不是这样的东西,它是 stackful 的。

线程涉及到用户态和内核态的切换,成本很高;协程是纯用户态实现的,成本很低

go coroutine 是用的 M:N 的并发模型,M 个线程调度 N 个 coroutine,coroutine 在运行的时候是用户态去切换,而线程切换是内核到用户态通信,然后用户态收到消息后作出响应的操作,所以线程更重一些,而 golang 的 coroutine 的是保存在 TLS 里面当要用的时候进行创建和销毁

学习了,感觉 go 的方法和 fiber 有点像

线程切换太重量级 而且各种锁乱飞


“ goroutine 的调度是抢占的”,求这句话的来源

我看到 goroutine 的调度不是抢占的
https://github.com/golang/go/issues/10958
https://github.com/golang/go/issues/11462

先澄清几个问题:

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 合理地用满。

两种实现方式都需要额外都内存来存储上下文,只不过编译器魔法保存上下文的内存使用概率可能高一点(因为明确知道上下文都数据大小)但是会丧失调用栈上下文的信息,而平台魔法的上下文数据通常是要预先分配(通常会过量分配)。

#3
协程是在一个线程空间内的,至少对于操作系统是这样的。
你要从寄存器上比较,那就太底层了,不同编译参数对寄存器的使用都是不同的,在这个层面比较没有意义。

我的理解
线程本身是操作系统概念,为了解决进程切换的高代价来实现的。
后来发现线程切换也不是很完美,那么就有了『用户态线程』以及『协程』,这两个我不是很区分有什么不同,但是确定的是都是在用户态直接切换的,比系统线程轻量,不需要进入内核等。
而通常意义上的协程例如 lua,切换是手动的或者确定的,你必须用 yield/await 来控制。但是 golang 里,你只需要把 goroutine 扔给 golang 的调度器,它自然帮你干好这些事情。实际上调度器开启了 N 个线程来分配 goroutine,也就是通常说的 M:N,比起手动控制,只简化为一种方式:后台运行,通过 channel 沟通,大大简化了我等程序员的逻辑负担……

原来是这么实现类似抢占式调度的。。。 好像 go 比较早的版本里 如果有一个很大的 for 循环不会自动让出,后来就是这么插代码做的啊

操作系统是不知道携程的,携程是用户态的线程
可以这么去理解

c#默认也是 M:N, 但是也可以自己实现 1:N

没用过 go。记得如果是用户态线程的话,不同的线程可能是调度在同一个内核线程上的,他们的内存空间是完全共享的,不知道这样会不会给 go 带来安全隐患。

1.2 之后实现的,这种方式弊端是方法不能被内联,否则还是不能让出时间片。

并不会呀,系统态线程本来就是内存空间完全共享呀,不共享的只有寄存器状态

如果你想理解他们的区别,那你最好知道它们的实现方式,thread 可以看 pthread,coroutine 的话可以去看看 lambda calculus 里的 CPS


协程的本质是函数调用的切换,和线程那么重量级的东西不是一回事
函数切换快不快?所以协程可以达到那么大的并发量
可以说协程之于线程 其实类似与 线程 之于进程

即使是 函数调用也有上下文跟栈帧的… 线程也没多多少东西

Go 里有两种调度, goroutine 和系统 thread 的.
runtime.GOMAXPROCS(1)下所有 goroutine 在一个 thread 下根据类似 greenlet 方法进行调度,在某些 call 里会 yield 才会切换 cpu 资源给下一个 goroutine.
但 runtime.GOMAXPROCS(n)下不同 goroutine 会跑在不同 thread 下,就存在同一个时间多个 goroutine 同时运行,这时你就得按传统多任务编程的方法去写代码,不然 crash 事小,数据紊乱事大

作为IT营GO语言方面的专家,对于Golang中协程与线程的区别有着深入的理解。针对您提出的“Golang中协程比线程轻量级,轻量级在哪里”的问题,以下是我的专业解答:

  1. 内存占用:协程的内存占用远小于线程。每个协程的初始栈大小通常只有几KB,并且可以根据需要动态增长,而线程的栈大小通常是固定的,一般为几MB。这意味着在相同内存条件下,可以创建更多的协程来处理并发任务。
  2. 创建与销毁开销:协程的创建和销毁开销也远小于线程。这是因为协程的调度和管理是由Go语言的运行时系统负责的,而线程的调度和管理则是由操作系统内核负责的。这种差异使得协程在频繁创建和销毁的场景下更加高效。
  3. 调度开销:协程之间的切换开销非常小,因为它们的调度是基于用户态的。而线程的切换开销相对较大,因为线程的调度涉及操作系统内核的参与。这种差异使得协程在并发编程中能够更高效地利用系统资源。

综上所述,Golang中的协程在内存占用、创建与销毁开销以及调度开销方面都比线程更加轻量级。这些特性使得协程在处理大量并发任务时具有更高的效率和更好的性能表现。

回到顶部