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

用简单的例子来理解 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 能存取改变该数。通过 sync.Mutex 实现线程安全

这次我们在 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())
}

不意外, 此时会再次出现竞争条件警告, 因为同时对 balance 去做读写操作会相互影响, 当然会跳出 race condition 警告

$ 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() 加上 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

假如银行每次存款和查询都需要 1 秒钟

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

就会发现, 每隔 1 秒才能处理一个操作, 5 次存款和 5 次查询总共需要 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. 使用 sync.Mutex 可以解决竞争条件, 但读写操作会互相阻塞
  3. 使用 sync.RWMutex 可以提升性能, 允许多个读操作同时进行, 但写操作仍会阻塞所有读写

go Benchmark

atomutex_test.go Benchmark: sync.RWMutex vs atomic.Value

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

最后更新于 2022-02-18
使用 Hugo 构建
主题 StackJimmy 设计