使用多个线程访问单个文件

16
我需要同时使用多个线程访问一个文件。出于性能原因,这需要并发完成,不得进行线程序列化。 特别地,该文件已经使用“临时”文件属性创建,以鼓励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缓存文件确实会被写入,但我很高兴全局序列化/交错读和写类型操作,只要我可以执行并发读取即可。

我知道有令人讨厌的潜在线程并发问题,并且我知道在其他情况下有数十种解决方案。我有这个特定的上下文,并且我正在尝试确定是否有一种方法可以允许文件内和进程内的并发线程读取访问。

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

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

5个回答

19

是的,程序可以从不同的线程多次打开同一个文件。但是您需要避免在读写文件时同时进行读取。您可以使用TMultiReadExclusiveWriteSynchronizer来控制对整个文件的访问。它比临界区串行化程度要低一些。如果需要更精细的控制,请查看LockFileEx,以在需要时控制对文件特定区域的访问权限。当写操作时,请求独占锁定;当读操作时,请求共享锁定。

至于您发布的代码,指定 File_Share_Write 作为初始共享标志意味着所有后续打开操作也必须共享该文件进行写入。引用自文档:

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

您的第二个打开请求表示,在该句柄保持打开状态时不允许任何其他人写入该文件。由于已经存在另一个允许写入的句柄,因此无法满足第二个请求。 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(现在看来很明显;-))。回顾起来,我在初始问题中放置的代码是错误的,经过你的解释,我会尝试一下并看看效果如何。 - Raymond Wilson
+1,编写了一些测试项目,并在C中验证了这一点。更新我的答案,附上我测试项目的下载链接。在进程之间也可靠地工作。 - meklarian
1
我在第一个创建文件中添加了FILE_SHARE_DELETE标志,并将其复制到第二个文件,嘿,哎呀,这样就可以工作了! - Raymond Wilson
@RobKennedy,为什么需要避免不同线程同时从同一文件读写?MSDN文档中有我没注意到的内容吗?我正要实现这样的功能,但由于算法的副作用,我可以保证在写入时不会读取正在写入的磁盘记录。您是在警告用户进程(而非Windows)中的竞争条件,即一个线程在写入文件的某个字节时,另一个线程可能正在读取相同的字节吗? - Cosmin Prund
出于同样的原因,@Cosmin,你不应该从多个线程同时读写任何东西。如果您正在写入您正在读取的相同区域,则存在竞争条件。就像我说的那样,如果您想更细粒度地保护对同一区域的读写,则请使用LockFileEx。然而,这种危险只限于您自己的程序;例如,您无需担心破坏文件系统。 - Rob Kennedy
关于多次调用及其文件共享模式,请参见TFileStream.Create-打开读写但允许其他人只读 - AmigoJack

6

更新 #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场景,因为您希望确保使用物理内存。

MSDN上的地址窗口扩展

使用AllocateUserPhysicalPages和VirtualAlloc来保留内存。

Windows上的AllocateUserPhysicalPages函数 @ MSDN
Windows上的VirtualAlloc函数 @ MSDN


初步

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

在Win32中管理内存映射文件@ MSDN

从您的CreateFile语句中可以看出,您希望跨线程或跨进程共享数据,只要有任何会话打开,就需要相同的文件。内存映射文件允许您在所有会话中使用相同的逻辑文件名。另一个好处是,您可以映射视图并安全地锁定映射文件的部分,以便在所有会话中使用。如果您有一个严格的服务器和N个客户端的情况,那么应该很容易实现。如果您有任何客户端都可能成为开放服务器的情况,则可能希望考虑使用其他机制来确保仅有一个客户端首先启动服务文件(例如全局互斥体)。

CreateMutex @ MSDN

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

命名管道(Windows)@ MSDN


我想在同一进程(服务器)的不同线程之间共享数据(或者至少是文件访问)。我考虑过使用内存映射文件,但是由于涉及的数据量以及Win32服务器进程本身使用的内存,这种方法并不实际。 - Raymond Wilson
如果所有线程都在同一个进程中,您可以简单地将文件句柄传递给需要它的所有线程。在同一进程中重用文件句柄没有任何限制。但是,正如Eric H.所提到的,如果您没有对文件的访问进行串行化,则所有赌注都会失效。您可以使用LockFile/UnlockFile手动限制视图,但这也可能不适合您的情况。 - meklarian
致以问候,AWE(和PAE)。它们可以起作用,但我们支持的操作系统(例如:WinXP)不允许我们将其用作选项。是的,我在玩弄系统。我真的没有选择。 :-( - Raymond Wilson
Raymond,AWE和PAE都是在Windows 2000中引入的;Windows XP不应该阻止您使用它们。(也许有什么东西会阻止您使用这些选项,但仅仅是Windows XP不是其中之一。) - Rob Kennedy
嗯...如果我没记错的话,WinXP通过AWE可访问的内存限制相当受限(4GB?)。我曾经看过一些文档详细说明了AWE的限制和限制条件(基本上,除非你使用Windows Server,否则它没有太多用处),但现在我无法找到它 :-( - Raymond Wilson
显示剩余2条评论

2

您可以这样做...

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

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。 也许这不是使用临时和关闭删除标志的有效操作... - Raymond Wilson
我的应用程序运行良好。主进程读取由线程创建的文件。我还在线程中使用FlushFileBuffers(FileHandle)! - GJ.
你尝试过在第一个CreateFile调用中添加其他标志吗? - Raymond Wilson
是的,我现在已经测试了这些属性,并且运行良好!请尽可能编写简短的测试用例,它必须能够正常工作。第二个(读取器)必须具有与我的上一个案例中相同的普通属性。 - GJ.
你是正确的,但似乎关键问题在于第二个文件打开时缺少FILE_SHARE_DELETE标志,正如Rob Kennedy所指出的那样。 - Raymond Wilson

1

我需要使用多个线程并发地访问一个文件。出于性能原因,这需要同时完成,而不需要进行线程串行化。

要么你不需要在不同的线程中使用相同的文件,要么你需要某种形式的序列化。

否则,你只是在为自己日后带来烦恼。


考虑到我只打算在并发上下文中读取文件,我很想知道你认为瓶颈在哪里。目前我的代码将访问文件串行化,这正是我想避免的 :-)如果该文件是磁盘驻留文件,则由于物理磁盘所施加的自然串行化,我可能不会费心。然而,情况并非如此,这就是我正在尝试的原因... - Raymond Wilson
2
如果你不进行序列化,几乎肯定会遇到某些东西被写入一半或读取一半的问题。我可以保证这种情况会在大型演示前一周的深夜发生。你永远无法重现这个错误,直到你处于那个大型演示的中途。说真的,我在这里想帮助你。你需要并发访问(使用某种序列化、无锁或有锁),或者不需要。如果你需要并发访问,但在序列化方面吝啬,以后你会后悔的。 - Eric H.
我感谢您的关注,并且我非常清楚线程并发可能存在的潜在问题(我同意,它们通常发生在深夜)!因此,如果我们限制自己只考虑读取情况,您是否建议操作系统不支持并发读取?我知道尝试使用相同的文件句柄进行并发读取肯定会引起麻烦,这就是为什么我的问题的一部分围绕如何打开多个文件(或者只是克隆文件句柄)以便在被Windows服务时并发读取不会互相干扰。 - Raymond Wilson

0

如果线程A/B同时对同一文件进行读/写操作,并且请求的读取|写入字节数等于或小于CPU(位宽)的数据总线大小,即通常为64位或8字节,则是完全线程安全的。我想这可以称为“在CPU访问上同步”的对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接