3.1 互斥锁和读写锁的性能

Kesa...大约 4 分钟golang

1. 互斥锁和读写锁

1.1 互斥锁(sync.Mutex)

互斥即不可同时运行。即使用了互斥锁的两个代码片段互相排斥,只有其中一个代码片段执行完成后,另一个才能执行。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  1. Lock 加锁
  2. Unlock 释放锁

通过在临界区前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行,也可以用 defer 语句来保证互斥锁一定会被解锁。

在一个 Go 协程调用 Lock 方法获得锁后,其他请求锁的协程都会阻塞在 Lock 方法,直到锁被释放。

1.2 读写锁(sync.RWMutex)

多读单写锁 (multiple readers, single writer lock),简称读写锁,读写锁分为读锁和写锁,读锁允许同时执行的,但写锁互斥的。一般来说,有如下几种情况:

  1. 读锁之间不互斥,没有写锁的情况下,读锁是无阻塞的,多个协程可以同时获得读锁
  2. 写锁之间是互斥的,存在写锁,其他写锁阻塞
  3. 写锁与读锁是互斥的,如果存在读锁,写锁阻塞,如果存在写锁,读锁阻塞

Go 标准库中提供了 sync.RWMutex 互斥锁类型及其四个方法:

  1. Lock 加写锁
  2. Unlock 释放写锁
  3. RLock 加读锁
  4. RUnlock 释放读锁

读写锁的存在是为了解决读多写少时的性能问题,读场景较多时,读写锁可有效地减少锁阻塞的时间。

2. 性能比较

接下来,测试三种情景下,互斥锁和读写锁的性能差异。

  • 读多写少(读占 90%)
  • 读少写多(读占 10%)
  • 读写一致(各占 50%)

2.1 测试用例

实现 2 个结构体 LockRWLock,并且都继承 RW 接口。

RW 接口中定义了 2 个操作,读(Read)和写(Write),为了降低其他指令对测试的影响,假定每个读写操作耗时 1 微秒。


type RW interface {
	Write()
	Read()
}

const cost = time.Nanosecond

type Lock struct {
	count int
	mu    sync.Mutex
}

func (l *Lock) Write() {
	l.mu.Lock()
	l.count++
	time.Sleep(cost)
	l.mu.Unlock()
}

func (l *Lock) Read() {
	l.mu.Lock()
	_ = l.count
	time.Sleep(cost)
	l.mu.Unlock()
}

type RWLock struct {
	count int
	mu    sync.RWMutex
}

func (rw *RWLock) Write() {
	rw.mu.Lock()
	rw.count++
	time.Sleep(cost)
	rw.mu.Unlock()
}

func (rw *RWLock) Read() {
	rw.mu.RLock()
	_ = rw.count
	time.Sleep(cost)
	rw.mu.RUnlock()
}

2.2 Benchmark


func benchLock(b *testing.B, rw RW, read, write int) {
	for i := 0; i < b.N; i++ {
		var wg sync.WaitGroup
		for k := 0; k < read*100; k++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				rw.Read()
			}()
		}

		for k := 0; k < write*100; k++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				rw.Write()
			}()
		}

		wg.Wait()
	}

}

func BenchmarkLockReadMore(b *testing.B)   { benchLock(b, &Lock{}, 9, 1) }
func BenchmarkLockReadMoreRW(b *testing.B) { benchLock(b, &RWLock{}, 9, 1) }

func BenchmarkLockWriteMore(b *testing.B)   { benchLock(b, &Lock{}, 1, 9) }
func BenchmarkLockWriteMoreRW(b *testing.B) { benchLock(b, &RWLock{}, 1, 9) }

func BenchmarkLockEqual(b *testing.B)   { benchLock(b, &Lock{}, 5, 5) }
func BenchmarkLockEqualRW(b *testing.B) { benchLock(b, &RWLock{}, 5, 5) }

BenchmarkLockReadMore-8               28          50612442 ns/op          115039 B/op       2026 allocs/op
BenchmarkLockReadMoreRW-8            188           7211384 ns/op          112240 B/op       2003 allocs/op
BenchmarkLockWriteMore-8              22          46486533 ns/op          115538 B/op       2037 allocs/op
BenchmarkLockWriteMoreRW-8            24          45870117 ns/op          115189 B/op       2034 allocs/op
BenchmarkLockEqual-8                  26          55572988 ns/op          115529 B/op       2037 allocs/op
BenchmarkLockEqualRW-8                68          24956568 ns/op          112778 B/op       2008 allocs/op
  • RW 9 : 1 时,读写锁是互斥锁约 8 倍
  • RW 1 : 9 时,两个差不多
  • RW 5 : 5 时,读写锁约为互斥锁 3 倍

2.3 改变操作时间

若将 cost 改为 0.1 微秒:

BenchmarkLockReadMore-8              846           1260106 ns/op          113234 B/op       2013 allocs/op
BenchmarkLockReadMoreRW-8           1988            602413 ns/op          112259 B/op       2003 allocs/op
BenchmarkLockWriteMore-8             810           1278744 ns/op          112762 B/op       2008 allocs/op
BenchmarkLockWriteMoreRW-8           784           1290452 ns/op          113024 B/op       2011 allocs/op
BenchmarkLockEqual-8                1092           1250835 ns/op          113073 B/op       2012 allocs/op
BenchmarkLockEqualRW-8              1179            962714 ns/op          112864 B/op       2009 allocs/op
  • RW 9 : 1 时,读写锁是互斥锁约 2 倍
  • RW 1 : 9 时,两个差不多
  • RW 5 : 5 时,读写锁约为互斥锁差不多

因为操作时间降低了,互斥锁和读写锁的差距变小。

若将时间改为 10 微秒:

BenchmarkLockReadMore-8                5         294074710 ns/op          132860 B/op       2175 allocs/op
BenchmarkLockReadMoreRW-8             50          29552382 ns/op          112582 B/op       2006 allocs/op
BenchmarkLockWriteMore-8               5         257616520 ns/op          128147 B/op       2169 allocs/op
BenchmarkLockWriteMoreRW-8             5         308358619 ns/op          123408 B/op       2119 allocs/op
BenchmarkLockEqual-8                   5         256004831 ns/op          132006 B/op       2175 allocs/op
BenchmarkLockEqualRW-8                10         135650405 ns/op          116502 B/op       2047 allocs/op

此时差距将会变大。

3. 互斥锁如何实现公平

互斥锁有两种状态:正常状态饥饿状态

在正常状态下,所有等待锁的 goroutine 按照FIFO顺序等待。唤醒的 goroutine 不会直接拥有锁,而是会和新请求锁的 goroutine 竞争锁的拥有。新请求锁的 goroutine 具有优势:它正在 CPU 上执行,而且可能有好几个,所以刚刚唤醒的 goroutine 有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的 goroutine 会加入到等待队列的前面。 如果一个等待的 goroutine 超过 1ms 没有获取锁,那么它将会把锁转变为饥饿模式。

在饥饿模式下,锁的所有权将从 unlock 的 goroutine 直接交给交给等待队列中的第一个。新来的 goroutine 将不会尝试去获得锁,即使锁看起来是 unlock 状态, 也不会去尝试自旋操作,而是放在等待队列的尾部。

如果一个等待的 goroutine 获取了锁,并且满足一以下其中的任何一个条件:(1)它是队列中的最后一个;(2)它等待的时候小于1ms。它会将锁的状态转换为正常状态。

正常状态有很好的性能表现,饥饿模式也是非常重要的,因为它能阻止尾部延迟的现象。

Reference

  1. https://geektutu.com/post/hpg-mutex.htmlopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2