如果你对通过内存访问同步处理并发的语言很熟悉,那么你可能会立即明白Mutex的使用方法。如果你没有这样的经验,没关系,Mutex很容易理解。Mutex代表"mutual exclusion(互斥)"。互斥提供了一种并发安全的方式来表示对共享资源访问的独占。下面是一个简单的两个goroutine,它们试图增加和减少一个公共值;,并使用Mutex来同步访问:
```
var count int
var lock sync.Mutex
increment := func() {
lock.Lock() // 1
defer lock.Unlock() // 2
count++
fmt.Printf("Incrementing: %d\n", count)
}
decrement := func() {
lock.Lock() // 1
defer lock.Unlock() // 2
count--
fmt.Printf("Decrementing: %d\n", count)
}
// Increment
var arithmetic sync.WaitGroup
for i := 0; i <= 5; i++ {
arithmetic.Add(1)
go func() {
defer arithmetic.Done()
increment()
}()
}
// Decrement
for i := 0; i <= 5; i++ {
arithmetic.Add(1)
go func() {
defer arithmetic.Done()
decrement()
}()
}
arithmetic.Wait()
fmt.Println("Arithmetic complete.")
```
1. 在这里,我们要求独占使用关键部分 - 在这种情况下,count变量由互斥锁保护。
2. 这里表明我们已经完成了对共享部分的锁定。
这会输出:
```
Decrementing: -1
Incrementing: 0
Decrementing: -1
Incrementing: 0
Decrementing: -1
Decrementing: -2
Decrementing: -3
Incrementing: -2
Decrementing: -3
Incrementing: -2
Incrementing: -1
Incrementing: 0 Arithmetic complete.
```
你会注意到我们总是使用defer在延迟声明中调用解锁。 使用互斥锁时,这是一个非常常见的习惯用法,以确保调用始终执行,即使在发生恐慌时也是如此。否则一旦未能解除锁定,可能会导致你的程序陷入死锁。
被锁定部分是程序的性能瓶颈,进入和退出锁定的成本有点高,因此人们通常尽量减少锁定涉及的范围。
可能在多个并发进程之间共享的内存并不是都要读取和写入,出于这样的考虑,你可以使用另一个类型的互斥锁:sync.RWMutex。
sync.RWMutex与Mutex在概念上是一样的:它保护对内存的访问;不过,RWMutex可以给你更多地控制方式。 你可以请求锁定进行读取,在这种情况下,你将被授予读取权限,除非锁定正在进行写入操作。 这意味着,只要没有别的东西占用写操作,任意数量的读取者就可以进行读取操作。 下面是一个演示生产者的示例:
```
producer := func(wg *sync.WaitGroup, l sync.Locker) { //1
defer wg.Done()
for i := 5; i > 0; i-- {
l.Lock()
l.Unlock()
time.Sleep(1) //2
}
}
observer := func(wg *sync.WaitGroup, l sync.Locker) {
defer wg.Done()
l.Lock()
defer l.Unlock()
}
test := func(count int, mutex, rwMutex sync.Locker) time.Duration {
var wg sync.WaitGroup
wg.Add(count + 1)
beginTestTime := time.Now()
go producer(&wg, mutex)
for i := count; i > 0; i-- {
go observer(&wg, rwMutex)
}
wg.Wait()
return time.Since(beginTestTime)
}
tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0)
defer tw.Flush()
var m sync.RWMutex
fmt.Fprintf(tw, "Readers\tRWMutext\tMutex\n")
for i := 0; i < 20; i++ {
count := int(math.Pow(2, float64(i)))
fmt.Fprintf(
tw, "%d\t%v\t%v\n", count,
test(count, &m, m.RLocker()), test(count, &m, &m),
)
}
```
1. producer函数的第二个参数是类型sync.Locker。 该接口有两种方法,锁定和解锁,互斥和RWMutex类型都适用。
2. 在这里,我们让producer休眠一秒钟,使其不那么活跃。
这会输出:
```
Readers RWMutext Mutex
1 5ms 5ms
2 5ms 5ms
4 5ms 5ms
8 5ms 5ms
16 5ms 5ms
32 5ms 5ms
64 5ms 5ms
128 5ms 5ms
256 5ms 5ms
512 5ms 5ms
1024 5ms 5ms
2048 5ms 5ms
4096 6ms 7ms
8192 8ms 8ms
16384 7ms 8ms
32768 9ms 11ms
65536 12ms 15ms
131072 29ms 31ms
262144 61ms 68ms
524288 121ms 137ms
```
你可以通过这个例子看到,RWMutext在大量级上相对于Mutex是有性能优势的,不过这同样取决于你在锁住的部分做了什么。通常建议在逻辑上合理的情况下使用RWMutex而不是Mutex。
* * * * *
学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。
- 前序
- 谁适合读这本书
- 章节导读
- 在线资源
- 第一章 并发编程介绍
- 摩尔定律,可伸缩网络和我们所处的困境
- 为什么并发编程如此困难
- 数据竞争
- 原子性
- 内存访问同步
- 死锁,活锁和锁的饥饿问题
- 死锁
- 活锁
- 饥饿
- 并发安全性
- 优雅的面对复杂性
- 第二章 代码建模:序列化交互处理
- 并发与并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并发哲学
- 第三章 Go的并发构建模块
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select语句
- GOMAXPROCS
- 结论
- 第四章 Go的并发编程范式
- 访问范围约束
- fo-select循环
- 防止Goroutine泄漏
- or-channel
- 错误处理
- 管道
- 构建管道的最佳实践
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 队列
- context包
- 小结
- 第五章 可伸缩并发设计
- 错误传递
- 超时和取消
- 心跳
- 请求并发复制处理
- 速率限制
- Goroutines异常行为修复
- 本章小结
- 第六章 Goroutines和Go运行时
- 任务调度