先回顾一下,在 C 或者其它编程语言的并发编程中,主要存在两种通信(IPC):
- 进程间通信:管道、消息队列、信号等
- 线程间通信:互斥锁、条件变量等
利用以上通信手段采取的同步措施,最终是为了达到以下两种目的:
- 控制流程管理,更好的协同工作
- 维持共享数据一致性,并发安全
Go语言中除了保留了传统的同步支持,还提供了特有的 CSP 并发编程模型。
接下来通过一个“做累加”的示例程序,展示竞争状态(race condition)。
开启 5000 个 goroutine,让每个 goroutine 给 counter 加 1,最终在所有 goroutine 都完成任务时 counter 的值应该为 5000,先试下不加锁的示例程序表现如何
func TestDemo1(t *testing.T) { counter := 0 for i := 0; i < 5000; i++ { go func() { counter++ }() } time.Sleep(1 * time.Second) t.Logf("counter = %d", counter) }
结果
=== RUN TestDemo1 a1_test.go:18: counter = 4663 --- PASS: TestDemo1 (1.00s) PASS
多试几次,结果一直是小于 5000 的不定值。
竞争状态下程序行为的图像表示
将刚刚的代码稍作改动
func TestDemo2(t *testing.T) { var mut sync.Mutex // 声明锁 counter := 0 for i := 0; i < 5000; i++ { go func() { mut.Lock() // 加锁 counter++ mut.Unlock() // 解锁 }() } time.Sleep(1 * time.Second) t.Logf("counter = %d", counter) }
结果
=== RUN TestDemo2 a1_test.go:35: counter = 5000 --- PASS: TestDemo2 (1.01s) PASS
counter = 5000,返回的结果对了。
这就是互斥锁,在代码上创建一个临界区(critical section),保证串行操作(同一时间只有一个 goroutine 执行临界区代码)。
那么互斥锁是怎么串行的呢?把每一步的执行过程打印出来看下
func TestDemo3(t *testing.T) { var mut sync.Mutex counter := 0 go func() { mut.Lock() log.Println("goroutine B Lock") counter = 1 log.Println("goroutine B counter =", counter) time.Sleep(5 * time.Second) mut.Unlock() log.Println("goroutine B Unlock") }() time.Sleep(1 * time.Second) mut.Lock() log.Println("goroutine A Lock") counter = 2 log.Println("goroutine A counter =", counter) mut.Unlock() log.Println("goroutine A Unlock") }
结果
=== RUN TestDemo3 2020/09/30 22:14:00 goroutine B Lock 2020/09/30 22:14:00 goroutine B counter = 1 2020/09/30 22:14:05 goroutine B Unlock 2020/09/30 22:14:05 goroutine A Lock 2020/09/30 22:14:05 goroutine A counter = 2 2020/09/30 22:14:05 goroutine A Unlock --- PASS: TestDemo3 (5.00s) PASS
通过每个操作记录下来的时间可以看出,goroutine A 的 Lock 一直阻塞到了 goroutine B 的 Unlock。
这时候有个疑问,那 goroutine B 上的锁,goroutine A 能解锁吗?修改一下刚才的代码,试一下
func TestDemo5(t *testing.T) { var mut sync.Mutex counter := 0 go func() { mut.Lock() log.Println("goroutine B Lock") counter = 1 log.Println("goroutine B counter =", counter) time.Sleep(5 * time.Second) //mut.Unlock() //log.Println("goroutine B Unlock") }() time.Sleep(1 * time.Second) mut.Unlock() log.Println("goroutine A Unlock") counter = 2 log.Println("goroutine A counter =", counter) time.Sleep(2 * time.Second) }
结果
=== RUN TestDemo5 2020/09/30 22:15:03 goroutine B Lock 2020/09/30 22:15:03 goroutine B counter = 1 2020/09/30 22:15:04 goroutine A Unlock 2020/09/30 22:15:04 goroutine A counter = 2 --- PASS: TestDemo5 (3.01s) PASS
测试通过,未报错,counter 的值也被成功修改,证明B上的锁,是可以被A解开的。
再进一步,goroutine A 不解锁,直接修改已经被 goroutine B 锁住的 counter 的值可以吗?试一下
func TestDemo6(t *testing.T) { var mut sync.Mutex counter := 0 go func() { mut.Lock() log.Println("goroutine B Lock") counter = 1 log.Println("goroutine B counter =", counter) time.Sleep(5 * time.Second) mut.Unlock() log.Println("goroutine B Unlock") }() time.Sleep(1 * time.Second) //log.Println("goroutine A Unlock") //mut.Unlock() counter = 2 log.Println("goroutine A counter =", counter) time.Sleep(10 * time.Second) }
结果
=== RUN TestDemo6 2020/09/30 22:15:43 goroutine B Lock 2020/09/30 22:15:43 goroutine B counter = 1 2020/09/30 22:15:44 goroutine A counter = 2 2020/09/30 22:15:48 goroutine B Unlock --- PASS: TestDemo6 (11.00s) PASS
测试通过,未报错,证明B上的锁,A可以不用解锁直接改。
当互斥锁不断地试图获得一个永远无法获得的锁时,它可能会遇到饥饿问题。
在版本1.9中,Go通过添加一个新的饥饿模式来解决先前的问题,所有等待锁定超过一毫秒的 goroutine,也称为有界等待,将被标记为饥饿。当标记为饥饿时,解锁方法现在将把锁直接移交给第一位等待着。
读写锁和上面的多也差不多,有这么几种情况
无论是互斥锁还是读写锁在程序运行时一定是成对的,不然就会引发不可恢复的panic。
文章示例代码