操作文件系统中的文件提供了底层的支持。但是,该类型的相关方法并没有对并发操作的安全性进行保证。换句话说,这些方法不是并发安全的。我只能通过额外的同步手段来保证这一点。鉴于这里需要分别对两类操作(即写操作和读操作)进行访问控制,所以读写锁在这里会比普通的互斥锁更加适用。不过,关于多个读操作要按顺序且不能重复读取的这个问题,我们需还要使用其他辅助手段来解决。
为了实现上述需求,我们需要创建一个类型。作为该类型的行为定义,我们先编写了一个这样的接口:
// 数据文件的接口类型。
type DataFile interface {
// 读取一个数据块。
Read() (rsn int64, d Data, err error)
// 写入一个数据块。
Write(d Data) (wsn int64, err error)
// 获取最后读取的数据块的序列号。
Rsn() int64
// 获取最后写入的数据块的序列号。
Wsn() int64
// 获取数据块的长度
DataLen() uint32
}
其中,类型Data被声明为一个[]byte的别名类型:
// 数据的类型
type Data []byte
而名称wsn和rsn分别是Writing Serial Number和Reading Serial Number的缩写形式。它们分别代表了最后被写入的数据块的序列号和最后被读取的数据块的序列号。这里所说的序列号相当于一个计数值,它会从1开始。因此,我们可以通过调用Rsn方法和Wsn方法得到当前已被读取和写入的数据块的数量。
根据上面对需求的简单分析和这个DataFile接口类型声明,我们就可以来编写真正的实现了。我们将这个实现类型命名为myDataFile。它的基本结构如下:
// 数据文件的实现类型。
type myDataFile struct {
f *os.File // 文件。
fmutex sync.RWMutex // 被用于文件的读写锁。
woffset int64 // 写操作需要用到的偏移量。
roffset int64 // 读操作需要用到的偏移量。
wmutex sync.Mutex // 写操作需要用到的互斥锁。
rmutex sync.Mutex // 读操作需要用到的互斥锁。
dataLen uint32 // 数据块长度。
}
类型myDataFile共有七个字段。我们已经在前面说明过前两个字段存在的意义。由于对数据文件的写操作和读操作是各自独立的,所以我们需要两个字段来存储两类操作的进行进度。在这里,这个进度由偏移量代表。此后,我们把woffset字段称为写偏移量,而把roffset字段称为读偏移量。注意,我们在进行写操作和读操作的时候会分别增加这两个字段的值。当有多个写操作同时要增加woffset字段的值的时候就会产生竞态条件。因此,我们需要互斥锁wmutex来对其加以保护。类似的,rmutex互斥锁被用来消除多个读操作同时增加roffset字段的值时产生的竞态条件。最后,由上述的需求可知,数据块的长度应该是在初始化myDataFile类型值的时候被给定的。这个长度会被存储在该值的dataLen字段中。它与DataFile接口中声明的DataLen方法是对应的。下面我们就来看看被用来创建和初始化DataFile类型值的函数NewDataFile。
关于这类函数的编写,读者应该已经驾轻就熟了。NewDataFile函数会返回一个DataFile类型值,但是实际上它会创建并初始化一个*myDataFile类型的值并把它作为它的结果值。这样可以通过编译的原因是,后者会是前者的一个实现类型。NewDataFile函数的完整声明如下:
func NewDataFile(path string, dataLen uint32) (DataFile, error) {
f, err := os.Create(path)
if err != nil {
return nil, err
}
if dataLen == 0 {
return nil, errors.New("Invalid data length!")
}
df := &myDataFile{f: f, dataLen: dataLen}
return df, nil
}
可以看到,我们在创建*myDataFile类型值的时候只需要对其中的字段f和dataLen进行初始化。这是因为woffset字段和roffset字段的零值都是0,而在未进行过写操作和读操作的时候它们的值理应如此。对于字段fmutex、wmutex和rmutex来说,它们的零值即为可用的锁。所以我们也不必对它们进行显式的初始化。
把变量df的值作为NewDataFile函数的第一个结果值体现了我们的设计意图。但要想使*myDataFile类型真正成为DataFile类型的一个实现类型,我们还需要为*myDataFile类型编写出已在DataFile接口类型中声明的所有方法。其中最重要的当属Read方法和Write方法。
我们先来编写*myDataFile类型的Read方法。该方法应该按照如下步骤实现。
(1) 获取并更新读偏移量。
(2) 根据读偏移量从文件中读取一块数据。
(3) 把该数据块封装成一个Data类型值并将其作为结果值返回。
其中,前一个步骤在被执行的时候应该由互斥锁rmutex保护起来。因为,我们要求多个读操作不能读取同一个数据块,并且它们应该按顺序的读取文件中的数据块。而第二个步骤,我们也会用读写锁fmutex加以保护。下面是这个Read方法的第一个版本:
func (df *myDataFile) Read() (rsn int64, d Data, err error) {
// 读取并更新读偏移量
var offset int64
df.rmutex.Lock()
offset = df.roffset
df.roffset += int64(df.dataLen)
df.rmutex.Unlock()
//读取一个数据块
rsn = offset / int64(df.dataLen)
df.fmutex.RLock()
defer df.fmutex.RUnlock()
bytes := make([]byte, df.dataLen)
_, err = df.f.ReadAt(bytes, offset)
if err != nil {
return
}
d = bytes
return
}
可以看