golang 互斥锁和读写锁

golang 互斥锁和读写锁

golang中sync包实现了两种锁Mutex(互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。

    type Mutex

        func (m *Mutex) Lock()

        func (m *Mutex) Unlock()

    type RWMutex

        func (rw *RWMutex) Lock()

        func (rw *RWMutex) Unlock()

        func (rw *RWMutex) RLock()

        func (rw *RWMutex) RUnlock()

        func (rw *RWMutex) RLocker() Locker

1、互斥锁

    func (m *Mutex) Lock():加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。

    func (m *Mutex) Unlock():解锁,如果在使用Unlock()前未加锁,就会引起一个运行错误.已经锁定的Mutex并不与特定的goroutine相关联,这样可以利用一个goroutine对其加锁,再利用其他goroutine对其解锁。

    互斥锁只能锁定一次,当在解锁之前再次进行加锁,便会死锁状态(导致当前线程阻塞),如果在加锁前解锁,便会报错“panic: sync: unlock of unlocked mutex”

2、读写锁

    RWMutex是一个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景.

    func (rw *RWMutex) Lock():写锁,如果在添加写锁之前已经有其他的读锁和写锁,则Lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的Lock调用会从获得的锁中排除新的读取锁,即写锁权限高于读锁,有写锁时优先进行写锁定。

    func (rw *RWMutex) Unlock():写锁解锁,如果没有进行写锁定,则就会引起一个运行时错误。

    func (rw *RWMutex) RLock():读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于“读多写少”的场景。

    func (rw *RWMutex) RUnlock():读锁解锁,RUnlock 撤销单次RLock调用,它对于其它同时存在的读取器则没有效果。若rw并没有为读取而锁定,调用RUnlock就会引发一个运行时错误。

        读写锁的写锁只能锁定一次,解锁前不能多次锁定,读锁可以多次,但读解锁次数最多只能比读锁次数多一次,一般情况下我们不建议读解锁次数多余读锁次数。

    基本遵循两大原则:

        1、可以随便读,多个goroutine同时读。

        2、写的时候,啥也不能干。不能读也不能写。

    读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是在同一时刻,它只允许有一个写操作在进行。

    并且在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。

    对已被写锁定的读写锁进行写锁定,会造成当前Goroutine的阻塞,直到该读写锁被写解锁。当然如果有多个Goroutine因此而被阻塞,那么当对应的写解锁被进行之时只会使其中一个Goroutine的运行被恢复。类似,对一个已被写锁定的读写锁进行读锁定,也会阻塞相应的Goroutine。

    但不同的是,一旦该读写锁被写解锁,那么所有因欲进行读锁定而被阻塞的Goroutine的运行都会被恢复。另一方面,如果在进行过程中发现当前的读写锁已被读锁定,那么这个写锁定操作将会等待直至所有施加于该读写锁之上的读锁定都被清除。

    同样的,在有多个写锁定操作为此而等待的情况下,相应的读锁定的全部清除只能让其中的某一个写锁定操作获得进行的机会。

    现在来关注写解锁和读解锁。如果对一个未被写锁定的读写锁进行写解锁,那么会引发一个运行时恐慌。类似的,当对一个未被读锁定的读写锁进行读解锁的时候也会引发一个运行时恐慌。写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的Goroutine。

    而读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的Goroutine。

    无论锁定针对的是写操作还是读操作,我们都应该尽量及时的对相应的锁进行解锁。对于写解锁,我们自不必多说。而读解锁的及时进行往往更容易被我们忽视。虽说读解锁的进行并不会对其他正在进行中的读操作产生任何影响,但它却与相应的写锁定的进行关系紧密。

    注意,对于同一个读写锁来说,施加在它之上的读锁定可以有多个。因此,只有我们对互斥锁进行相同数量的读解锁,才能够让某一个相应的写锁定获得进行的机会。否则,后者会继续使进行它的Goroutine处于阻塞状态。

    由于sync.RWMutex和*sync.RWMutex类型都没有相应的方法让我们获得已进行的读锁定的数量,所以这里是很容易出现问题的。还好我们可以使用defer语句来尽量避免此类问题的发生。请记住,针对同一个读写锁的写锁定和读锁定是互斥的。

    无论是写解锁还是读解锁,操作的不及时都会对使用该读写锁的流程的正常执行产生负面影响。

    除了我们在前面详细讲解的那两对方法之外,*sync.RWMutex类型还拥有另外一个方法——RLocker。这个RLocker方法会返回一个实现了sync.Locker接口的值。sync.Locker接口类型包含了两个方法,即:Lock和Unlock。

    细心的读者可能会发现,*sync.Mutex类型和*sync.RWMutex类型都是该接口类型的实现类型。实际上,我们在调用*sync.RWMutex类型值的RLocker方法之后所得到的结果值就是这个值本身。

    只不过,这个结果值的Lock方法和Unlock方法分别对应了针对该读写锁的读锁定操作和读解锁操作。

    换句话说,我们在对一个读写锁的RLocker方法的结果值的Lock方法或Unlock方法进行调用的时候实际上是在调用该读写锁的RLock方法或RUnlock方法。这样的操作适配在实现上并不困难。我们自己也可以很容易的编写出这些方法的实现。

    通过读写锁的RLocker方法获得这样一个结果值的实际意义在于,我们可以在之后以相同的方式对该读写锁中的“写锁”和“读锁”进行操作。这为相关操作的灵活适配和替换提供了方便。
上一篇:CSS+DIV入门第一天基础视频 CSS选择器层叠性和继承性


下一篇:2019级软件工程应用与实践-人工智能快递柜(代码分析7)