likes
comments
collection
share

Golang中Mutex 的简单使用

作者站长头像
站长
· 阅读数 23

序言

本文简单介绍了并发编程中的基本概念、举例简单说明了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 中。 因为不是原子操作,就可能 有并发的问题。 再比如,

  1. 10个goroutine同时读取到 count 的值为 9333,
  2. 接着各自按照自己的逻辑加 1,值变成了 9334,
  3. 然后把这个结果再写回到 count 变量。 但是,实际上,此时我们增加的 总数应该是 10 才对,这里却只增加了 1,好多计数都被“吞”掉了。这便是并发访问共享数据的常见错误

Golang中Mutex 的简单使用

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)

Golang中Mutex 的简单使用

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)
}

执行结果

Golang中Mutex 的简单使用

总结

mutex是golang并发中比较重要的一个同步原语,通过对他的灵活使用,可以构建出我们自己需要的并发安全的类型,例如golang中并发安全的Map,sync.Map的底层便封装好了一个mutex互斥锁,用来实现map的并发安全,这样可以得到并发安全的map提供给我们直接使用。总的来说,掌握各种并发原语并理解它们的底层实现是我们熟练解决Golang并发问题的关键技能。

下节预告: sync.Mutex的数据结构是怎样的,它是如何解决并发安全问题的?

转载自:https://juejin.cn/post/7383017171180322870
评论
请登录