协程goroutine
go中的多线程称为协程goroutine。和java的Thread是有区别的。
goroutine相比java的Thread更加轻量,(在一些地方被称为绿色线程)。goroutine创建的用户态线程。java的thread是内核态线程,是真正的系统级线程。
(linux操作系统分为用户态和内核态)。所以thread进行上下文切换时需要进行内核态和用户态的切换,此时的性能开销就比较大。goroutine就没有这些切换开销,只要内存充足,一个go程序可以轻松支持上万的并发goroutine。
Go不支持创建系统线程,所以协程是一个Go程序内部唯一的并发实现方式。
每个Go程序启动的时候只有一个对用户可见的协程,称之为主协程。 一个协程可以开启更多其它的协程。
在Go中,开启一个新的协程是非常简单的。 只需在一个函数调用之前使用一个go关键字,即可让此函数调用运行在一个新的协程之中。 当此函数调用退出后,这个新的协程也随之结束了。
1 | package main |
只能分别执行3次,因为当main主协程结束后,子协程也就随之结束了。
这里使用log标准库的打印函数是因为log标准库的打印函数是经过了同步处理的,如果使用fmt的Println可能会有并发问题,不同协程的打印会交织在一起(这个例子来说,概率较低)。
如果改用java写
1 | public class ThreadTest { |
猜猜这个代码会打印什么结果?
结果是十次全部执行完毕。
在java的main方法中,并没有使用类似go的main函数的sleep方法。java程序会等待所有线程执行完后才会彻底退出。
原因是java的程序的运行是由jvm控制的,jvm的退出由以下条件决定:
- jvm退出的时机是所有的非守护进程结束了。(守护进程是setDaemon(true)设置的)
- main线程是一个用户线程。是jvm启动的运行用户程序的一个用户线程。
- 用户在main线程里开启的其他线程还是用户线程。
- 即使main线程退出了,只要有其他用户线程还在运行,jvm就不会退出
协程的调度
我们可以很容易的启动多个线程。但并不是所有处于运行状态的协程都在执行。在同一时刻,只能最多有和主机逻辑cpu个数一样多的协程在同时执行。
go运行时来保证cpu不断在多个协程之间切换,从而使多个协程得以并行执行。这和操作系统并行调度执行系统级线程的原理使一样的,只是这里由go-runtime来做这个事。

协程的详细状态图,注意睡眠和等待都属于运行状态。
go运行时采用一种被称为M-P-G模型的算法来实现协程调度。 其中,M表示系统线程,P表示逻辑处理器(并非上述的逻辑CPU),G表示协程。 大多数的调度工作是通过逻辑处理器(P)来完成的。 逻辑处理器P像一个监工一样通过将不同的处于运行状态协程(G)交给不同的系统线程(M)来执行。 一个协程在同一时刻只能在一个系统线程中执行。一个执行中的协程运行片刻后将自发地脱离让出一个系统线程,从而使得其它处于等待子状态的协程G得到执行机会。
并发同步措施
多线程代码我们最主要的是需要保障数据的并发安全问题。
WaitGroup
和java并发包中的CountDownLatch含义一致,用法也相似。用来等待其他线程/协程执行全部执行完毕后,才继续进行后续逻辑。
1 | package main |
WaitGroup主要用三个方法
Add:声明等待几个任务完成
Done:完成其中1个任务
Wait:组合等待,知道全部任务完成。
和java的CountDownLatch用法几乎一样