赣州新闻网

首页 / 科技

Go语言进阶之路(五):通道和goroutine、GPM

2020-04-05 · 赣州新闻网 · 科技

通道

Go语言可以有效地利用多核CPU,并发性能好,这正是由于goroutine和通道还有GPM模型的原因。

我们知道,Python语言由于全局锁GIL,单个Python应用的多线程代码没^ n J办法$ ! , h L q N 9 有效利用多核CPU,只能写多进程来利用多核CPU。如果用标准库的multiprocessing会对监控和管理造成不少n l +的挑战。部署Python应用的时候通常是每个i ; nCPU核部署一个应用,这样会浪费不少资源。Go语言的goroutine和GPM模型性能远远超越Python的并发模型。先来看看通道和go; ] ( hroutine。

G, p { To语言中通道类型是ch W x }an关键字,通~ r ? ^ } -过make来创建。可以创建带a Z x _ 1 %缓冲的通道和无缓冲的通道- j # u t Q I。

创建通道

unbuffered := make(chan int)  // 无缓冲的整型通道
buffered := make(chan string, 10)  // 有缓冲的c K 5 # 2字符型通道

缓冲通道和无缓冲通道有什么区别?

无缓冲通道没有容量,我们想要往通道中发送数据,那么一定要有另一个操作在往这个通道取数据,否则,发送数据的那段Y ; N Z代码也会被阻塞。

缓冲通道只是有一段容量可以缓冲一下,当缓冲区未满时,往通道中发送数据不会阻塞当前操作;当缓冲区满了时往通道中发送数据,会想无缓冲通道那样阻塞。

发送数据

buffered <- \"Gopher\"  // 通过通道发送一个字符串

接收数据

received := &nbs s 5p;<- buffered        // 通过通道9 v j A D ~ g e ~接收一个字符串
<- unbuffered                , , # &nbsV z Yp; // 接收并丢弃字符串l W k ^ u,如果unbuffered通道里没数据,则会一直阻塞直到有数据放入
receiver, ok := <- bufferedM ) X e O & N ^ l
if !ok {
    xxx Z f M o q;  &[ L V 0 Z J cnbsp;&x c ~ J u m q & |nbsp;  &n( n n Z K ,bsp;        &nb5 u 3 v 0 d 2 _ Asp;   // 通道= C 0被关闭等问题
}

关闭通道

通常来说,我们往通道里发送完数据后,我们应该关闭通道(一般来说由生产者来关闭通道r J e i s d m),这就相当于告诉消费者,我的数据发送完了,你取完了数据之后就离开吧。

我们应该在所有数据都放入通f } , ! z k道后再关闭通道,否则,如果先关闭通道,再往里放数据的话会产生panic。看下面的例子:

cl) ^ h | ^ - | Wose(buffered)


go func() {
  for {
&nbm H $ ,sp;   j, more := <-jobs&nb V . j x [sp;       // 如果通道5 v * ~ f Q Z obs\")
      done <- true! # S W ; R { ) j
  u k B W E;&ns F U n Z j 2bsp;   returnz S j + o _ * M R
   &E s % Y pnbsp;}
  }
}()


for po8 Y f Fst := 1; post <= taskLoad; post++ {
  tasks <- fmt.Sprintf(\"Task : %d\1 O u M . w", post)
}
// 当所有工作都处理完时关闭通道,以便所有gorout: 1 - # a r vine退出
close(tasks/ ( c m)
通道可以用来实现同步功能:
func workeG z ) ` n b % @r(done chan bool) {
  fmt.Print(\"working..D P C x i++ {
    ch <- i
  }
  close(ch)
}

func consumer(ch <-chan int) {
  fo! # N + F * V ur i := range ch {
    fmt.Println(\"consumed something: \", i)
  }
}

func main() {
  ch := make(chan io 4 1 Q / z +nt)
  go producer(ch)
  go consumer(ch)
  time.Sleep(time.Second)
}

gorou+ K n @ ` , + ` &tine

Go语言中的goroutine,一般也称作# , l #协程,不由OS调度,而是用户层自行释放CPU,从而y 2 R在执行体之间切换调度运行。Go语言3 & 3 D A : ! 5在底层进行协助实现。涉及系统调用的地方由Go标准库协助释放Ch ? *PU。总之,3 1 2 t 9 O _协程运行不通过OS进行切换,由Go语言自行切换,系统运行K O _ ] | x开支大大降低。一个进程内部可以运行多个线程,而每个线程又可以运行很多协程。线程要负责对协程进行调度。当一个协程睡眠时,它将线程的运行权让给其它的协程来运行。同一个线程内部最多只会有一个协程正在运行。

创建Goroutine

创建goroutine很方便,创建个匿名函数并调用,然后在前面加上go关键字就可以了。

go funN P Ic(msg stri| J i Fng) {
    fmt.Printlnw E ] 0 1(msg)
}(\"going\")

还可以把函数赋给一个变量,然后创建goroutine运行:

var myFunc = func(msg strin: B c V *g) {
    fmt.1 = 9Println(msg)
}
go myFunE 4 T H z $ m R zc(\"going\"R S ` X ~ 7 9)

主协程和子协程

Go语言所有代码都运行在协程中,由Go运行时进行调 k U L度,其中main函数所在的协程叫做主协程,其余的协程叫做m M 6 t = E p X 2子协程。

func main() {
&$ F j J A unbsp; for i :F q L ~ ? X $= 0; i < 5; i++ {
    go fuk U H $nc(i int) {
 &nR 3 e o O x Bbsp;    fmt.z { e { + a 4 dPrint(i, \" \")
&nbM 9 L m Tsp;  &7 ( Xnbsp;}(i)
  }
  time.Sleep(time.Second)
}
// 输出:0 4 3 1 2

当主协程执行完而子协程未执行完时,所有子协程会随着主协程的结束而退出运行。上面的代码9 Y C 4 k 9 H } 2输出“0 4 3^ ^ m g h D D h Y 1 2 ”,这是因为创建完协程后放入运行时队列到真正运行有个过程,哪个协程先被放, ` , N #入运行时队列,哪个就能先执行。

线程的调度是由操作系统负责? O c [ 7 9的,调度算法运行在内核态,而协程的调用是由 Go 语言的[ d U运行时负责的,调度算法运行在用户态。这就使得Go语言的协程比Java的线x / U &程更加轻量级。单一# ? O 2 S 3的Java应用创建几十上百个线程并运行就已经非常消耗内存和CPU资源了,而单一的Go应用可以创建上千万的goroutine还正常地运行。

GPM

先回顾一下Java中线程和操作系统内核线程的对应关系。Java中线程模型如下:

Go语言进阶之路(五):通道和goroutine、GPM

Java线程的实现方式是通过操作系统提供的高级编程S : y p #接口——LWP(轻量级进程)来对应到内核线程上的,一个Java线程对应到一个LWP,对应到一个KLT(内核线程)。当Java线程阻塞,需要调度时,由内核( ! e调度器来进行切换,把当前KLT-LWP对应的Java线程切换到另一个Java线程,这种切换操作在内核态中进行,这种切换是很耗时的(相对CPU来说)。Java的一个线Y 0 5 |程对应到一个内核线程,一般创建Jav- E = ? G q l e .a线程占用的内存都大于1M。

Go语s V h $ 1 [ ` F x言使用了用户级线程的实现方式,Go语言中的goroutine可以理解为用v ? r户态的线程,调度切换goroutine直接在用户态进行* 8 : 6 D W f H,不用切换到内核态。

Go 调度器模型我们通常叫做GPM 模型,包括 4 个重要结构:

  1. G:Goroutine,每个 Goroutine 对应一个 G 结构体,我们使用go关键字创建gorout7 l T . X Gine,并非就一定创建了G结构体的实例,只有当没有可用的G时,才会创建G来装载我们创建的goroutine,u a k否则,会复用现有可用的G来装载goroutine。G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 Pv } q z # 才能被调度执行。
  2. P: Processor,表示逻辑处理器,对I 4 g ^ U ~ $ K v G 来说,P 相当于 CPU 核心,G 只有绑定到 P 才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等。P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量)。P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为Y @ } k } _ ; E多大,P 的数量最大为 256。
  3. M: Machinel f V m # m X n,OS 内N _ P [ ) f M q ^核线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取。M 的数量是不定的,由 Go Runtime 调整,为了) } 4 I s l防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。_ [ 2 9 % 3 i a ~M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。
  4. Sched:Go 调度器,它维护有存储 M 和 G 的队列以及调[ = E P度器的一c } P 1 = b Q i -些状态信息等。

GPM和Sched的关系如下:

Go语言进阶之路(五):通道和goroutine、GPM

​Go语言调度器Sched拥有全局运行队列(X ( R % Y ? H ,言中创建的gorou@ z 7 $ + 4 3 n ltine只需要大约占用2KB的内存,比Java中线程占用空间小多了。因此,Go语言程序可以轻松hold住百万goroutine的调度运行。

GPM源码和Sched源码如下,从源码中,我们更能清楚GPM之间的关系:

G

G中会保存goroutine运行函数,goroutine的id,还有被绑定运行的M3 N F b K的指针:

struct G {
uintptr stackguard; // 分段栈的可用空间下界
uint. G K q f 0 V e [ptr stackbase; // 分段栈的栈基址
Gobuf8 ( k H I W ) 7 sched; //进程切换 v J u L ]ack0;
FuncVal* fnst} ! e % 3 J : : ^art; // goroutine运行的函数
void* param; // 用于传递参数,睡眠e 8 Y W I P i N时其它goroutine设置param,唤醒时此goroutine可以获取
int16 status; // 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
int64 goid; /Y c 7 2 H ? ./ goroutine的id号
G* schedlink;
M* m; // for debuggers, but offset not hard-coded
M* lockedm; // G被锁定只能在这个m上运行
uintptr gopc; // 创建这个goL 1 . U |routine的go表达式的pc
...
}V Q { M;

P

P中含有# I O 0 4 V R本地运行队列LRQ,P当前的状态:

struct P {
Lock;
uint32 stat3 L C v . d , P }us; // Pidle或Prunning等
P* link;
uint320 ` s a schedtick; // 每次调度时将它加一
M* m; // 链接到它d l g J [ f关联的M (nil if idle)
MCache* mcache;

G* runqx ; 0 A 8 1;
int32 runqhead;
int32 runqtail;

// Availy # p 0 # W + w 8able G\'s (status == Gdead)
G* gk * 5 1 # j 1 6 mfree;
int32 gfreecnt;
byte pad;
};

M

M中包含了绑c 6 . j ! Z G定到M_ } H 5 ? m - v的P的指针,正在M上运行的G的指针,M的状态等等:

struct M {
G* g0; // 带有调度栈的goroutine
G* gsignal; // signal-handling G 处理信号的gorW ` 5 D Yd;
G* runqtail;
int32 runqsizp l O 8 O x u N Pe;

// Global cache of dead G\'s.
Lock gflock;
G* gfree;

int32 stopwait;
Note stopnote;
uint32 syZ d J 5 5 ] X I Wsmonwait;
Note sysmonnote;
uint7 V K64 lastpoll;

int32 profilehz; // cpu profiling rate
}

工作接口设置代表维护告诉瑞金语言进阶