如何使多线程复制文件



我想在一个文件中复制多个文件,但使用多线程,假设文件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 ?多线程复制文件真的很有效率吗,我说的是复制文件的速度。感谢您的回复

使用多线程复制文件是完全可能的。您通常会使用单个生产者线程和多个消费者线程来完成这项工作。在您的例子中,您正在连接。因此,您需要计算出每个源文件的起始点和结束点,然后让线程在预先计算的位置上写入目标文件的不同部分。当然可以。

然而,这不是一个好主意。当任务受CPU限制时,多线程可以很好地工作。文件复制是磁盘绑定的,没有任何额外的线程可以帮助。事实上,您最终可能会使性能变得更差,因为多个线程在争夺共享磁盘资源时只会相互妨碍。

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

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

  2. 将目标文件划分为逻辑部分,每个部分在文件中具有开始和结束偏移量,并根据需要将这些部分分配给线程。让每个线程打开自己的本地句柄,指向相同的目标文件。这将允许每个线程独立地查找和写入。

  3. 确保每个线程不离开其分配的部分,这样它就不会破坏其他线程的写入数据。
例如:

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;

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

如果你尝试这样做,多个线程共享相同的文件句柄,你最终会有一个大问题,确保一个线程不移动文件位置使用Seek命令,而另一个线程试图写一些数据。

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

现在,即使你以某种方式设法得到这个工作,以便每个线程都在文件的自己的部分中写入,并且它们不会相互干扰,你仍然会失去一些性能,由于硬盘驱动器的限制(HDD磁头需要重新定位到正确的位置-大量的来回移动)。

嘿,但是你可以使用多个线程去准备内存中的最终文件,然后再将其写入硬盘驱动器。这很容易做到,因为内存访问非常快,您实际上不会因为来回跳转而损失任何性能。唯一的问题是,如果您正在连接几个较大的文件,您可能会很快耗尽内存。

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

最新更新