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/RUnlock和Lock/Unlock。
总结
通过这个示例,我们学习了如何使用Go语言中的读写锁来实现并发安全的计数器。我们还展示了几种错误使用锁的方法,以帮助你避免常见的陷阱。
Go语言教程:使用读写锁实现并发安全的计数器
https://blog.ithuo.net/posts/concurrent-safe-counter-using-rwmutex-in-golang/