c# - FileStream.Position 具有线程不安全的副作用

标签 c# .net .net-core

以下代码将 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 ) 维护一个 bool 标志 _exposeHandle ,如果它认为它内部使用的句柄已被外部暴露,则该标志为 true。如果 _exposeHandle 为 true,那么当您读取 Position 时,它会运行一个私有(private)方法VerifyOSHandlePosition(),该方法在返回之前将其自己的内部位置值与句柄的内部位置值同步。由于同步代码不是线程安全的,因此同时发生的写入和读取可能会搞砸。

现在 FileStream 并不声称是线程安全的。但这是一种弱酱防御,因为每个人都期望 FileStream 当然对于状态更改读取和写入来说是不安全的,但纯属性读取仍然应该是无副作用的,因此本质上是线程安全的。例如,List 和 Dictionary 不是线程安全的,但读取它们的 Count 属性不会破坏另一个线程上发生的读写操作。

我可以猜出为什么 FileStream 的作者添加了这个。它允许您持有外部句柄,并使用 FileStream 和句柄进行(同步)读取和写入。但我认为这不是正确的方式。如果您拥有的外部资源也被另一个类在内部使用(例如,一个数组也被您交给它的类或方法使用),那么您就不能搞砸了。该类不应该尝试以改变类功能方式的方式进行补偿(并使所有操作也受到性能影响),而是应该创建一个公共(public) SynchronizeHandlePosition() 方法,并告诉那些想要这种情况的人使用它。

既然 FileStream 就是这样,请记住:

  • 如果可能,请避免使用带有暴露句柄的 FileStream。
  • 要知道,具有公开句柄的 FileStream 的位置具有线程不安全的副作用。
  • 要知道,具有公开句柄的 FileStream 会降低性能。

如果 Microsoft 更新文档来说明这些内容,那就太好了。

关于c# - FileStream.Position 具有线程不安全的副作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56085208/

相关文章:

c# - Wcf 服务仅在传输复杂对象时接收 null

c# - 将项目添加到列表中的递归算法

.net - DataTable 是否实现了 IListSource?

c# - 如何从 C# 中的资源播放 .mp3 文件?

c# - CompareTo 方法逻辑如何在列表排序功能中工作?

amazon-web-services - 如何扩展 SQS FIFO 队列多个监听器

c# - 内部带有 flowlayout 面板且 autosize = true 的 Groupbox 会收缩,就像它是空的一样

c# - 我的 Azure DocumentDB 文档类是否应该继承自 Microsoft.Azure.Documents.Document?

c# - 测试类找不到连接字符串

c# - ILogger 不返回 JSON 格式的消息和对象