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

某些语言的协程机制,其作用是什么,是否会造成额外的开销

  •  
  •   maxxfire · 2020-08-31 09:36:30 +08:00 · 6200 次点击
    这是一个创建于 1549 天前的主题,其中的信息可能已经有所发展或是发生改变。
    一般情况下线程就够用了,线程也是系统的最小调度单位。
    有些语言又提供了协程机制,那么在运行的时候不是还要多开一个线程(调度器或虚拟机)来调度这些协程?这不是一种资源的浪费吗。当然有些简单的协程可能直接用 siglongjmp 堆栈还原来实现了。
    像传统 C/C++,直接编译机器码,直接就跑了,简单粗暴,还整什么调度器。
    第 1 条附言  ·  2020-08-31 10:20:24 +08:00
    我的疑惑是这样子的,比如说 Go 语言的协程,它需要一个 Go 协程调度器,那么这个调度器我认为本身就是一种资源浪费。因为系统已经有线程调度器了,现在又在 Go 语言层面再创建一个 90%功能冗余的调度器。
    就好比如说,本来你可以直接去市场买菜,却自己去种菜。
    51 条回复    2020-09-01 11:47:03 +08:00
    wangyanrui
        1
    wangyanrui  
       2020-08-31 09:38:54 +08:00 via Android
    用户线程和内核线程有 1 比 1,1 比 n,m 比 n...
    从这方面入手了解一下,协程还是挺有用的
    maxxfire
        2
    maxxfire  
    OP
       2020-08-31 09:41:09 +08:00
    @wangyanrui 我的意思就是这样,为了实现协程无故多开线程,造成资源浪费。
    des
        3
    des  
       2020-08-31 09:42:52 +08:00   ❤️ 1
    这种基础问题,搜索一下很难吗
    https://www.zhihu.com/question/20511233

    协程最大的优势,不需要进入到内核空间
    对于“直接就跑了,简单粗暴,还整什么调度器”,我觉得你需要了解下为什么需要有多线程
    wangyanrui
        4
    wangyanrui  
       2020-08-31 09:43:44 +08:00 via Android
    @maxxfire 😂那为了实现多线程,创建了多个内核态线程,不是更浪费吗
    noe132
        5
    noe132  
       2020-08-31 09:44:34 +08:00
    因为很多 io 需要等,吞吐量要大就必须多线程。线程创建和切换都有一定的开销
    协程一般都是用 event loop 调度的,一般来说有 vm 的语言协程实现起来比多线程资源开销更小
    araaaa
        6
    araaaa  
       2020-08-31 09:49:47 +08:00 via iPhone
    写异步回调代码麻烦,从而使用协程进行阻塞变成同步命令式调用
    vk42
        7
    vk42  
       2020-08-31 09:52:40 +08:00
    @maxxfire 不知道你具体说的哪个语言,协程和“无故多开线程“没有直接联系。通用原理可以理解为用户态线程,避免普通线程切换的代价,但不同语言实现机制不同所以需要具体讨论
    zengming00
        8
    zengming00  
       2020-08-31 09:52:55 +08:00
    一句话解释清楚:线程由系统强行调度,可以做到几个人同时跑,协程是自己让出 CPU,这个人跑一会那个人跑一会
    Jirajine
        9
    Jirajine  
       2020-08-31 10:23:11 +08:00 via Android   ❤️ 1
    misaka19000
        10
    misaka19000  
       2020-08-31 10:24:08 +08:00
    因为协程的切换比线程的切换要快
    linvon
        11
    linvon  
       2020-08-31 10:26:21 +08:00
    这不是种菜,这是在楼下超市买菜。一方面大大减少买菜需要花费的时间和路费,另一反面方便快捷不需要知道怎么去菜市场
    buffzty
        12
    buffzty  
       2020-08-31 10:35:42 +08:00
    你觉得自己管理内存成本和 gc 管理内存成本哪个高
    Jooooooooo
        13
    Jooooooooo  
       2020-08-31 10:38:04 +08:00
    重新学一下进程和线程的概念吧

    主要分用户态的线程和内核态的线程
    tabris17
        14
    tabris17  
       2020-08-31 10:39:52 +08:00
    协程和线程 /进程的主要区别是协程切换行为是用户主动进行的,而不是操作系统进行的。

    不同语言的协程实现方式不同,主要分成 stackless coroutine 和 stackful coroutine,前者比后者节省内存,也能解决栈内存分配的性能损耗。但是语法丑陋
    lolizeppelin
        15
    lolizeppelin  
       2020-08-31 10:41:22 +08:00
    当然要有开销,一般的协程也有专门的调度器啊,
    没调度器要写傻掉, 你总不能所有异步的地方都自己 yeid 出去吧
    MrTreasure
        16
    MrTreasure  
       2020-08-31 10:42:27 +08:00
    go 一个协程占用的空间 4kb 并且可以动态扩容。创建一个线程的开销远大于一个协程。协程之间切换不用切换内核空间和用户空间,线程需要。

    当然最方便的还是 go func 就能开启新的协程。比 java 高到不知道哪里去了
    ysc3839
        17
    ysc3839  
       2020-08-31 10:49:41 +08:00 via Android
    C++ 的协程是无栈协程,语言本身也没有提供调度功能,可以看作可以中途返回的函数。在遇到 co_await 的时候可以得到一个用于恢复执行的 coroutine_handle,后面要怎么继续运行,是单线程还是多线程都是由用户自己决定了。
    LokiSharp
        18
    LokiSharp  
       2020-08-31 10:49:50 +08:00
    是这样的啊有多线程要什么协程
    xylophone21
        19
    xylophone21  
       2020-08-31 10:56:03 +08:00
    > 多开一个线程(调度器或虚拟机)来调度这些协程
    简单的一个逻辑问题, 假设多开一个线程额外消耗 1000,每次协程切换比线程切换少消耗 10,那么管理 100 个就回本了,管理 1000 个优势就上来了。
    RudyC
        20
    RudyC  
       2020-08-31 11:04:19 +08:00   ❤️ 2
    我的理解是这样的,不知道对不对:

    比如说一个时间片是 10ms,你的线程本来可以执行 10ms 的代码,这段代码里你需要进行到网络 IO,所以因为 IO 问题,这个线程才执行了 1ms 就被系统调度走,结果你这个线程才执行了 1ms

    如果你在线程里使用协程,因为不需要切换到内核态,那么你在用协程进行网络 IO 的同时还可以运行其它代码,这样你可以充分利用 CPU,完成这 10ms 的时间

    再加上协程的资源占用不高,而且不需要切换空间,感觉这个才是重点
    sagaxu
        21
    sagaxu  
       2020-08-31 11:08:56 +08:00 via Android
    当然有额外开销,不过起 100 万个协程很轻松,换成 100 万个线程就呵呵了
    lbp0200
        22
    lbp0200  
       2020-08-31 11:17:03 +08:00
    楼主说的非常对,协程只是用来解决异步 IO 的,所以没事别用协程。
    shuax
        23
    shuax  
       2020-08-31 11:17:31 +08:00
    原因很简单,线程资源消耗太大。
    SaKuLa
        24
    SaKuLa  
       2020-08-31 11:22:16 +08:00
    这个看不同语言的实现吧,Kotlin 的协程和线程开销差不多,但是能够将异步回调处理成同步的方法
    InkStone
        25
    InkStone  
       2020-08-31 11:24:10 +08:00
    且不说内核用户的切换开销,光是一个线程几 M 的内存,就已经吃不消了。
    mmggll
        26
    mmggll  
       2020-08-31 11:25:06 +08:00
    @ysc3839 这只是编译器部分的支持,实现要自己写。真正到 std 标准库提供,估计还得好久。。。
    lewis89
        27
    lewis89  
       2020-08-31 11:32:10 +08:00
    @RudyC #20

    是多协程工作的时候,多个协程的 IO 操作让 golang 给接管了,golang 应该有一个机制 将 IO 调用单独用一个线程来处理 派发 响应,像 epoll 调用就可以使用水平触发来 监听多个 fd,这样语言层面上,只要一个线程就能接管跟系统调用的操作,这样其它 golang 的线程就不用频繁进入内核态了,进入内核态首先要切换 MMU 的页表,L1 L2 cache 可能还会被 invalid 掉,单次内核态切换开销小,但是如果上千个线程的内核态频繁切换开销就大了,切换少了,很容易把线程给饿死,不频繁切换是不可能的。但是像 Java 这种线程池模型,你只能拿线程池死扛,没有其它好办法,因为语言层面上就没做协程,后续的版本可能会推出。

    另外协程也有自己的问题,就是公平调度的问题,万一一个协程长期跑着,不退让 CPU,这样可能其它协程就饿死了,在调度算法上面还需要做很多处理,至于协程的中断,可以参考 Java 的 GC safePoint 实现,应该是使用 Linux 的 mprotect 系统调用,在特定的汇编地址下,插入一些 nop 代码,当操作系统检测到 CPU 运行到这个地址的时候,就会触发软中断跳转到 mprotect 事先设置的回调调用

    有兴趣的朋友最好了解一下 epoll 跟 mprotect 调用,这两个函数读完说明书,基本上就了解协程是如何实现的了
    lewis89
        28
    lewis89  
       2020-08-31 11:34:57 +08:00
    @RudyC #20 这也是我称 golang 是虚拟机的原因,因为其协程本身就是虚拟了一套操作系统的调度功能,并且会在编译后的特定的汇编代码处插入特定指令,此时会触发一些 golang 协程系统内部机制的一些调用来完成调度功能
    misaka19000
        29
    misaka19000  
       2020-08-31 11:38:40 +08:00
    RudyC
        30
    RudyC  
       2020-08-31 11:40:49 +08:00
    @lewis89 是的,我记得在学习 golang 时看到有一个叫 netpoller 的东西,大概就是 epoll 的功能,只是 golang 利用 netpoller 来接管了网络 io 操作

    golang 用得不多,所以也不确定自己理解得对不对,感觉协程主要发挥空间是在 IO 密集场景 lol
    lewis89
        31
    lewis89  
       2020-08-31 11:42:56 +08:00
    另外协程这一套机制等于把协程的栈空间 全部放在 golang 整个进程的堆空间,如果频繁向操作系统申请释放内存空间,会造成内存抖动,因为现代操作系统有一套复杂的堆内存管理机制,需要整理碎片大小的内存空间,如果 golang 进程频繁申请释放内存空间,就会频繁触发操作系统的内存整理,而传统的线程,其栈空间都是预设好的,会随着调用增加,会随着返回减少,操作系统有特定的机制整理栈空间。
    RudyC
        32
    RudyC  
       2020-08-31 11:42:56 +08:00
    @lewis89 感谢评论,看完你的评论之后理解更多了
    lewis89
        33
    lewis89  
       2020-08-31 11:44:35 +08:00
    @RudyC #30 是的,协程本身就是应付互联网的 IO 密集型场景,计算密集型,其实没有多线程的必要,很多算法并不能并行。
    lewis89
        34
    lewis89  
       2020-08-31 11:49:09 +08:00
    @RudyC #32 其实简单来说,就是接管了调用,让 epoll 去完成多路 IO 复用,这样当 epoll 告诉你可以读这个 fd 的时候,就让协程回到运行态,协程应该算是被发明出来对抗回调地狱用的,毕竟大部分人习惯线性思维,而不是 callback hell
    Nugine0
        35
    Nugine0  
       2020-08-31 13:06:28 +08:00 via Android   ❤️ 3
    所有语言的异步 IO 都需要操作系统的非阻塞支持,比如 epoll 。
    应用注册 IO,当 IO 完成后,系统会通知应用处理对应事件,这样一个线程就能同时进行多个 IO 操作,不用被阻塞调用卡住。

    拿 js 举例,无栈异步语法分为回调、Promise 、async/await 三种,第一种是回调地狱,第二种链式调用开火车,第三种用同步格式写异步,最人性化。

    再拿 go 举例,有栈异步语法与同步一致,调度器会在进行 IO 时自动把协程切走。py 在没有 async 语法时用的是 gevent 有栈协程,把同步操作自动换成异步,无需修改代码。

    js 本身就是事件循环,无栈协程是加糖解决回调地狱。

    py 有两种协程,gevent 的有栈协程,asyncio 的无栈协程,都是为了提高 IO 效率。

    go 是有栈协程,m:n 调度,多个线程上可以运行多个协程,卡住时其他线程还会偷走多余的任务。本来一个线程只能进行一个 IO 操作,现在可以同时进行多个,提高 IO 效率。

    c++20 的是无栈协程,但可能有隐式分配。

    Rust 的是无栈协程,没有隐式分配,调度器要自己选择第三方库,基本上都有工作窃取算法。其中的无栈协程可以不用分配直接放栈上执行,也可以交给调度器作为顶层 future 执行。而且 Rust 没有 GC,实时性可以有保证。

    个人认为,在各种语言中,Rust 的无栈协程是最轻量的。
    owenliang
        36
    owenliang  
       2020-08-31 13:23:21 +08:00
    那点内存资源和 CPU 开销都不是重点吧,更好的编程模型让开发者更爽的表达自己的想法,这才是背后的意图。
    charlie21
        37
    charlie21  
       2020-08-31 13:34:08 +08:00   ❤️ 1
    你的感觉是对的

    它 不是因为替你种菜而优秀,而是把种菜这件辛苦事做成了一个 robust 的 layer,一个健壮的层。这是它的意义所在。它方便了那些想种菜而能力不足的人。

    本身就不想种菜,你不需要 它
    本身就在想种菜的时候自己种得非常好( robust ),那么你也不需要 它

    当你关注的问题是 菜是不是 robust,那么 谁种的菜也无所谓

    自己种菜的人是大有人在的,在它出现之前的日子里。

    (它的出现令种菜大师暗淡无光,令无名小卒洋洋得意:看 我是垃圾,但我也能有菜吃,它种的。我不会种,我会调用它种,就 OK 。你可想而知吹它的都是什么菜)

    -
    guanhui07
        38
    guanhui07  
       2020-08-31 14:41:18 +08:00
    协程 占用资源更少,能创建协程更多
    no1xsyzy
        39
    no1xsyzy  
       2020-08-31 14:46:30 +08:00
    @Nugine0 #35 还有 Lisp 系,基本没什么库,直接拿 call/cc 手撸就成(
    @charlie21 #37 然而楼主的问题是:“这种出来的菜能吃?”
    NakeSnail
        40
    NakeSnail  
       2020-08-31 14:59:52 +08:00
    感觉楼主的思考更具有启发意义,上面几位告诉楼主线程和协程优缺点的貌似没有楼主理解的深入
    fasionchan
        41
    fasionchan  
       2020-08-31 17:42:27 +08:00
    引入协程是为了解决 IO 阻塞问题,在高并发场景你不可能每个连接都分配一个线程去处理,这时协程的作用就体现出来了。协程比线程更加轻量级,占用资源更小。开销的话看实现,一般协程不需要很复杂的调度,在 IO 不可用时让出执行权,在 IO 就绪时重新执行,开销相对而言并不大。对于计算密集型场景而言,协程并没有什么用处,相反是个累赘。
    tlday
        42
    tlday  
       2020-08-31 17:45:13 +08:00
    我觉得 golang 的现状可能是因为开发组写 C++发现多线程加异步太费脑子掉头发。总结规律以后发现“用传递消息来共享数据,而不靠共享数据来传递消息”可以完美解决各种锁 /同步问题,减轻心智负担。

    但是如果这样做,就会导致本来不需要 IO 的场合平添了 IO,如果简单用线程实现,会有大量的 CPU 时间片被用在等待 IO 里,一个数据从这个线程到队列里面再到另一个线程,时间和资源全浪费在线程调度,上下文切换,内核 /用户态切换,Cache 页置换上了。

    这个时候就发现需要“框架”来接管这一层,这一层把线程间的 IO 改为线程间内存共享,对上一层屏蔽内部实现来减轻“框架”使用者的心智负担,做了这个“框架”之后,会发现 C++的语法来实现这些东西过于丑陋了,而且需要一种机制来确保编译期就把一些“数据被传递给另一个线程之后又被当前线程继续使用了”这种“框架”不能容忍的情况报错。于是干脆出了一个编程语言。

    如果你在做优化时,发现 golang 的协程对你的应用场景已经太重,那么你完全可以用没有“虚拟机”的语言来拿“心智负担”换运行速度,golang 只是提出了一种当前很多场景下更好的解决方案与设计理念,填补了“多 IO 场景下低心智负担的多线程编译型语言”这一空白。我记得实际上协程在 golang 出现之前是日渐式微的,因为自己实现调度算法大部分时间并不如操作系统内核的调度算法高效,有点类似于自己用 volatile 不如 gcc 的编译器优化高效,所以现在已经不推荐这么做一样。

    上面回答的比我专业的多,也没有我这么多“臆测”,但可能没有准确解释楼主的疑问,我就献丑了。
    xiaoliu926
        43
    xiaoliu926  
       2020-08-31 17:47:23 +08:00
    终于看到有我 kotlin 的身影了
    saberlong
        44
    saberlong  
       2020-08-31 18:01:28 +08:00 via Android   ❤️ 1
    @tlday 有个地方有问题。推荐传递不推荐共享的思想和协程没有关系。之前看到一篇文章,目前 go 写的代码中,还是共享的比例高。
    yufpga
        45
    yufpga  
       2020-08-31 18:27:53 +08:00
    首先一点,要知道一台 4 核 4 线程的计算机, cpu 最大只能并行处理 4 个任务, 也就是说最多 4 个线程能同时跑在 cpu 上。假如一个程序,创建了 100 个线程, 那么同一时间只有 4 个线程能跑;如果我创建的是 4 个线程和 100 个协程, 虽然我也只能同时跑 4 个线程, 但是考虑到协程上下文切换以及协程本身所消耗的资源要比线程小很多, 你说哪个划算. 而且不同语言的协程方式实现不一样,并不一定需要和你说的一样需要创建一个线程来专门调度这些协程. 比如有的是栈协程(python 等), 还有 go 语言的 goroutine 调度也并不是专门给协程调度创建一个新的系统线程,而是通过 goroutine 绑定到系统线程(这个系统线程可并不是专门为协程调度服务的)
    tlday
        46
    tlday  
       2020-08-31 18:49:53 +08:00
    @saberlong 我个人觉得 go 用协程主要就是为了解决“线程间消息传递”的性能问题,“线程间消息传递”又是为了解决多线程程序的锁,内存共享,线程同步,内存释放的心智负担问题。绕过协程直接共享在可以自己解决这些问题的况下,确实比传递轻量也简单的多。感谢指正。
    janxin
        47
    janxin  
       2020-08-31 18:52:42 +08:00
    LZ 的疑惑应该是没有弄清楚协程本身的背景。这种本身具有切换逻辑,只是是协程思想的具体实现之一。而调度器只是作为这种实现的一种落地实现方式而已。

    https://zh.wikipedia.org/zh-hans/%E5%8D%8F%E7%A8%8B#%E5%90%8C%E7%BA%BF%E7%A8%8B%E7%9A%84%E6%AF%94%E8%BE%83

    具体怎么实现和理念并不冲突的,比如协程其实也区分为 stackful coroutine 和 stackless coroutine 。是否包含调度只不过可以理解是根据不同场景的不同取舍或者设计,带有调度器并不是只有 Go 一种语言,另外多种语言也包含了调度器实现,比如 Rust 、Python 。

    Go 中比较不一样的点是 goroutine 我个人认为其实是混杂了 fiber 和 coroutine 的实现的,既有用户态线程实现又有 coroutine 的内容,主要是为了解决实现用同步方式编写异步代码的需求。
    RubyJack
        48
    RubyJack  
       2020-08-31 19:35:00 +08:00
    协程+异步 io 可以模拟出同步阻塞的效果, 减轻人心智负担的同时, 也拥有足够出色的性能
    secondwtq
        49
    secondwtq  
       2020-08-31 20:12:59 +08:00
    请楼主立刻拆除电脑里所有 GPU 换上 IBM 8514 。

    比如说一个现代的 GPU,它需要占一个 PCIe 的插槽(或者 CPU 的 die area ),还要耗费额外的电力,最重要的还要花钱买,,那么这些我认为都是极大的资源浪费。因为 CPU 什么运算都能做,图形运算也可以,现在又单独搞那么一块功能 100% 冗余的 GPU 。
    就好比如说,本来你可以直接去市场买菜,却自己去种菜。
    littlewing
        50
    littlewing  
       2020-09-01 03:12:48 +08:00 via iPhone
    首先你要了解,线程切换的成本为什么会很高
    skinny
        51
    skinny  
       2020-09-01 11:47:03 +08:00
    我非常好奇某些人举例的几十万、百万协程的使用场景是不是实际经验,还是信口胡诌……
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4611 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 10:09 · PVG 18:09 · LAX 02:09 · JFK 05:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.