951 words
5 minutes

Go语言教程:使用读写锁实现并发安全的计数器

在本教程中,我们将通过一个简单的计数器示例,学习如何使用Go语言中的读写锁(sync.RWMutex)来实现并发安全的操作。我们将详细解释代码的每一部分,并提供一些额外的示例来帮助你更好地理解。

代码结构#

首先,让我们看一下完整的代码:

package main
import (
"log"
"sync"
"time"
)
// counter 代表计数器。
type counter struct {
num uint // 计数。
mu sync.RWMutex // 读写锁。
}
// number 会返回当前的计数。
func (c *counter) number() uint {
c.mu.RLock()
defer c.mu.RUnlock()
return c.num
}
// add 会增加计数器的值,并会返回增加后的计数。
func (c *counter) add(increment uint) uint {
c.mu.Lock()
defer c.mu.Unlock()
c.num += increment
return c.num
}
func main() {
c := counter{}
count(&c)
redundantUnlock()
}
func count(c *counter) {
// sign 用于传递演示完成的信号。
sign := make(chan struct{}, 3)
go func() { // 用于增加计数。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= 10; i++ {
time.Sleep(time.Millisecond * 500)
c.add(1)
}
}()
go func() {
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= 20; j++ {
time.Sleep(time.Millisecond * 200)
log.Printf("The number in counter: %d [%d-%d]",
c.number(), 1, j)
}
}()
go func() {
defer func() {
sign <- struct{}{}
}()
for k := 1; k <= 20; k++ {
time.Sleep(time.Millisecond * 300)
log.Printf("The number in counter: %d [%d-%d]",
c.number(), 2, k)
}
}()
<-sign
<-sign
<-sign
}
func redundantUnlock() {
var rwMu sync.RWMutex
// 示例1。
//rwMu.Unlock() // 这里会引发panic。
// 示例2。
//rwMu.RUnlock() // 这里会引发panic。
// 示例3。
rwMu.RLock()
//rwMu.Unlock() // 这里会引发panic。
rwMu.RUnlock()
// 示例4。
rwMu.Lock()
//rwMu.RUnlock() // 这里会引发panic。
rwMu.Unlock()
}

代码解析#

1. 定义计数器结构体#

type counter struct {
num uint // 计数。
mu sync.RWMutex // 读写锁。
}

counter结构体包含一个无符号整数num用于存储计数值,以及一个读写锁mu用于保护对num的并发访问。

2. 获取当前计数值的方法#

func (c *counter) number() uint {
c.mu.RLock()
defer c.mu.RUnlock()
return c.num
}

number方法使用读锁(RLock)来保护对num的读取操作,并在读取完成后释放读锁(RUnlock)。

3. 增加计数值的方法#

func (c *counter) add(increment uint) uint {
c.mu.Lock()
defer c.mu.Unlock()
c.num += increment
return c.num
}

add方法使用写锁(Lock)来保护对num的写操作,并在写入完成后释放写锁(Unlock)。

4. 主函数#

func main() {
c := counter{}
count(&c)
redundantUnlock()
}

main函数中,我们创建一个counter实例,并调用count函数和redundantUnlock函数。

5. 并发计数函数#

func count(c *counter) {
// sign 用于传递演示完成的信号。
sign := make(chan struct{}, 3)
go func() { // 用于增加计数。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= 10; i++ {
time.Sleep(time.Millisecond * 500)
c.add(1)
}
}()
go func() {
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= 20; j++ {
time.Sleep(time.Millisecond * 200)
log.Printf("The number in counter: %d [%d-%d]",
c.number(), 1, j)
}
}()
go func() {
defer func() {
sign <- struct{}{}
}()
for k := 1; k <= 20; k++ {
time.Sleep(time.Millisecond * 300)
log.Printf("The number in counter: %d [%d-%d]",
c.number(), 2, k)
}
}()
<-sign
<-sign
<-sign
}

count函数启动了三个并发的goroutine:

  • 第一个goroutine每500毫秒增加一次计数。
  • 第二个和第三个goroutine分别每200毫秒和300毫秒读取并打印当前计数值。

sign通道用于等待所有goroutine完成。

6. 错误解锁示例#

func redundantUnlock() {
var rwMu sync.RWMutex
// 示例1。
//rwMu.Unlock() // 这里会引发panic。
// 示例2。
//rwMu.RUnlock() // 这里会引发panic。
// 示例3。
rwMu.RLock()
//rwMu.Unlock() // 这里会引发panic。
rwMu.RUnlock()
// 示例4。
rwMu.Lock()
//rwMu.RUnlock() // 这里会引发panic。
rwMu.Unlock()
}

redundantUnlock函数展示了几种错误使用读写锁的方法,这些错误会导致程序崩溃(panic)。正确的使用方式是成对调用RLock/RUnlockLock/Unlock

总结#

通过这个示例,我们学习了如何使用Go语言中的读写锁来实现并发安全的计数器。我们还展示了几种错误使用锁的方法,以帮助你避免常见的陷阱。

Share

If this article helped you, please share it with others!

Go语言教程:使用读写锁实现并发安全的计数器
https://blog.ithuo.net/posts/concurrent-safe-counter-using-rwmutex-in-golang/
Author
Derick
Published at
2022-05-25
License
CC BY-NC-SA 4.0
Last updated on 2022-05-25,1325 days ago

Some content may be outdated

Table of Contents