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

golang 面试之协程比线程更轻量级?

  •  
  •   xmge · 2020-06-04 20:21:15 +08:00 · 7309 次点击
    这是一个创建于 1625 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在 golang 面试,一般都会设计到为什么 go 语言更好地支持高并发,

    答:在高并发场景下,创建一个协程比创建一个线程消耗的资源少很多,一般线程创建需要 8Mb,而协程只需要 2kb,所以在同性能的服务器下,协程可以支持更多的并发量。

    有的面试官就会问:协程比线程为什么少占这么多资源?

    h 在网上搜索相关文章,并没有找到很直接,很具体的说法,

    是不是只有我一个人不知道啊,望知道的大佬给普及下,感谢....

    第 1 条附言  ·  2020-06-05 11:01:47 +08:00
    42 条回复    2020-06-06 21:07:07 +08:00
    Austaras
        1
    Austaras  
       2020-06-04 20:58:55 +08:00   ❤️ 2
    neoblackcap
        2
    neoblackcap  
       2020-06-04 22:05:01 +08:00
    goroutine 只是在用户态创建一个数据结构,然后给它分配对应的堆。内核里面是没有对应的线程对象创建的,显然就低很多了
    CEBBCAT
        3
    CEBBCAT  
       2020-06-04 23:15:02 +08:00 via Android
    GMP,请。

    这个问题其实是有点点难度的,首先要了解线程开销,然后还要明白 Go 是怎么调度协程的。对于进程上下文、kernel&CPU 工作原理应该都要有了解吧
    CEBBCAT
        4
    CEBBCAT  
       2020-06-04 23:15:30 +08:00 via Android
    @CEBBCAT 我记得加了“其实我也是菜鸡”的,怎么发出来没有了。。。
    sadwin
        5
    sadwin  
       2020-06-04 23:31:05 +08:00   ❤️ 1
    一言蔽之:协程不是内核线程,是 runtime 自己支持的调度机制,本质上是单线程
    ke1e
        6
    ke1e  
       2020-06-04 23:32:28 +08:00 via Android
    因为它只是个数据结构
    wellsc
        7
    wellsc  
       2020-06-04 23:38:41 +08:00
    建议买本操作系统的书看看
    lasuar
        8
    lasuar  
       2020-06-04 23:51:45 +08:00
    这个问题就要深入了解 go 的两级线程调度模型了,简单来说 go 是自己的 runtime 中实现了用户线程和系统线程的相互转换调度的过程,其他大部分语言的用户线程基本都是直接 1:1 系统线程,线程调度交给了内核,而 go 的用户线程与内核线程的配比是 M:N,go 在这两种线程之间的转换调度做了不少工作,正式这个做了这个优化才使得 go 的用户线程相比于其他语言的用户线程而言是十分轻量的。
    chihiro2014
        9
    chihiro2014  
       2020-06-05 00:07:03 +08:00
    其实感觉就是个 Java 中的单线程线程池(讲的不对,但感觉就是这么一回事)
    linvon
        10
    linvon  
       2020-06-05 00:09:50 +08:00
    GMP 调度模型, 用户态调度与内核态调度的区别,动态栈增长
    gdt
        11
    gdt  
       2020-06-05 00:18:43 +08:00
    协程上下文切换不需要切换到内核态,上下文切换的代价小。
    gdt
        12
    gdt  
       2020-06-05 00:20:07 +08:00
    为什么协程切换的代价比线程切换低? - 暗淡了乌云的回答 - 知乎
    https://www.zhihu.com/question/308641794/answer/572499202
    gdt
        13
    gdt  
       2020-06-05 00:23:04 +08:00
    为什么协程切换的代价比线程切换低? - 张凯(Kyle Zhang)的回答 - 知乎
    https://www.zhihu.com/question/308641794/answer/570701196
    snxq1995
        14
    snxq1995  
       2020-06-05 00:41:48 +08:00 via Android
    协程是用户态的,线程是内核态的。内存占有小
    协程是 go 运行时调度,线程是系统调度。切换成本小。
    sagaxu
        15
    sagaxu  
       2020-06-05 00:51:08 +08:00 via Android
    一般线程创建需要 8Mb 吗?

    即便笨重如 Java,一个线程也就 1M
    wangyzj
        16
    wangyzj  
       2020-06-05 01:16:43 +08:00
    不是 epoll 吗?
    ppphp
        17
    ppphp  
       2020-06-05 01:51:27 +08:00
    楼上说的很对,M:N 的模型保证了它的性能和足够好用,其实这个实现起来并不容易
    vk42
        18
    vk42  
       2020-06-05 05:30:48 +08:00
    线程创建要 8MB ???进程表示瑟瑟发抖……
    hercule
        19
    hercule  
       2020-06-05 07:39:56 +08:00 via iPhone
    这么多人回答,连问题点都没理解到,别人说的为什么协程比线程资源占用少,不是什么调度机制,什么内核态用户态
    hercule
        20
    hercule  
       2020-06-05 07:40:12 +08:00 via iPhone
    虽然我也不知道答案
    gimp
        21
    gimp  
       2020-06-05 09:03:33 +08:00
    线程运行在进程中,协程运行在线程中。
    ipwx
        22
    ipwx  
       2020-06-05 09:09:18 +08:00
    因为切换线程是内核管的,要存储 /恢复一堆上下文。因为线程是通用的模型,所以操作系统内核为了不出错,会比知道更多程序运行细节的 go 编译器做更多的备份 /还原操作。另外切换线程进内核是需要中断触发的,又是一套比较复杂的流程。
    ipwx
        23
    ipwx  
       2020-06-05 09:11:07 +08:00
    @hercule 肯定是内核态用户态啊。协程是 go 模拟出来的,一堆协程运行在同一个线程上,Go 因为知道更多程序运行信息,不需要做 overkill 的上下文备份 /还原,上下文开销自然就小了。而且不用过中断,不用进内核(进内核需要改 CPU 特权等级,有不少操作要做),省了太多事情。
    129tyc
        24
    129tyc  
       2020-06-05 09:17:03 +08:00 via Android
    答案很简单,因为线程创建会分配更大的调用栈空间,比如 linux 下可以是 10M,而 go 协程创建默认的调用栈大小是 2K
    justicelove
        25
    justicelove  
       2020-06-05 09:25:32 +08:00
    就是内核线程和用户线程的区别, java1.2 之前也是用户线程,后来改成了内核线程
    BingoXuan
        26
    BingoXuan  
       2020-06-05 09:26:20 +08:00
    现实当中,裸机跑程序效率最高的。加入 OS 这么方便的一层后效率就低很多了。所以要高效最好不要让操作系统重新调度资源。因而在操作系统最小调度单位线程下,通过协程做并发处理好过用操作系统线程调度。如果进程是厂房,线程是流水线,那么协程流水线自动适应生产不同产品,避免闲置流水线或新建流水线来生产。让 n 条流水线生产出 m 种产品( m>n )。
    ylsc633
        27
    ylsc633  
       2020-06-05 09:49:45 +08:00
    你这个问题 我去某大厂面试也被问到! 一模一样的! 可惜当时我并没有看过也不记得操作系统相关

    emmmm.. 现在也没怎么看,就看了一些大佬关于 gmp 的!

    然后根据文章 汇总了一篇文章

    http://interview.wzcu.com/Golang/goroutine.html#goroutine-%E5%92%8C-thread-%E7%9A%84%E5%8C%BA%E5%88%AB

    希望对你有帮助(里面还有上百套笔试题,很多都是我面试亲自遇到收集的)! 另外, 我的想法是 还是得深入研究下 操作系统..(毕业多年,已完全忘光)
    ylsc633
        28
    ylsc633  
       2020-06-05 09:51:20 +08:00   ❤️ 1
    对了 记得看下这个概念 http://interview.wzcu.com/Golang/morestack.html
    keshawnvan
        29
    keshawnvan  
       2020-06-05 10:08:37 +08:00
    1.堆栈刚开始比较小
    2.比起线程去掉了线程局部存储
    fiypig
        30
    fiypig  
       2020-06-05 10:19:44 +08:00
    @ylsc633 大佬可以的 ,收藏起来了
    Philippa
        31
    Philippa  
       2020-06-05 10:53:54 +08:00
    大家看看第一个回答的视频,一个个技术要求来不断进化设计,看 20 分钟就好了,后面太多细节。就是那貌似是俄语口音的英语也只有自动英语字幕,不容易听。
    mengzhuo
        32
    mengzhuo  
       2020-06-05 11:04:55 +08:00
    1. 线程没这么多,一个 Goroutine 现在是 384 字节
    2. 轻量是因为,线程上下文切换不仅大,还需要 flush TBL
    haha370104
        33
    haha370104  
       2020-06-05 11:11:56 +08:00
    本质上就是单线程,我的个人理解是 runtime 机制带来的程序内的控制权转交
    cholerae
        34
    cholerae  
       2020-06-05 13:14:01 +08:00
    @mengzhuo 同进程的线程切换为什么要刷 tlb,而且新一些的 cpu 上有 asid 不用把 tlb 全刷掉了。
    yukiloh
        35
    yukiloh  
       2020-06-05 13:44:40 +08:00
    一楼一上来是个空白,吓了我一跳..
    mengzhuo
        36
    mengzhuo  
       2020-06-05 14:03:44 +08:00
    @cholerae 搞错哈,现在不用刷的话性能应该没有什么区别了吧。
    liuxingdeyu
        37
    liuxingdeyu  
       2020-06-05 15:35:13 +08:00
    在想一个问题,是不是还有个原因,就是协程使用的时间成本相对小于线程,因为协程的时间粒度更小
    lff0305
        38
    lff0305  
       2020-06-05 16:10:26 +08:00
    @sagaxu 没这么大, C/C++ call createThread api 的时候可以设置栈大小, 默认好像是 1M
    早几年记得 Java 好像是 512k 默认, 后来改成 128k 了 (可以用 -Xss 设置)
    xsen
        39
    xsen  
       2020-06-05 16:33:29 +08:00   ❤️ 1
    1. 协程是轻量级的线程,就是说资源占用少很多

    2. 可以认为协程是用户态的轻量级线程,不会涉及到上下文切换——那自然效率更高、性能更好
    这个类似用户态的网络协议栈,一样的道理

    其实从模型上来说,协程的底层机制类似基于 mq 的任务分发机制;只是协程底层针对一般的 mq 任务分发,多做了一层虚拟资源的调度中心(调度逻辑 cpu 资源)
    xsen
        40
    xsen  
       2020-06-05 16:39:11 +08:00
    也就是把一个线程当成一个逻辑 cpu,然后 go 做了一个基于时间片的调度中心,最小任务是协程
    因为都是在用户态(单个线程),所以资源消耗比线程小,没有上下文切换

    所以性能可以更好
    chanchancl
        41
    chanchancl  
       2020-06-05 17:39:23 +08:00
    Golang 中,创建一个协程,仅仅是在用户态下创建一个 Goroutine 数据结构
    而一般的线程,则要到内核态去创建,这之间就涉及到 CPU 在两个状态之间的转换。

    其次 GMP 调度中,G 的调度始终是由 M 来完成的,M 由依赖于后面实际绑定的 P
    P 一般来说就是原生的线程了。在 M 调度不同的 G 也就是 Go routine 时,
    不需要从用户态切换到内核态,只需要将 Go routine 的上下文保存,并从自身队列或者全局队列寻找需要调度的 G,
    如果由,则进行调度,没有,则在积累一定次数之后,解除与 P 的绑定,并休眠,等待一些 singal 的触发。

    本质上来说就是楼上很多人讲的多核:多线程的 M:N 模型,所以调度效率较高,使用的资源较少
    Jony4Fun
        42
    Jony4Fun  
       2020-06-06 21:07:07 +08:00
    @ylsc633 好强!感觉很给力
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2469 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 16:05 · PVG 00:05 · LAX 08:05 · JFK 11:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.