温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Golang中的Mutex怎么使用

发布时间:2023-05-10 17:25:08 来源:亿速云 阅读:125 作者:iii 栏目:开发技术

本篇内容介绍了“Golang中的Mutex怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

    Mutex

    基本概念

    MutexGo 语言中互斥锁的实现,它是一种同步机制,用于控制多个 goroutine 之间的并发访问。当多个 goroutine 尝试同时访问同一个共享资源时,可能会导致数据竞争和其他并发问题,因此需要使用互斥锁来协调它们之间的访问。

    Golang中的Mutex怎么使用

    在上述图片中,我们可以将绿色部分看作是临界区。当 g1 协程通过 mutex 对临界区进行加锁后,临界区将会被锁定。此时如果 g2 想要访问临界区,就会失败并进入阻塞状态,直到锁被释放,g2 才能拿到临界区的访问权。

    结构体介绍

    type Mutex struct {
        state int32
        sema  uint32
    }

    字段:

    state

    state 是一个 int32 类型的变量,它存储着 Mutex 的各种状态信息(未加锁、被加锁、唤醒状态、饥饿状态),不同状态通过位运算进行计算。

    sema

    sema 是一个信号量,用于实现 Mutex 的等待和唤醒机制。

    方法:

    Lock()

    Lock() 方法用于获取 Mutex 的锁,如果 Mutex 已经被其他的 goroutine 锁定,则 Lock() 方法会一直阻塞,直到该 goroutine 获取到锁为止。

    UnLock()

    Unlock() 方法用于释放 Mutex 的锁,将 Mutex 的状态设置为未锁定的状态。

    TryLock()

    Go 1.18 版本以后,sync.Mutex 新增一个 TryLock() 方法,该方法为非阻塞式的加锁操作,如果加锁成功,返回 true,否则返回 false

    虽然 TryLock() 的用法确实存在,但由于其使用场景相对较少,因此在使用时应该格外谨慎。TryLock() 方法注释如下所示:

    // 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.

    代码示例

    我们先来看一个有并发安全问题的例子

    package main
    
    import (
       "fmt"
       "sync"
    )
    
    var cnt int
    
    func main() {
       var wg sync.WaitGroup
       for i := 0; i < 10; i++ {
          wg.Add(1)
          go func() {
             defer wg.Done()
             for j := 0; j < 10000; j++ {
                cnt++
             }
          }()
       }
       wg.Wait()
       fmt.Println(cnt)
    }

    在这个例子中,预期的 cnt 结果为 10 * 10000 = 100000。但是由于多个 goroutine 并发访问了共享变量 cnt,并且没有进行任何同步操作,可能导致读写冲突(race condition),从而影响 cnt 的值和输出结果的正确性。这种情况下,不能确定最终输出的 cnt 值是多少,每次执行程序得到的结果可能不同。

    在这种情况下,可以使用互斥锁(sync.Mutex)来保护共享变量的访问,保证只有一个 goroutine 能够同时访问 cnt,从而避免竞态条件的问题。修改后的代码如下:

    package main
    
    import (
       "fmt"
       "sync"
    )
    
    var cnt int
    var mu sync.Mutex
    
    func main() {
       var wg sync.WaitGroup
       for i := 0; i < 10; i++ {
          wg.Add(1)
          go func() {
             defer wg.Done()
             for j := 0; j < 10000; j++ {
                mu.Lock()
                cnt++
                mu.Unlock()
             }
          }()
       }
       wg.Wait()
       fmt.Println(cnt)
    }

    在这个修改后的版本中,使用互斥锁来保护共享变量 cnt 的访问,可以避免出现竞态条件的问题。具体而言,在 cnt++ 操作前,先执行 Lock() 方法,以确保当前 goroutine 获取到了互斥锁并且独占了共享变量的访问权。在 cnt++ 操作完成后,再执行 Unlock() 方法来释放互斥锁,从而允许其他 goroutine 获取互斥锁并访问共享变量。这样,只有一个 goroutine 能够同时访问 cnt,从而确保了最终输出结果的正确性。

    易错场景

    忘记解锁

    如果使用 Lock() 方法之后,没有调用 Unlock() 解锁,会导致其他 goroutine 被永久阻塞。例如:

    package main
    
    import (
       "fmt"
       "sync"
       "time"
    )
    
    var mu sync.Mutex
    var cnt int
    
    func main() {
       go increase(1)
       go increase(2)
    
       time.Sleep(time.Second)
       fmt.Println(cnt)
    }
    
    func increase(delta int) {
       mu.Lock()
       cnt += delta
    }

    在上述代码中,通常情况下,cnt 的结果应该为 3。然而没有解锁操作,其中一个 goroutine 被阻塞,导致没有达到预期效果,最终输出的 cnt 可能只能为 12

    正确的做法是使用 defer 语句在函数返回前释放锁。

    func increase(delta int) {
       mu.Lock()
       defer mu.Unlock() // 通过 defer 语句在函数返回前释放锁
       cnt += delta
    }

    重复加锁

    重复加锁操作被称为可重入操作。不同于其他一些编程语言的锁实现(例如 JavaReentrantLock),Gomutex 并不支持可重入操作,如果发生了重复加锁操作,就会导致死锁。例如:

    package main
    
    import (
       "fmt"
       "sync"
       "time"
    )
    
    var mu sync.Mutex
    var cnt int
    
    func main() {
       go increase(1)
       go increase(2)
    
       time.Sleep(time.Second)
       fmt.Println(cnt)
    }
    
    func increase(delta int) {
       mu.Lock()
       mu.Lock()
       cnt += delta
       mu.Unlock()
    }

    在这个例子中,如果在 increase 函数中重复加锁,将会导致 mu 锁被第二次锁住,而其他 goroutine 将被永久阻塞,从而导致程序死锁。正确的做法是只对需要加锁的代码段进行加锁,避免重复加锁。

    基于 Mutex 实现一个简单的线程安全的缓存

    import "sync"
    
    type Cache struct {
       data map[string]any
       mu   sync.Mutex
    }
    
    func (c *Cache) Get(key string) (any, bool) {
       c.mu.Lock()
       defer c.mu.Unlock()
       value, ok := c.data[key]
       return value, ok
    }
    
    func (c *Cache) Set(key string, value any) {
       c.mu.Lock()
       defer c.mu.Unlock()
       c.data[key] = value
    }

    上述代码实现了一个简单的线程安全的缓存。使用 Mutex 可以保证同一时刻只有一个 goroutine 进行读写操作,避免多个 goroutine 并发读写同一数据时产生数据不一致性的问题。

    对于缓存场景,读操作比写操作更频繁,因此使用 RWMutex 代替 Mutex 会更好,因为 RWMutex 允许多个 goroutine 同时进行读操作,只有在写操作时才会进行互斥锁定,从而减少了锁的竞争,提高了程序的并发性能。

    “Golang中的Mutex怎么使用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI