V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
kingofzihua
V2EX  ›  Linux

问一个协程方面的问题

  •  4
     
  •   kingofzihua · 2021-12-13 15:04:45 +08:00 · 13567 次点击
    这是一个创建于 1079 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

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

    各位大佬,救救孩子吧。

    第 1 条附言  ·  2021-12-13 15:49:54 +08:00
    我看楼下不少说是协程调度的问题,和线程切换的问题, 我不理解的地方是 为什么要切换协程啊,

    我理解的是如果你有阻塞问题,可以切到其他的协程。

    但是感觉这样不对。如果阻塞是系统调用阻塞,线程就会挂起,调度到其他的线程了,你协程没用,如果不是系统阻塞,那我完全可以在线程内执行其他的,后面再回来执行
    155 条回复    2022-09-22 17:14:57 +08:00
    1  2  
    kingofzihua
        101
    kingofzihua  
    OP
       2021-12-14 08:43:58 +08:00
    @trcnkq 赞同
    kingofzihua
        102
    kingofzihua  
    OP
       2021-12-14 08:51:34 +08:00
    @kekxv 感觉这个例子不太理解,读文件一般是阻塞 IO ,操作系统会直接挂起当前线程,然后切换到其他的线程了,让出 cpu 时间片,这时候当前的线程已经没法继续了,所以 如果你要是大量打开文件,一个协程会对应一个线程,导致线程暴涨,或者是线程池僵死之类的吧。
    ai277014717
        103
    ai277014717  
       2021-12-14 09:02:44 +08:00
    使用成本更低,因为框架层面 /语言层面来管理线程。对程序员要求更低,
    kingofzihua
        104
    kingofzihua  
    OP
       2021-12-14 09:07:56 +08:00
    @letking IO 阻塞中的线程也会被调度吗?线程调度不是应该只调度已就绪的吗?
    lux182
        105
    lux182  
       2021-12-14 09:17:22 +08:00
    @crazywhalecc 多谢
    b1iy
        106
    b1iy  
       2021-12-14 10:02:53 +08:00
    作为从 Rxjava 过来的 Androider

    我觉得协程精简了线程切换代码量。
    Rheinmetal
        107
    Rheinmetal  
       2021-12-14 10:05:38 +08:00
    协程是 cooperative multitaksing 的一种实现形式
    尽量减少切换上下文开销
    kujio
        108
    kujio  
       2021-12-14 10:17:55 +08:00
    @kingofzihua 多线程是为了解决并发任务出现的,协程同样也是解决并发任务的,两个作用基本上是一样的,协程本质是一种伪线程,他解决了并发任务的同时比多线程轻量简单,更擅长处理高并发小任务
    letking
        109
    letking  
       2021-12-14 11:06:43 +08:00
    @kingofzihua 操作系统*不知道*你的线程什么时候会就绪。
    你可以在代码里写 sleep(10)或者 sleep(100),但操作系统可不会读你的代码,从而判断出来应该 10s 或 100s 后再来,它只能间隔的执行每一个线程。
    dany813
        110
    dany813  
       2021-12-14 11:10:30 +08:00
    看下来,前端的 async 也有点协程的感觉
    liuhan907
        111
    liuhan907  
       2021-12-14 11:32:42 +08:00
    @kingofzihua 你先理解一个事情,我们常见的 IO 都有非阻塞模式,不管 callback 也罢还是协程也罢都是为了在等待 IO 完成后继续执行剩下的代码。协程只是给开发者提供一个更简单的方式表达“后续代码”这么个东西而已。
    cnoder
        112
    cnoder  
       2021-12-14 11:43:00 +08:00
    就像开水烧和接电话,等开水烧好的时候你人( cpu )是闲的,这时候就可以开协程接电话,等开水烧了再回来取。
    到底是两秒钟看一眼开水开没开,还是等开水响了回去处理,这就是协程调度器了
    ipwx
        113
    ipwx  
       2021-12-14 11:45:10 +08:00   ❤️ 2
    @2i2Re2PLMaDnghL 就我个人而言,Actor 模型和 callback / future / 协程不是一个维度的。

    actor model 的优势在于 state + mailbox 。每个 actor 上面有个单独的等待队列,即便你在线程池上跑 actor model ,也不会让一个 actor 在多个线程上运行。这使得你可以很安全地编写出来双向交互的 actors ,而不用担心每个 actor 内部的数据存在竞争关系。

    线程、协程、callback 、future ,数据流一般是单向的,一个 IO 线程在启动一个后台任务以后就陷入等待,不会处理任何其他事务。而 actor model 每个 actor 都有一个微型 event loop ,完全是全双工的。

    你可以很容易用线程池实现 actor model ,避免阻塞调用就能实现完全非阻塞。你也可以在协程上实现 actor model 。或者你也可以在 event loop 上实现 actor model 。总之 actor model 是高于并发的一个抽象模式,核心在于数据封装。
    ipwx
        114
    ipwx  
       2021-12-14 11:48:01 +08:00   ❤️ 1
    @kingofzihua 对既然提到了 actor model ,楼主如果有兴趣可以去看看 actor model ,这个东西的思考方式会让你改变很多对并发的看法。比较成熟的 actor model 的实现大概只有 scala akka 。

    actor model 的核心在于,每个 actor 只有一个工作线程。actor 尽量避免阻塞。即使你在 10 个线程的线程池上运行 10000 个 actor ,每个 actor 要同时处理其他 9999 个 actor 发来的消息,也保证只有一个线程在运行一个特定的 actor 。这个是 actor model 最重要的特性。
    echoechoin
        115
    echoechoin  
       2021-12-14 11:48:30 +08:00
    python 的携程就是 select/poll/epoll 的高级封装
    ipwx
        116
    ipwx  
       2021-12-14 11:48:49 +08:00   ❤️ 1
    …… 不过实现一个基本的 actor framework 也不是那么困难的。。。

    再比如我自己在自己的项目里面用 C++ 实现了一个。
    lxdlam
        117
    lxdlam  
       2021-12-14 11:59:09 +08:00   ❤️ 6
    “协程”是一个大而空的筐,啥鸡蛋都往里装。Go 的 Goroutine 是 stackful 且在 Go 1.14 之后是基于 signal 的抢占式调度的,Kotlin 是 stackless 但是协作调度的,但是这俩都有人叫做协程。

    实际上的“协程”,是一种用户调度抽象。有基本 OS 概念的人都知道,进程( Process )是负责资源隔离的,而真正的运行调度单元是线程( OS Thread )。线程不是免费的,这导致我们如果创建一大堆线程,必定涉及了资源的扩张,而实际上的很多 task 虽然对实时性、执行效率有要求,却不需要这么多资源(比如 IO 任务),我们该如何让这些任务能够更好地执行,而不每次都开一个线程呢?这就是协程的本来目的。

    通俗来说,一个语言有没有协程,并不意味着这个语言没有这种抽象调度能力。Lisp 系的 call/cc 和 C 的 setjmp/longjmp 也可以实现某种程度上的协程,甚至更简单的,只要一个语言有能够将一个运行过程封装到一个内存结构上的能力(例如,C++ 有 operator() 的 struct ),基于这个能力写一个 scheduler ,你也发明了一个协程。

    协程为何性能高?这也是一个网络迷思:如果你的 task 如上面描述的,是一个低资源占用但是高实时消耗的,那当然是显而易见的性能提升(线程内核态调度延迟一般是比协程用户态调度更慢的),但是这不是银弹,如果你的 task 需要独占资源,那么你的线程调度同样能做到跟协程一样快。

    那么,我们该如何认识协程?实际上来说,这是一个异步计算结构的问题:
    1. 抢占式调度( preemptive ) vs 协作式调度( cooperative )
    2. 有栈结构( stackful ) vs 无栈结构( stackless )
    3. 跟系统 Thread 的 mapping 关系:1:1, N:1 和 N:M
    等等。这些关键词网络上都有非常优秀的资料,就轮不到我在这里扯淡了。认识协程最关键的部分,实际上还是看语言提供的能力(语言结构、标准库等),以及语言的目标领域。

    楼主举得 NIO 的例子实际上跟这个话题相去甚远:NIO 用的是 kernel 提供的 IO Multiplexing 或者 Async IO 能力( epoll/kqueue/io_uring/IOCP ),这个在内核态实现的调度反而还有点类似协程的味道。
    a1562619919
        118
    a1562619919  
       2021-12-14 12:03:18 +08:00 via Android
    @whoosy 进程涉及内核调度,但是线程并不涉及
    ipwx
        119
    ipwx  
       2021-12-14 12:05:31 +08:00   ❤️ 1
    P.S. @kingofzihua 补充一句,actor model 里面每个 actor 其实是个状态机。差不多就是


    def __init__(self):
    ....self.state = 0

    def on_message(self, msg):
    ....if state == 0:
    ........do something with msg
    ........self.state = 1
    ....elif state == 1:
    ........do something with msg
    ........self.state = 2

    核心就是这个 on_message 处理函数。你是期待框架把新的数据推送给你(或者别的专门读数据的 actor ),而不是在每个 actor 里面都调用 read 。

    这个的好处在于,你可以把 IO actor 和业务 actor 切分,业务不用管太多 IO 细节,实现了解耦。单元测试的时候你可以直接对业务 actor 伪造输入进行测试。特别爽。

    而且状态机可以回退,这个就可以实现很复杂的东西。
    lxdlam
        120
    lxdlam  
       2021-12-14 12:06:04 +08:00 via Android
    @ipwx Erlang 的 Process 也是很经典的 Actor 实现,在爱立信已经跑了 20 年了
    ipwx
        121
    ipwx  
       2021-12-14 12:08:08 +08:00   ❤️ 1
    @kingofzihua 另外 actor 模型可以很容易插入中间件。在一个 IO actor 和一个业务 actor 之间你可以完全不用更改两边,插入流控 actor ,或者插入错误重发 actor 。一个业务 actor 也可以搭配各种不同的协议 actor ,比如把一个 HTTP 协议 actor 丢在 SSL 信道 actor 或者 TCP 信道 actor 上都不用更改 HTTP 协议 actor 的任何一点代码。
    ipwx
        122
    ipwx  
       2021-12-14 12:08:56 +08:00
    @lxdlam 哦对。不过 Erlang 我也不太熟 23333

    Akka 受限于 Scala 这个语言太。。。。学院派,不太关心工程实践,语法糖 >> 性能和可维护性,导致它不太广泛使用。
    joshu
        123
    joshu  
       2021-12-14 12:23:11 +08:00   ❤️ 1
    如果我对连接池的理解没有错的话,我觉得传统的 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
        124
    kingofzihua  
    OP
       2021-12-14 12:42:52 +08:00
    @ipwx 感谢大佬的回答,了解了不少东西,发现之前对协程的理解本身是错的,连问的问题都是错的 QAQ ,下去补基础了
    ipwx
        125
    ipwx  
       2021-12-14 13:39:18 +08:00
    @joshu 说实话,Go 这种内置协程时间片切分的机制,在我看来,是一种工程实践上的妥协。

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

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

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

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

    网络编程的崛起之于协程已经是很早的事情了,实际上,nginx 和 Redis 都是单线程 IO 模型( Nginx 1.17 和 Redis 6 才支持的多线程网络 IO 模型),性能并不比后面很多所谓用了多线程 or 协程的应用差,那么协程在这里的作用是什么?从另一个角度,OpenResty 往 Nginx 里面嵌入了一个 LuaJIT ,底层 IO 逻辑完全没变的情况下,为什么比 Nginx 性能好那么多?这些和协程崛起相关没有任何直接联系。

    关于二三点,既然你已经提到了 go ,为什么不去看看 go 的源码实现和 Scheduler 设计? Design Doc ( https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#heading=h.c3s328639mw9 )和 1.17 的源码 ( https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/runtime/proc.go;bpv=1;bpt=0 )都说明了 Go 的工作方式正是我所说的在多核系统下,会用另一个线程去跑 blocking syscall ,对于你的程序来说,这部分 syscall 就是 non-blocking 的。同样在 Java 最新的协程方案 Project Loom 里面,工作方式也是一样的 ( https://blogs.oracle.com/javamagazine/post/going-inside-javas-project-loom-and-virtual-threads )。特别要提示一点,虽然现在 Go 确实在可以 poll 读文件的情况下使用 epoll ,但是在 bsd 环境下 Go 现在还是使用 blocking syscall ,而更早的 Go 版本(我没记错是 1.12 以及之前)同样使用的 blocking 的文件 syscall 去读文件。

    实际上,如果你真的对所谓的“只是对有无调用栈的取舍”做出了理解的话,你就不会陷入这种强加因果的关系。对于传统的线程,我们会有两种程度的开销:一个是线程自身占用 memory 的开销,这个和系统的默认栈大小有关,这导致了我们的内存占用会随着线程申请数量,这个线程自身的申请也需要通过 syscall ,时间和内存资源占用都存在 overhead ;另一个角度,虽然根据机器和系统不同数据有差异,但是普遍来说,native thread 切换的开销并不小,对于 常用 Linux 发行版来说,这个切换通常不少于 1ms 。当我们实现了用户侧调度,我们可以通过巧妙地 GC 管理等减少这种异步结构的申请和释放开销,通过剔除和当前应用无关的系统 OS 数据字段同步达到更快的上下文切换,甚至直接基于状态机模型舍弃这部分开销,得到普遍更好的执行时间。更进一步来说,OS Level 的调度是 generic 的,内核针对的是任何任务的调度,而对于我们的应用,我们和 runtime 更容易知道我们的调度侧重点和优势,能够把时间和优先级排布做的更精妙,自然能够带来一定的性能提升,这才是用户侧调度的意义所在,也是协程这类结构的性能提升所在。

    我同意异步的 IO 会比起同步 IO 有性能提升,虽然平台不同变量较大,但在普遍比较下 IOCP 确实性能和系统利用率是强于 epoll 的,io_uring 确实是未来的方向。但是这部分的性能提升和线程以及协程本身开销优化没有任何关系,假如我们认为线程和协程切换都是零开销的话,那使用哪种结构都影响不了在同一种 IO 下的调度开销。顺带补充一点,线程的上下文切换和调度也是要 involve syscall 的,用户侧实现调度的话能够减少这些 syscall 的次数。

    最后,异步是一个很复杂的概念,但是我们可以抛弃一些严谨性的情况下认为异步就是并发。对于并发来说,根据你观察的 level 不同,多进程对于操作系统来说是并发的,多线程对于应用是并发的,而我多次提到,所谓的协程就是一种用户调度抽象,实际上和多线程相比就是执行模型的差距,本质上和多线程没有区别。

    最后,如果你认为在当时文章里描述的 LWP 和 Linux Thread 的调度没有任何区别的话,那可能不求甚解的是你。Java 1.3 是 2002 年发布的,而 Linux 真正实现了内核可抢占是在 2003 年的 2.6 版本。由于 1:N 的设计,当某个 task 开始调用 blocking syscall 的时候,其他的所有 task 都没有机会被唤醒,更别提调度到其他 core 去执行;而对于 Solaris 的调度,LWP 可以任意被调度到可用的 kernal thread 上,进而有机会被调度到其他 CPU core 上( https://www.usenix.org/legacy/publications/library/proceedings/sa92/eykholt.pdf )。这些历史背景才是当时选择的原因。当然,Linux 现在对实时性的支持已经非常好了,所以 Project Loom 重拾历史积淀,再次把用户侧线程引入 Java 。
    lxdlam
        142
    lxdlam  
       2021-12-17 14:10:55 +08:00
    @ipwx stackful 和 stackless 最大的区别不是保存调用栈的问题,是可不可以在任意函数中启动异步调用的问题,这个问题最经典的例子就是 js 所谓的有色函数,或者说 async/await 的传染问题。考虑一个非常经典的例子:

    ```
    async function g() { return 123; }

    function f(n) {
    let x = (await g()) + n;
    return x;
    }
    ```

    假定这个函数是可以运行的,那就有一个问题:`g` 要被调度走,肯定有其他函数要切入运行,栈要被更改,那当 `g` 执行完的时候,`x` 和 `n` 咋办?我们有两种思路:
    1. 调用 `g` 的时候,我们记一下调用栈,这下 `f`, `x` 和 `n` 我都记下来了;
    2. 让 `f` 也变成 `g` 一样的东西,这样 `f` 自己就会记录 `x` 和 `n` 的关系。

    正好就是有栈和无栈协程的区别。

    Project Loom 也打算把 stack 存下来放在 Java Heap 里面,搞成 stackful 的。至于 Fibers 为啥挂了,是因为 Fibers 把一个指挥内核调度的可能性给了用户,误用的可能性更高,比如可能跟系统的带锁操作形成死锁( fiber 在等一个 syscall 之后 yield ,但是系统在等 fiber 释放执行这个 syscall 的资源),也是一种优先级反转的特例。种种情况下,Fibers 虽然 API 没有被干掉,但是用户不咋用了。
    ipwx
        143
    ipwx  
       2021-12-17 14:34:19 +08:00
    @lxdlam 在我看来 f(n) 就应该也是 async 的,虽然对于这方面 js 我不太熟。

    我的技术背景是 C++、Python 比较熟,曾经用 Scala 写过 Future / Promise 的程序不过已经很久没有用了。C++ 自己写过 Actor framework ,Python 曾经用过 tornado 和 gevent ,现在挺喜欢用 async / await 。

    因为我的这些背景,所以我觉得程序员非常准确明白所有这些并发方法(线程池、event loop 、callback 、future promise 、async await )是基本功,而且不同任务可能偏重于不同的技术。当一个任务需要特别准确把握延迟的时候,随意上下文切换是不可接受的,那么就得手撸 event loop ,顶多在 event loop 上自己封装一下比如 actor model 或者 future / promise 呗。

    所以我比较 anti go 语言这种类似于线程的协程吧,总感觉它管得太多了。
    statumer
        144
    statumer  
       2021-12-17 14:35:25 +08:00
    @ipwx C# 的 Async, Await 的问题是, C# 无法要求所有库开发者都用异步网络编程, 如果你的调用栈是异步调用同步, 同步再调用异步, 异步 -> 同步 -> 异步, 线程还是被同步 syscall 阻塞. 在这个方面, C++则更加糟糕, 一部分类库(比如 brpc) 甚至会自己实现协程和协程调度(一个上下文切换保存 xmm 的协程,呵呵[1]), 而不是被一个统一的调度器管理. 写 C++ Server 的时候,redis/mysql/grpc/http 如果网络框架不统一可真是折磨,逼大家写 port 。

    Go 在实现方面完全有能力做到像你说的那样, 只在 syscall 的时候切换上下文, 但是考虑这样一个问题,如果其他 goroutine 在等待锁,等待其他 goroutine 的消息,而你的计算任务又没那么重要,你怎么让出控制流?最理想的方式肯定是让开发者自己去让渡控制权,超细粒度控制,但是这样做无疑给开发者造成了额外的负担(比 if err!= nil 还严重),所以搞成了像现在这样的编译器自动插入 hook 。

    [1]: https://github.com/apache/incubator-brpc/blob/master/src/bthread/context.cpp
    ipwx
        145
    ipwx  
       2021-12-17 14:43:00 +08:00
    @statumer C# 那种可以通过一个线程池(专门排队做阻塞任务) + 非阻塞的 IO 。不过确实,非阻塞 IO 的库开发需要时间。所以这就造成了 Go 语言在网络微服务等领域特别被人追捧,因为在这里面编译期自动插入 hook 确实省事得多,一下子就可以适配所有阻塞的库。

    这其实就是工程性的妥协了。C++ 和 C# 没有编译器的 hack ,导致大家不得不开发真正的非阻塞库。但是反过来,这样就倒逼 C++ C# 这种语言用户开发出真正高性能的非阻塞库了。然而毕竟真正需要高性能非阻塞的库不多,Network IO, MySQL / PostgreSQL / MongoDB ,加上一定的 File IO 和基础框架,就解决了。

    其他的并不需要处理这种事情。应用逻辑方面需要解决“锁”的阻塞问题,其实“不阻塞”才是更正确的方案。逻辑复杂的应用从多线程并发阻塞模型换成 Actor model ,你会发现写起来就是第二次工业革命和第三次工业革命的区别。

    Go 语言编译器允许大家偷懒,大家自然没有动力去精益求精,“够用就行”。反而抑制了更精巧的程序 —— 当然这也是做 “工作” 和做 “艺术品” 态度的差别吧 hhh
    lemonf233
        146
    lemonf233  
       2021-12-17 14:52:20 +08:00
    楼楼有空要不附言总结下?楼里面各种主张都有...太迷惑了
    lxdlam
        147
    lxdlam  
       2021-12-17 15:08:25 +08:00
    @ipwx 其实“非常准确明白这些并发方法”本身是一种提升了异步编程门槛的行为,这一串概念能引出来七八个名词和一大堆文章。我个人是非常乐意看见 goroutine 出现的,虽然确实是 dirty and hack 的,但是它足够简单,确确实实解决了实际问题(虽然我还是认为~~go 是垃圾~~)。

    对于语言来说,提供一个尽可能统一的抽象,并让不同的 runtime 去实现不同的做法,再实现不同的生态,是一个非常好的做法,也是 Rust 社区正在采纳的做法。这确实会导致出现“事实标准”这一情况(比如 tokio 现在的绝对领导地位),但是给了用户在 consistent 的 interface 下,可以自主根据 workload 切换运行方式的自由。

    实际上,阻塞和不阻塞不是异步问题的本源,归根到底还是我们希望每个任务都能够拿到足够多的时间片去跑,而不是要么被无用的 task 频繁占用 CPU ,要么在等一些其实没有意义事情而导致时间流失。从这两个角度出发,一方面我们尝试通过各种方式来更巧妙地让 task 在正确的时候和地方执行,而另一方面我们也在尽力去把各种有这种所谓“无用等待”的地方做成 pubsub 的事件行为把等待让出来,给其他 task 时间。说来也比较奇妙,CPU 和操作系统设计的时候已经采纳了基于终端的事件通知机制,但是应用层全面用到这个特性还是过去了很久。

    至于 Worse is better 还是 Do the right thing ,我们都可以另起一次讨论了 :)。
    lxdlam
        148
    lxdlam  
       2021-12-17 15:11:06 +08:00
    @lxdlam 基于终端 -> 基于中断
    ipwx
        149
    ipwx  
       2021-12-17 15:37:02 +08:00   ❤️ 1
    @lxdlam 你说的很对,所以虽然我个人不喜欢 2333 ,虽然 Go 是一种工程妥协( Go 到处是工程妥协,比如现在还没完全上线的泛型),但是它毫无疑问是有用的。可以说 Go 语言这种编译器抽象对于大部分 Web 应用都是有效的,这本就是它的专长。

    无效的领域,其实倒也不少。比如追求精确控制延迟的 real-time system 、高频交易之类的低延迟系统;比如本来就需要精确控制所有开销的游戏引擎、OS 内核、数据库系统。比如各种传统计算机算法。比如科学计算…… 但说实话这些本来就不是 Go 的专长。

    我对于本楼有些微词的地方是,明明有这么多不同的场景,很多 Go 语言拥蹙就只知道互联网业务这一亩三分地,以为 Go 的协程就是圣经。。。互联网程序员现在网上的声音太大了,以至于 “技术” 就只有互联网并发 hhh
    janus77
        150
    janus77  
       2021-12-17 18:48:00 +08:00
    我也不是很清楚,看了一些文章,总的来说就是 2 点:1 更轻量,开销更小; 2 写起来语法爽,开发成本更低。
    coldear
        151
    coldear  
       2021-12-18 01:35:38 +08:00
    @ipwx kotlin coroutine 是用 callback 实现的
    kingofzihua
        152
    kingofzihua  
    OP
       2021-12-20 13:23:08 +08:00
    @lemonf233 兄弟,你太看的起我了,我要是真的懂,我就不会问了,,你把所有评论全看一遍,就发现不只是简单几句话能说明白的,还是看看大家说的,然后自己慢慢理解吧,我也在一点点的尝试看懂他们说的,目前能力还没达到能做总结 QAQ
    hongweiliuruige
        153
    hongweiliuruige  
       2022-01-04 17:17:52 +08:00
    @ipwx nodejs 的协程不是 stackless 的吗,,那岂不是很完美,,大多数生态也都支持 async await ,即使不支持,通过 promise 包裹一下也都支持了。。。
    hongweiliuruige
        154
    hongweiliuruige  
       2022-01-04 17:38:48 +08:00
    @lxdlam 那么 stackless 和 stackful 有什么优劣呢
    gy123
        155
    gy123  
       2022-09-22 17:14:57 +08:00
    例如一个基于 epoll 的 nio 的非阻塞服务 ServerSocketChannel,此时来了 10w 请求,如果需要同时并发处理:
    1.使用线程:
    (1)采用 reactor 模式,处理操作连接的线程一个就够了;然后就是分发读写就绪的连接,进行处理;
    (2)socketchannel 由于设置为了非阻塞的,所以读写每次都是直接返回,所以需要轮询监听,此时不会阻塞线程;那么就需要 10w 的线程同时处理,由于操作系统抢占式的调度线程,10w 线程来回切换,资源消耗极大; (当然此时可以使用到线程池,使用后的效果是,并发执行最大线程池数量的任务,然后依次到队列取后续任务执行)
    2.使用协程:
    (1)依然采用 reactor 模式,处理操作连接的线程一个就够了;然后就是分发读写就绪的连接,进行处理;
    (2)直接创建 10w 个协程,处理连接的读写;此时为用户态切换执行,资源消耗很小;(此时用到的线程数是很少的,并且执行效果是 10w 个并发执行)

    所以看出来 协程是更优的线程池实现处理任务的方式;

    不知道说的对不对,欢迎指正~
    1  2  
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3519 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 11:05 · PVG 19:05 · LAX 03:05 · JFK 06:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.