设为首页 加入收藏

TOP

golang:1.并发编程之互斥锁、读写锁详解(二)
2017-09-30 13:37:28 】 浏览:9727
Tags:golang 并发 编程 读写 详解
中最简单的一个。只要遵循前面提及的几个小技巧,我们就可以以正确、高效的方式使用互斥锁,并用它来确保对共享资源的访问的唯一性。下面我们来看看稍微复杂一些的锁实现——读写锁。

二、读写锁

      读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是,在同一时刻,它只允许有一个写操作在进行。并且,在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说,读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。

      这样的规则对于针对同一块数据的并发读写来讲是非常贴切的。因为,无论读操作的并发量有多少,这些操作都不会对数据本身造成变更。而写操作不但会对同时进行的其他写操作进行干扰,还有可能造成同时进行的读操作的结果的不正确。例如,在32位的操作系统中,针对int64类型值的读操作和写操作都不可能只由一个CPU指令完成。在一个写操作被进行的过程当中,针对同一个只的读操作可能会读取到未被修改完成的值。该值既不与旧的值相等,也不等于新的值。这种错误往往不易被发现,且很难被修正。因此,在这样的场景下,读写锁可以在大大降低因使用锁而对程序性能造成的损耗的情况下完成对共享资源的访问控制。

      在Go语言中,读写锁由结构体类型sync.RWMutex代表。与互斥锁类似,sync.RWMutex类型的零值就已经是立即可用的读写锁了。在此类型的方法集合中包含了两对方法,即:

代码如下:
func (*RWMutex) Lock

func (*RWMutex) Unlock

代码如下:
func (*RWMutex) RLock

func (*RWMutex) RUnlock 

前一对方法的名称和签名与互斥锁的那两个方法完全一致。它们分别代表了对写操作的锁定和解锁。以下简称它们为写锁定和写解锁。而后一对方法则分别表示了对读操作的锁定和解锁。以下简称它们为读锁定和读解锁。

      对已被写锁定的读写锁进行写锁定,会造成当前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方法获得这样一个结果值的实际意义在于,我们可以在之后以相同的方式对该读写锁中的“写锁”和“读锁”进行操作。这为相关操作的灵活适配和替换提供了方便。

三、锁的完整示例

     我们下面来看一个与上述锁实现有关的示例。在Go语言的标准库代码包os中有一个名为File的结构体类型。os.File类型的值可以被用来代表文件系统中的某一个文件或目录。它的方法集合中包含了很多方法,其中的一些方法被用来对相应的文件进行写操作和读操作。

     假设,我们需要创建一个文件来存放数据。在同一个时刻,可能会有多个Goroutine分别进行对此文件的进行写操作和读操作。每一次写操作都应该向这个文件写入若干个字节的数据。这若干字节的数据应该作为一个独立的数据块存在。这就意味着,写操作之间不能彼此干扰,写入的内容之间也不能出现穿插和混淆的情况。另一方面,每一次读操作都应该从这个文件中读取一个独立、完整的数据块。它们读取的数据块不能重复,且需要按顺序读取。例如,第一个读操作读取了数据块1,那么第二个读操作就应该去读取数据块2,而第三个读操作则应该读取数据块3,以此类推。对于这些读操作是否可以被同时进行,这里并不做要求。即使它们被同时进行,程序也应该分辨出它们的先后顺序。

     为了突出重点,我们规定每个数据块的长度都是相同的。该长度应该在初始化的时候被给定。若写操作实际欲写入数据的长度超过了该值,则超出部分将会被截掉。

     当我们拿到这样一个需求的时候,首先应该想到使用os.File类型。它为我们

首页 上一页 1 2 3 4 5 下一页 尾页 2/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇5步搭建GO环境 下一篇转发器---转发网络通信

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目