Go语言编程笔记9:使用共享变量实现并发
数据竞态
在多线程编程中,遇到的最大麻烦就是当多个线程对同一个数据进行操作时,因为代码交错执行引发的一些问题:
package main
import (
"fmt"
"sync"
)
type bank struct {
amount int
}
func (b *bank) SaveMoney(amount int) {
b.amount += amount
}
func (b *bank) GetAmount() int {
return b.amount
}
func main() {
for i := 0; i < 10; i++ {
bankTest()
}
// bank amount: 300
// bank amount: 300
// bank amount: 300
// bank amount: 100
// bank amount: 300
// bank amount: 300
// bank amount: 300
// bank amount: 300
// bank amount: 300
// bank amount: 300
}
func bankTest() {
var b bank
var gwg sync.WaitGroup
gwg.Add(1)
go func() {
defer gwg.Done()
b.SaveMoney(100)
fmt.Println("bank amount: ", b.GetAmount())
}()
gwg.Add(1)
go func() {
defer gwg.Done()
b.SaveMoney(200)
}()
gwg.Wait()
}
这是根据《Go程序设计语言》中的示例代码改写的一个例子,这里构建了一个结构体bank
,代表一个共享的银行账户,可以存钱和查看余额。bankTest
是测试并发访问下可能出现问题的函数,主goroutine启动两个goroutine来分别存钱,并且其中一个goroutine会在存完钱后查看余额。
实际运行多次我们发现结果并不一致,这是因为两个goroutine是同时运行的,并非是顺序执行,所以是可能A线程存完100块后,B线程存了200,然后A线程再查看余额,此时就是300块。除了这种情况以外,也有可能是A全部执行完后,B再存钱,此时输出结果就是100块。当然也可能是B存完钱后A再执行,此时结果也是300块。
但可能的情况并不仅仅是这三种,因为计算机实际执行程序时是以底层的汇编指令为最小执行单元来执行的,并非是高级语言的单行代码,这在多线程编程中尤其致命。
比如b.amount += amount
这条代码,实际执行中可能会拆分成两条汇编语句:
c = b.amount + amount
b.amount = c
也就是说+
运算和赋值运算可能并非是在一条汇编语句内完成的,这就会导致更诡异的结果,比如A线程执行到这里,刚完成+
运算,还没有来得及进行赋值操作,此时B线程完成了赋值操作,也就是说此时的b.amount
已经是200了。然后A再执行赋值的汇编语句,结果b.amount
又变成了100。也就是说B线程存的200块“不翼而飞”。
《Go程序设计语言》对此的一句评论相当有趣——“不要相信你在多线程编程时的直觉,因为那往往是错的”。
在学习Python时我也看到过类似的话,关于多线程编程最好的告诫就是——“不要多线程编程”。当然这并不是说不要用编写并发程序,而是说不要写传统的多线程编程,因为那样你会遇到很多麻烦,且很难排查和解决。事实上很多编程语言在语言层面尝试解决该问题,比如Python的全局线程锁,这可以看作是试图将多线程这头老虎关在笼子里的做法,在此基础上使用并发或异步都可以很好地解决并发问题。当然这也并非没有代价,但综合来看是相当值得的。与Python相比,Go语言的goroutine更像是传统的多线程编程,不过采用了其它方式来避免传统多线程编程的问题,在后面我们会详细说明。
上面示例中展现的这种,因为并发线程交错执行代码,并针对同一个变量进行访问产生的问题,称作是“数据竞态”,在这种情况下,并发访问普通变量是“并发不安全的”,而如果一个变量可以安全地并发访问(比如通道或者net.Connect
),则会被称为并发安全的。此外,如果一个类型的所有方法都可以安全地并发访问,则这种类型可以被视作是并发安全的。
解决数据竞态
针对数据竟态,有以下几种方式可以解决:
不写入数据
之所以会出现数据竟态,是因为多个线程并发访问共享数据时,有至少一个尝试写入,如果所有的并发线程都只尝试读取,而不写入,自然也就不存在数据竟态。
在编程中,我们经常会遇到一种“延迟初始化”的问题:
package main
type student struct {
name string
age int
}
type students struct {
stds map[string]*student
}
func (s *students) getStudent(name string) *student {
student, ok := s.stds[name]
if !ok {
student := initStudent(name)
s.stds[name] = student
}
return student
}
func initStudent(name string) *student {