并发编程表现为程序由若干个自主的执行单元组成,在Go里,每一个并发执行的活动称为goroutine。从宏观作用上看,goroutine类似于操作系统或其他编程语言中的进程/线程,但实现却大不相同。
Go语言中,当一个程序执行时,只有一个goroutine来调用main函数,这个routine称为主goroutine,新的goroutine通过关键字 go 进行创建——在函数/方法调用前加上go。这使得函数在一个新创建的goroutine中调用,go语句本身的执行立即完成。一个简单的例子如下:
package main
import(
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s, time.Now())
}
}
func main() {
go say("Hello")
say("World")
}
运行结果参考如下:
需要注意的是,goroutine是异步执行的,因此在并发编程时使用go关键字必须要仔细考虑函数的并发调用是否安全,经常发生的情况是主程序退出时goroutine还未执行完导致错误结果。
goroutine之间经常需要进行通信,通道channel就是goroutine之间的通信连接,它是可以让一个goroutine发送特定值到另一个goroutine的通信机制。每一个通道都有具体类型,这叫做通道的元素类型,它规定了通过这个通道能发送和接收的数据类型。例如:一个收发int类型数据的通道写为 chan int。
使用内置的make函数来创建一个通道,像map一样,得到的返回值是一个使用make创建的数据结构的引用。当复制或者作为参数传递到一个函数时,复制的是一个引用,这样调用者和被调用者都使用同一份数据结构。通道之间可比较,零值是nil。通道的接收和发送操作由操作符 <-控制。
ch := make(chan int) //make函数还可以接受第二个参数,用来创建一个有缓冲通道
var x, y int
x = 3
ch <- x //发送语句
y = <- ch //接收语句
<- ch //合法接收语句,丢弃结果
通道的关闭可以通过调用close函数来完成,对关闭后的通道执行发送操作将会触发panic。在一个已经关闭的通道上进行接收操作,将获取所有已经发送的值,直到通道为空,这时任何接收操作会立即完成,同时获取到一个通道元素类型对应的零值。
Go语言还支持单向通道的定义,在使用单向通道的时候如果违反通道的定义类型会在编译时被检查出来。
out := chan<- int //out是一个发送通道
in := <-chan int //in是一个接收通道
在参数传递过程中,双向通道可以隐式转换为单向通道,但单向通道不能转换为双向通道。
需要注意的是,关闭通道的close函数通过设置标志位来说明通道上没有数据再发送,仅仅在发送方goroutine上才能调用它,所以试图关闭一个仅能接收的通道会在编译时报错。
使用make函数创建一个通道时,第二个参数为0或省略不写时,会创建一个无缓冲通道。无缓冲通道上的发送操作将会阻塞,直到另一个goroutine在对应的通道上执行接收操作,这时值传送完成,两个goroutine都可以继续执行。相反,如果接收操作先执行,接收方goroutine将阻塞,直到另一个goroutine在同一个通道上发送值。
显而易见,使用无缓冲通道进行的通信会导致发送和接收的goroutine同步化,因此无缓冲通道也称为同步通道。
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v*v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
使用make创建通道时,可以用一个正整数值指定通道的缓冲区大小,这样的通道称为缓冲通道。缓冲通道有一个元素队列,队列最大长度在创建时已经指定。
缓冲通道上的发送操作在队列的尾部插入元素,接受元素则从队列头部移除元素。如果通道满了,发送操作会阻塞直到另一个goroutine对通道进行接收元素操作;反之如果通道是空的,接收操作会阻塞直到另一个goroutine发送数据。
内置的cap和len函数可以分别获取缓冲通道的容量和当前通道内元素个数。
select语句用于在多个发送/接收通道中进行选择。select语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select会随机选择其中一个执行。select的语法与switch类似,但select中的每个case语句都是通道操作。select语句的通用语法如下:
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
理解select作用的关键在于case的选择是基于通道的状态来决定的,如果通道为空,则此case不会执行。另外,如果通道值为nil的话也不会执行。当default被省略时,若所有case都不满足,则select语句会一直阻塞,导致死锁;所有的case中的通道值都为nil时也会导致死锁。
break语句可以用在select中,它会立即结束select的执行。
本文部分内容摘自《Go程序设计语言》,有改动