Golang Go语言中 Go routines 和 Java 线程池的区别有哪些?
Golang Go语言中 Go routines 和 Java 线程池的区别有哪些?
比如使用上,原理设计上
go routine 是官方提供的,线程加协程池
更多关于Golang Go语言中 Go routines 和 Java 线程池的区别有哪些?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
用户态 vs 内核调度
Go 不只是线程,还包括协程,具体调度原理就不清楚了
1.协程的成本开销比线程小
2.协程是用户调度,线程是交给系统调度
使用上协程应该比线程要多操点心,但 Go 自己有调度器协程使用上其实不怎么操心
其他的,可能就是省了创建线程管理线程的代码,只写一个关键字就行了
go 的用户态调度,是不是可以抽象理解为把阻塞代码前后进行分割,分成小的代码块,放入线程池中执行呢
没那么简单,但一般情况下如此。
不清楚 java 的线程池,没办法比较
如果想了解 goroutine,推荐一篇文章: https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/
如果想从源码的角度来了解实现,可以看下《 Go 语言学习笔记》的源码剖析,虽然是基于 1.5 版本,但是调度器方面在以后的的版本中更新不多,所以完全没问题
go 语言的包管理器现在完善的如何?
基本都 go mod 了。
go 的协成是一种轻量级的线程,一个线程通过 go 的调度动态地与多个协程绑定。
线程
一个线程跑满,就吃饱一个 cpu
N 个线程跑满对应 N 个 cpu,只要与 cpu 数量相同多线程就能吃饱 cpu
但是实际代码里会阻塞,一阻塞会自动让出 cpu, 所以在阻塞写法里,程序的线程超过 cpu 数量能提高性能
但是过多的线程数带来的上下文切换回拖慢整体
协程
通过寄存器保存代码片段,遇到阻塞(一般是 io,或者自定义的协程锁),会切换到其他代码片段
所有代码片段的入口存放一个不停排序的队列
一个循环(相当于一个线程)不停的排序这个队列,并弹出最前面的数据获取到代码片段并执行
一般只有一个主线程,由于代码片段的切换由程序自己决定,没有系统级上下文切换,性能好,缺点是单线程
所以性能最好的方式是结合多线程与协程,但是非语言级很难用一个库来实现支持多线程的协程
目前除了 go, dart (也就是 google 的 flutter 所用语言)也有多线程协程支持
我瞎鸡巴说的…我没写过 go 哈哈哈哈哈哈
有兴趣的其实去可以了解一下
CSP ( Communicating Sequential Process )理论
卧槽 大佬,你这段话我要加入到面试复习笔记里
goroutine 的底层也是线程池,G 相当于一个 Runable。
goroutine 的优化是将阻塞都放在用户态,自己调度,就不会创建很多的系统线程了。
比如,将所有的 IO 操作都封装了,底层使用 epoll 之类的非阻塞接口,对外暴露同步阻塞接口,所以不会真正阻塞底层线程。
同步,锁之类的都是在用户态
协程的本质就是 continuation
不如问一下,go 的协程和 kt 的协程有什么区别。
个人认为 kt 的用起来更简单
大部分的 thread pool 都不用实现了 , 在一定程度上协程随便开。
Ps:一定程度上 ,我没试过开上万个 goroutine, 超过一定程度我估计也会吃不消
记这个不如好好把一个协程库的代码读透,比如 python 的 eventlet
读透了搞清楚模型模型了就自然理解了…啥语言都一个样,记下来真没用…
话说…好像这些都是从 erlang 里出来的?
协程 vs 线程
『用户态调度,是不是可以抽象理解为把阻塞代码前后进行分割,分成小的代码块,放入线程池中执行呢』
原理上是这样没错,但是有个重要的区别:
协程:主动放弃 cpu
线程:抢占式调度
所以,对于 io 型应用来说,前者少了很多后者『抢到了 cpu,发现数据还没准备好,又放弃 cpu 』的消耗。
楼上的一个线程跑满吃掉一个 CPU,多个线程多个 CPU 似乎是没分清线程和进程的区别
如果我没理解错的话,进程一次只能在一个 CPU 上,进程可以调度到其他 CPU 上,但是一次只会在 1 个地方 而线程,是运行在进程的上下文中的。
#22
抢占式调度怎么理解呢,通常来说,支持抢占式不是更好吗? Go 不是也有说要支持抢占式调度的协程。
您说的 『抢到了 cpu,发现数据还没准备好,又放弃 cpu 』 是在有锁的情况下才会争抢吧?而且像 Java 的 AQS,抢不到锁也是直接排队等待解锁了。争抢的情况似乎也不是很明显。
我的理解只是这样调度的粒度更小,所以效率提高了。
#22
如果是说线程和 CPU 数量相同,可以减少线程之间被 CPU 核切换。那其实线程池也是可以配置的。
线程池还是靠 OS 来调度线程,go 的携程是语言内置的调度器来调度,当然还是受操作系统调度(
『线程是 cpu 调度的基本单位』。
协程理论上不需要抢占式调度,而且理论上协程不可中断(没有 handle 指向它),除非你把他的执行线程干掉,至于为什么搞出个抢占式的,我只能这么理解『为了防止某个协程一直不主动释放,导致其它饿死』,实现也是怪怪的,做的太多就变成"用户态线程了"。
至于为什么说协程调度效率高:对于线程调度来说,就是给自己打个 runnable 的标记,然后等调度器赏光,但是对于调度器大爷来说,它不理解你的任务细节,每次给你 300 个时间单位,你可能 10 个单位时间就干完了,剩下的 290 只能还回去(对于 IO 密集型应用这很常见),但是对于协程来说,我干完了可以把这机会给兄弟们用,直到用完这 300 个单位。
线程池没有调用栈,做不到协程的效果。
#27 谢谢,你讲的微观细节很透彻,我从来没考虑过,不过真的有这么多时间片被浪费了吗
"粒度更小"一般对应的是”更加灵活“与”效率更低“
当然不是说一刀切效率就一定更高,一刀切效果好的前提是切的地方准确
协作式多任务就正好满足这一条件,进程在明确自己不需要 CPU 时放弃 CPU,而在做事情时操作系统不来烦你,实际就保证了 CPU 一直都在做有用的事情
可以类比在你写代码时没事总是来催你的产品,没事总是来找你”支持“的队友
缺点是如果你完成了任务一直不汇报,那整个项目就 block 在你这了
我认为现代编程语言(好吧 … 强行把 Go 称为现代编程语言也过得去)对这个问题提供了很好的抽象,所以现在才有这么多人觉得这么好用。这在 Windows 3.1 和 MacOS Classic 那个年代大概是难以想象的吧
如果只是协作式确实有阻塞其他协程饥饿问题,但 goroutine 调度目前是有 timeout 检测一旦阻塞过久会尝试主动让出执行权,只是让出时机是在特定情况下,并不全面。所以现在看 goroutine 有点半(伪)抢占的意思。后面版本据说会更进一步做成抢占式,不过这样竞态问题会更频繁?
现在的意思好像是,死循环,长时间阻塞到内核态这种情况,就会把执行这个协程的线程扔出调度器?相对于是自动分开一个快的调度线程池和一个慢速线程池。
1.14 版本将会加入基于信号的抢占调度,所以 goroutine 从来就不是协程,它的定义就是并发执行的单元,只不过早期的实现是协作式的。在加入函数入口处的抢占检查之后就已经是半协作半抢占调度了,现在要加入完备的抢占调度了。goroutine 的语义也一直和线程接近而不是协程。什么时候调度,程序员没法判断,并发访问内存也需要上锁,就是当作线程来用的,就是一种用户态线程的实现。
针对Golang中Go routines(goroutines)和Java线程池的区别,可以从以下几个方面进行阐述:
- 轻量级与重量级:goroutines是轻量级的,每个仅需大约2KB的内存,而Java线程是重量级的,每个线程需要更多的内存和资源。
- 调度与管理:Go运行时负责调度goroutines,根据系统负载自动调整数量。Java线程池由JVM管理,但不像Go运行时那样动态调整线程数量,不过它允许开发者更精细地控制线程的数量和生命周期。
- 上下文切换:由于goroutines的轻量级特性,上下文切换的开销较小。然而,在处理大规模并发任务时,Go可能会创建大量goroutines,导致频繁的上下文切换增加CPU开销。相比之下,Java线程池在这方面可能更加稳定。
- 性能:在并发任务数量不是特别多的情况下,goroutines的性能通常优于传统的线程池。但对于大规模并发任务,Java线程池可能会因为更好的线程控制而表现出更稳定的性能。
综上所述,goroutines和Java线程池各有优势,选择哪种并发模型取决于具体的应用场景和系统资源。在高并发、需要轻量级线程的场景中,goroutines更具优势;而在需要精细控制线程数量和生命周期的场景中,Java线程池可能更合适。