问一个协程方面的问题

2021-12-13 15:04:45 +08:00
 kingofzihua

协程究竟解决了什么问题,都在吹协程,像是 go 、kotlin 都有协程,java 本身没有协程,

还有相比于线程,协程的优势是什么,为啥 java 没有协程,性能框架和 go 框架不相上下,协程这么牛逼为啥 rust 没有内置协程(听说都已经写好了,但是不符合理念,没合并)

操作系统调度的时候是以线程为单位调度的,又不知道协程的存在,即使你用协程了,操作系统还是只知道你是线程啊。

各种博客,都只说了比线程更轻量,占用内存小,切换成本小,但是线程切换是操作系统决定的,系统切换线程,你协程也没用啊。

各位大佬,救救孩子吧。

13026 次点击
所在节点    Linux
155 条回复
ipwx
2021-12-14 12:08:08 +08:00
@kingofzihua 另外 actor 模型可以很容易插入中间件。在一个 IO actor 和一个业务 actor 之间你可以完全不用更改两边,插入流控 actor ,或者插入错误重发 actor 。一个业务 actor 也可以搭配各种不同的协议 actor ,比如把一个 HTTP 协议 actor 丢在 SSL 信道 actor 或者 TCP 信道 actor 上都不用更改 HTTP 协议 actor 的任何一点代码。
ipwx
2021-12-14 12:08:56 +08:00
@lxdlam 哦对。不过 Erlang 我也不太熟 23333

Akka 受限于 Scala 这个语言太。。。。学院派,不太关心工程实践,语法糖 >> 性能和可维护性,导致它不太广泛使用。
joshu
2021-12-14 12:23:11 +08:00
如果我对连接池的理解没有错的话,我觉得传统的 pool 模式连接池之于 single 模式(大体上类似于 http2 那种利用的形式,或者说 pipeline ?),类似于线程池之于协程,这两样事物的思想上有共通之处。单独地谈线程与协程的关系没有太大意义,因为协程某种意义是作为一种通用的形式出现,来解决线程池所存在的问题的。协程的某些实现也不是脱离线程池存在的,它更多的是改变了线程池里的工作线程的底层逻辑。

池模式是用来解决资源分配问题的。首先就是资源有限,不管连接(毕竟由内核管理)也好,线程(和 CPU 核心挂钩,一般计算密集的会和某个核心进行绑定)。而这个资源受限不仅体现在内存占用上,还体现在创建和销毁以及切换和使用上,换言之,如果连接(特别是短连接,会引发 TIMEWAIT 之类的问题)以及线程的创建和销毁代价都和复用一样,那连接池和线程池就不会有存在的意义了,因为我可以无限地开连接和线程,无需额外管理,一切都交给系统来完成就行,但实际上这样做的负面代价较高
第二个问题则是,资源的独占。
我所理解的连接池,当某个线程使用了传统的 socket ,这个 socket 在得到对端的返回之前便是独占的,独占意味着这条连接不会在返回结果之前回归连接池。
类比一下线程池里的线程,线程池的一个经典使用模式就是消费阻塞队列里的任务,当一个线程在消费队列里的任务时,其它任务是无法抢夺这个线程的使用权的,对于计算密集的任务而言,这其实没有什么不好的,但对于 IO 密集的任务而言,事实上线程在等待 IO 返回的时候处于闲置的状态,这一段闲置状态由于没能主动释放资源,其它任务也得不到消费,产生了资源浪费。这时候我们一个可能的做法是对于 IO 密集类的任务开一个大一些的线程池,由操作系统来切换处于阻塞状态的线程。
所以,对于连接池来说,使用 singel 模式进行替代,使得一条连接可以被多个调用者直接复用,连接除了少数状态之外都处于共享可用的状态,调用者不需要因为连接池无可用连接而被阻塞,或者是去开一个临时的短连接。调用者在等待对端回复消息的同时,其它的连接也可以向这条连接写入消息,并等待对应的回复报文。
对于协程而言,其消费的对象并没有发生本质的改变,改变的只是线程的逻辑。在这里,协程的概念更类似于其消费的对象(或称之为任务)的一个完整生命周期,一个协程可以由 A 线程处理,也可以由 B 线程处理,也可以先由 A 线程再由 B 线程处理,即协程不与线程绑定(某些实现是这么做的,但也有实现是与线程绑定的)。
一个协程在遇到 IO 等待的时候,会主动让出线程的使用,把自己塞入等待队列(或者由继任者将其塞入等待队列,或者无继任者时由默认逻辑的空转逻辑将其塞入等待队列),直到 IO 等待结束(可能是定时器超时,或者得到了对端的返回),由对应的对象将等待队列的协程中加入到就绪队列中等待业务线程的调度。总而言之,就是线程的业务逻辑在操作协程,只不过通常的线程池业务逻辑是直接消费任务,而这里的线程是在处理协程的流转。
因为协程有状态,有上下文,所以某些实现方法就将这一上下文存放在栈空间上,通过切换堆上的存放的栈空间来实现协程任务的切换(比如说恢复寄存器变量来实现这一目的)。
通常协程框架的实现方法我见过两种,一种是真的去 hoook socket 族的函数了,于是你调用 C socket 族函数时,实际上调用的是被 hook 的函数。另一种是实现了一套完整的生态,对 socket 、锁、定时器之类的逻辑做了封装,如果要正确地使用这一种框架,就必须使用这些封装后的类库,而不是原生的系统调用。
以上关于协程的解释只是其中一种实现方法,不同的库可能有不同的实现。

golang 的一个优点其实是它的协程不像某些协程框架那样,是主动切换的。它有类似于时间片的概率,会在协程时间片到期后切换协程,从而更具实时性。另外它实现了一个通用的阻塞队列,也就是 channel ,当然 channel 的灵活程度是比阻塞队列要高一些的。因为它解决了这两个问题,所以使用一些多线程的业务的使用门槛变得低了很多。
kingofzihua
2021-12-14 12:42:52 +08:00
@ipwx 感谢大佬的回答,了解了不少东西,发现之前对协程的理解本身是错的,连问的问题都是错的 QAQ ,下去补基础了
ipwx
2021-12-14 13:39:18 +08:00
@joshu 说实话,Go 这种内置协程时间片切分的机制,在我看来,是一种工程实践上的妥协。

无论多轻量级的切换,一定是耗时的。减少不必要的切换可以使得总体耗时降到最低。你不会想要一个 for (1000000) 的计算任务被打断一千次吧,对吧对吧。

如果你能控制 IO 非阻塞让渡控制权,而且每对 IO 之间的操作都比较轻量(微妙级),你完全可以只在 IO 上切换。这样总体效能是最高的。

但是工程上不能保证所有类库都这样,也不能保证所有程序员都写的对,所以 Go 的这种协程才有用吧 hhh
buffzty
2021-12-14 14:11:57 +08:00
@1423 我感觉你说这话才像空谈吧 用协程不就是因为 go 支持 `go` 和 `chan` 这 2 个语法吗 可以让程序员少写代码. go 的协程跟现在 java,c++的差不多好吧,就是 java,c++需要多些一些字符. 我肯定选择更短的
SlipStupig
2021-12-14 14:54:05 +08:00
@ming159 性能提升肯定是目的之一啊,像 python 这种 GIL 设计,多线程几乎等于单线程,coroutine 在单线程里面做上下文切换开销小多了,但是 IO 性能可以大幅度提升
Anarchy
2021-12-14 18:18:28 +08:00
协程目前看来主要用处还是可以不用写异步回调,可以按照同步代码的方式写异步程序。定义的话正常函数一个调用一个返回,协程可以多个调用点,并且可以多次返回。
mmdsun
2021-12-15 00:05:49 +08:00
推荐腾讯的文章《从根本上了解异步编程体系》 https://mp.weixin.qq.com/s/q6BfOINeqgm5nffrHu4kQA
kingofzihua
2021-12-15 09:20:24 +08:00
@mmdsun 好文
dvsilch
2021-12-15 17:02:38 +08:00
@mmdsun
关于这个文章我有个疑问,在中间“那我们可以考虑给开发操作系统的 Linux Torvads 大爷提需求,系统需要支持这样的两个函数”这个部分的时候,它下面的示例代码用了 while ,在我的理解看来线程并没有被挂起而是一直在轮询占用 CPU ,直到 is_empty()返回 true ,可以麻烦解答一下吗?
lbp0200
2021-12-15 17:33:13 +08:00
都在吹协程,
——
这个“吹”字用的非常好,说明你的内心对协程这个技术概念,很抵触、很鄙视
既然是 Java 栈的,就看这个吧
https://www.zhihu.com/question/65444004
statumer
2021-12-16 09:54:29 +08:00
@lxdlam 你的理解是错误的,协程会火起来就是因为 epoll 。协程、绿色线程这些东西 40 年前就有了,从 Java1.1 开始就有绿色线程了,但是用起来很垃圾因为无法调阻塞 syscall 。你谈的都是协程如何实现的问题,和协程快不快没有任何联系。协程如果大量调文件系统 API 的话和线程一样烂。
soraping
2021-12-16 11:13:21 +08:00
协程是自己调度吧,不是系统调度,这样肯定比系统切换线程来得方便
lxdlam
2021-12-16 16:24:21 +08:00
@statumer 协程能不能火起来跟 epoll 没有任何直接联系。

epoll 的出现是因为需要解决 IO 过程中信号等待的问题,能把时间交出来去干一些其他事情,这也就是我说的这个“在内核态实现的调度反而还有点类似协程的味道”。而 syscall 或者 IO 阻塞和协程本身没有任何联系,这都是因为协程是一个用户侧调度的 continuation ,系统层面只能感知到 thread ,如果多个协程按照 1:N 模型跑在同一个线程里面,任意一个有阻塞的 syscall 一定会 block 住,和你是否使用协程无关,反过来说,在使用 epoll 的情况下,把 fd 注册完后扔到一个线程里面去等待,跟你是否使用协程也没有本质的性能区别(如果无视线程切换开销的话)。换句话说,如果我们是一个多核系统,完全可以引入第二个线程在不同的核来做轮询同步 IO ,并在成功后向第一个线程发送消息,对于第一个线程来说,这个过程也是 non-blocking 的,和使用线程或者协程同样没有关系。epoll 就相当于这个“第二个线程”,只是由 Kernel 在机制上做了优化,保证了在单核上也能执行这种高效操作。

如果你要谈为什么 Java 1.1 引入 Green Thread 但是 Java 1.3 之后废弃了 Green Thread ,Oracle 的官方文档解释了这个问题 https://web.archive.org/web/20040211225937/http://java.sun.com/developer/technicalArticles/Programming/linux/,最重要的问题在于 1.1 引入 Green Thread 是配合了 Solaris 的 LWP 的实现,而这个 1:N model 在 Linux 下是做不到真实的并行的,而不是所谓的 syscall 阻塞问题。

协程快不快的问题核心是 focus 在用户调度和内核调度带来的各种切换问题,在不同的执行模型、结构模型下天差地别,而不是简单的异步操作的问题。
statumer
2021-12-16 22:30:18 +08:00
@lxdlam 看得出来, 你只搞懂了协程依赖于控制流调度, 没有搞懂为什么必须要用户态调度, 以及它和异步网络编程之间的联系。

协程的崛起本质是异步网络编程的崛起。 在没有协程支持, 只能写基于 callback 的 poll/epoll 异步的时候, 已经可以通过共享一个上下文结构体实现异步执行流了。 协程只是为这种写法提供了语言层面的支持, 提供了异步堆栈和异步上下文。 有栈无栈只是对调用栈的取舍; 对称非对称, 协程还是 fiber 只是这些"函数片段"如何调度的一些策略。

用户线程可以用来调度一些与 I/O 无关的计算任务的这个想法, 从历史来看不过是人们的一种想象中的需求, 实际上从来没有实用过。

你对 epoll 的理解也挺歪的, epoll 和现在新的 fs syscall io uring 的设计目的都是为了减少 I/O 所需的 syscall 调用次数和 overhead 。 你说的什么只需要另一个线程去轮询就能模拟 non-blocking 的想法, 在没有异步 syscall 的前提下是完全不可能的(Linux 的文件系统 syscall 就是这样), 并发的读写文件请求只能让线程池慢慢消费(nodejs) , 或者多开线程处理(go), 在性能方面完全丧失和传统多线程网络编程范式竞争的资本。

另外贴链接的时候好好看看文章是不是你说的那个意思。 你稍微了解一下 LWP 也知道, LWP 也是内核完成调度的线程, 它和 Linux 线程的区别仅仅是 Linux 内核对线程的调度和对进程的调度是没有任何区别的, 而 Windows 和 Solaris 对线程调度有一定的优化。

https://docs.oracle.com/cd/E19455-01/806-3461/6jck06gqe/index.html
https://docs.oracle.com/cd/E19455-01/806-5257/6je9h0339/index.html
ipwx
2021-12-17 01:33:36 +08:00
@statumer 我原先也完全是你这个理解思路,直到我在这一楼层里面看到了有人说 Go 语言的协程是可以切换上下文的——

https://www.jianshu.com/p/fb1ccbd0d0ff

我 tm ,瞬间五雷轰顶,并且明白了为啥 Go 吹那么多的原因。引用我 125L 的观点:

> Go 这种内置协程时间片切分的机制,在我看来,是一种工程实践上的妥协。
> 无论多轻量级的切换,一定是耗时的。减少不必要的切换可以使得总体耗时降到最低。
> 如果能控制 IO 非阻塞让渡控制权,而且每对 IO 之间的操作都比较轻量(微妙级),就完全可以只在 IO 上切换。这样总体效能是最高的。
> 但是工程上不能保证所有类库都这样,也不能保证所有程序员都写的对,所以 Go 的这种协程才有用吧 hhh

所以本质上这是一个,因为不会用 event loop 的程序员太多了,Go 语言大爷们就说:算了,你们这群傻逼,干脆我语言创造一个比系统线程更轻的协程给你们用算了。

----

所以总结一下,Go 语言的协程和其他语言的协程是不同的。C++、Python 这种经典语言的协程 tm 根本不可能内置 Go 虚拟机的时间线分片。JVM 和 C# 也从未想过要这么干。微软的 Fiber 还差羽而归。因为它们发现,替程序员越俎代庖搞这种抽象,吃力不讨好。反正追求性能的有 event loop 或者基于 event loop 的单线程协程,为啥还要搞个不上不下的多线程(有上下文切换)的协程呢?

毕竟任何上下文切换,哪怕你说 Go 协程切换只要三个寄存器,tm 还是实打实有这个耗时的啊。
----

但是在一个没有泛型被认为是大道至简的语言里面,这种独一份的奇葩设计也没啥可奇怪的。
zxCoder
2021-12-17 09:53:48 +08:00
@crazywhalecc 请问一下,“对应协程中就是,比如遇到 HTTP API 请求,需要 0.5 秒,可以让这个进程继续先干别的,请求后回来继续当前处理完 HTTP API 请求的上下文” 如果没有协程,这种情况会怎么样呢?这个线程就只能死等吗
cwcc
2021-12-17 10:00:58 +08:00
@zxCoder 如果没有任何异步逻辑,确实会死等的。最简单的例子就是 PHP 的 curl 调用时候如果只开一个进程一个线程,假设请求时间较长,那么其他处理 Web 请求就只能等待结束。

异步和协程其实很像,对于没有抢占线程式的协程实现来说,协程就是一个以同步逻辑编写异步代码的方式。
zxCoder
2021-12-17 11:00:48 +08:00
@dvsilch 我也有这个疑问,不过无论怎么异步,总是要有一个 while ture 得线程一直来轮询才对

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

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

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

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

© 2021 V2EX