在 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 等高级技巧,我们可以更加灵活地控制锁的使用,提高程序的并发性能和稳定性。
评论