当我们在谈并发安全时,一般会谈到以下两种程序上的实现:
- 通过同步共享内存实现并发安全,比如使用锁
- 通过通信来实现同步操作,比如使用 channel
以上是显式实现并发安全的方案,但也许很多人忽视了,我们还可以通过隐式的数据保护实现并发安全。所谓隐式,就是通过巧妙的设计,使一个程序中原本存在的资源竞争问题不存在,消灭了问题的来源,也就不需要费力解决了。
实现隐式的并发安全,核心的思路是避免数据的变化。不可变的数据天然就是线程安全的。使用这样的程序设计,一方面可以降低开发者的认知负担,另一方面能获得更好的性能。
具体实现上,我们首先可以考虑声明常量而不是变量,Swift 可以使用 let,Java 可以使用 final,C++ 和 Go 可以使用 const。可惜的是,Go 不支持传递常量的指针,只能采用值拷贝的方式——但即便如此,性能往往也好于同步共享内存。其次,我们还可以通过设定约束(confinement)的方式将变量保护起来,来看以下代码:
data := make([]int, 4) // need protection
loopData := func(handleData chan<- int) {
for i := range data {
handleData <- data[i]
}
close(handleData)
}
handleData := make(chan int) // need protection
go loopData(handleData)
for num := range handleData {
fmt.Println(num)
}
上面这个程序片段中,我们对切片 data 进行了多进程操作,并将计算结果发往通道 handleData,尽管代码这么写没有错,但它埋下了隐患。更好的做法是通过词法约束(lexical confinement),将切片和通道保护起来,从一开始避免读写的可能:
dataAndChanOwner := func() <-chan int {
data := make([]int, 4)
ch := make(chan int)
go func() {
for i := range data {
ch <- data[i]
}
close(ch)
}()
return ch
}
// consumer
ch := dataAndChanOwner()
for num := range ch {
fmt.Println(num)
}