今天有个面试官和我讲 go 的协程比系统的线程更慢,这个我不能理解

191 天前
 qxdo1234
我不知道他的回答和我的回答哪个是有依据的,麻烦有大佬知道的,指正我一下,仅是探讨技术对错问题,谢谢。

他一上来问我 go 的协程能否做到线程不能做到的事,而且至少重复问了我 3 次。我回:总的来说是可以加快程序的运行效率。他就讲出了他的理论和依据,既然 go 协程是要由线程去接管运行的,资源也是从线程分来的,那么何谈加快运行效率,你原本线程要做的事还是没变,而且还多了管理协程的开销。后来他又提了一些问题试图来让我相信他这个理论和依据,不知道其中某个问题的时候,我回的是:不耗费资源的操作时,协程要更快,在耗费资源较多时,还是线程更快。然后他还是在反复和我纠结这个问题。在我看来 go 的协程实现是分割原本线程的资源,做到更轻量化和更灵活的资源调度。调用完资源空闲了就可以及时 gc ,就可以用更少的资源去做更多的事。到最后,他才说,我的大前提是,要做的事是非常耗费资源的操作,就感觉很搞不懂。

虽然我面试问题回答的很差,但是我依旧想知道这个问题,不知道有没有大佬来和我指正一下,
另外他还有第二个问题,既然协程这套理论这么牛逼,那么 c++ 为什么没有呢?(在我印象里 c++只有线程)
9276 次点击
所在节点    Go 编程语言
76 条回复
williamx
189 天前
如果单纯的比较执行速度,当然是线程更快。但是协程轻量,占用资源少,简单。

所以,“不耗费资源的操作时,协程要更快,在耗费资源较多时,还是线程更快。”这个说法是完全错误的。
jeffmingup
189 天前
协程优势在上下文切换和初始化占用内存少上,适用于 io 密集场景( web ),可以开很多协程。cpu 密集场景上,一个协程对比一个线程就没啥优势了。
bbao
189 天前
你可以反问他一句:用户态和内核态的区别,话题终结。
bbao
189 天前
@bronyakaka
1 、goroutine 初始栈 2KB (会动态增长的,并不是说一定省内存了),而操作系统线程的栈通常 1MB
线程大小通常可以设置,但是也远比 2KB 要大;

缺点也有:
1 、没法控制怎么分配到 cpu 核上,开几个协程可能都挂一个线程上,,利用不了多核资源
GMP 模型的 M 就是利用了 Cpu 的资源,至于什么时候系统会额外的创造 M ,可以进一步了解 GMP 原理;你这第一个缺点不成立

3 、协程一旦执行阻塞系统调用,会把整个线程阻塞,导致该线程无法执行其他 goroutines ,降低并发效率
两个问题:
「 1 」,请问哪一个语言 (串行之行时,一旦阻塞了当前线程,当前线程后续还能之行动作)
「 2 」,当某个 G 被挂起,它仅会阻塞当前 G ,P 且会脱离 M ,自行寻找其他 M ,如果 M 此时不足且需要创建的话,会临时创建“M”,这恰好是优点

4 、协程不适合 CPU 密集型任务,因为没什么 io ,上下文切换反而增加了开销,,调度器也有损耗,不如用多线程直接绑定到核心上
bronyakaka
189 天前
@bbao #64 你第一、二点回复和我说的有矛盾吗? 3.2 临时创建 M 就没代价了?而且受还 GOMAXPROCS 限制,不就是降低并发效率。你是补充还是反驳?反驳也没反驳到点上啊 还是你没看懂我说的
bbao
189 天前
@bronyakaka 看清了,降低并发性这部分:

我认为"在 IO 密集型业务中,哪怕存在 G 阻塞,M 挂起的行为时,Goroutine 的性能也是优秀的比线程并发强,所以它与自己比较,会降低并发行,但是这点对于其他线程并发场景,并不重要,本身就很高。" 也就不算缺点。
lesismal
189 天前
> thread 和 coroutine 最大的区别就是上下文切换是否自主可控。因为 thread 切换不可控,所以要应付数据一致性的地方比 coroutine 多得多。

@moudy #51 我感觉这没说到点子上,绝大多数应用层自己主动使用 go runtime.Gosched 也只是出让调度、业务逻辑本身并没有改变、runtime 调度回来后用户的逻辑代码还是那个执行顺序。

我觉得根本的点是 thread 、goroutine 的成本和数量、以及对应着是否需要用户主动操作实例(例如 conn )在多个 thread 或者多个 goroutine 之间的切换。

高在线业务,不可能为每个 conn 都创建一个 thread ,所以每个 conn 是在不同的 thread 中流转,比如处理网络的 io 线程池、cpu 消耗的 逻辑/worker 线程/线程池、数据库等其他基础设施的线程池,不同的线程池上下游之间要有 task 队列串联起来。用 thread 的语言和方案的框架封装和使用上更复杂,而且不能写同步代码。async await yield resume 那些手动档 coroutine 和 goroutine 、erlang 进程的自动挡是不一样的、远不如自动档方便。goroutine 和 erlang 进程轻量,每个连接一个、同步代码一把梭就完事了。
thread 方案对一致性的要求更高,goroutine/erlang 进程除非涉及连接之间的复杂交互之类的、否则不需要对一致性做太多麻烦的事情
lesismal
189 天前
我个人对这个面试双方的评价,不一定对:
1. OP 的编码主要是应用层,对基础知识不那么深入;
2. 面试官是杠精;

面试这种问题、如果候选人的回答已经反映出了 1 ,就没有必要像 2 这样纠结基础知识了:
1. 如果是招来做系统工程师之类的基础设施、底层研发,直接 pass 不考虑了,没必要继续杠
2. 如果是做 curd 之类的业务开发,了解这些基础知识也没用、只需要用来判定技术深度和技术等级、薪资就可以了,也没必要继续杠
lxdlam
189 天前
@w568w #9 如果你*每次*都使用 `launch(newSingleThreadContext()){}` 的话,它确实能被*认为*是一种线程的调度集合。但这只是 Kotlin 给出了底层调度的用户调优空间,Kotlin 的协程仍然是基于状态机的调度。
w568w
189 天前
@lxdlam 是的。我举这些边界情况其实想说明的是每个人口中的「协程」可能都是不同的意思,因此最好不要随便使用这个已经被滥用的无效术语。就叫 用户线程/生成器/线程池 就好了。
lxdlam
189 天前
所有的 Coroutine ,无论有栈无栈还是夹在中间的什么混合模式,这个代码总是要被执行的。谁来执行呢?我援引 Solaris 最经典的 LWP 设计:



本质来说,从一个纯粹的 User program 或者被语言包裹好的这个"乌托邦"来看,OS Thread 就是新的 CPU Cores ,我们不妨叫他 Coroutine Process Unit ,其他的概念几乎等效,无非就是在这个新的 CPU Cores 上看这个新的调度器在做什么操作罢了;只不过我们有一个得天独厚的优势,就是这一切都发生在 User Space ,所有的 context switch 之类的操作都有很大空间不会切换到 Kernel Space ,也不需要跟特定 OS 信息交互,这样就能节省一大部分开销。通常认为的所谓

但是这能说明谁比谁更快吗?尝试用你的 workload 回答我下面三个问题:
1. 这个程序对 CPU 的 locality 要求有多高?无论是基于 NUMA 的同 Node 访问或者 Cache line 的访问优化,你需要它表现到什么样子?
2. 这个程序对 IO 的定制需求如何?一个 epoll/kqueue 就足够,还是需要 DMA/RDMA 这种需要外部 driver 交互的 IO 支持? Runtime 跟你的通信调度如何处理?
3. 这个程序的计算密集程度如何?是可以切成无互访的无状态并发,直接分发完数据 CPU 猛算,还是有高频的互访,实际上存在特定的通信效率瓶颈?

这三个问题只是一个例子,如何去考虑线程跟 Coroutine 的开销。大部分 Coroutine 退化到最后就是一堆普通的线程,插入了一些语言 Runtime 带来的额外钩子,这些钩子的成本显然随着 Coroutine 的增加会有所上升。

但是这个边际成本出现剧增的点在哪儿?不同 Workload 的答案不一样,拍脑袋得不出答案。不要想当然认为线程跟 coroutine 谁更好谁更快,做工程的要拿 benchmark 说话,而不是讨论假大空的概念。如果真对这个问题感兴趣,可以在一些 HPC 任务上跑跑 poc ,看看 HPC 工程师调 CPU Affinity 和 openmp 的观察和优化逻辑。
lxdlam
189 天前
@lxdlam #68 第二段漏了:通常认为的所谓 -> 通常认为的所谓 Coroutine 更快,建立在那些需要频繁 context switch 的任务上,比如最经典的就是 I/O polling 。
sampeng
189 天前
大部分人讨论技术问题。其实是个人情世故。。。
cenbiq
189 天前
为什么会有线程和协程哪个快这样的问题?当然是线程执行更快啊。能说出协程更快的怕是连协程是干什么的都没搞明白,协程对于开发更好用是因为协程帮助开发者自动化的管理了线程的利用率和上下文切换时机。这对于高频 IO 类型的程序更加友好(尤其是对于做应用层开发而言),让人产生了协程更快的“错觉”,应该说协程让整体系统资源运行更协调,而不是更快。
w0017
189 天前
你是超级高并发业务吗?不是的话,别折腾了。
guonaihong
188 天前
如果让我回答这题.

1. 首先 go 的协程的实现是跑在线程上面的,简单理解,就是一个在线程循环里面,从一个队列里面不停取 callback 执行,这个 callback 就是协程,一个 thread 默认要占用 10MB(当然线程也能调整默认栈的大小,只是会加大爆栈的可能性)。内存,一个 callback 可能只要 2KB(维护上下文的栈指针)。所以起 100w 个线程/协程,内存占用分别是
100 万个 10MB 对象:约 9.54 TB
100 万个 2KB 对象:约 1.91 GB , 协程可以大量节约内存,所以算回答协程能做到,线程不好做到的事情

2. 协程可以提升 thread 的利用率,减少 cpu 摸鱼时间。
假如代入到 c 语言里面,你写了一个前面用 epoll 解析网络协议的代码+业务放在多线程里面处理。这里面要在业务代码访问第三方的 sdk ,而且还是阻塞式的 socket 。当调用到这个 sdk 时,你的线程就在阻塞。这时就会造成线程浪费(主要是内存)。如果换种方式,用 go 举例。同样使用第三方 sdk ,第三方的 socket 都被 epoll 管理,所以阻塞只要标记下阻塞的 callback 依赖 gobuf(协程栈),这时候你的物理线程要不有活就干,没活就自我结束。当用时才有物理线程,就是利用率最高的表现。

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

https://ex.noerr.eu.org/t/1120244

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

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

© 2021 V2EX