说到 Go 语言,被人讨论最多的就是 Go 很擅长做高并发,并且不需要依赖外部的库,语言本身就支持高并发。Go 实现这一能力的秘密是 goroutine,也经常被称之为协程,goroutine 是 Go 对协程的实现。在这篇文章中,会介绍协程的基本概念,以及 goroutine 的基本使用。1.什么是协程
协程(Coroutine),又被称之为微线程,这个概念出现的时间很早,在 1963 年就有相关的文献发表,但协程真正被用起来的时间很短。对于操作系统来说,线程是最小的调度单位,但对于一些高并发的环境,线程处理起来就比较吃力,一方面操作系统能够分配的线程数量有限,另外线程之间的切换相对来说也比较大。所以对于 Java 这类以线程为调度单位的语言,一般会依靠外部的类库来做到高并发,比如 Java 的Netty 就是一个开始高并发应用必不可少的库。协程和线程非常类似,只是比线程更加轻量级,具体表现在协程之间的切换不需要涉及系统调用,也不需要互斥锁或者信号量等同步手段,甚至都不需要操作系统的支持。协程与线程的行为基本一致,但是协程是在语言层面实现的,而线程是操作系统实现的。2. Go 语言的协程
在 Go 语言中,支持两种并发编程的模式,一种就是以 goroutine 和 channel 为主,这种方式称之为 CSP 模式,这种方式的核心是在 goroutine 之间传递值来来实现并发。还有一种方式是传统的共享内存式的模式,通过一些同步机制,比如锁之类的机制来实现并发。Go 程序通过 main 函数来启动,main 函数启动的时候也会启动一个 goroutine,称之为主 goroutine。然后在主 goroutine 中通过 go 关键字创建新的 goroutine。go 语句是立马返回的,不会阻塞当前的 goroutine。一个 Go 程序中可以创建的 goroutine 数量可以比线程数量多很多,这也是 Go 程序可以做到高并发的原因,goroutine 的实现原理,我们后续的文章再详细聊,下面来看看看 goroutine 的使用。3. goroutine 的基本使用
goroutine 的使用很简单,只需要在调用的函数前面添加 go 关键字,就会创建一个新的 goroutine:func goroutine1() {
fmt.Println("Hello goroutine")
}
func main() {
go goroutine1()
fmt.Println("Hello main")
}
预想中的 Hello goroutine
并没有出现,因为 main 方法执行完成之后,main 方法 所在的 goroutine 就销毁了,其他的 goroutine 都没有机会执行完。可以通过设置一个休眠时间来阻止主 goroutine 执行完成。func goroutine1() {
fmt.Println("Hello goroutine")
}
func main() {
go goroutine1()
time.Sleep(1 * time.Second)
fmt.Println("Hello main")
}
Hello goroutine
Hello main
但是这种方法也存在一些问题,这个休眠时间不太好设置,设置的过长,会浪费时间,设置的过短, goroutine 还没运行完成,所以最好的方式是让 goroutine 自己来决定。我们再改动一下代码:func goroutine2(isDone chan bool) {
fmt.Println("child goroutine begin...")
time.Sleep(2 * time.Second)
fmt.Println("child goroutine end...")
isDone <- true
}
func main() {
isDone := make(chan bool)
go goroutine2(isDone)
<-isDone
close(isDone)
fmt.Println("main goroutine end..")
}
在上面的代码中,我们使用了 chan
类型,这个类型我们后续会详细讲解,暂时只需要知道创建一个 chan 类型的变量,传入到一个子 goroutine 之后,它就会阻塞当前的 goroutine,直到子 goroutine 执行完成。这种方式比上面设置休眠时间的方式要优雅很多,也不会产生一些意料之外的结果。child goroutine begin...
child goroutine end...
main goroutine end..
但这种方式还是不完美,现在只启动了一个 goroutine,如果要启动多个 goroutine,这种方式就不管用了。当然,肯定还是有解决办法的,看下面的代码:func goroutine3(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("child goroutine %d begin...\n", id)
time.Sleep(time.Second)
fmt.Printf("child goroutine %d end...\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go goroutine3(i, &wg)
}
wg.Wait()
}
这个代码看起来要复杂不少,其中 sync
包中包括了 Go 语言并发编程的所有工具,我们用到的 WaitGroup
就是其中的一个工具。首先创建一个 WaitGroup 类型的变量 wg,每创建一个 goroutine,就向 wg 中加 1,每个 goroutine 执行完成之后,就调用 wg.Done,这样 wg 就会减 1,wg.Wait() 会阻塞当前 goroutine,直到 wg 中的值清零。如果熟悉其他语言同步机制的人就会想到,这不就是信号量么,是的,这就是使用信号量来实现的。这个 WaitGroup 与 Java 语言中的 CountDownLatch
功能是一样的。child goroutine 4 begin...
child goroutine 0 begin...
child goroutine 3 begin...
child goroutine 2 begin...
child goroutine 1 begin...
child goroutine 1 end...
child goroutine 2 end...
child goroutine 3 end...
child goroutine 4 end...
child goroutine 0 end...
到这里,我们了解了 goroutine 的基本使用,但很多情况下,goroutine 不是独立运行的,而经常需要与其他的 goroutine 通信,在下一篇文章中,我们将详细的聊一聊 goroutine 之间的通信方式。4. 小结
在这篇文章中,我们了解了协程的概念,并且知道了 goroutine 是 Go 语言对协程的实现。也知道了如何通过启动一个新的 goroutine 并发的去做一些事情,同时也知道了如何让 main goroutine 来等待其他 goroutine 工作完成再退出的几种方法。