Go 简单例子来理解 sync.Mutex 和 sync.RWMutex

标签: none

用简单的例子来理解 sync.Mutexsync.RWMutex

盖一间银行

假设有一间银行,可以存款和查询余额。

package main

import (
    "fmt"
)

type Bank struct {
    balance int
}

func (b *Bank) Deposit(amount int) {
    b.balance += amount
}

func (b *Bank) Balance() int {
    return b.balance
}

func main() {
    b := &Bank{}

    b.Deposit(1000)
    b.Deposit(1000)
    b.Deposit(1000)

    fmt.Println(b.Balance())
}
$ go run main.go
3000

执行之后结果是 3000 没问题,1000+1000+1000=3000

同时存款

银行不太可能让人一个一个排队存款,也需要支持同时存款,当今天存款的动作是并行的,会发生什么事呢?

这次用 sync.WaitGroup 去等待所有 goroutine 执行完毕,之后再打印出余额。

func main() {
    var wg sync.WaitGroup
    b := &Bank{}

    wg.Add(3)
    go func() {
        b.Deposit(1000)
        wg.Done()
    }()
    go func() {
        b.Deposit(1000)
        wg.Done()
    }()
    go func() {
        b.Deposit(1000)
        wg.Done()
    }()

    wg.Wait()

    fmt.Println(b.Balance())
}
$ go run main.go
3000

还是 3000 没问题,那我们同时存款 1000 次的时候会发生什么事呢?

func main() {
    var wg sync.WaitGroup
    b := &Bank{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println(b.Balance())
}
$ go run main.go
958000

诶奇怪,正常来说 1000 * 1000 = 1000000 吗?怎么数字不正确!

我们这次多带一个参数 -race 跑看看

-race 参数是 go 的 Race Detector,内建整合工具,可以轻松检查出是否有 race condition
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c00012c088 by goroutine 8:
  main.(*Bank).Deposit()
      .../main.go:13 +0x3c
  main.main.func1()
      .../main.go:28 +0x37

Previous write at 0x00c00012c088 by goroutine 7:
  main.(*Bank).Deposit()
      .../main.go:13 +0x4e
  main.main.func1()
      .../main.go:28 +0x37

Goroutine 8 (running) created at:
  main.main()
      .../main.go:27 +0x97

Goroutine 7 (finished) created at:
  main.main()
      .../main.go:27 +0x97
==================
1000000
Found 1 data race(s)
exit status 66

发现原来有 race condition, 因为同时去对 Bank.balance 去做存取的动作,数量少的时候可能没问题,当量大的时候就可能出错。

sync.Mutex

为了防止这种状况发生,就可以用互斥锁 sync.Mutex 来处理这个问题,同时间只有一个 goroutine 能存取该变数。

这次我们在 Deposit() 存款前先 Lock(),存款后再 Unlock()

type Bank struct {
    balance int
    mux     sync.Mutex
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock()
    b.balance += amount
    b.mux.Unlock()
}

func (b *Bank) Balance() int {
    return b.balance
}
$ go run -race main.go
1000000

这次结果正确了,而且也没跳出 race condition 的警告。

同时存款和查询

想当然会有多人一起存款,就会有多人一起查询余额。

多加一组查询 1000 次的 goroutine 再执行看看。

func main() {
    var wg sync.WaitGroup

    b := &Bank{}

    n := 1000
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            wg.Done()
        }()
    }
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            _ = b.Balance()
            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println(b.Balance())
}
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c00012a080 by goroutine 56:
  main.(*Bank).Balance()
      .../main.go:20 +0x3c
  main.main.func2()
      .../main.go:39 +0x37

Previous write at 0x00c00012a080 by goroutine 53:
  main.(*Bank).Deposit()
      .../main.go:15 +0x5d
  main.main.func1()
      .../main.go:32 +0x3e

Goroutine 56 (running) created at:
  main.main()
      .../main.go:38 +0x184

Goroutine 53 (finished) created at:
  main.main()
      .../main.go:31 +0xa4
==================
1000000
Found 1 data race(s)
exit status 66

不意外,因为同时对 balance 去做读写,当然跳出 race condition 的警告。

我们一样在 Balance() 加上 Lock()Unlock() 后执行。

type Bank struct {
    balance int
    mux     sync.Mutex
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock()
    b.balance += amount
    b.mux.Unlock()
}

func (b *Bank) Balance() (balance int) {
    b.mux.Lock()
    balance = b.balance
    b.mux.Unlock()
    return
}
$ go run -race main.go
1000000

结果成功了,也没有 race 的警告了。

读写互相阻塞

目前这边看起来都还不错,但以现在的情况来说,只要有人读,或只要有人写,就会被 block。

假如银行存款和查询各要上花一秒

package main

import (
    "log"
    "sync"
    "time"
)

type Bank struct {
    balance int
    mux     sync.Mutex
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock()
    time.Sleep(time.Second) // spend 1 second
    b.balance += amount
    b.mux.Unlock()
}

func (b *Bank) Balance() (balance int) {
    b.mux.Lock()
    time.Sleep(time.Second) // spend 1 second
    balance = b.balance
    b.mux.Unlock()
    return 
}

func main() {
    var wg sync.WaitGroup
    b := &Bank{}

    n := 5
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            b.Deposit(1000)
            log.Printf("Write: deposit amonut: %v", 1000)
            wg.Done()
        }()
    }
    wg.Add(n)
    for i := 1; i <= n; i++ {
        go func() {
            log.Printf("Read: balance: %v", b.Balance())
            wg.Done()
        }()
    }

    wg.Wait()
}
$ go run -race main.go
2022/02/18 15:12:49 Write: deposit amonut: 1000
2022/02/18 15:12:50 Write: deposit amonut: 1000
2022/02/18 15:12:51 Write: deposit amonut: 1000
2022/02/18 15:12:52 Write: deposit amonut: 1000
2022/02/18 15:12:53 Write: deposit amonut: 1000
2022/02/18 15:12:54 Read: balance: 5000
2022/02/18 15:12:55 Read: balance: 5000
2022/02/18 15:12:56 Read: balance: 5000
2022/02/18 15:12:57 Read: balance: 5000
2022/02/18 15:12:58 Read: balance: 5000

就会发现,每隔一秒才能处理一个 action,以各五次读写来说,总共就要花上 10 秒,但对读来说,应该可以疯狂读,每次读都会是安全的, 值也都会是一样,除非当下有写的动作,它不应该被其他读的动作 block。

sync.RWMutex

sync.RWMutex 是一个读写锁(multiple readers, single writer lock),多读单写,可以允许多个读并发,单个写。

sync.Mutex 换成 sync.RWMutex

type Bank struct {
    balance int
    mux     sync.RWMutex // read write lock
}

func (b *Bank) Deposit(amount int) {
    b.mux.Lock() // write lock
    time.Sleep(time.Second)
    b.balance += amount
    b.mux.Unlock() // wirte unlock
}

func (b *Bank) Balance() (balance int) {
    b.mux.RLock() // read lock
    time.Sleep(time.Second)
    balance = b.balance
    b.mux.RUnlock() // read unlock
    return
}
$ go run -race main.go
2022/02/18 15:16:29 Write: deposit amonut: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:31 Write: deposit amonut: 1000
2022/02/18 15:16:32 Write: deposit amonut: 1000
2022/02/18 15:16:33 Write: deposit amonut: 1000
2022/02/18 15:16:34 Write: deposit amonut: 1000

执行之后会发现,本来要花 10 秒,已经缩短成 5 秒了,只要当下是读的时候,都会同时进行,并不会互相影响,写的时候就会 block 读和写,只有一个写会发生。

总结

  1. 在写 goroutine 的时候,需要考虑 race condition,在执行或测试上可以加上 -race 去检查,以免结果与预期不符
  2. 遇到 race condition 的时候可以考虑用 sync.Mutex 来解决,有读写阻塞的时候可以用 sync.RWMutex
  3. syncRWMutex 可以有同时允许多个 RLockRUnlock 但只能有一个 LockUnlock

go Benchmark

Benchmark: sync.RWMutex vs atomic.Value

atomutex_test.go

/*
Benchmark_RWMutex-4                100000000            18.1 ns/op
Benchmark_Atomic-4                 200000000             8.01 ns/op
Benchmark_RWMutex_parallel-4       30000000            46.7 ns/op
Benchmark_Atomic_parallel-4        1000000000             2.61 ns/op
*/
package main

import (
    "sync"
    "sync/atomic"
    "testing"
)

func Benchmark_RWMutex(b *testing.B) {
    var lock sync.RWMutex
    m := map[int]int{1: 2}

    for i := 0; i < b.N; i++ {
        lock.RLock()
        _ = m[1]
        lock.RUnlock()
    }
}

func Benchmark_Atomic(b *testing.B) {
    var ptr atomic.Value
    ptr.Store(map[int]int{1: 2})

    for i := 0; i < b.N; i++ {
        m := ptr.Load().(map[int]int)
        _ = m[1]
    }
}

func Benchmark_RWMutex_parallel(b *testing.B) {
    var lock sync.RWMutex
    m := map[int]int{1: 2}

    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            lock.RLock()
            _ = m[1]
            lock.RUnlock()
        }
    })
}

func Benchmark_Atomic_parallel(b *testing.B) {
    var ptr atomic.Value
    ptr.Store(map[int]int{1: 2})

    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            m := ptr.Load().(map[int]int)
            _ = m[1]
        }
    })
}
$ go mod init benchmark
$ go test -cpu 1,2,4,8,36 -bench .
goos: windows
goarch: amd64
pkg: benchmark
cpu: Intel(R) Core(TM) i9-10980XE CPU @ 3.00GHz
Benchmark_RWMutex                       84223528                14.31 ns/op
Benchmark_RWMutex-2                     82506548                14.48 ns/op
Benchmark_RWMutex-4                     80424642                14.46 ns/op
Benchmark_RWMutex-8                     82474226                14.82 ns/op
Benchmark_RWMutex-36                    72245634                14.67 ns/op
Benchmark_Atomic                        298416230                4.013 ns/op
Benchmark_Atomic-2                      294261531                4.014 ns/op
Benchmark_Atomic-4                      298208067                4.050 ns/op
Benchmark_Atomic-8                      278849458                4.110 ns/op
Benchmark_Atomic-36                     289697505                4.060 ns/op
Benchmark_RWMutex_parallel              82412020                14.31 ns/op
Benchmark_RWMutex_parallel-2            39356391                30.50 ns/op
Benchmark_RWMutex_parallel-4            37746673                31.62 ns/op
Benchmark_RWMutex_parallel-8            38003095                31.19 ns/op
Benchmark_RWMutex_parallel-36           41561199                28.36 ns/op
Benchmark_Atomic_parallel               297627019                4.096 ns/op
Benchmark_Atomic_parallel-2             566516064                2.016 ns/op
Benchmark_Atomic_parallel-4             1000000000               1.029 ns/op
Benchmark_Atomic_parallel-8             1000000000               0.5073 ns/op
Benchmark_Atomic_parallel-36            100000000               14.80 ns/op
PASS
ok      benchmark       29.463s

Go 簡單例子來理解 sync.Mutex 和 sync.RWMutex
sync.RWMutex
atomutex_test.go


扫描二维码,在手机上阅读!

添加新评论