极客时间--golang并发编程实战课--Mutex学习总结
互斥锁的实现机制
互斥锁是并发控制的一个手段,是为了避免竞争而建立的一种并发控制机制。在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。可以说,临界区就是一个被共享的资源,或者说是一个整体的一组共享资源,比如对数据库的访问、对某一个共享数据结构的操作、对一个 I/O 设备的使用、对一个连接池中的连接的调用,等等。如果很多线程同步访问临界区,就会造成访问或操作错误,这当然不是我们希望看到的结果。所以,我们可以使用互斥锁,限定临界区只能同时由一个线程持有。当临界区由一个线程持有的时候,其它线程如果想进入这个临界区,就会返回失败,或者是等待。直到持有的线程退出临界区,这些等待线程中的某一个才有机会接着持有这个临界区。
Mutex的使用方法
数据结构:
Mutex实现了如下接口,
type Locker interface{
Lock()
UnLock()
}
Mutex提供了如下两个方法:进入临界区调用Lock(),退出临界区调用UnLock()
func (m *Mutex)Lock()
func (m *Mutex)UnLock()
当一个 goroutine 通过调用 Lock 方法获得了这个锁的拥有权后,其它请求锁的goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放并且自己获取到了这个锁的拥有权。
下面来看一段代码:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex
type Mutext struct {
}
func main() {
var value = 0
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
//lock.Lock()
value++
//lock.Unlock()
}
}()
}
wg.Wait()
fmt.Println(value)
}
上述代码中,value++就是一个临界区的概念,如果没有对其加锁,那么最终输出的value结果不是我们希望得到的,只有当对其加锁/解锁之后,才能保证在每次执行该操作只有一个goroutine(协程)。
同样,我们可以基于golang中结构体去优化上部分代码,谈不上优化,就是使用结构体去封装一个线程安全的计数器,可以将sync.Mutex作为一个字段,嵌入到结构体中去
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex
type Value struct {
mu sync.Mutex
value int
}
func main() {
var value Value
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
value.mu.Lock()
value.value++
value.mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(value.value)
}
额外当前的 Mutex 最重要的变化,就是增加饥饿模式。第 12 行将饥饿模式的最大等待时间阈值设置成了 1 毫秒,这就意味着,一旦等待者等待的时间超过了这个阈值,Mutex 的处理就有可能进入饥饿模式,优先让等待者先获取到锁,通过加入饥饿模式,可以避免把机会全都留给新来的 goroutine,保证了请求锁的goroutine 获取锁的公平性,对于我们使用锁的业务代码来说,不会有业务一直等待锁不被处理。