【问题标题】:Accessing a single file with multiple threads使用多个线程访问单个文件
【发布时间】:2009-10-27 17:39:41
【问题描述】:

我需要使用多个线程同时访问一个文件。这需要同时完成,出于性能原因没有线程序列化。

该文件特别是使用“临时”文件属性创建的,该属性鼓励 Windows 将文件保存在系统缓存中。这意味着大多数时候读取的文件不会靠近磁盘,而是会从系统缓存中读取文件的一部分。

能够同时访问此文件将显着提高我的代码中某些算法的性能。

所以,这里有两个问题:

  1. windows 是否可以从不同的线程同时访问同一个文件?
  2. 如果是这样,您如何提供这种能力?我尝试创建临时文件并再次打开该文件以提供两个文件句柄,但第二次打开没有成功。

这里是创建:

FFileSystem := CreateFile(PChar(FFileName),
                          GENERIC_READ + GENERIC_WRITE,
                          FILE_SHARE_READ + FILE_SHARE_WRITE,
                          nil,
                          CREATE_ALWAYS,
                          FILE_ATTRIBUTE_NORMAL OR
                          FILE_FLAG_RANDOM_ACCESS OR
                          FILE_ATTRIBUTE_TEMPORARY OR
                          FILE_FLAG_DELETE_ON_CLOSE,
                          0);

这是第二次打开:

FFileSystem2 := CreateFile(PChar(FFileName),
                          GENERIC_READ,
                          FILE_SHARE_READ,
                          nil,
                          OPEN_EXISTING,
                          FILE_ATTRIBUTE_NORMAL OR
                          FILE_FLAG_RANDOM_ACCESS OR
                          FILE_ATTRIBUTE_TEMPORARY OR
                          FILE_FLAG_DELETE_ON_CLOSE,
                          0);

到目前为止,我已经尝试了各种标志组合,但没有成功。第二个文件打开总是失败,并提示无法访问该文件,因为它正被另一个进程使用。

编辑:

好的,更多信息(我希望不要在这里迷路......)

有问题的进程是在 WinXP 64 上运行的 Win32 服务器进程。它正在维护大型空间数据库,并希望将尽可能多的空间数据库保留在内存中的 L1/L2 缓存结构中。 L1 已经存在。 L2 作为“临时”文件存在,它保留在 Windows 系统缓存中(这有点肮脏,但在某种程度上绕过了 win32 内存限制)。 Win64 意味着我可以让系统缓存使用大量内存,因此用于保存 L2 缓存的内存确实计入进程内存。

多个(可能很多)线程想要同时访问 L2 缓存中包含的信息。目前,访问是序列化的,这意味着一个线程可以读取它的数据,而大多数(或其余)线程被阻塞等待该操作的完成。

L2 缓存文件确实被写入,但我很乐意全局序列化/交错读取和写入类型操作,只要我可以执行并发读取。

我知道存在令人讨厌的潜在线程并发问题,并且我知道在其他情况下有几十种方法可以给这只猫剥皮。我有这个特定的上下文,我正在尝试确定是否有办法允许在文件内和同一进程内进行并发线程读取访问。

我考虑过的另一种方法是将二级缓存分成多个临时文件,其中每个文件以当前单个二级缓存文件的方式序列化线程访问。

是的,这种有点绝望的方法是因为 64 位 Delphi 不会很快出现在我们身边 :-(

谢谢, 雷蒙德。

【问题讨论】:

    标签: windows multithreading delphi file-io


    【解决方案1】:

    是的,一个程序可以从不同的线程多次打开同一个文件。不过,您需要避免在写入文件的同时读取文件。您可以使用TMultiReadExclusiveWriteSynchronizer 来控制对整个文件的访问。它的序列化程度低于临界区。如需更精细的控制,请查看LockFileEx 以根据需要控制对文件特定区域的访问。写入时,请求排他锁;读取时,共享锁。

    对于你发布的代码,在初始共享标志中指定File_Share_Write意味着所有后续打开操作也必须共享文件以进行写入。引用the documentation:

    如果未指定此标志,但文件或设备已打开以进行写访问或具有可写访问的文件映射,则函数失败。

    您的第二个打开请求是说它不希望在该句柄保持打开状态时允许其他任何人写入文件。由于已经打开了另一个确实允许写入的句柄,因此无法完成第二个请求。 GetLastError 应该返回 32,即 Error_Sharing_Violation,这正是文档所说的应该发生的情况。

    指定File_Flag_Delete_On_Close意味着所有后续打开请求都需要共享文件以进行删除。再次文档:

    除非指定了FILE_SHARE_DELETE 共享模式,否则对文件的后续打开请求将失败。

    然后,由于第二个打开请求共享文件以进行删除,所有其他打开的句柄也必须共享该文件以进行删除。文档:

    如果文件存在现有的打开句柄,则调用将失败,除非它们都以FILE_SHARE_DELETE 共享模式打开。

    底线是每个人都分享相同的内容,或者根本没有人分享。

    FFileSystem := CreateFile(PChar(FFileName),
      Generic_Read or Generic_Write
      File_Share_Read or File_Share_Write or File_Share_Delete,
      nil,
      Create_Always,
      File_Attribute_Normal or File_Flag_Random_Access
        or File_Attribute_Temporary or File_Flag_Delete_On_Close,
      0);
    
    FFileSystem2 := CreateFile(PChar(FFileName),
      Generic_Read,
      File_Share_Read or File_Share_Write or File_Share_Delete,
      nil,
      Open_Existing,
      File_Attribute_Normal or File_Flag_Random_Access
        or File_Attribute_Temporary or File_Flag_Delete_On_Close,
      0);
    

    也就是说,除了第五个参数之外,所有的参数都是一样的。

    这些规则适用于在相同线程上打开的两次尝试以及来自不同线程的尝试。

    【讨论】:

    • 有趣的是 - 我的第一个版本的代码非常相似,除了我没有指定 File_Share_Delete(现在看起来很明显 ;-) )。事后看来,我在最初的问题中放置的代码是完全错误的。我会试一试,看看效果如何。
    • +1,编写了一些测试项目并在 C 中验证了这一点。更新我的答案并提供我的测试项目的下载链接。也可以跨流程可靠地工作。
    • 我在第一个创建文件中添加了 FILE_SHARE_DELETE 标志并为第二个文件复制了,嘿,这样就可以了!
    • @RobKennedy,为什么需要避免从不同线程同时读取和写入同一文件?我在 MSDN 文档中遗漏了什么吗?我即将实现类似的东西,但作为我算法的副作用,我可以保证我正在写入的磁盘记录在写入时不会被读取。您是否针对在文件中写入某个字节的线程之间的内部(即:用户进程,而不是 Windows)竞争条件发出警告,而另一个线程可能会读取相同的字节?
    • 出于同样的原因,您不应该同时从多个线程读取和写入 anything,@Cosmin。如果你正在写你正在阅读的同一个区域,那么你就有一个竞争条件。就像我说的,如果您想要更细粒度的保护以防止读取和写入同一区域,请使用LockFileEx。但是,危险仅限于您自己的程序;例如,您不必担心会损坏文件系统。
    【解决方案2】:

    更新 #2

    我用 C 语言编写了一些测试项目来尝试解决这个问题 - 尽管 Rob Kennedy 在我离开时击败了我。正如他所概述的,这两种情况都是可能的,包括跨进程。如果其他人想看到这个,这里是一个链接。

    SharedFileTests.zip (VS2005 C++ Solution) @ meklarian.com

    共有三个项目:

    InProcessThreadShareTest - 测试创建者和客户端线程。
    InProcessThreadShareTest.cpp Snippet @ gist.github

    SharedFileHost - 创建一个运行 1 分钟并更新文件的主机。
    SharedFileClient - 创建一个运行 30 秒并轮询文件的客户端。
    SharedFileHost.cpp and SharedFileClient.cpp Snippet @ gist.github

    所有这些项目都假定位置 C:\data\tmp\sharetest.txt 是可创建和可写的。


    更新

    鉴于您的情况,听起来您需要大量内存。您可以使用 AWE 访问超过 4Gb 的内存,而不是玩系统缓存,尽管您需要一次映射部分内存。这应该涵盖您希望确保使用物理内存的 L2 场景。

    Address Windowing Extensions @ MSDN

    使用 AllocateUserPhysicalPages 和 VirtualAlloc 来预留内存。

    AllocateUserPhysicalPages Function (Windows) @ MSDN
    VirtualAlloc Function (Windows) @ MSDN


    初始

    鉴于您正在使用标志 FILE_FLAG_DELETE_ON_CLOSE,您是否有任何理由不考虑使用内存映射文件?

    Managing Memory-Mapped files in Win32 @ MSDN

    从我在您的 CreateFile 语句中看到的内容来看,您似乎希望跨线程或跨进程共享数据,仅涉及在任何会话打开时存在相同的文件。内存映射文件允许您在所有会话中使用相同的逻辑文件名。另一个好处是您可以在所有会话中安全地映射视图并锁定映射文件的部分。如果你有一个严格的服务器和 N 客户端场景,它应该很容易实现。如果您遇到任何客户端可能是打开服务器的情况,您可能希望考虑使用其他机制来确保只有一个客户端首先启动服务文件(也许通过全局互斥锁)。

    CreateMutex @ MSDN

    如果您只需要单向传输数据,也许您可​​以改用命名管道。
    (编辑)这最适合 1 个服务器到 1 个客户端。

    Named Pipes (Windows) @ MSDN

    【讨论】:

    • 我想在同一进程(服务器)中的线程之间共享数据(好吧,无论如何都要访问文件)。我考虑过使用内存映射文件,但是 Win32 服务器进程本身使用的内存所涉及的数据量并不实用。
    • 如果所有线程都在同一个进程中,您可以简单地将文件句柄传递给所有需要它的线程。在同一进程中重用文件句柄没有任何限制。但是,正如 Eric H. 所提到的,如果您不序列化对文件的访问,那么所有的赌注都将失败。您可以使用 LockFile/UnlockFile 手动限制视图,但这也可能不适合您的情况。
    • 当前线程无论如何都使用相同的文件句柄,它们只是在使用它时被序列化。目前没有任何文件读取会重叠,尽管我不会认为这会是一个问题。
    • 问候 AWE(和 PAE)。这些都可以,但是,我们支持的操作系统(例如:WinXP)不允许我们将其用作选项。是的,我在玩系统。我真的别无选择。 :-(
    • Raymond、AWE 和 PAE 都是在 Windows 2000 中引入的; Windows XP 不应阻止您使用它们。 (也许有阻止您选择这些选项的东西,但仅仅是 Windows XP 不是。)
    【解决方案3】:

    你可以这样做......

    具有读/写访问权限的第一个线程必须首先创建文件:

    FileHandle := CreateFile(
      PChar(FileName),
      GENERIC_READ or GENERIC_WRITE,
      FILE_SHARE_READ,
      nil,
      CREATE_ALWAYS,
      FILE_ATTRIBUTE_NORMAL,
      0);
    

    只有读取权限的第二个线程然后打开同一个文件:

      FileHandle := CreateFile(
        PCHar(FileName),
        GENERIC_READ,
        FILE_SHARE_READ + FILE_SHARE_WRITE,
        nil,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        0);
    

    我没有测试是否适用于...

    FILE_ATTRIBUTE_TEMPORARY,
    FILE_FLAG_DELETE_ON_CLOSE
    

    属性...

    【讨论】:

    • 我尝试了您的建议,但我仍然收到“进程无法访问该文件,因为它正被另一个进程使用”的 SysErrorMessage。也许这对于关闭标志的临时和删除不是一个有效的操作......
    • 在我的应用程序中运行良好。主进程读取由线程创建的文件。我也在线程中使用 FlushFileBuffers(FileHandle)!
    • 您是否尝试将其他标志添加到第一个 CreateFile 调用中?
    • 是的,我现在也用这个属性进行了测试,效果很好!做尽可能短的测试用例它必须工作。第二个(读者)必须像我的大写一样具有普通属性。
    • 您是对的,但关键问题似乎是 Rob Kennedy 指出的第二个打开的文件中缺少 FILE_SHARE_DELETE 标志。
    【解决方案4】:

    我需要使用多个线程同时访问一个文件。这需要同时完成,出于性能原因,没有线程序列化。

    要么你不需要在不同的线程中使用同一个文件,要么你确实需要某种序列化。

    否则,你只会让自己心痛。

    【讨论】:

    • 鉴于我只打算在并发上下文中读取文件,我对您认为心痛是感兴趣的。目前我的代码确实对文件的访问进行了序列化,这就是我想要做的事情:-) 如果文件是磁盘驻留文件,那么由于物理强加的自然序列化,我倾向于不打扰磁盘。但是,事实并非如此,这就是我尝试的原因...
    • 如果你不序列化它,你几乎肯定会遇到半写半读的问题。我可以保证这会发生在深夜,在大型演示前一周。除非您处于那个大演示的中间,否则您将永远无法重现该错误。说真的,我想在这里帮你。要么您需要并发访问(使用某种序列化,无论是否无锁),或者您不需要。如果你需要并发访问,但在序列化上吝啬,你以后会后悔的。
    • 感谢您的关注,我非常清楚线程并发的潜在缺陷(我同意,它们通常发生在深夜)!那么,如果我们将自己限制在阅读案例中,您是否建议操作系统不支持并发读取?我知道尝试使用相同的文件句柄进行并发读取肯定是在自找麻烦,这就是为什么我的 Q 部分围绕如何多次打开文件(或者可能只是克隆文件句柄)所以并发被windows服务时,reads不会踩到对方的脚趾。
    【解决方案5】:

    线程 A/B 分别对同一文件同时写入/读取,当且仅当请求读取|写入的字节数等于或小于数据大小时,才是完全线程安全的CPU的总线(位宽);这些天通常是 64 位,或 8 个字节。我想这可以称为“同步 CPU 访问”对象。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-12-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多