在一个共享的数据需要被多个线程访问的时候就会出现很多读写问题(由于读写问题有很多变种,所以用许多来形容)。有两种类型的线程需要访问数据—读线程和写线程。读线程仅仅读数据,写线程修改数据。当写线程有权限访问数据的时候,其他线程(包括读线程和写线程)是不可以访问这个共享的数据。这个限制在日常生活中是真的发生的,当写线程无法以原子性的操作修改数据的时候,读线程必须被阻塞,以防读取到脏数据(译者注:为了使得说明的更加清晰,后面用写goroutine和读goroutine分别代替写读线程)。有许多核心问题的变种如下:
- 写线程不能处于饥饿状态(无限的等待他们执行的机会)
- 读线程不能处于饥饿状态
- 不应该有线程可以处于饥饿状态
多读/一写的互斥锁的具体实现(例如 sync.RWMutex)解决了读写问题的的其中之一。让我们看看这在Go总是如何做到的以及是它给到了一种到达什么样程度的保证。
作为一个奖励,我们深入的理解简化的竞争互斥锁。
使用方式
在深入到实现细节之前,让我们看看如何在实践中使用sync.RWMutex
。如下的程序使用了读写互斥用来保护关键部分的操作—sleep()
。通过对关键部分正在执行的读写线程进行计数来对整个程序执行过程的可视化(source code)。
package main
import (
"fmt"
"math/rand"
"strings"
"sync"
"time"
)
func init() {
rand.Seed(time.Now().Unix())
}
func sleep() {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
}
func reader(c chan int, m *sync.RWMutex, wg *sync.WaitGroup) {
sleep()
m.RLock()
c <- 1
sleep()
c <- -1
m.RUnlock()
wg.Done()
}
func writer(c chan int, m *sync.RWMutex, wg *sync.WaitGroup) {
sleep()
m.Lock()
c <- 1
sleep()
c <- -1
m.Unlock()
wg.Done()
}
func main() {
var m sync.RWMutex
var rs, ws int
rsCh := make(chan int)
wsCh := make(chan int)
go func() {
for