V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sunny1688
V2EX  ›  程序员

对协程的理解

  •  
  •   sunny1688 · 2022-09-24 22:58:31 +08:00 · 3872 次点击
    这是一个创建于 550 天前的主题,其中的信息可能已经有所发展或是发生改变。

    以前对协程的概念是很模糊的,搞不清协程到底是什么,有人说goroutine才是协程,yield不是协程,为了搞清楚疑惑,我总结了几点

    什么是协程?

    1 、可以主动让出,和被动恢复,这里不针对具体语言的实现

    2 、让出和恢复不需要 CPU 调度,由用户(写代码的人)根据逻辑完成

    3 、协程在语言的实现有不同,Go 的 goroutine ,JavaScript 的 Promise ,Rust 的 Future ,PHP 的 yield 、都是协程的实现

    协程解决了什么问题?

    如 Java 使用的 nio 有非常好的性能,单线程的 JavaScript 异步 io 也很出色,可为什么到了协程这块我就会陷入一个误区那就是协程肯定比异步快,因为 Go 语言所以接触到了协程,感觉协程就是 Go 语言的(先入为主的概念),其它语言就是没有(或者不是 Go 那样开启协程也不是协程,如 yield )

    先回顾一下异步是什么样的,函数立即执行,有结果后执行回调函数,业务复杂也就会有回调地狱的问题

    io 可以分为两个阶段,io 发起(10ms),io 完成(300ms),异步不就是这样吗,io 发起后立即返回,剩下的时间可以去执行其它代码

    那协程又是什么样的,记住最重要的一点,协程是可以主动让出的,被动恢复,可以想象一下,当 io 发起后,我让出执行权,io 完成时恢复执行,这是不是就解决了回调地狱的问题呢?

    所以协程是解决回调地狱的问题,编写同步代码,异步执行!

    总结

    单线程下,同一时刻只有一个协程在运行(之前老和并行搞混)

    协程用于 io 密集场景,也就是利用等 io 的这段时间去执行其它的代码,异步不也是这样吗?协程和异步是互补的关系

    像 php 的 yield 、JavaScript 的 Promise 、Rust 的 Future 都是协程的本质实现

    goroutine 要单独拿出来说,它远远超出了协程的本质,它有一个强大的调度器

    欢迎交流讨论,提成不同的看法~

    23 条回复    2022-09-26 11:45:48 +08:00
    gy123
        1
    gy123  
       2022-09-24 23:02:01 +08:00 via iPhone
    我感觉可以再加一个,适用于网络 io 密集型,或是非阻塞多路复用的场景。也欢迎提出看法
    24bit
        2
    24bit  
       2022-09-25 00:32:47 +08:00
    协程本质上可以看作可挂起恢复的用户态轻量级线程,可以在没有多线程的环境下模拟并发,也可以在多线程环境下替代系统线程降低切换消耗,支持更多并发。

    只不过现在大多数语境下,协程的挂起和恢复条件通常就是 I/O 操作,异步 I/O 操作正好和协程的挂起恢复匹配,所以协程和异步 I/O 也经常一起讲。

    而 goroutine 、yield 、Promise 和 Future 都是协程的实现,只不过实现方式不一样。goroutine 是 stackful 实现,其他是 stackless 的。
    hangbale
        3
    hangbale  
       2022-09-25 00:57:00 +08:00 via iPhone
    协程是语言级的实现 可以认为是给程序员提供了一个任务调度的接口
    BeautifulSoap
        4
    BeautifulSoap  
       2022-09-25 01:07:42 +08:00 via Android   ❤️ 1
    一个有趣的现象就是,协程这种极度符合人类直觉和经验的东西到了程序员这就成了有点理解门槛的概念了。

    人类一直以来都是以协程的方式在工作生活。把自己当成一个 cpu ,手上做的事当成 cpu 处理的工作,协程就是你做饭的时候先到灶台前打开煤气灶开始烧水,然后不干等水烧,开而是趁着烧水空档移动到切菜板那去切菜,然后水热了你得到通知(或者时不时瞅两眼锅子),把切好的菜扔进去煮,之后如果手边还有工作就切换到另一个工作上下文继续工作,如果没有工作了那就在锅子前干等汤煮好。计算机里这种就是协程。而每次放下手上工作去处理另一件事就是在切换上下文。所以协程只适合用在 io 处理这类把任务交出去干等就行的场景。对于 cpu 密集型的工作协程没有用处。类比到生活例子就是手上忙一件事根本抽不开身(你这个 cpu 被占满了),这时候即便再多通知过来让你去做另一件事也没空,那么那些事情就没法被执行了
    Jirajine
        5
    Jirajine  
       2022-09-25 03:11:27 +08:00   ❤️ 19
    槽点太多,各个概念都搞混了。
    协程 coroutine 重点在于 cooperative ,协作式,相对应的是 preemptive ,抢占式,最经典的实现既 Thread ,线程。
    这两者都是 multitasking 多任务的机制。
    协作式就是各个任务之间相互合作,程序主动在需要等待的时候让出控制权,体现在代码里就是 yeild/await 等机制,golang 则是由编译器隐式插入 yield 点。
    抢占式自然是各个程序不协作,相互争抢,由外部调度器决定运行哪个。
    前者的优点在于因为是程序自己协作,自然知道挂起 /恢复时需要哪些信息,而 OS 线程调度器不知道这些,就得把整个 call stack/ register 的状态都存下来以后才能恢复。这就是协程性能好、更轻量的原因。
    抢占式的优点在于不需要用户代码关心和处理协作的细节,用户代码更简单且责任少。操作系统如果用协作式的多任务,那一个程序不让出控制权就能卡死整个系统,这也是通用操作系统实现线程的原因。而嵌入式操作系统等用户态可控的环境下,则可以实现协程作为多任务机制。

    stackful/stackless 是协程实现的方式,前者使用类似 stack 的结构保存状态,优点是编写时更接近传统线程,aka 绿色线程。后者则是生成状态机,只保存确实需要的状态,性能最好。

    异步是一种调用机制,指调用函数立即返回而不是完成后返回,完成后通过某种方式通知调用方再获取结果。回调和某些语言的 async/await 语法就是为了表达这个过程。
    异步 IO 是指通过异步的方式提供 IO 接口,操作系统提供 epoll/io_uring/IOCP 等接口,上层应用 /运行时再封装,常常配和协程、async/await 语法等结合起来以简化使用。

    说异步 /协程提升性能主要指因为使用了异步 IO 接口,线程不会被阻塞,所以只需要少量线程而不是大量线程等待,少量线程内部再调度协程等待 IO 。因为协程更轻量,所以高并发场景下开销小,自然性能就好了。

    Promise/Future 等是对异步过程的封装对象,和协程是相关但不同的两个概念。

    goroutine 之所以叫 goroutine 而不是 coroutine 就是因为 goroutine 不是完全协作式的,也存在抢占式调度。

    可以去了解一下 async rust ,async rust 因为太难用以至于你不得不对 async 、携程相关的概念、实现都完全理解才能真正用起来。
    aecra1
        6
    aecra1  
       2022-09-25 09:27:26 +08:00 via Android
    “有人说 goroutine 才是协程”,这两天面试有面试官问我协程了,我说许多语言都有协程支持,面试官貌似不认可,原来还有这种坑
    documentzhangx66
        7
    documentzhangx66  
       2022-09-25 10:33:14 +08:00   ❤️ 3
    1.CPU 最小执行单位是硬件线程。开了超线程功能 HT ,硬件线程是半个物理核( IBM Power CPU 的 SMT4 能到 1/4 个物理核)。

    2.OS 的最小执行单位是线程,进程是线程的容器,OS 是进程的容器。OS 不认识纤程或协程,也就是对于 OS 来说,没纤程或协程这回事。

    3.编程语言引擎,自己管理线程,减少了线程数量,相当于减少了线程的内存与缓存的开销,还减少了操作系统频繁创建、切换、杀死线程的开销。

    简单来说,操作系统执行用户代码,是招聘一个线程,跑完后,开除线程。而编程语言引擎,是招聘一个线程,线程干完后,清理一下他的工作台,让他继续执行下一份用户代码。这样上下文切换的开销也省了很多。线程在这种使用方法下,被称为纤程或协程。
    ColorfulBoar
        8
    ColorfulBoar  
       2022-09-25 10:36:11 +08:00   ❤️ 1
    低情商:____搞不清什么是协程
    高情商:远远超出了协程的本质
    sunny1688
        9
    sunny1688  
    OP
       2022-09-25 13:21:51 +08:00
    @24bit “所以协程和异步 I/O 也经常一起讲。”,非常认同,之前一直理不清
    pkupyx
        10
    pkupyx  
       2022-09-25 13:25:29 +08:00
    2 、让出和恢复不需要 CPU 调度,由用户(写代码的人)根据逻辑完成

    不需要 CPU 调度

    ???
    sunny1688
        11
    sunny1688  
    OP
       2022-09-25 13:26:51 +08:00
    @gy123 协程通常是和异步联系在一起,异步一定是非阻塞的(也就是内核会返回 EAGAIN ),那异步也基本是和多路复用联系在一起,所以我觉得这是一连串的关系
    sunny1688
        12
    sunny1688  
    OP
       2022-09-25 13:30:57 +08:00
    @hangbale 对的,而大多数语言只实现了协程的特性,但配套的组件如:事件循环,异步 IO ,协程调度器可能没有提供(如 PHP ),Go 语言就把这一套都给实现了,一般来说也需要语言层面出厂自带才能使用起来
    sunny1688
        13
    sunny1688  
    OP
       2022-09-25 13:31:48 +08:00
    @BeautifulSoap 例子很易懂,接地气。
    sunny1688
        14
    sunny1688  
    OP
       2022-09-25 13:33:44 +08:00
    @Jirajine @documentzhangx66 👍🏻👍🏻
    sunny1688
        15
    sunny1688  
    OP
       2022-09-25 13:37:22 +08:00
    @pkupyx 对于操作系统来说是感知不到协程的,那进程线程都需要 CPU 去调度,协程肯定是被包含在其中的

    “由用户(写代码的人)根据逻辑完成”,这句话的意思是协程之间的调度,你可以去试一下 yield 怎么玩
    sunny1688
        16
    sunny1688  
    OP
       2022-09-25 13:38:05 +08:00
    @aecra1 面试过了吗
    pkupyx
        17
    pkupyx  
       2022-09-25 16:31:49 +08:00
    @sunny1688 先分清 CPU 和 OS 。。。
    cooleggs
        18
    cooleggs  
       2022-09-25 16:42:32 +08:00 via iPhone
    协程分对称和非对称,比如 lua 就是非对称的,go ,python ,js ,swift 这些就是对称的。可以沿着这条线去深入理解。

    另外 go 的协程跟其他语言的不太一样了,以前 io 阻塞了协程就一直挂起,现在也可以抢占式的拿出来执行,即时环境变量限制了协程数量,有点类似线程
    wanguorui123
        19
    wanguorui123  
       2022-09-25 20:47:13 +08:00
    作者:匿名用户
    链接: https://www.zhihu.com/question/538436096/answer/2536546723
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    有栈协程是真的给你开了一个栈,例如一个 2K 大小的栈,这个栈用起来跟真正的线程栈是差不多的,像 go 那种有栈协程方案就是自己搞了一套类似操作系统调度线程的方案,只不过其分配的栈很小,调度也不需要线程切换,而且各种调度策略都可以针对场景优化。而目前主流的无栈协程方案(例如 C++,C#,Rust 等),是把一个协程函数编译成状态机的逻辑,然后用一块临时分配的堆内存去保存这个函数里的变量和协程状态机以及上下文等内容,这区别就很明显了。首先它只用保存你用到了的变量,而不是所有协程就直接开一个栈,比如我只用了一个 int ,那就只用存一个 int ,而不用去开一个 2k 的栈。说白了,无栈就是按需用内存,用状态机+按需保存的方式来让函数可以保存自己执行到哪个位置,能访问哪些变量。而有栈就是不管需求,直接给个大栈让你用,甚至其中某些方案基本就等于把操作系统调度线程的那套方案移到了用户态。所以无栈不管从效率,内存占用看当然是更优的方案,但是无栈需要编译器支持,有栈只需要编写同一套上下文切换的代码,而无栈没有编译器支持就必须手写切换的部分(因为这些部分是跟执行的程序逻辑有关的,例如状态机转换),不管是对实现者还是使用者都不够友好。而且如果你想把一些老代码用协程跑起来,通常移植到有栈协程上比无栈也要相对简单,现在主流的无栈协程基本都需要进行侵入式的修改,比如要加 asnyc await 等关键字标记等等,而像 Go 的协程直接 hook 系统调用,你在 go 程里阻塞等于让出协程执行权,这样可以几乎不用修改就能用协程并发跑一些老的,没有使用协程的同步阻塞代码。所以有栈方案没有被无栈干掉,而是两者都有人用。
    buster
        20
    buster  
       2022-09-26 08:58:14 +08:00
    通常来说,同步的业务逻辑,上协程,只是优化了你的执行逻辑,减少了 context 切换,不是密集型业务就直接略过吧
    SmiteChow
        21
    SmiteChow  
       2022-09-26 11:36:41 +08:00
    cpu 提供的并行叫多核,系统提供的并行叫多进程,多线程,语言提供的并行叫协程。不要抠其他细节了,只是一个概念而已。
    Yain
        22
    Yain  
       2022-09-26 11:45:27 +08:00
    Yain
        23
    Yain  
       2022-09-26 11:45:48 +08:00
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3373 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 13:16 · PVG 21:16 · LAX 06:16 · JFK 09:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.