设为首页 加入收藏

TOP

golang:1.并发编程之互斥锁、读写锁详解(四)
2017-09-30 13:37:28 】 浏览:9726
Tags:golang 并发 编程 读写 详解
到,在读取并更新读偏移量的时候,我们用到了rmutex字段。这保证了可能同时运行在多个Goroutine中的这两行代码:

复制代码代码如下:

offset = df.roffset

 

df.roffset += int64(df.dataLen)

 

的执行是互斥的。这是我们为了获取到不重复且正确的读偏移量所必需采取的措施。

另一方面,在读取一个数据块的时候,我们适时的进行了fmutex字段的读锁定和读解锁操作。fmutex字段的这两个操作可以保证我们在这里读取到的是完整的数据块。不过,这个完整的数据块却并不一定是正确的。为什么会这样说呢?

请想象这样一个场景。在我们的程序中,有3个Goroutine来并发的执行某个*myDataFile类型值的Read方法,并有2个Goroutine来并发的执行该值的Write方法。通过前3个Goroutine的运行,数据文件中的数据块被依次的读取了出来。但是,由于进行写操作的Goroutine比进行读操作的Goroutine少,所以过不了多久读偏移量roffset的值就会等于甚至大于写偏移量woffset的值。也就是说,读操作很快就会没有数据可读了。这种情况会使上面的df.f.ReadAt方法返回的第二个结果值为代表错误的非nil且会与io.EOF相等的值。实际上,我们不应该把这样的值看成错误的代表,而应该把它看成一种边界情况。但不幸的是,我们在这个版本的Read方法中并没有对这种边界情况做出正确的处理。该方法在遇到这种情况时会直接把错误值返回给它的调用方。该调用方会得到读取出错的数据块的序列号,但却无法再次尝试读取这个数据块。由于其他正在或后续执行的Read方法会继续增加读偏移量roffset的值,所以当该调用方再次调用这个Read方法的时候只可能读取到在此数据块后面的其他数据块。注意,执行Read方法时遇到上述情况的次数越多,被漏读的数据块也就会越多。为了解决这个问题,我们编写了Read方法的第二个版本:

复制代码代码如下:

func (df *myDataFile) Read() (rsn int64, d Data, err error) {

 

// 读取并更新读偏移量

// 省略若干条语句

//读取一个数据块

rsn = offset / int64(df.dataLen)

bytes := make([]byte, df.dataLen)

for {

df.fmutex.RLock()

_, err = df.f.ReadAt(bytes, offset)

if err != nil {

if err == io.EOF {

df.fmutex.RUnlock()

continue

}

df.fmutex.RUnlock()

return

}

d = bytes

df.fmutex.RUnlock()

return

}

}

 

在上面的Read方法展示中,我们省略了若干条语句。原因在这个位置上的那些语句并没有任何变化。为了进一步节省篇幅,我们在后面也会遵循这样的省略原则。

第二个版本的Read方法使用for语句是为了达到这样一个目的:在其中的df.f.ReadAt方法返回io.EOF错误的时候继续尝试获取同一个数据块,直到获取成功为止。注意,如果在该for代码块被执行期间一直让读写锁fmutex处于读锁定状态,那么针对它的写锁定操作将永远不会成功,且相应的Goroutine也会被一直阻塞。因为它们是互斥的。所以,我们不得不在该for语句块中的每条return语句和continue语句的前面都加入一个针对该读写锁的读解锁操作,并在每次迭代开始时都对fmutex进行一次读锁定。显然,这样的代码看起来很丑陋。冗余的代码会使代码的维护成本和出错几率大大增加。并且,当for代码块中的代码引发了运行时恐慌的时候,我们是很难及时的对读写锁fmutex进行读解锁的。即便可以这样做,那也会使Read方法的实现更加丑陋。我们因为要处理一种边界情况而去掉了defer df.fmutex.RUnlock()语句。这种做法利弊参半。

其实,我们可以做得更好。但是这涉及到了其他同步工具。因此,我们以后再来对Read方法进行进一步的改造。顺便提一句,当df.f.ReadAt方法返回一个非nil且不等于io.EOF的错误值的时候,我们总是应该放弃再次获取目标数据块的尝试而立即将该错误值返回给Read方法的调用方。因为这样的错误很可能是严重的(比如,f字段代表的文件被删除了),需要交由上层程序去处理。

现在,我们来考虑*myDataFile类型的Write方法。与Read方法相比,Write方法的实现会简单一些。因为后者不会涉及到边界情况。在该方法中,我们需要进行两个步骤,即:获取并更新写偏移量和向文件写入一个数据块。我们直接给出Write方法的实现:

复制代码代码如下:

func (df *myDataFile) Write(d Data) (wsn int64, err error) {

 

// 读取并更新写偏移量

var offset int64

df.wmutex.Lock()

offset = df.woffset

df.woffset += int64(df.dataLen)

df.wmutex.Unlock()

 

//写入一个数据块

wsn = offset / int64(df.dataLen)

var bytes []byte

if len(d) > int(df.dataLen) {

bytes = d[0:df.dataLen]

} else {

bytes = d

}

df.fmutex.Lock()

defer df.fmutex.Unlock()

_, err = df.f.Write(bytes)

return

}

 

这里需要注意的是,当参数d的值的长度大于数据块的最大长度的时候,我们会先进行截短处理再将数据写入文件。如果没有这个截短处理,我们在后面计算的已读数据块的序列号和已写数据块的序列号就会不正确。

有了编写前面两个方法的经验,我们可以很容易的编写出*myDataFile类型的Rsn方法和Wsn方法:

复制代码代码如下:

func (df *myDataFile) Rsn() int64 {

 

df.rmutex.Lock()

defer df.rmutex.Unlock()

return df.roffset / int64(df.dataLen)

}

func (df *myDataFile) Wsn() int64 {

df.wmutex.Lock()

defer df.wmutex.Unlock()

return df.woffset / int64(df.dataLen)

}

 

这两个方法的实现分别涉及到了对互斥锁rmutex和wmutex的锁定操作。同时,我们也通过使用defer语句保证了对它们的及时解锁。在这里,我们对已读数据块的序列号rsn和已写数据块的序列号wsn的计算方法与前面示例中的方法是相同的。它们都是用相关的偏移量除以数据块长度后得到的商来作为相应的序列号(或者说计数)的值。

至于*myDataFile类型的DataLen方法的实现,我们无需呈现

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

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目