goroutine 是 Go 并发能力的核心要素。但是,goroutine 到底是什么?
叫做 goroutine 是因为已有的短语 — 线程、协程、进程等等 — 传递了不准确的含义。 goroutine 有简单的模型:它是与其他 goroutine 并行执行的,有着相同地址空间的函数。它是轻量的,仅比分配栈空间多一点点消耗。而初始时栈是很小的,所以它们也是廉价的,并且随着需要在堆空
间上分配(和释放)。
goroutine 是一个普通的函数,只是需要使用保留字 go 作为开头。
ready("Tea", 2) // 普通函数调用
go ready("Tea", 2) // ready() 作为 goroutine 运行
看下面一个 goroutine 例子
func ready(w string, sec int) {
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, "is ready! ")
}
func main() {
go ready("Tea", 2)
go ready("Coffee", 1)
fmt.Println("I'm waiting")
time.Sleep(5 * time.Second)
}
程序输出:
I’m waiting ← 立刻
Coffee is ready! ← 1 秒后
Tea is ready! ← 2 秒后
注意:主 goroutine 如果不等待内部启动的 goroutine 完成就终止,则任何正在执行的 goroutine 也都将停止。为了避免该情况,可以使用 channel 进行 goroutine 间通信。
channel
channel 可以与 Unix sehll 中的双向管道做类比:可以通过它发送或者接收值。channel 有三种:只发送,只接收,可同时发送、接收。
必须使用内置函数 make 创建 channel。
//只能发送的通道
csi := make(chan<- int)
//只能接收的通道
cri := make(<-chan int)
// 可同时发送和接收的通道
ci := make(chan int) // 用于发送和接收整数
cs := make(chan string) // 发送和接收 string
cf := make(chan interface{}) // 空接口满足各种类型
发送和接收数据是通过操作符 <-
完成的,根据其位置判断是发送还是接收数据。
ci<- 1 // 发送 1 到 channel ci
<-ci // 从 ci 接收整数
i := <-ci // 从 ci 接收整数并赋值给 i
带缓冲的 channel
创建 channel 时也可以指定 channel 缓冲的大小,就是 channel 可以存储多少个元素。
ch := make(chan type, size)
当 size == 0
时,为不带缓冲的 channel,此时如果 ch 中有数据未读取,则再次向 ch 中发送数据时发送 goroutine 会被阻塞,直到其它 goroutine 从 ch 读取数据才能写入。当 size > 0
时,前 size
个元素可以无阻塞写入,当写入第 size + 1
个元素时,代码将被阻塞,直到其他 goroutine 从 ch 中读取一些元素,腾出空间为止。
range 和 close
使用 for val := range ch
能够不断的读取 channel 里面的数据,直到 channel 被显示的关闭。关闭 channel 之后就无法再发送任何数据了。
可以通过“逗号 ok”测试 channel 是否别关闭,
val, ok := <-ch
如果 ok 返回 false,则说明 channel 已经没有任何数据且已经被关闭。
注意: 记住应该让发送数据的一端关闭 channel,而不是接收的一端关闭它,这样容易引起 panic。不需要经常关闭 channel,只有确定没有任何数据发送或者想显示的结束 range 循环之类的时候才关闭它。
select
当有多个 channel 的时候,我们如何知道那个 channel 准备好?可以通过 select
监听 channel 上的数据流动。
select 默认是阻塞的,只有当监听的 channel 中有发送或接收可以进行时才会运行, 当多个 channel 都准备好的时候, select 是随机的选择一个执行的。
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x: // c 准备好
x, y = y, x + y
case <-quit: // quit 准备好
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
在select 里面有 default 语法, select 其实就是类似 switch 的功能, default 就是当监听的channel 都没有准备好的时候, 默认执行的( select 不再阻塞等待 channel)。
select {
case i := <-c:
// use i
default:
// 当 c 阻塞的时候执行这里
}
超时
有时候会出现 goroutine 阻塞的情况,那么我们如何避免整个的程序进入阻塞的情况呢?我们可以利用 select 来设置超时,通过如下的方式实现:
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second): // 超时退出
println("timeout")
o <- true
break
}
}
}()
<- o
}
并行运行
虽然 goroutine 是并发执行的,但是它们并不是并行运行的。如果不告诉 Go 额外的东西,同一时刻只会有一个 goroutine 执行。利用 runtime.GOMAXPROCS(n) 可以设置 goroutine 并行执行的数量。
来自文档:
GOMAXPROCS 设置了同时运行的 CPU 的最大数量,并返回之前的设置。如果 n < 1,不会改变当前设置。当调度得到改进后,这将被移除。
如果不希望修改任何源代码,同样可以通过设置环境变量 OMAXPROCS 为目标值。
版权声明:本文为博主原创文章,未经博主允许不得转载。