图解Go协程调度原理

阅读本文仅需五分钟,golang协程调度原理,小白也能看懂,超实用。

什么是协程

对于进程、线程,都是有内核进行调度,有CPU时间片的概念,进行抢占式调度。协程,又称微线程,纤程。英文名Coroutine。协程的调用有点类似子程序,如程序A调用了子程序B,子程序B调用了子程序C,当子程序C结束了返回子程序B继续执行之后的逻辑,当子程序B运行结束了返回程序A,直到程序A运行结束。但是和子程序相比,协程有挂起的概念,协程可以挂起跳转执行其他协程,合适的时机再跳转回来。

协程调度原理

N:1模型,多个用户空间线程在1个内核空间线程上运行。优势是上下文切换非常快,因为这些线程都在内核态运行,但是无法利用多核系统的优点。
1:1模型,1个内核空间线程运行一个用户空间线程。这种充分利用了多核系统的优势但是上下文切换非常慢,因为每一次调度都会在用户态和内核态之间切换。POSIX线程模型(pthread)就是这么做的。
M:N模型,内核空间开启多个内核线程,一个内核空间线程对应多个用户空间线程。效率非常高,但是管理复杂。

goroutine调度原理

本质上goroutine就是协程,但是完全运行在用户态,借鉴了M:N模型。如下图

https://cdn.llfc.club/21XVjuUKvgtqB5wYvFSyjTLV7Le.jpg

相比其他语言,golang采用了MPG模型管理协程,更加高效,但是管理非常复杂。
M:内核级线程
G:代表一个goroutine
P:Processor,处理器,用来管理和执行goroutine的。

G-M-P三者的关系与特点:
P的个数取决于设置的GOMAXPROCS,go新版本默认使用最大内核数,比如你有8核处理器,那么P的数量就是8
M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。
P包含一个LRQ(Local Run Queue)本地运行队列,这里面保存着P需要执行的协程G的队列
除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列GRQ(Global Run Queue),这个队列存储的是所有未分配的协程G。

假设我们的主机是单核的,那么协程运行图是这样:

https://cdn.llfc.club/21XWHrB8CklBfbdaieyTqmU8nzW.jpg

红色部分表示挂起和休眠,黄色部分表示准备就绪等待运行,绿色部分表示正在运行。
主机是单核的所以只有一个处理器P,但是系统初始化了两个线程M0和M1,处理器P优先绑定了M0线程,M1进入休眠状态。
P的LRQ队列里有G1,G2,G3等待处理。P目前正在处理G0,全局等待队列GRQ里保存着G4,G5,表示这两个协程还未分配给P。
如果G0在短时间内处理完,P就会从LRQ中取出G1继续处理。并且将GRQ全局队列中的部分协程加入LRQ中。

如下图

https://cdn.llfc.club/21XWhuzrkT8aGnatlAjqSbrkgNC.jpg

假设现在G1处理速度很慢,系统就会让M0线程休眠,挂起协程G1,唤醒线程M1进行处理其他的协程。这里M1会将M0未处理的协程取走处理。

https://cdn.llfc.club/21XWvnnNzjSVwVIzPb0OVJjWxH0.jpg

等到M1协程队列中所有协程处理完再次唤醒M0,或者M1处理某个协程时间较长被挂起,M0也会被唤醒。
上面的讨论是单核主机情况,如果是多核的,就会运行多个P和M,如下图

https://cdn.llfc.club/21XX2lkN1zJ4fdnLg1aI6eBbUlf.jpg

M0和M1分别运行在不同的内核中,M0处理G1,G2,G3,M1处理G4,G5,G6。
有人会问,当M0处理完所有的协程,而M1还未处理完,系统会如何做呢?
M0会取走M1的一半数量未处理的协程。

总结

golang协程设计非常优秀,一方面极大的利用了内核线程和处理器资源,另一方面每个处理器的LRQ队列的协程都处于用户态,这些协程的处理和挂起操作都是用户态的,协程切换开销非常小。相比其他语言的线程设计,更加轻量和高效。

热门评论

热门文章

  1. vscode搭建windows C++开发环境

    喜欢(596) 浏览(98549)
  2. 使用hexo搭建个人博客

    喜欢(533) 浏览(13974)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(15631)
  4. MarkDown在线编辑器

    喜欢(514) 浏览(15897)
  5. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(7196)

最新评论

  1. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  2. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  3. C++ 线程池原理和实现 mzx2023:两种方法解决,一种是改排序算法,就是当线程耗尽的时候,使用普通递归,另一种是当在线程池commit的时候,判断线程是否耗尽,耗尽的话就直接当前线程执行task
  4. 利用指针和容器实现文本查询 越今朝:应该添加一个过滤功能以解决部分单词无法被查询的问题: eg: "I am a teacher."中的teacher无法被查询,因为在示例代码中teacher.被解释为一个单词从而忽略了teacher本身。
  5. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗

个人公众号

个人微信