go语句及其执行规则
学习之前先看一下下面这句话:
Don’t communicate by sharing memory; share memory by communicating.
不要通过共享数据来通讯,要以通讯的方式共享数据。
通道(也就是 channel)类型的值可以被用来以通讯的方式共享数据。更具体地说,它一般被用来在不同的goroutine之间传递数据。
这篇主要讲goroutine是什么。简单来说,goroutine代表着并发编程模型中的用户级线程。
调度器
Go语言不但有着独特的并发编程模型,以及用户级线程goroutine,还拥有强大的用于调度goroutine、对接系统级线程的调度器。
这个调度器是Go语言运行时系统的重要组成部分,它主要负责统筹调配Go并发编程模型中的三个主要元素:
- G(goroutine 的缩写),用户级线程
- P(processor 的缩写),一种可以承载若干个G,且能够使用这些G适时的与M进行对接,并得到真正运行的中介
- M(machine 的缩写),系统级线程
主goroutine
这里需要知道一个与主goroutine有关的重要特性,一旦主goroutine中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。
先看下面这个例子:
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
}
上面的程序运行之后,不会有打印任何内容。
只要go语句本身执行完毕,Go程序完全不会等待go函数的执行,它会立刻去执行后面的语句。这就是所谓的异步并发地执行。
在上面的例子中,在for语句执行完毕后,里面包装的10个goroutine还没有获得运行的机会,主goroutine中的代码执行完了,Go程序就会立即结束运行。
使用Sleep等待
上面的例子中,如果要让程序在其他goroutine运行完之后再退出。最简单粗暴的办法是Sleep一段时间:
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
这个办法可行,但是Sleep的时间需要预估。太长会浪费时间,太短则不能保证所有goroutine都运行完毕。不容易预估时间,最好是让其他的goroutine在运行完毕后发送通知。
让主goroutine等待其他goroutine
使用通道,通道的长度与启用的goroutine的数量一致。每个goroutine运行完毕前,都向通道发送一个值。在主goroutine则是从这个通道接收值,接收了足够数量的次数后就说明所有goroutine都运行完毕了,可以继续往下执行了(就是退出):
package main
import "fmt"
func main() {
sign := make(chan struct{}, 10)
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
sign <- struct{}{}
}()
}
for j := 0; j < 10; j++ {
<- sign
}
}
这里声明的通道的类型是 chan struct{} ,是一个空结构体。它谭勇的内存空间是0字节。这个值在整个Go程序中永远都只会存在一份。虽然可以无数次的使用这个值字面量,但是用到的都是同一个值。当把通道仅仅刀座是传递某个简单信号的介质的时候,使用空结构体是最好的。
其他方式
在标准库中,有一个sync包,里面有一个sync.WaitGroup类型。这应该是一个更好的实现方式。不过这要等后面讲sync包的时候再说了。
让多个goroutine按照既定的顺序执行
首先改造一下一只使用的例子,把变量i的值传递给每个goroutine,这样输出的是0-9各一次,不过是乱序的:
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i)
sign <- struct{}{}
}
讲师的例子
package main
import (
"fmt"
"sync/atomic"
"time"
)
var count uint32
func trigger (i uint32, fn func()) {
for {
if n := atomic.LoadUint32(&count); n == i {
fn()
atomic.AddUint32(&count, 1)
break
}
time.Sleep(time.Nanosecond)
}
}
func main() {
for i := uint32(0); i < 10; i++ {
go func(i uint32) {
fn := func() {
fmt.Println(i)
}
trigger(i, fn)
}(i)
}
trigger(10, func() {})
}
主要就是trigger函数。在trigger里会检查i,并把要执行的语句打包成fn函数也传入,只有在trigger里判断后符合条件,就会执行fn函数的语句。
trigger里会检查i和count是否相等,在执行了fn函数后,需要把count加1,这里用了原子操作。里有是trigger函数会被多个goroutine并发的调用,所以这个变量被多个用户级线程共用了。因此对它的操作就产生了竞态条件(race condition),破坏了程序的并发安全性。
在最后退出的时候,应该有了trigger函数,只要检查count是否到10了,就表示其他goroutine都执行完了,所以也就不需要通道了。
另外在trigger函数里,是一个for语句的无限循环,在判断条件不成立后,先进行了一个1纳秒的Sleep。如果不加这句的话,测试下来,偶尔会出现程序卡住的情况(甚至是死机)。这里加上Sleep语句应该是希望这个时候程序可以进行一下切换,否则当前应该执行的那个goroutine如果拿不到执行的机会,其他goroutine也都无法通过if条件的判断。
自己的实现
package main
import (
"fmt"
"time"
)
func main() {
sign := make(chan struct{}, 10)
var count int
for i := 0; i < 10; i++ {
go func(i int) {
for {
if count == i{
fmt.Println(i)
count ++
sign <- struct{}{}
break
}
time.Sleep(time.Nanosecond)
}
}(i)
}
for j := 0; j < 10; j++ {
<- sign
}
}
主要两个问题,当时没有意识到在for无限循环之后,进入下一个迭代前,这个1纳秒Sleep的意义。还有就是我没有使用原子操作。不过这里即使不用原子操作也没问题的样子,因为逻辑上通知只有一个goroutine满足条件会去操作共用的变量count。所以这里和上面讲师的示例就差在对变量count的比较和判断是否是原子操作的问题上了。
原子操作
这里再自我做一些补充。
原子操作,即执行过程不能被中断的操作(并发)。
经典问题:i++是不是原子操作?
答案是否,因为i++看上去只有一行,但是背后包括了多个操作:取值,加法,赋值。
有疑问加站长微信联系(非本文作者)