(C#) 使文件 read/write 线程安全(并考虑性能)
(C#) to make file read/write thread-safe (and considering performance)
这是我的示例代码,使 FileUtil class 成为一个 thead 安全的文件 IO 处理程序。
public static class FileUtil {
private static ConcurrentDictionary<string, ReaderWriterLock> s_locks = new ConcurrentDictionary<string, ReaderWriterLock>();
public static string ReadFile(string path) {
var rwLock = s_locks.GetOrAdd(path, new ReaderWriterLock());
rwLock.AcquireReaderLock(1000);
string data = File.ReadAllText(path);
rwLock.ReleaseReaderLock();
return data;
}
public static void WriteFile(string path, string data) {
var rwLock = s_locks.GetOrAdd(path, new ReaderWriterLock());
rwLock.AcquireWriterLock(1000);
using (StreamWriter sw = new StreamWriter(path, true)) {
sw.Write(data);
}
rwLock.ReleaseWriterLock();
}
}
如你所见,我创建了一个并发字典,为不同的文件持有不同的锁,以避免所有文件 IO 使用一个锁。我的实现是否正确?
不,原因不多。
首先不要忘记文件系统为您提供了您需要的并发性,然后您就不需要专门实现任何东西。请注意,线程安全并不意味着不能并发访问资源,而是它的使用不会导致失败(在非常广泛的意义上)。
依赖OS并发的一种可能实现是:
public static string ReadFile(string path) {
for (int retry=0; retry < 3; ++retry) {
try {
return File.ReadAllText(path);
}
catch (IOException e) {
// 0x80070020 is value for ERROR_SHARING_VIOLATION
if (Marshal.GetHRForException(e) == 0x80070020) {
Thread.Sleep(1000); // Wait and try again
continue;
}
throw;
}
}
}
WriteFile()
的代码很简单。
请注意,通过这种方式,您还将处理不同进程之间的共享冲突,并且您将遵守打开文件时声明的共享规则;例如 File.WriteAllText()
指定 FileShare.Read
允许并发读取但不允许并发写入,请小心,因为读者可能会读取到不是最新的内容,如果这不是您想要的,那么您应该删除 File.WriteAllText() 在 FileStream
构造函数中指定 FileShare.None
。
关于一般用法的一些其他注意事项。
您正在使用具有立即值的 ConcurrentDictionary<TKey, TValue>.GetOrAdd()
。这意味着,即使字典已经包含给定路径的 ReaderWriterLock
,您每次都会构造一个新对象。在初始化期间未获取锁,但它仍然是一项昂贵的操作,那么您应该(在其他代码中!)使用其他重载:
var rwLock = s_locks.GetOrAdd(path, () => new ReaderWriterLock());
这样 ReadWriterLock
只有在需要时才会创建。
第二点是ReadWriterLock
本身,你可能想用ReadWriterLockSlim
代替,它是一个新的轻量级资源效率版本。还要考虑资源获取超时怎么办,抛出异常?重试?
最后一点是关于用法的:想想你为 1000 个文件使用该代码后,你的字典将包含 1000 个(可能未使用的)对象。在另一种情况下,您可以考虑在每次使用后处理它们。
这是我的示例代码,使 FileUtil class 成为一个 thead 安全的文件 IO 处理程序。
public static class FileUtil {
private static ConcurrentDictionary<string, ReaderWriterLock> s_locks = new ConcurrentDictionary<string, ReaderWriterLock>();
public static string ReadFile(string path) {
var rwLock = s_locks.GetOrAdd(path, new ReaderWriterLock());
rwLock.AcquireReaderLock(1000);
string data = File.ReadAllText(path);
rwLock.ReleaseReaderLock();
return data;
}
public static void WriteFile(string path, string data) {
var rwLock = s_locks.GetOrAdd(path, new ReaderWriterLock());
rwLock.AcquireWriterLock(1000);
using (StreamWriter sw = new StreamWriter(path, true)) {
sw.Write(data);
}
rwLock.ReleaseWriterLock();
}
}
如你所见,我创建了一个并发字典,为不同的文件持有不同的锁,以避免所有文件 IO 使用一个锁。我的实现是否正确?
不,原因不多。
首先不要忘记文件系统为您提供了您需要的并发性,然后您就不需要专门实现任何东西。请注意,线程安全并不意味着不能并发访问资源,而是它的使用不会导致失败(在非常广泛的意义上)。
依赖OS并发的一种可能实现是:
public static string ReadFile(string path) {
for (int retry=0; retry < 3; ++retry) {
try {
return File.ReadAllText(path);
}
catch (IOException e) {
// 0x80070020 is value for ERROR_SHARING_VIOLATION
if (Marshal.GetHRForException(e) == 0x80070020) {
Thread.Sleep(1000); // Wait and try again
continue;
}
throw;
}
}
}
WriteFile()
的代码很简单。
请注意,通过这种方式,您还将处理不同进程之间的共享冲突,并且您将遵守打开文件时声明的共享规则;例如 File.WriteAllText()
指定 FileShare.Read
允许并发读取但不允许并发写入,请小心,因为读者可能会读取到不是最新的内容,如果这不是您想要的,那么您应该删除 File.WriteAllText() 在 FileStream
构造函数中指定 FileShare.None
。
关于一般用法的一些其他注意事项。
您正在使用具有立即值的 ConcurrentDictionary<TKey, TValue>.GetOrAdd()
。这意味着,即使字典已经包含给定路径的 ReaderWriterLock
,您每次都会构造一个新对象。在初始化期间未获取锁,但它仍然是一项昂贵的操作,那么您应该(在其他代码中!)使用其他重载:
var rwLock = s_locks.GetOrAdd(path, () => new ReaderWriterLock());
这样 ReadWriterLock
只有在需要时才会创建。
第二点是ReadWriterLock
本身,你可能想用ReadWriterLockSlim
代替,它是一个新的轻量级资源效率版本。还要考虑资源获取超时怎么办,抛出异常?重试?
最后一点是关于用法的:想想你为 1000 个文件使用该代码后,你的字典将包含 1000 个(可能未使用的)对象。在另一种情况下,您可以考虑在每次使用后处理它们。