来自图书《Concurrency In Go》
1,Race Conditions 竞争关系
var data int
go func() {
data ++
}()
if data == 0{
fmt.Printf("value is %v \n", data)
}
这样的代码就有3种可能,我们当然可以在go协程后面加上一句 time.Sleep(1*time.Second)来临时解决这个问题。但这并不是从根本上解决问题的方法,只是降低了意外结果发生的可能性,因为其并没有做到逻辑正确(logically correct)。
竞争关系是程序开发中最常见的隐蔽性bug。
2,Atomicity 原子性
考虑这点,最关键的是context或scope,也就是代码的上下文。书中例举一个例子,06年的时候暴雪起诉一家做魔兽世界外挂的公司,这公司就是在游戏运行前,将代码注入了系统操作环境的上下文中从而避开了暴雪Warden反外挂程序的监控,因为后者监控的是进程的内存上下文空间。
如 i++ 这个操作。实际上这个操作是三个步骤:
获取i的值;增加i的值;保存i的值。
这三个步骤本身都是原子性的,但叠加到一起就不是了。所以这里还要讲和原子性密切相关的两个形容词:indivisible and uninterruptible
不可分性与不可打断性。那么如果在go的并发编程中,你要保持这种原子性,你就得保证在这个特定的行为中,你的go协程没有把context暴漏给其他协程。
原子性是非常重要的,其直接关系到我们的程序代码是否真正的逻辑正确,而不是结果正确。(这句话很有启发,应该不断地询问自己,代码的逻辑是否真正地严密)
3,Memory Access Synchronization 内存访问同步
还是回到上面的例子,将代码改成
var data int
go func() { data++}()
if data == 0 {
fmt.Println("the value is 0.")
} else {
fmt.Printf("the value is %v.\n", data)
}
当我们监视代码的时候,应该标记所有会访问共享资源的代码,并称它们为Critical Section。比如上面代码中的:data++; if data ;fmt.Printf(...) 这三块代码。那么可以这样写:
var memoryAccess sync.Mutex
var value int
go func() {
memoryAccess.Lock()
value++
memoryAccess.Unlock()
}()
memoryAccess.Lock()
if value == 0 {
fmt.Printf("the value is %v.\n", value)
} else {
fmt.Printf("the value is %v.\n", value)
}
memoryAccess.Unlock()
这样就能保证内存访问的同步性,但是需要注意的是,这绝对不是golang语言推荐的编码风格!只是希望让开发者意识到,任何时候如果想接触数据变量的内存空间,都要使用Lock,并在使用完数据后解锁。(defer lock.unlock())
直到现在为止,我们都没有真正地解决竞争关系。因为即便加了锁,我们任然无法确定上面代码具体的执行顺序。到底是go协程先完成了,还是下面的判断动作先完成。
在后面会去探讨这些困难相应的解决方案。现在我们应该带着2个问题:
a,在我的代码中,Critical Section是否重复的进入和退出(这里是指代码块的enter和exit)
b,Critical Section的大小规模应该如何控制
4,Deadlocks,Livelocks and Starvation 死锁、活锁和 (挨饿在计算机名词中应该怎么翻译)
解决了前面的问题,还有各种锁问题等待着我们。死锁,是指程序陷入并发进程互相等待而阻塞的异常状态。在这种状态下,除非外部终止,否则程序无法恢复正常。golang的运行环境会尝试去探测一些死锁的存在,但无法从根本上预防死锁的发生。
