提到互斥锁,我们先来抽象一个场景
现在有个厕所,只有一个坑,多个人来到厕所,想方便方便,但是,只有一个坑 于是,只能依次进去,同时进去之后,为了防止后面的人趁你没方便完又进来方便,就会把厕所门锁上,搞定之后,就会开锁,出来腾出位置,让第二个人进去方便,第二个进去后进行前面一样的上锁、解锁的动作 以上的行为,在程序里理解就称为 互斥锁
再来看互斥锁的说明
在并发程序中,会存在
临界资源安全问题
。就是`多个协程来访问共享的数据资源,那么这个共享资源是不安全的。为了解决协程同步的问题我们使用了channel,但是Go语言也提供了传统的同步工具。什么是锁呢?就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。一般用于处理并发中的临界资源问题。
Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。
Mutex 是最简单的一种锁类型,互斥锁,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。
每个资源都对应于一个可称为 “互斥锁” 的标记,这个标记用来保证在
任意时刻,只能有一个协程(线程)访问该资源。其它的协程只能等待
。互斥锁是传统并发编程对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。
在使用互斥锁时,一定要注意:
对资源操作完成后,一定要解锁,否则会出现流程执行异常,死锁等问题
。通常借助defer。锁定后,立即使用defer语句保证互斥锁及时解锁。
官方的建议
建议使用channel来处理同步,但是也提供这种传统的同步工具,所以选型上,还是要根据业务
看下代码
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// wc 的坑位数量 默认为零值,也就是 = 0
var pit int
// 定义一个锁的对象
var mutex sync.Mutex
func main() {
// 这里模拟 5 个人去上WC
go wc("甲")
go wc("乙")
go wc("丙")
go wc("丁")
go wc("卯")
// 主 goroutine 让出时间片让子 goroutine 去抢占执行
time.Sleep(3 * time.Second)
fmt.Println("打扫。。。")
}
func wc(human string) {
// 增加随机种子
rand.Seed(time.Now().UnixNano())
// 这个动作,在程序里可以理解为,当程序开始运行时,第一个随机的子 goroutine 访问时就开始上锁
// 抽象理解可以为第一个上 wc 的人
fmt.Println(human, "尝试开门")
if pit == 0 {
fmt.Println(human, "进门,开始办事")
// 坑位已占
pit = 1
fmt.Println(human, "办事中。。。")
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
fmt.Println(human, "办完出门")
// 虚坑以待。。
pit = 0
} else {
fmt.Println(human, "发现有人")
}
}
没加锁的结果
# 上面的逻辑,倒不会产生一个坑被多个人占的现象,但是仔细观察,会发现只有随机 1~2 个人成功执行了"办事",其它人尝试开门没成功后,并没有执行"办事",最后程序结束
乙 尝试开门
乙 进门,开始办事
卯 尝试开门
卯 发现有人
乙 办事中。。。
甲 尝试开门
甲 发现有人
丙 尝试开门
丙 发现有人
乙 办完出门
丁 尝试开门
丁 进门,开始办事
丁 办事中。。。
丁 办完出门
打扫。。。
互斥锁处理
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// wc 的坑位数量 默认为零值,也就是 = 0
var pit int
// 定义一个锁的对象
var mutex sync.Mutex
func main() {
// 这里模拟 5 个人去上WC
go wc("甲")
go wc("乙")
go wc("丙")
go wc("丁")
go wc("卯")
// 主 goroutine 让出时间片让子 goroutine 去抢占执行
time.Sleep(3 * time.Second)
fmt.Println("打扫。。。")
}
func wc(human string) {
// 增加随机种子
rand.Seed(time.Now().UnixNano())
// 这个动作,在程序里可以理解为,当程序开始运行时,第一个随机的子 goroutine 访问时就开始上锁
// 抽象理解可以为第一个上 wc 的人
mutex.Lock()
fmt.Println(human, "尝试开门")
if pit == 0 {
fmt.Println(human, "进门,上锁")
// 坑位已占
pit = 1
fmt.Println(human, "办事中。。。")
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
fmt.Println(human, "办完出门")
// 虚坑以待。。
pit = 0
} else {
fmt.Println(human, "发现有人")
}
mutex.Unlock()
}
上锁结果
甲 尝试开门
甲 进门,上锁
甲 办事中。。。
甲 办完出门
卯 尝试开门
卯 进门,上锁
卯 办事中。。。
卯 办完出门
丁 尝试开门
丁 进门,上锁
丁 办事中。。。
丁 办完出门
丙 尝试开门
丙 进门,上锁
丙 办事中。。。
丙 办完出门
乙 尝试开门
乙 进门,上锁
乙 办事中。。。
乙 办完出门
打扫。。。
再看前面 临界资源安全问题
的例子出现超卖问题,使用互斥锁
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 商品数量
var commodity int = 10
var m sync.Mutex
func main() {
// 为了加快程序的响应,这里启动 4 个 goroutine 来处理卖的动作
go buy("①")
go buy("②")
go buy("③")
go buy("④")
time.Sleep(3 * time.Second)
}
func buy(user string) {
// 增加随机种子
rand.Seed(time.Now().UnixNano())
for {
m.Lock()
// 当商品数量足够时
if commodity > 0 {
// 这里模拟购买的处理时间,毕竟真实的场景受到服务器资源、IO处理、网络因素的影响
// 处理的时长都不会完全一致
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
commodity--
fmt.Println(user, "窗口卖走1个,剩余:", commodity)
} else {
// 条件不满足时也要解锁
mutex.Unlock()
fmt.Println("哦吼,售罄卖完了。。。")
// 结束循环
break
}
m.Unlock()
}
}
购买例子执行结果
④ 窗口卖走1个,剩余: 9
④ 窗口卖走1个,剩余: 8
④ 窗口卖走1个,剩余: 7
④ 窗口卖走1个,剩余: 6
④ 窗口卖走1个,剩余: 5
④ 窗口卖走1个,剩余: 4
④ 窗口卖走1个,剩余: 3
④ 窗口卖走1个,剩余: 2
④ 窗口卖走1个,剩余: 1
④ 窗口卖走1个,剩余: 0
哦吼,售罄卖完了。。。
结合同步等待组,去除主 goroutine 的睡眠
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 商品数量
var commodity int = 10
var m sync.Mutex
// 同步等待组
var w sync.WaitGroup
func main() {
// 为了加快程序的响应,这里启动 4 个 goroutine 来处理卖的动作
// 设置 4 个等待组
w.Add(4)
go buy("①")
go buy("②")
go buy("③")
go buy("④")
// 进入阻塞
w.Wait()
}
func buy(user string) {
// 增加随机种子
rand.Seed(time.Now().UnixNano())
// 运行结束,等待组 -1
defer w.Done()
for {
m.Lock()
// 当商品数量足够时
if commodity > 0 {
// 这里模拟购买的处理时间,毕竟真实的场景受到服务器资源、IO处理、网络因素的影响
// 处理的时长都不会完全一致
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
commodity--
fmt.Println(user, "窗口卖走1个,剩余:", commodity)
} else {
m.Unlock()
fmt.Println("哦吼,售罄卖完了。。。")
// 结束循环
break
}
m.Unlock()
}
}