先回顾一下,在 C 或者其它编程语言的并发编程中,主要存在两种通讯(IPC):git
- 进程间通讯:管道、消息队列、信号等
- 线程间通讯:互斥锁、条件变量等
利用以上通讯手段采起的同步措施,最终是为了达到如下两种目的:github
- 维持共享数据一致性,并发安全
- 控制流程管理,更好的协同工做
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) }
结果spa
=== 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。