Go 了解协程

134 阅读2分钟

为什么要用协程

我们经常会用到多线程去处理海量的请求,然而在Go中,是用多协程代替多线程去实现并发的。那协程和线程的区别是什么?Go中为什么要用协程呢?

协程和线程的区别

  • 资源方面

    linux及mac上默认的线程栈大小一般为8MB,而协程栈一般为2KB,节省内存。

  • 调度方面

    线程由操作系统调度,占用cpu时间,开销较大;协程由线程调度,开销小。

由于操作系统眼里只有一个线程,但这个线程中背地里操控着大量的协程。对于并发而言,更加轻便敏捷,因此Go用协程来解决并发。

协程的本质

且看下src库是如何实现协程的。

g结构体

Go将协程信息记录在src\runtime\runtime2.go下的g结构体,该结构体字段较多,以下只选择部分重要字段说明。

type g struct {
   stack        stack  // 记录了协程的高地址和低地址
   sched        gobuf  // 记录程序运行时的信息
   atomicstatus uint32 // 协程的状态
   goid         int64  // 协程的id号
   ...
}

对每个字段拆分讲解:

stack,记录了协程的高地址和低地址。

type stack struct {
   lo uintptr // 低地址
   hi uintptr // 高地址
}

sched,是一个gobuf结构体,部分结构如下:

type gobuf struct {
   sp   uintptr // stackPointer 栈地址
   pc   uintptr // programCounter 协程计数器,记录程序运行的当前行数
   ...
}

atomicstatus, 记录协程的状态。

goid,协程的id号。

程序演示

image.png

以上程序写了三个简单函数,并在主函数中启用协程do1()。同时对13行的do3()进行断点,然后启动调试。

程序运行后会卡在13行do3()位置,这时相对应的g结构体如下:

image.png

stack的高低地址分别指向运行栈中地址的起始和末尾。

sched则记录了do2()这个正在运行函数的位置(断点卡住还没有运行do3函数),以及计数器记录了运行代码的13行位置。

线程

协程我们知道了是g结构体,而协程是基于某线程上运行的,那这个线程的信息Go记录在哪呢?

答案是在src\runtime\runtime2.go下的m结构体上,m的结构如下,重点是记录了g0协程和当前运行的协程。

type m struct {
   g0      *g     // 线程运行的第一个go协程
   curg    *g     // 当前正在运行的协程
   mOS            // 针对不同的操作系统储存不同的线程信息
   ...
}

总结

以上只是简单介绍下线程和协程长什么样子,下一篇要深入细节研究下协程是如何在线程上运行的