你好,Go程

译者注

Goroutine在本书中均译为"Go程",意思为"Go语言例程"。之所以不翻译为协程,是因为Goroutine与Coroutine有较大区别。

一般的,协程(coroutine)都有yield或await语义的关键词,用于明确地表示协程之间的关系,谁把执行权让给谁。而Go程(goroutine)并没有相关语义的关键词,它的并发行为更像是传统的线程。

PS.当然,我们也可以基于goroutine继续封装,实现await语义。

简单地说,Linux内核调度基本单位是线程(pthreads,POSIX threads),更抽象的层次叫做内核调度实体(KSE, Kernel Scheduling Entity)或称SE(Scheduling Entity),更具体的层次的实现叫原生线程(NPTL, Native POSIX Threads Library),它们通过内核调度器进行调度。

例如Java、Python的Threads库,就是对原生线程pthreads的封装,对应关系是1:1,即1个语言级线程对应1个系统级线程,只存在1个调度级别,由系统调度器进行调度。

而像Python、JavaScript 的语言级原生Coroutine,调度对象称为协程,与内核线程的对应关系是N:1,即多个用户态协程对应1个系统级线程,协程的调度归语言标准库库实现的。以协程视角看,只存在1个调度级别,即语言级维护的用户态调度器;以语言视角看,它虽然可以感知有2个调度级别,但只用操心自己实现的协程调度器,而不用关心容纳协程的线程是如何被系统调度的;以系统视角看,只有1个调度级别,即原生线程(pthreads)的调度。

而Gevent,是对Greenlet的封装,调度对象被称为伪线程(pseudothreads),又有称绿色线程(greenlets)或微线程(micro-threads),都是一回事。内核线程与它的对应关系也是N:1,多个伪线程对应1个系统级线程。

Gevent/Greenlet的伪线程与Python原生协程的不同点在于,伪线程封装后的表现形式更像线程,伪线程之间彼此没有实现await操作,它们之间要传递数据,是通过channel/queue实现的,而且Gevent也为获取伪线程的返回值而提供了比较友好的封装,例如spawn的对象可以在wait以后get返回值,也可以通过AsyncResult对象来传递值。

Golang的Go程与内核线程的对应关系为N:M,即多对多的关系,Go程之间也通过channel传递数据(也可以通过共享内存/共享全局变量的形式,但不安全不推荐)。以Go程视角看,它只能感知到1个调度级别,即语言维护的用户态调度器;以语言视角看,存在2个调度级别(所以N:M模型又称为两级(用户态级别和系统级别)线程模型),即用户态Go程的调度,以及系统级线程在语言中的封装的对象的调度(这一级别的调度,也只是操作Go程和系统级线程对象的绑定关系,并不真正关心线程对象和CPU的绑定关系,也不关心线程对象状态的转换);以系统视角看,只存在原生线程的调度。

存在于传说中的原生高并发语言Erlang的BEAM虚拟机所实现的并发模型,比上述三种都要复杂一些。以系统角度看,Erlang程序都存在于1个进程,K个线程中(K等于CPU核数),OS操心的只是这1个进程和K个线程的调度,抢占式。以语言角度看,也是两级调度,但Erlang的封装并非是用户态线程,也不是用户态协程,更应该称用户态进程,不止因为Erlang的术语就叫他process,而是它的封装后的性质跟OS的process类似:相互隔离,不共享内存,抢占式调度。不过Erlang语言级别的process的创建和调度开销比OS级别的process小很多,process之间也通过channel传递数据。

单个通道(channel),单个协程,一个写,一个读。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个用于传递string数据类型的通道
	ch := make(chan string)

	// 启动一个新的匿名协程
	go func() {
		time.Sleep(time.Second)
		// 发送 "Hello World" 到通道中
		ch <- "Hello World"
	}()
	// 从通道中读取
	msg, ok := <-ch
	fmt.Printf("msg='%s', ok='%v'\n", msg, ok)
}

// Output
// msg='Hello World', ok='true'

上面代码中的ch是无缓冲通道,又叫同步通道。

time.Sleep是为了解释main()函数会在ch上等待,意思是字面量函数会被当做协程执行,该协程通过通道发送数据需要一点时间。接收操作符<-ch会使main()函数的执行被阻塞。

译者注

Go语言中所称的字面量函数(Function literals)就是匿名函数,它可以被赋值给变量,也可以直接调用。而且它们还可以是闭包,因为它们可能引用外部包围函数中的变量,这些变量将在包围函数和字面量函数之间共享,它们不再被访问后才会回收。

如果main()函数不阻塞等待,则协程将在main()函数结束时被杀死,该协程甚至都没有机会把数据发送到通道。

译者注

go func() 语句的作用实际上是将协程加入调度,而不是真正立刻开始执行协程代码。协程真正开始和结束的时刻并不可预测,取决于系统中协程的调度顺序和协程自身的执行耗时。

main()结束时,所有的协程也都会被杀死,不论协程是否执行完完毕。所以要安全地结束协程,应该用无缓冲(同步)通道或sync.WaitGroup来保证协程都执行完毕。

最后更新于