写点什么

释放 Go Mutex 的威力:编写线程安全代码的技巧和诀窍

作者:Jack
  • 2023-04-03
    广东
  • 本文字数:2609 字

    阅读完需:约 9 分钟

释放Go Mutex的威力:编写线程安全代码的技巧和诀窍

在 Go 语言中,mutex 是一种常用的同步机制,用于保护共享资源的访问。除了基本的锁操作之外,mutex 还有一些高级使用技巧,可以更加灵活地控制锁的粒度和性能。在本文中,我们将介绍几个 golang mutex 的高级使用技巧,并通过线程安全队列的例子来说明它们的应用。

RWMutex 读写锁

RWMutex 是一种读写锁,它允许多个读操作并发进行,但只允许一个写操作进行。在读多写少的情况下,使用 RWMutex 可以提高程序的并发性能。


例如,在线程安全队列中,我们可以使用 RWMutex 来保护队列的读写操作:


type ConcurrentQueue struct {    items []interface{}    rwMutex sync.RWMutex}
// 入队操作func (q *ConcurrentQueue) Enqueue(item interface{}) { q.rwMutex.Lock() defer q.rwMutex.Unlock() q.items = append(q.items, item)}
// 出队操作func (q *ConcurrentQueue) Dequeue() interface{} {
q.rwMutex.Lock() defer q.rwMutex.Unlock() if len(q.items) == 0 { return nil } item := q.items[0] q.items = q.items[1:] return item}
func (q *ConcurrentQueue) Len() int {
q.rwMutex.RLock() defer q.rwMutex.RUnlock() return len(q.items)}
复制代码


在上面的代码中,我们使用 RWMutex 来保护队列的读写操作。Enqueue 和 Dequeue 方法使用 Lock 方法获取写锁,确保只有一个线程可以访问队列的写操作;而 Len 方法使用 RLock 方法获取读锁,允许多个线程同时读取队列的长度,不会阻塞其他线程的读操作。

TryLock

TryLock 是一种非阻塞锁,它尝试获取锁,如果锁已经被其他线程持有,则返回 false,不会阻塞当前线程。与普通的 Lock 方法相比,TryLock 可以减少锁的等待时间,提高程序的并发性能。


在实际开发中,如果要更新配置数据,我们通常需要加锁,这样可以避免同时有多个 goroutine 并发修改数据。有的时候,我们也会使用 TryLock。这样一来,当某个 goroutine 想要更改配置数据时,如果发现已经有 goroutine 在更改了,其他的 goroutine 调用 TryLock,返回了 false,这个 goroutine 就会放弃更改。



go 官方对于 tryLock 的解释如下:


// TryLock tries to lock m and reports whether it succeeded.//// Note that while correct uses of TryLock do exist, they are rare,// and use of TryLock is often a sign of a deeper problem// in a particular use of mutexes.func (m *Mutex) TryLock() bool {  old := m.state  if old&(mutexLocked|mutexStarving) != 0 {    return false  }
// There may be a goroutine waiting for the mutex, but we are // running now and can try to grab the mutex before that // goroutine wakes up. if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) { return false }
if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return true}
复制代码


例如,在线程安全队列中,我们可以使用 TryLock 来保护队列的读写操作:


type ConcurrentQueue struct {    items []interface{}    mutex sync.Mutex}
func (q *ConcurrentQueue) Enqueue(item interface{}) {
for { if q.mutex.TryLock() { q.items = append(q.items, item) q.mutex.Unlock() break } }}
func (q *ConcurrentQueue) Dequeue() interface{} {
for { if q.mutex.TryLock() { defer q.mutex.Unlock() if len(q.items) == 0 { return nil } item := q.items[0] q.items = q.items[1:] return item } }}
复制代码


在上面的代码中,我们使用 TryLock 方法来获取锁,如果锁已经被其他线程持有,则不会阻塞当前线程,而是继续尝试获取锁。Enqueue 和 Dequeue 方法都使用 TryLock 方法获取锁,确保只有一个线程可以访问队列的写操作和读操作,从而保证了队列的线程安全。

Cond

Cond 是一种条件变量,它可以用于线程之间的通信和协调。当一个线程需要等待某个条件成立时,它可以调用 Cond 的 Wait 方法,使自己进入等待状态,直到条件成立或超时。当另一个线程修改了共享资源,并且认为条件已经成立时,它可以调用 Cond 的 Signal 或 Broadcast 方法,通知等待的线程继续执行。


例如,在线程安全队列中,我们可以使用 Cond 来实现队列的阻塞操作:



type ConcurrentQueue struct { items []interface{} mutex sync.Mutex cond *sync.Cond capacity int}
func NewConcurrentQueue(capacity int) *ConcurrentQueue { q := &ConcurrentQueue{capacity: capacity} q.cond = sync.NewCond(&q.mutex) return q}
func (q *ConcurrentQueue) Enqueue(item interface{}) { q.mutex.Lock() defer q.mutex.Unlock() for len(q.items) == q.capacity { q.cond.Wait() } q.items = append(q.items, item) q.cond.Signal()}
func (q *ConcurrentQueue) Dequeue() interface{} { q.mutex.Lock() defer q.mutex.Unlock() for len(q.items) == 0 { q.cond.Wait() } item := q.items[0] q.items = q.items[1:] q.cond.Signal() return item}
func (q *ConcurrentQueue) Len() int { q.mutex.Lock() defer q.mutex.Unlock() return len(q.items)}
复制代码


在上面的代码中,我们增加了一个 capacity 参数,用于指定队列的容量。在 Enqueue 方法中,如果队列已满,则调用 Cond 的 Wait 方法让当前线程进入等待状态,直到队列不满或超时。在 Dequeue 方法中,如果队列为空,则调用 Cond 的 Wait 方法让当前线程进入等待状态,直到队列不为空或超时。当 Enqueue 或 Dequeue 方法成功修改了队列的状态时,它们会调用 Cond 的 Signal 方法通知等待的线程继续执行。


需要注意的是,在使用 Cond 时,要保证对共享资源的访问都在 Mutex 的保护下进行,否则可能会出现竞态条件和死锁等问题。同时,要注意 Cond 的 Signal 和 Broadcast 方法的使用时机,避免出现信号丢失和无限等待等问题。


总之,线程安全队列是并发编程中常用的数据结构之一,实现起来需要注意锁的粒度和性能优化等问题。通过使用 golang 的 Mutex、RWMutex、TryLock 和 Cond 等高级技巧,我们可以更加灵活地控制锁的使用,提高程序的并发性能和稳定性。

发布于: 刚刚阅读数: 3
用户头像

Jack

关注

还未添加个人签名 2019-05-12 加入

还未添加个人简介

评论

发布
暂无评论
释放Go Mutex的威力:编写线程安全代码的技巧和诀窍_Jack_InfoQ写作社区