c# - FileStream.Position 具有线程不安全的副作用
问题描述
下面是一些将 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 都会被写入。请注意,我们实际上并没有对句柄做任何事情。只是阅读它会导致不当行为。
解决方案
这里发生的事情是 FileStream ( https://referencesource.microsoft.com/#mscorlib/system/io/filestream.cs,e23a38af5d11ddd3 ) 维护一个布尔标志 _exposedHandle 如果它认为它在内部使用的句柄已被暴露则为真外部。如果 _exposedHandle 为真,那么当您读取 Position 时,它会运行一个私有方法 VerifyOSHandlePosition(),该方法在返回之前将其自己的内部位置值与句柄的位置值同步。由于该同步代码不是线程安全的,因此同时发生的写入和读取可能会搞砸。
现在 FileStream 并不声称是线程安全的。但这是一个弱小的防御措施,因为每个人都希望 FileStream 对于改变状态的读取和写入当然是不安全的,但纯属性读取仍然应该是无副作用的,因此本质上是线程安全的。例如,List 和 Dictionary 不是线程安全的,但是读取它们的 Count 属性不会搞砸另一个线程上发生的读写操作。
我可以猜到为什么 FileStream 的作者添加了这个。它允许您持有一个外部句柄,并使用 FileStream 和句柄进行(同步)读取和写入。但我认为这不是正确的方法。如果你持有的外部资源也被另一个类在内部使用(例如,一个数组也被你交给它的类或方法使用),那么你就不能搞砸了。该类不应该尝试以改变类功能方式的方式进行补偿(并使所有操作也受到性能影响),而是应该创建一个公共 SynchronizeHandlePosition() 方法并告诉想要这种情况的人用它。
由于 FileStream 就是这样,请记住:
- 尽可能避免使用带有暴露句柄的 FileStream。
- 知道具有暴露句柄的 FileStream 的 Position 具有线程不安全的副作用。
- 知道带有暴露句柄的 FileStream 会降低性能。
如果 Microsoft 更新文档来说明这些内容,那就太好了。
推荐阅读
- python - 多参数的 CatBoostClassifier
- git - 拥有多个 git 遥控器有缺点吗
- javascript - 无法通过 JS API 调用访问 json 密钥(未定义)
- r - 如何重新排序在函数中创建的相关矩阵的行和列
- spring - 在SpringBoot中使用rest模板打印数组中的Json数据
- python - 为什么我的 Pearson 系数热图返回 Nan 值?
- python - 如何更改现有数组的形状,同时保持内容居中并用 0 填充新空间?
- r - 导入数据的 R 闪亮基本方差分析产生此错误:model.frame.default 中的错误:变量“input$var3”的类型无效(NULL)
- java - 用 jpql 过滤 redis 数据不起作用
- html - 在开发工具中找不到 CSS 颜色