11

Go 并发编程之 Mutex

 3 years ago
source link: https://studygolang.com/articles/31613
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

友情提示:此篇文章大约需要阅读 5分钟45秒,不足之处请多指教,感谢你的阅读。 订阅本站

我们比较常见的大型项目的设计中都会出现并发访问问题,并发就是为了解决数据的准确性,保证同一个临界区的数据只能被一个线程进行操作,日常中使用到的并发场景也是很多的:

  • 计数器 :计数器结果不准确;
  • 秒杀系统 :由于同一时间访问量比较大,导致的超卖;
  • 用户账户异常 :同一时间支付导致的账户透支;
  • buffer 数据异常 :更新 buffer 导致的数据混乱。

上面都是并发带来的数据准确性的问题,决绝方案就是使用 互斥锁 ,也就是今天并发编程中的所要描述的 Mutex 并发原语。

实现机制

互斥锁 Mutex 就是为了避免并发竞争建立的并发控制机制,其中有个“临界区”的概念。

在并发编程过程中,如果程序中一部分资源或者变量会被并发访问或者修改,为了避免并发访问导致数据的不准确,这部分程序需要率先被保护起来,之后操作,操作结束后去除保护,这部分被保护的程序就叫做 临界区

使用互斥锁,限定临界区只能同时由一个线程持有,若是临界区此时被一个线程持有,那么其他线程想进入到这个临界区的时候,就会失败或者等待释放锁,持有此临界区的线程退出,其他线程才有机会获得这个临界区。

FFrqQn6.jpg!mobile

go mutex 临界区示意图

Mutex 是 Go 语言中使用最广泛的同步原语,也称为并发原语, 解决的是并发读写共享资源,避免出现数据竞争 data race 问题

基本使用

互斥锁 Mutex 提供了两个方法 Lock 和 Unlock:进入到临界区使用 Lock 方法加锁,退出临界区使用 Unlock 方法释放锁 :lock:。

type Locker interface {
    Lock()
    Unlock()
}

func(m *Mutex)Lock()
func(m *Mutex)Unlock()

当一个 goroutine 调用 Lock 方法获取到锁后,其他 goroutine 会阻塞在 Lock 的调用上,直到当前获取到锁的 goroutine 释放锁。

接下来是一个计数器的例子,是由 100 个 goroutine 对计数器进行累加操作,最后输出结果:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    countNum := 0

    // 确认辅助变量是否都执行完成
    var wg sync.WaitGroup

    // wg 添加数目要和 创建的协程数量保持一致
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                mu.Lock()
                countNum++
                mu.Unlock()
            }
        }()
    }
    wg.Wait()
    fmt.Printf("countNum: %d", countNum)
}

实际使用

很多时候 Mutex 并不是单独使用的,而是嵌套在 Struct 中使用,作为结构体的一部分, 如果嵌入的 struct 有多个字段,我们一般会把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔开来。

甚至可以把获取锁、释放锁、计数加一的逻辑封装成一个方法。

package main
import (
    "fmt"
    "sync"
)

// 线程安全的计数器
type Counter struct {
    CounterType int
    Name        string

    mu    sync.Mutex
    count uint64
}

// 加一方法
func (c *Counter) Incr() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// 取数值方法 线程也需要受保护
func (c *Counter) Count() uint64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    // 定义一个计数器
    var counter Counter

    var wg sync.WaitGroup
    wg.Add(100)

    for i := 0; i < 100; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                counter.Incr()
            }
        }()
    }
    wg.Wait()

    fmt.Printf("%dn", counter.Count())
}

思考问题

Q:你已经知道,如果 Mutex 已经被一个 goroutine 获取了锁,其它等待中的 goroutine 们只能一直等待。那么,等这个锁释放后,等待中的 goroutine 中哪一个会优先获取 Mutex 呢?

A:FIFO,先来先服务的策略,Go 的 goroutine 调度中,会维护一个保障 goroutine 运行的队列,当获取到锁的 goroutine 执行完临界区的操作的时候,就会释放锁,在队列中排在第一位置的 goroutine 会拿到锁进行临界区的操作。

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK