Go语言中文网,致力于每日分享,欢迎关注我!
在某个数据需要被多个线程共享访问的时候,会出现读者-写者问题(这里的「问题」是复数形式的,因为读者-写者问题有多个变种)。访问共享数据的线程有两种类型:读者和写者。读者只会读取数据,而写者则是修改它。当写者拥有了访问数据的权限后,其它的线程(不管是读者还是写者)都不能访问这个数据。这种约束的需求在现实中是存在的,比如:当写者不能原子性地修改某个数据(例如数据库)时,在修改完成之前,要读取这个数据的读者要被阻塞,以免读者获取到损坏的数据(脏数据)。对于读者-写者问题的核心,还有很多修订的限制,比如:
- 写者不能饿死(无限地等待执行的机会)
- 读者不能饿死
- 所有线程都不能饿死
像 sync.RWMutex 这种多读者-单写者互斥锁的实现,可以解决其中的一些读者-写者问题。我们现在来看看要怎么用 Go 解决这些问题,并了解这种解决方案能给到怎样的保证。
作为彩蛋,我们还会了解到怎么对互斥体(mutex)进行性能分析。
用法
在我们深入到代码实现细节之前,我们先来实战演练一下 sync.RWMutex 的用法。下面的程序使用读写锁来保护临界区 —— sleep()。为了展示整个过程,临界区还添加了一些代码来记录当前有多少读者和写者正在临界区里面。(完整源代码)
在 play.golang.org 网站上运行的程序,它们的执行环境是固定的,(比如时间的初始值,time.Now().Unix() 每次都会返回一个固定的值),所以 rand.Seed(time.Now().Unix()) 会产生相同的随机数种子,从而导致程序输出每次都一样。你可以每次都手动给定一个不一样的随机数种子,或者可以在你的机器上运行上述代码。
示例输出:
WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRWRRRRRRRRRRRRRRRRW
当在临界区里面的 go routine 数量发生变化时,程序就会换行。这些输出可以体现 RWMutex “要么允许多个读者访问,要么允许一个写者访问”的特性。
还有一个重点是,一旦有一个写者调用 Lock() 后 ,后续试图访问临界区的读者将会被阻塞,如果当前已经有读者正在临界区内,则写者会等待这些读者离开临界区后,再锁定临界区。这个特性体现在程序的输出上,就是每次 W 出现的前几行, R 的数量都是逐个减少的。
...RRRRRRRRRRRRRRRW...
当写者执行完毕后,前面被阻塞的读者就可以恢复运行,并且新的写者也可以开始尝试进入临界区了。值得一提的是,如果一个写者刚刚执行完毕,而现在同