Golang中Mutex 的简单使用
序言
本文简单介绍了并发编程中的基本概念、举例简单说明了golang中Mutex互斥锁的基本使用,
1.什么是并发
说到并发,就免不了要说要秒杀系统、抢票系统这类经典的并发场景,就拿抢票系统
为例.
我们都知道,火车上,一张票只有一个指定的座位,在购买者角度看来,抢票貌似是个一气呵成的过程,下单支付,票就到了你的手上,但是,在实际的业务操作中,抢票过程,包含着许多内部的业务操作,点击下单、查询余票、锁定余票、建立订单信息、支付、余票刷新等等一系列操作,这一系列操作中,如果中间某一步失败,就可能导致同一个座位同一张票卖给了两个人的情况,这样多个请求申请对同一个临界资源
的访问,就是并发。
并发可能会导致商品的超买超卖、数据竞争导致死锁,数据不一致等等一系列问题。
2. Golang是如何解决并发的?
go就是golang中开启一个协程的关键字,这里又提到了协程goroutine,不得不再提到golang的GMP模型,GMP模型不是本专栏的重点,所以这里简单体提及,总的来说,golang使用如下几个大块来解决高并发问题。
1.GMP调度模型
,高效控制goruntine的运行和调度。
2.一系列并发原语
,channel、waitGroup、context、以及本文提到的同步原语mutex互斥锁
3.CSP模型
的思想,不要通过共享内存来通信,而应该通过通信来共享内存。
3. 同步原语Mutex介绍
mutex 是使用最广泛的同步原语。简单来说就是一把互斥锁,用来锁住可能会产生并发安全的临界资源,只有获取到这把锁,才能对临界资源进行访问,否则只能等待锁的释放。mutex中有golang封装好的一系列方法,例如Lock()加锁,锁定临界资源,UnLock解锁,释放临界资源,用来解决并发安全问题。
4. 并发案例
func testNoMutex() {
var count = 0
// 使用WaitGroup等待10个goroutine完成
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 对变量count执行10万次加1
for j := 0; j < 100000; j++ {
count++
}
}()
}
// 等待10个goroutine完成
wg.Wait()
fmt.Println(count)
}
4.1 案例说明
开启10个协程,分别对count变量执行10万次加1,预期结果是100万(100 0000),不使用锁的情况下,可以看到每次执行的结果都不是100万(100 0000),并且结果也各不相同,出现了预期外的结果,此时就是产生了并发安全的问题。 结果异常原因:count++的过程中,多个协程同步进行,但count++并不是原子操作,它至少包含几个步骤,比如 1.读取变量 count 的当前值, 2.对这个值加 1, 3.把结果再保存到 count 中。 因为不是原子操作,就可能 有并发的问题。 再比如,
- 10个goroutine同时读取到 count 的值为 9333,
- 接着各自按照自己的逻辑加 1,值变成了 9334,
- 然后把这个结果再写回到 count 变量。 但是,实际上,此时我们增加的 总数应该是 10 才对,这里却只增加了 1,好多计数都被“吞”掉了。这便是并发访问共享数据的常见错误
4.2 使用Mutex互斥锁的解决方案
使用mutex.Lock加锁,mutex.UnLock释放锁,将count++过程变为原子操作
4.2.1 方式1:直接使用Mutex
func testMutex() {
// 互斥锁保护计数器
var mu sync.Mutex
// 计数器的值
var count = 0
// 辅助变量,用来确认所有的goroutine都完成
var wg sync.WaitGroup
wg.Add(10)
// 启动10个gourontine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 累加10万次
for j := 0; j < 100000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}
运行结果可以看到,每一次运行都成功输出了100万(100 0000)
4.2.2 方式2:封装数据结构与方法,间接使用Mutex,实现自���需要的并发安全的数据类型
这里可以将count进一步封装,直接调用count.Lock方法,根据具体需求,将业务代码写到count.Lock和count.UnLock中。
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Lock() {
//写具体的业务需求
c.mu.Lock()
}
func (c *Counter) UnLock() {
//写具体的业务需求
c.mu.Unlock()
}
func CounterStructByMutest() {
// 计数器的值
var count Counter
// 辅助变量,用来确认所有的goroutine都完成
var wg sync.WaitGroup
wg.Add(10)
// 启动10个gourontine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 累加10万次
for j := 0; j < 100000; j++ {
//直接使用封装好的Lock方法
count.Lock()
count.count++
count.UnLock()
}
}()
}
wg.Wait()
fmt.Println(count.count)
}
执行结果
总结
mutex是golang并发中比较重要的一个同步原语,通过对他的灵活使用,可以构建出我们自己需要的并发安全的类型,例如golang中并发安全的Map,sync.Map的底层便封装好了一个mutex互斥锁,用来实现map的并发安全,这样可以得到并发安全的map提供给我们直接使用。总的来说,掌握各种并发原语并理解它们的底层实现是我们熟练解决Golang并发问题的关键技能。
下节预告: sync.Mutex的数据结构是怎样的,它是如何解决并发安全问题的?
转载自:https://juejin.cn/post/7383017171180322870