golang 基础-golang里面的读写锁实现与核心原理分析

前言

golang面试中,我们经常会被问到golang里面的读写锁的原理,golang里的读写锁解决了什么问题,golang读写锁的底层原理是什么?下面我们一起学习了解一下golang里面的读写锁实现与核心原理


一、golang里的读写锁是什么?

golang读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。
golang读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是,在同一时刻,它只允许有一个写操作在进行。并且,在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说,读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。
这样的规则对于针对同一块数据的并发读写来讲是非常贴切的。因为,无论读操作的并发量有多少,这些操作都不会对数据本身造成变更。而写操作不但会对同时进行的其他写操作进行干扰,还有可能造成同时进行的读操作的结果的不正确。
在Go语言中,读写锁由结构体类型sync.RWMutex代表。与互斥锁类似,sync.RWMutex类型的零值就已经是立即可用的读写锁了

二、golang读写锁rwmutex.go的源码:

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sync

import (
	"internal/race"
	"sync/atomic"
	"unsafe"
)

// There is a modified copy of this file in runtime/rwmutex.go.
// If you make any changes here, see if you should make them there.

// A RWMutex is a reader/writer mutual exclusion lock.
// The lock can be held by an arbitrary number of readers or a single writer.
// The zero value for a RWMutex is an unlocked mutex.
//
// A RWMutex must not be copied after first use.
//
// If a goroutine holds a RWMutex for reading and another goroutine might
// call Lock, no goroutine should expect to be able to acquire a read lock
// until the initial read lock is released. In particular, this prohibits
// recursive read locking. This is to ensure that the lock eventually becomes
// available; a blocked Lock call excludes new readers from acquiring the
// lock.
type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // semaphore for writers to wait for completing readers
	readerSem   uint32 // semaphore for readers to wait for completing writers
	readerCount int32  // number of pending readers
	readerWait  int32  // number of departing readers
}

const rwmutexMaxReaders = 1 << 30

// Happens-before relationships are indicated to the race detector via:
// - Unlock  -> Lock:  readerSem
// - Unlock  -> RLock: readerSem
// - RUnlock -> Lock:  writerSem
//
// The methods below temporarily disable handling of race synchronization
// events in order to provide the more precise model above to the race
// detector.
//
// For example, atomic.AddInt32 in RLock should not appear to provide
// acquire-release semantics, which would incorrectly synchronize racing
// readers, thus potentially missing races.

// RLock locks rw for reading.
//
// It should not be used for recursive read locking; a blocked Lock
// call excludes new readers from acquiring the lock. See the
// documentation on the RWMutex type.
func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		// A writer is pending, wait for it.
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

// TryRLock tries to lock rw for reading and reports whether it succeeded.
//
// Note that while correct uses of TryRLock do exist, they are rare,
// and use of TryRLock is often a sign of a deeper problem
// in a particular use of mutexes.
func (rw *RWMutex) TryRLock() bool {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	for {
		c := atomic.LoadInt32(&rw.readerCount)
		if c < 0 {
			if race.Enabled {
				race.Enable()
			}
			return false
		}
		if atomic.CompareAndSwapInt32(&rw.readerCount, c, c+1) {
			if race.Enabled {
				race.Enable()
				race.Acquire(unsafe.Pointer(&rw.readerSem))
			}
			return true
		}
	}
}

// RUnlock undoes a single RLock call;
// it does not affect other simultaneous readers.
// It is a run-time error if rw is not locked for reading
// on entry to RUnlock.
func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		// Outlined slow-path to allow the fast-path to be inlined
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}

func (rw *RWMutex) rUnlockSlow(r int32) {
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		throw("sync: RUnlock of unlocked RWMutex")
	}
	// A writer is pending.
	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

// Lock locks rw for writing.
// If the lock is already locked for reading or writing,
// Lock blocks until the lock is available.
func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// First, resolve competition with other writers.
	rw.w.Lock()
	// Announce to readers there is a pending writer.
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// Wait for active readers.
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

// TryLock tries to lock rw for writing and reports whether it succeeded.
//
// 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.
func (rw *RWMutex) TryLock() bool {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	if !rw.w.TryLock() {
		if race.Enabled {
			race.Enable()
		}
		return false
	}
	if !atomic.CompareAndSwapInt32(&rw.readerCount, 0, -rwmutexMaxReaders) {
		rw.w.Unlock()
		if race.Enabled {
			race.Enable()
		}
		return false
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
	return true
}

// Unlock unlocks rw for writing. It is a run-time error if rw is
// not locked for writing on entry to Unlock.
//
// As with Mutexes, a locked RWMutex is not associated with a particular
// goroutine. One goroutine may RLock (Lock) a RWMutex and then
// arrange for another goroutine to RUnlock (Unlock) it.
func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// Announce to readers there is no active writer.
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		race.Enable()
		throw("sync: Unlock of unlocked RWMutex")
	}
	// Unblock blocked readers, if any.
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// Allow other writers to proceed.
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

// RLocker returns a Locker interface that implements
// the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
func (rw *RWMutex) RLocker() Locker {
	return (*rlocker)(rw)
}

type rlocker RWMutex

func (r *rlocker) Lock()   { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }

RWMutex 是读写器互斥锁。
锁可以由任意数量的读取器或单个写入器持有。
RWMutex 的零值是未锁定的互斥锁。
RWMutex 首次使用后不得复制。
如果一个 goroutine 持有一个 RWMutex 用于读取,而另一个 goroutine 可能
调用 Lock,没有 goroutine 应该期望能够获得读锁
直到初始读锁被释放。 特别是,这禁止
递归读锁定。 这是为了确保锁最终变为
可用的; 阻塞的 Lock 调用将新读者排除在获取
锁。
实现的方法

方法 作用
RLock RLock 锁定 rw 以供读取
TryRLock TryRLock 尝试锁定 rw 以进行读取并报告它是否成功
RUnlock RUnlock 撤消单个 RLock 调用;
rUnlockSlow
Lock Lock 锁定 rw 以进行写入。
TryLock TryLock 尝试锁定 rw 进行写入并报告是否成功。
Unlock Unlock 解锁 rw 以进行写入。 如果 rw 是运行时错误
RLocker RLocker返回一个Locker接口,实现通过调用 rw.RLock 和 rw.RUnlock 的 Lock 和 Unlock 方法。

三、golang读写锁结构及原理图

golang读写锁结构:

type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // 用于writer等待读完成排队的信号量
	readerSem   uint32 // 用于reader等待写完成排队的信号量
	readerCount int32  // 读锁的计数器
	readerWait  int32  // 等待读锁释放的数量
}

golang 基础-golang里面的读写锁实现与核心原理分析_第1张图片

读锁加锁逻辑

golang 基础-golang里面的读写锁实现与核心原理分析_第2张图片

读锁释放逻辑

golang 基础-golang里面的读写锁实现与核心原理分析_第3张图片

写锁实现
golang 基础-golang里面的读写锁实现与核心原理分析_第4张图片

释放写锁

golang 基础-golang里面的读写锁实现与核心原理分析_第5张图片

四、golang读写锁代码示例

var (
    x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex
    rwlock sync.RWMutex
)
func myadd() {
    // lock.Lock()   // 加互斥锁
    rwlock.Lock() // 加写锁
    x = x + 1
    time.Sleep(2 * time.Millisecond) // 假设写操作耗时2毫秒
    rwlock.Unlock()                   // 解写锁
    // lock.Unlock()                  // 解互斥锁
    wg.Done()
}
func myget() {
    // lock.Lock()                  // 加互斥锁
    rwlock.RLock()               // 加读锁
    time.Sleep(time.Millisecond) // 摸㧡读操作耗时1毫秒
    rwlock.RUnlock()             // 解读锁
    // lock.Unlock()             // 解互斥锁
    wg.Done()
}
func main() {
    start := time.Now()
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go myadd()
    }
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go myget()
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
}

五、总结

我们通过golang读写锁与 golang Mutex 对比得出答案。Mutex 是不区分 goroutine 对共享资源的操作行为的,在读操作、它会上锁,在写操作,它也会上锁,当一段时间内,读操作居多时,读操作在 Mutex 的保护下也不得不变为串行访问,对性能的影响也就比较大了。

golang RWMutex 读写锁的诞生为了区分读写操作,在进行读操作时,goroutine 就不必傻傻的等待了,而是可以并发地访问共享资源,将串行读变成了并行读,提高了读操作的性能。

读写锁针对解决一类问题:readers-writes ,同时有多个读或者多个写操作时,只要有一个线程在执行写操作,其他的线程都不能进行读操作。

读写锁其实有三种工作模型:

Read-perferring 优先读设计,可能会导致写饥饿
Write-prferring 优先写设计,避免写饥饿
不指定优先级 不区分优先级,解决饥饿问题

Golang 中的读写锁,工作模型是 Write-prferring 方案。

你可能感兴趣的:(golang,tutorial,golang,开发语言,后端)