【问题标题】:how to make a multithread copy files如何制作多线程复制文件
【发布时间】:2014-06-06 08:18:14
【问题描述】:

我想在一个文件中复制多个文件,但是使用多线程,假设文件 A 是不同线程复制数据的文件,在这种情况下,每个线程意味着复制文件 A 中的一个文件,使用以下过程:

procedure ConcatenateFiles(const InFileNames: array of string;
const OutFileName: string);
var
i: Integer;
InStream, OutStream: TFileStream;
begin
OutStream := TFileStream.Create(OutFileName, fmCreate);
try
 for i := 0 to high(InFileNames) do
 begin
  InStream := TFileStream.Create(InFileNames[i], fmOpenRead);
  try
    OutStream.CopyFrom(InStream, InStream.Size);
  finally
    InStream.Free;
  end;
 end;
finally
 OutStream.Free;
end;

结束;

首先,在这种情况下是否可以实现多线程复制文件,因为OutFileName是一个全局变量,两个线程不能同时使用它,这是我得到的错误, 如果这是可能的,我如何同步线程以避免同时多个进程使用 OutFileName? 而且制作多线程复制文件真的有效率吗,我说的是复制文件的速度。 感谢您的回复

【问题讨论】:

  • 如果你要连接一大堆文件——从多个文件创建一个文件——那么你不想用多个线程来做这件事,因为数据会交错。如果您正在谈论同时从多个线程调用 ConcatenateFiles 函数,那应该是可能的。但是请注意,这样做可能会减慢您的应用程序的速度。如果所有磁盘访问都在一个驱动器上,那么多线程复制操作没有任何好处。
  • @JimMischel 我想您可以通过预先分配文件的大小然后写入文件的特定部分来并行进行一个连接。但它不会提高性能。
  • 如果您还没有,您可能会得到一份 RichCopy 实用程序的副本。它使您能够调整用于各种事情(例如文件读取和写入)的线程数,并且对于哪些有帮助,哪些没有帮助非常有指导意义。特别是更多的线程更快的性能(必然) - 你仍然坚持物理定律。

标签: multithreading file delphi synchronization copy


【解决方案1】:

使用多线程复制文件是完全可能的。您通常会使用单个生产者线程和多个消费者来完成工作。在您的情况下,您正在连接。因此,您需要计算出每个源文件的起点和终点,然后让线程在预先计算的位置写入目标文件的单独部分。当然可以。

但是,这不是一个好主意。当作业受 CPU 限制时,多线程运行良好。文件复制是磁盘绑定的,没有多少额外的线程可以提供帮助。实际上,您最终可能会使性能变得更糟,因为多个线程会在争夺共享磁盘资源的同时相互干扰。

【讨论】:

  • 好的,那我继续用一个线程复制文件。谢谢
  • 这里有一条评论。当源和目标位于不同的驱动器上时,线程确实有意义。所以你可以让一个线程从一个驱动器读取,另一个线程写入另一个驱动器。
  • @Gray 他们也需要在不同的公共汽车上。
  • 是的,我忘了公共汽车了。
  • 实际上,即使您在同一总线上有两个驱动器,线程也是有益的。如何?通常,当您复制文件时,过程如下:首先,您将原始文件中的数据块读取到缓冲区中。然后将该缓冲区的内容写入目标文件。并且因为您在间歇性地进行评分和写作,所以您正在失去性能。但是通过使用两个不同的线程,您还可以使用两个缓冲区,这样当您的一个线程将其缓冲区的内容写入目标文件时,另一个线程可以从原始文件中读取数据。稍后您只需切换角色即可。
【解决方案2】:

如果您想将多个输入文件并行连接到一个目标文件中,您可以这样做:

  1. 预分配目标文件。创建文件,寻找预期的最终连接文件大小,并设置 EOF 以在文件系统上分配文件。对于TFileStream,这可以通过简单地将TFileStream.Size 属性设置为预期大小来完成。否则,直接使用 Win32 API,您将不得不使用 CreateFile()SetFilePointer()SetEndOfFile()

  2. 将目标文件分成逻辑部分,每个部分在文件中都有一个起始和结束偏移量,并根据需要将这些部分分配给您的线程。让每个线程打开它自己的本地句柄到同一个目标文件。这将允许每个线程独立地寻找和写入。确保每个线程都不会离开其分配的部分,以免损坏另一个线程的写入数据。

例如:

type
  TFileInfo = record
    InFileName: String;
    OutFileName: String;
    OutFileStart: Int64;
    OutFileSize: Int64;
  end;

  TCopyThread = class(TThread)
  protected
   FFileInfo: TFileInfo;
   procedure Execute;
  public
    constructor Create(const AFileInfo: TFileInfo);
  end;

constructor TCopyThread.Create(const AFileInfo: TFileInfo);
begin
  inherited Create(False);
  FFileInfo := AFileInfo;
 end;

procedure TCopyThread.Execute;
var
  InStream: TFileStream;
  OutStream: TFileStream;
begin
  InStream := TFileStream.Create(FFileInfo.InFileName, fmOpenRead or fmShareDenyWrite);
  try
    OutStream := TFileStream.Create(FFileInfo.OutFileName, fmOpenWrite or fmShareDenyNone);
    try
      OutStream.Position := FFileInfo.OutFileStart;
      OutStream.CopyFrom(InStream, FFileInfo.OutFileSize);
    finally
      OutStream.Free;
    end;
  finally
    InStream.Free;
  end;
end;

procedure ConcatenateFiles(const InFileNames: array of string; const OutFileName: string);
var
  i: Integer;
  OutStream: TFileStream;
  FileInfo: array of TFileInfo;
  TotalSize: Int64;
  sr: TSearchRec;
  Threads: array of TCopyThread;
  ThreadHandles: array of THandle;
  NumThreads: Integer;      
begin
  SetLength(FileInfo, Length(InFileNames));
  NumThreads := 0;
  TotalSize := 0;

  for i := 0 to High(InFileNames) do
  begin
    if FindFirst(InFileNames[i], faAnyFile, sr) <> 0 then
      raise Exception.CreateFmt('Cannot retrieve size of file: %s', [InFileNames[i]]);

    if sr.Size > 0 then
    begin
      FileInfo[NumThreads].InFileName := InFileNames[i];
      FileInfo[NumThreads].OutFileName := OutFileName;
      FileInfo[NumThreads].OutFileStart := TotalSize;
      FileInfo[NumThreads].OutFileSize := sr.Size;
      Inc(NumThreads);
      Inc(TotalSize, sr.Size);
    end;

    FindClose(sr); 
  end;

  OutStream := TFileStream.Create(OutFileName, fmCreate);
  try
    OutStream.Size := TotalSize;
  finally
    OutStream.Free;
  end;

  SetLength(Threads, NumThreads);
  SetLength(ThreadHandles, NumThreads);

  for i := 0 to NumThreads-1 do
  begin
    Threads[i] := TCopyThread.Create(FileInfo[i]);
    ThreadHandles[i] := Threads[i].Handle;
  end;

  i := 0;
  while i < NumThreads do
  begin
    WaitForMultipleObjects(Min(NumThreads-i, MAXIMUM_WAIT_OBJECTS), ThreadHandles[i], TRUE, INFINITE);
    Inc(i, MAXIMUM_WAIT_OBJECTS);
  end;

  for i := 0 to NumThreads-1 do
  begin
    Threads[i].Terminate;
    Threads[i].WaitFor;
    Threads[i].Free;
  end;
end;

【讨论】:

  • 我很惊讶你写了代码。为什么?你知道性能会很糟糕。我想实际运行它会证明这一点。 Free on terminate 肯定会使那些句柄无效。您需要在启动线程之前欺骗他们。
  • 我没有考虑性能,只是证明这是可能的。可以调整性能,例如使用文件的内存映射视图,甚至用重叠 I/O 替换线程。
【解决方案3】:

正如前面提到的,从多个线程写入同一个文件并不是一个好主意。

如果您尝试以多个线程共享同一个文件句柄的方式进行操作,您最终会遇到一个大问题,即确保一个线程不会使用 Seek 命令移动文件位置,而另一个线程正在尝试写入一些数据。

如果您尝试以每个线程创建自己的文件句柄的方式执行此操作,那么您最终会遇到问题,即操作系统通常不允许同时拥有多个具有写入能力的文件句柄,因为这可能会导致灾难(数据损坏)。

现在,即使您以某种方式设法使其正常工作,以便每个踏板都写入文件的自己的部分并且它们不会相互混淆,您仍然会由于硬盘驱动器限制而失去一些性能(HDD 磁头需要重新定位到正确的位置 - 很多来回运动)。

嘿,但是在将最终文件写入硬盘驱动器之前,您可以使用多线程来准备内存中的最终文件。这可以很容易地完成,因为内存访问速度非常快,您几乎不会因为来回跳跃而失去任何性能。唯一的问题是,如果您要连接多个较大的文件,您可能会很快耗尽内存。

编辑:顺便说一句,如果您有兴趣,我可以分享我几年前制作的两个线程双缓冲文件复制示例的代码示例。请注意,它不提供任何数据验证功能,因为它只是为了测试理论而编写的,或者我应该说打破一个理论,即不能仅使用 Delphi 复制文件(不使用 Windows 的文件复制 API)。在同一个 HDD 上进行文件复制时,它比内置 Windows 例程要慢一些,但是当从一个 HDD 复制到另一个 HDD 时,它达到与 Windows 内置例程相同的速度。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-03
    • 2011-12-03
    • 2019-06-15
    • 2010-10-06
    • 1970-01-01
    • 1970-01-01
    • 2012-04-19
    相关资源
    最近更新 更多