goroutine
go中的并发主要靠协程(goroutine)。不同于C和C++中常用的多线程,协程并不与操作系统中的线程一一对应,操作系统是不知道有协程的存在的,协程间的调度由用户程序自己控制。go在runtime、系统调用等多方面对goroutine调度进行了封装处理,从语言层面支持了协程,这也是go的一大特色。只需要在函数前加一个go关键字就可以创建一个协程。
协程就是在应用层模拟的线程,降低了线程间切换的耗费。协程也是基于线程实现的,程序在内部维护一组数据结构和线程,真正执行的还是线程,而协程执行的代码被放进一个队列中,由工作线程从队列中拉出来执行。
go的协程是目前各类有协程概念的语言中实现得最完整和成熟的,十万个协程同时运行也毫无压力,虽然我们不会这么写代码。go对各种io函数进行了封装,当这些异步函数阻塞时go就会利用这个时机将现有的执行序列压栈,切换到另外一个协程。但是由于协程是非抢占式的调度,无法实现公平的任务调用,也无法直接利用多核优势,所以我们也不能直接说协程是比线程更高级的技术。
虽然在任务调度上,协程弱于线程,但资源消耗方面协程远远小于线程。一个线程的内存在MB级别,而协程只需要KB级别。我们可以把协程的基本特点归纳为:
- 协程调度机制无法实现公平调度
- 协程的资源开销非常低,一台普通的服务器就可以支持百万协程
在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行,当函数返回时该goroutine也自动结束,且返回值会被丢弃:
func Add(x, y int) {
z := x+y
fmt.Println(z)
}
func main() {
Add(1, 1)
go Add(1, 1)
}
上面的代码创建了一个协程,并在主函数与协程中打印值。但是运行一下我们会发现只有一个输出。因为在协程开始工作之前,主函数已经退出了。
为了阻止主函数过早退出,显然我们可以Sleep一下,但是这并不是一个优雅的解决方案。想想C++中的thread库为我们提供了join函数用来等待线程执行完毕,这里我们也需要一个东西来让主函数阻塞住,在go里可以用channel来达到这个目的。
channel
channel用于goroutine之间的通信,类似于管道。可以看做一个FIFO队列。它的操作符是箭头,<-
它的类型定义如下:
ChannelType = ("chan" | "chan" "<-" | "<-" "chan") ElementType
chan T // 可以接收或发送类型为T的数据
chan<- float64 // 只能发送类型为float64的数据
<-chan int // 只能接收类型为int的数据
如果没有指定方向就是双向,既可以接收,也可以发送。
应该在生产者处关闭channel,如果在消费者处关闭容易引起panic:往一个已经关闭的channel发送数据会panic,接收会返回0值,可以用一个额外的参数检查channel是否已经关闭:
x, ok := <- ch
使用make初始化channel,并且可以设置缓冲区容量。如果channel没有缓存,那么只有通信两端都准备好后才会开始通信,否则保持阻塞。如果设置了缓存,则缓存满时发送方才会阻塞,缓存空时接收方才会阻塞。
往一个nil channel发送或接收数据会一直阻塞。
可以在多个goroutine中读写同一个channel,不必加锁。
// 使用make建立一个channel
var channel chan int = make(chan int)
// 或者这样写也行
channel := make(chan int)
// 初始化并设置缓存容量
channel := make(chan int, 1024)
// 写channel,在有其他goroutine来读之前会保持阻塞
ch <- value
// 读channel,如果channel之前没有写入数据,也会一直阻塞直至有人写入数据
value := <-ch
value, ok := <-ch
可以通过类型转换将一个channel转换为单向的,从而限制对其的操作(类似于C++中对const的使用):
ch4 := make(chan, int)
ch5 := <-chan int(ch4)
ch6 := chan<- int(ch4)
channel与slice类似,也对应一个由make创建的底层数据结构的引用。当我们复制一个channel或者将channel作为函数参数时,我们只是拷贝了一个引用,它们指向的底层数据结构是一致的。
所以使用channel改写一下上面的那个例子,使协程可以正常执行:
var complete chan int = make(chan int)
func Add(x, y int) {
z := x+y
fmt.Println(z)
complete <- 0 // 发个消息通知已经执行完了
}
func main() {
Add(1, 1)
go Add(1, 1)
<-complete // 直到取到消息之前会在这里阻塞住
}