下面是一些代码,它通过定期在后台线程上打印写流的位置来监视磁盘的写入进度,同时将10 GB的数据写入磁盘:
string path = "test.out";
long size = 10 * 1000L * 1000L * 1000L;
using (FileStream writer = new FileStream(path, FileMode.Create, FileAccess.Write))
{
// Get a handle (and don't do anything with it)
var handle = writer.SafeFileHandle;
// Start a background position reader
ThreadPool.QueueUserWorkItem(s =>
{
while (true)
{
Console.WriteLine(writer.Position);
Thread.Sleep(10);
}
});
// Write out the bits
byte[] buffer = new byte[4096];
long position = 0;
while (position < size)
{
int count = (int)Math.Min(size - position, buffer.Length);
writer.Write(buffer, 0, count);
position += count;
}
Console.ReadLine();
}
如果运行此代码,则会看到少于10 GB的内容被写入。基本上,随机的一小部分写入操作会被忘记,并且不会进入磁盘。
这个问题很少发生。此代码尝试写入的10 GB中,有99%以上成功写入。如果您较少阅读“位置”,则问题发生的频率会更少。我们发现此问题的原因是,我们有一些代码可以监视机器对机器文件副本的吞吐量(通过在后台线程上每30秒读取一次位置),并且在数十亿个文件中检测到数百个文件损坏的实例。我们每天制作的副本。但是从另一个线程监视流进度的基本方案似乎非常普遍,因此这可能会打击很多人,尽管其发生率非常低。
效果不取决于是否使用旧的线程池API或新的基于任务的API,是否使用Write或WriteAsync或对Dispose / Close的谨慎程度。这确实取决于是否公开了文件句柄:如果注释掉读取SafeFileHandle属性的行,则会写入所有10 GB的内容。注意,我们实际上对手柄没有任何操作;仅仅阅读它就会引起不当行为。
答案 0 :(得分:4)
这里发生的是FileStream(https://referencesource.microsoft.com/#mscorlib/system/io/filestream.cs,e23a38af5d11ddd3)维护一个布尔标志_exposedHandle,如果它认为内部使用的句柄已在外部公开,则为true。如果_exposedHandle为true,则在读取Position时,它将运行一个私有方法VerifyOSHandlePosition(),该方法将自己的内部位置值与该句柄的内部值同步,然后返回它。由于该同步代码不是线程安全的,因此同时进行的读写操作可能会很麻烦。
现在FileStream不再声称是线程安全的。但这是一种弱势防御,因为每个人都希望FileStream对于状态更改的读取和写入当然并不安全,但是纯属性读取仍然应该没有副作用,因此本质上是线程安全的。例如,List和Dictionary不是线程安全的,但是读取其Count属性不会破坏发生在另一个线程上的读写。
我可以猜测为什么FileStream的作者添加了此内容。它允许您持有一个外部句柄,并使用FileStream和该句柄进行(同步)读写。但是我认为这不是正确的方法。如果您拥有另一个类也在内部使用的外部资源(例如,您所交给的类或方法也使用的数组),那么就不要搞砸了。该类不应尝试以改变类功能的方式进行补偿(并使所有操作也受到性能影响),而应制作一个公共的SynchronizeHandlePosition()方法,并告诉想要此方案的人使用它。
因为FileStream是它的本质,所以请记住:
如果Microsoft更新了文档以说明这些内容,那就太好了。