【问题标题】:Multiple FileStreams for reading/writing different sections of same file concurrently in independent threads多个 FileStreams 用于在独立线程中同时读取/写入同一文件的不同部分
【发布时间】:2012-06-23 12:04:51
【问题描述】:

我有一个大磁盘文件(大约 8 GB),其中包含我需要读取、在内存中处理和写回另一个文件的数百万条记录。所有记录的长度都是固定的(比如 100 字节)。

我正在考虑将我的进程并行化以在多个线程(通常为 4-8 个)上运行,每个线程将(专门)分配文件的特定部分以进行处理(例如,1 GB 块)。由于每个线程都会将其读取和写入限制在分配给它的文件部分,因此我的代码不存在竞争风险。

我是否允许初始化多个线程,每个线程都有自己的FileStream,以便在没有锁定的情况下读取/写入同一个文件,而不会有损坏的风险?假设目标文件已经提前展开到完整大小(使用FileStream.SetLength),并且在打开每个FileStream 时都指定了适当的FileShare 标志。

另外,如果多个线程同时访问同一个文件,我是否会冒着因缓冲丢失而导致减速的风险?我担心FileStream class 上的 MSDN 文档中的“检测流位置变化”部分,其中指出:

当FileStream 对象的句柄没有独占保留时,另一个线程可以同时访问文件句柄并更改与文件句柄关联的操作系统文件指针的位置。 […]

如果在调用Read 方法时检测到句柄位置发生意外更改,.NET Framework 将丢弃缓冲区的内容并再次从文件中读取流。这可能会影响性能,具体取决于文件的大小以及可能影响文件流位置的任何其他进程。

这是否适用于我的情况,或者FileStream 实例创建的文件句柄是否不同且独立,即使访问同一个文件?

【问题讨论】:

  • 写入目标文件时是否需要保留记录的顺序?
  • 如果文件大小未知,你会怎么做文件分区?您的情况可能是您(大致)知道文件大小,我问如果文件大小是随机的怎么办?
  • @stt106:我不久前写了这个问题。现在,在大多数情况下,我可能会选择动态调度——例如,我会用占位符填充文件中所有 100MB 块的并发队列,然后让每个线程尽快从队列中选择下一个块完成。如果你的进程是 I/O-bound,你应该瞄准大块来促进顺序磁盘访问。类似于 OpenMP 的“引导式”调度策略的东西可能会在最后提供顺序访问和负载平衡之间的最佳折衷。

标签: c# .net multithreading file-io


【解决方案1】:

这是绝对安全的。

不存在 MSDN 文章中提到的问题的风险,因为它仅适用于您自己更改底层句柄的情况。您根本没有访问句柄。

您会注意到随机磁盘 IO 会破坏性能。您可能希望通过从文件中读取大块(16MB 左右)并使用锁来防止并发读取和写入调用来缓解这种情况。请注意,即使在不同的 FileStream 实例上,您也需要防止并发调用,因为操作系统不会自动处理 IO。在内部,它们被分成小尺寸,以实现公平和可预测的延迟。这会导致随机 IO。

为什么不创建一个读取器线程,将缓冲区推送到 BlockingCollection 中?您可以在多个线程上使用 Parallel.ForEach 处理该集合。

【讨论】:

  • 关于读取大块以最小化随机访问的好建议。但是,我相信操作系统将拆分为的最小 I/O 访问是扇区大小(例如 4096 字节),对吗?我试图避免BlockingCollection,因为我想尽可能减少锁定。
  • 是的,这是最小的尺寸,太可怕了。在我的机器上,我得到了 256KB 的块(用 procmon 和高级输出测量)。这远太小了。在您的磁盘进行单次寻道时,它可能已经读取了大约 1MB!你不想寻找。
  • 不要害怕锁定。每秒锁定 1000 次左右不算什么。通过将大块放入BlockingCollection,您不需要经常锁定。
  • 感谢您的建议;然后我会注意尽量减少随机访问,看看我是否可以切换我的逻辑以使用BlockingCollection。
  • 在读取大文本文件的情况下,例如csv 或 txt; Parallel.ForEach(File.ReadLines(path)) 会胜过用单线程处理File.ReadLines() 的结果吗?因为,正如您所说,硬件仍会限制对同一文件的并行读取,因此处理结果的多个线程甚至可能会减慢速度?
【解决方案2】:

“内存映射文件将文件的内容映射到应用程序的逻辑地址空间。内存映射文件使程序员能够处理非常大的文件,因为内存可以同时管理,并且它们允许对文件无需查找。内存映射文件也可以在多个进程之间共享。

CreateFromFile 方法从指定路径或磁盘上现有文件的 FileStream 创建内存映射文件。取消映射文件时,更改会自动传播到磁盘。

CreateNew 方法创建一个未映射到磁盘上现有文件的内存映射文件;并且适用于为进程间通信(IPC)创建共享内存。

内存映射文件与名称相关联。

您可以创建内存映射文件的多个视图,包括文件部分的视图。您可以将文件的同一部分映射到多个地址以创建并发内存。为了使两个视图保持并发,它们必须从同一个内存映射文件创建。使用两个视图为同一个文件创建两个文件映射不提供并发性。”

http://msdn.microsoft.com/en-us/library/system.io.memorymappedfiles.memorymappedfile.aspx

【讨论】:

  • +1:感谢您的建议;我没有考虑过MemoryMappedFile。但是,我认为我无法将它用于这个特定问题,因为文件的大小超过了可用内存的大小。
  • 在任何情况下,映射文件都会在您的特定情况下导致随机 IO,将您减少到每秒 2MB 和磁盘左右。而不是 120MB/秒。
  • @douglas 抱歉,您低估了内存映射文件技术,我已经看到实际上 >50 GB 的地理位置文件图表在 1-5 秒内发生。当使用常规文件 api 时,这是滞后的。在此之上,您可以通过拥有单个 mm 实例和多个视图以及您希望的处理来进行线程化。 ;)
  • @HarshBaid:感谢您的提醒。我没用过MemoryMappedFile,所以我也说不清楚,但我会记住的!
猜你喜欢
  • 1970-01-01
  • 2013-03-06
  • 1970-01-01
  • 1970-01-01
  • 2018-11-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多