这是我参与「第五届青训营 」笔记创作活动的第15天。
今天继续复习在青训营中学到的golang基础语法。
在实际开发中,经常有需要起多个goroutine互相协作,它们之间又需要共享一些数据的情况。但是如果不加限制的话,就会出现一些问题。
var a = 0
var wg sync.WaitGroup
func add() {
a++
wg.Done()
}
func main() {
for i := 0; i < 1000; i++ {
wg.Add(1)
go add()
}
wg.Wait()
fmt.Println(a)
}
这个程序最终的输出是990,而不是1000。原因在于a++并不是原子操作,它在实际执行中大致分为三步:
- 读取a的值到寄存器
- 寄存器值加1
- 将寄存器中的值写回a
假设只有两个gorouti,一旦第二个goroutine在上一个goroutine写回a之前读了a原始值,那么两个goroutine最后的结果都是将初始值加1写回a,而不是初始值加2,这样就出错了。
一个简单粗暴的办法就是,在第一个goroutine执行之前不让别的goroutine访问a。这可以用锁的方式实现:
var m sync.Mutex
func add() {
m.Lock()
a++
wg.Done()
m.Unlock()
}
m就是互斥锁,一个goroutine在进入add函数后会尝试获取这把锁,即m.Lock()。若获取失败则会阻塞,直到锁可用为止。获取成功后即可执行a++。执行完之后便可释放这把锁,让这把锁可用,并唤醒等待锁的goroutine使之继续运行。
但是goroutine之间的关系除了以上的互斥,还有同步。比如说我有四台“机器”,第一台产生一个数,第二台将收到的数字+1,第三台将收到的数字平方,第四台输出收到的数字。这样一来,每台机器可以看成一个goroutine,每个goroutine能够运行的前提是上一个goroutine给它提供了输入。这样一来这些goroutine之间就有了同步关系,可以用“传送带”也就是通道来实现。
var wg sync.WaitGroup
func produce(out chan int, c chan bool) {
for i := 0; i < 5; i++ {
out <- i
}
c <- true
wg.Done()
}
func output(in chan int, c chan bool) {
for {
select {
case val := <-in:
fmt.Println(val)
case <-c:
wg.Done()
return
}
}
}
func main() {
data := make(chan int)
closeSig := make(chan bool)
wg.Add(2)
go produce(data, closeSig)
go output(data, closeSig)
wg.Wait()
}
produce函数不断往data通道中放入int,放完之后向closeSig通道放入一个布尔值(具体是什么无所谓,也可以放空结构体省空间),表示已经放完了。
output函数中有个select语句,它会依次尝试从in和c中取数据,前面的for死循环会让它不断执行。如果从in中取到数据,就输出;如果从c中取到数据,就结束。由于是按序先尝试从in中取数据,如果从c中取到了数据,说明所有数据都处理完了。
最后的结果是依次输出0到4的值。