Копирование файлов, которые основной поток добавляет в список строк, используя поток

У меня есть программа для создания веб-сайтов, которая при создании сайта создает сотни файлов.

Когда корневая папка Интернета находится на локальном компьютере, программа работает нормально. Если корневая папка в Интернете расположена на сетевом диске, копирование созданной страницы занимает больше времени, чем создание самой страницы (создание страницы достаточно оптимизировано).

Я думал создать файлы локально, добавить имена созданных файлов в TStringList и позволить другому потоку скопировать их на сетевой диск (удалив скопированный файл из TStringList).

Хауэрвер, я никогда раньше не использовал потоки, и я не мог найти существующий ответ в других вопросах Delphi, связанных с потоками (если бы мы только могли использовать оператор and в поле поиска), поэтому я Теперь я спрашиваю, есть ли у кого-нибудь рабочий пример, который делает это (или может указать мне какую-то статью с рабочим кодом Delphi)?

Я использую Delphi 7.

EDITED: мой образец проекта (спасибо исходному коду от mghie - кого настоящим еще раз благодарим).

  ...
  fct : TFileCopyThread;
  ...

  procedure TfrmMain.FormCreate(Sender: TObject);
  begin
     if not DirectoryExists(DEST_FOLDER)
     then
        MkDir(DEST_FOLDER);
     fct := TFileCopyThread.Create(Handle, DEST_FOLDER);
  end;


  procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
  begin
     FreeAndNil(fct);
  end;

  procedure TfrmMain.btnOpenClick(Sender: TObject);
  var sDir : string;
      Fldr : TedlFolderRtns;
      i : integer;
  begin
     if PickFolder(sDir,'')
     then begin
        // one of my components, returning a filelist [non threaded  :) ] 
        Fldr := TedlFolderRtns.Create();
        Fldr.FileList(sDir,'*.*',True);
        for i := 0 to Fldr.TotalFileCnt -1 do
        begin
           fct.AddFile( fldr.ResultList[i]);
        end;
     end;
  end;

  procedure TfrmMain.wmFileBeingCopied(var Msg: Tmessage);
  var s : string;
  begin
     s := fct.FileBeingCopied;
     if s <> ''
     then
        lbxFiles.Items.Add(fct.FileBeingCopied);
     lblFileCount.Caption := IntToStr( fct.FileCount );
  end;

и блок

  unit eFileCopyThread;
  interface
  uses
     SysUtils, Classes, SyncObjs, Windows, Messages;
  const
    umFileBeingCopied = WM_USER + 1;
  type

    TFileCopyThread = class(TThread)
    private
      fCS: TCriticalSection;
      fDestDir: string;
      fSrcFiles: TStrings;
      fFilesEvent: TEvent;
      fShutdownEvent: TEvent;
      fFileBeingCopied: string;
      fMainWindowHandle: HWND;
      fFileCount: Integer;
      function GetFileBeingCopied: string;
    protected
      procedure Execute; override;
    public
      constructor Create(const MainWindowHandle:HWND; const ADestDir: string);
      destructor Destroy; override;

      procedure AddFile(const ASrcFileName: string);
      function IsCopyingFiles: boolean;
      property FileBeingCopied: string read GetFileBeingCopied;
      property FileCount: Integer read fFileCount;
    end;

  implementation
  constructor TFileCopyThread.Create(const MainWindowHandle:HWND;const ADestDir: string);
  begin
    inherited Create(True);
    fMainWindowHandle := MainWindowHandle;
    fCS := TCriticalSection.Create;
    fDestDir := IncludeTrailingBackslash(ADestDir);
    fSrcFiles := TStringList.Create; 
    fFilesEvent := TEvent.Create(nil, True, False, ''); 
    fShutdownEvent := TEvent.Create(nil, True, False, ''); 
    Resume; 
  end; 

  destructor TFileCopyThread.Destroy; 
  begin 
    if fShutdownEvent <> nil then 
      fShutdownEvent.SetEvent; 
    Terminate;
    WaitFor;
    FreeAndNil(fFilesEvent);
    FreeAndNil(fShutdownEvent);
    FreeAndNil(fSrcFiles);
    FreeAndNil(fCS);
    inherited;
  end;

  procedure TFileCopyThread.AddFile(const ASrcFileName: string);
  begin
    if ASrcFileName <> ''
    then begin
      fCS.Acquire;
      try
        fSrcFiles.Add(ASrcFileName);
        fFileCount := fSrcFiles.Count;
        fFilesEvent.SetEvent;
      finally
        fCS.Release;
      end;
    end;
  end;

  procedure TFileCopyThread.Execute;
  var
    Handles: array[0..1] of THandle;
    Res: Cardinal;
    SrcFileName, DestFileName: string;
  begin
    Handles[0] := fFilesEvent.Handle;
    Handles[1] := fShutdownEvent.Handle;
    while not Terminated do
    begin
      Res := WaitForMultipleObjects(2, @Handles[0], False, INFINITE);
      if Res = WAIT_OBJECT_0 + 1 then
        break;
      if Res = WAIT_OBJECT_0
      then begin
        while not Terminated do
        begin
          fCS.Acquire;
          try
            if fSrcFiles.Count > 0
            then begin
              SrcFileName := fSrcFiles[0];
              fSrcFiles.Delete(0);
              fFileCount := fSrcFiles.Count;
              PostMessage( fMainWindowHandle,umFileBeingCopied,0,0 );
           end else
               SrcFileName := '';
           fFileBeingCopied := SrcFileName;
            if SrcFileName = '' then
              fFilesEvent.ResetEvent;
          finally
            fCS.Release;
          end;

          if SrcFileName = '' then
            break;
          DestFileName := fDestDir + ExtractFileName(SrcFileName);
          CopyFile(PChar(SrcFileName), PChar(DestFileName), True);
        end;
      end;
    end;
  end;

  function TFileCopyThread.IsCopyingFiles: boolean;
  begin 
    fCS.Acquire; 
    try 
      Result := (fSrcFiles.Count > 0) 
        // last file is still being copied 
        or (WaitForSingleObject(fFilesEvent.Handle, 0) = WAIT_OBJECT_0); 
    finally 
      fCS.Release; 
    end; 
  end; 

  // new version - edited after receiving comments 
  function TFileCopyThread.GetFileBeingCopied: string; 
  begin 
     fCS.Acquire; 
     try 
        Result := fFileBeingCopied; 
     finally 
        fCS.Release; 
     end; 
  end; 

  // old version - deleted after receiving comments 
  //function TFileCopyThread.GetFileBeingCopied: string;
  //begin
  //  Result := '';
  //  if fFileBeingCopied <> ''
  //  then begin
  //    fCS.Acquire;
  //    try
  //      Result := fFileBeingCopied;
  //      fFilesEvent.SetEvent;
  //    finally
  //      fCS.Release;
  //    end;
  //  end;
  //end;

  end.

Мы будем очень благодарны за любые дополнительные комментарии.

Читая комментарии и просматривая примеры, вы найдете разные подходы к решениям, с комментариями за и против каждого из них.

Проблема при попытке реализовать сложную новую функцию (как потоки для меня) заключается в том, что вы почти всегда находите что-то, что кажется работающим ... сначала. Только позже вы на собственном горьком опыте узнаете, что все должно было быть сделано по-другому. И потоки - очень хороший тому пример.

Такие сайты, как StackOverflow, великолепны. Что за сообщество.


person Edelcom    schedule 17.01.2010    source источник


Ответы (3)


Быстрое и грязное решение:

type
  TFileCopyThread = class(TThread)
  private
    fCS: TCriticalSection;
    fDestDir: string;
    fSrcFiles: TStrings;
    fFilesEvent: TEvent;
    fShutdownEvent: TEvent;
  protected
    procedure Execute; override;
  public
    constructor Create(const ADestDir: string);
    destructor Destroy; override;

    procedure AddFile(const ASrcFileName: string);
    function IsCopyingFiles: boolean;
  end;

constructor TFileCopyThread.Create(const ADestDir: string);
begin
  inherited Create(True);
  fCS := TCriticalSection.Create;
  fDestDir := IncludeTrailingBackslash(ADestDir);
  fSrcFiles := TStringList.Create;
  fFilesEvent := TEvent.Create(nil, True, False, '');
  fShutdownEvent := TEvent.Create(nil, True, False, '');
  Resume;
end;

destructor TFileCopyThread.Destroy;
begin
  if fShutdownEvent <> nil then
    fShutdownEvent.SetEvent;
  Terminate;
  WaitFor;
  FreeAndNil(fFilesEvent);
  FreeAndNil(fShutdownEvent);
  FreeAndNil(fSrcFiles);
  FreeAndNil(fCS);
  inherited;
end;

procedure TFileCopyThread.AddFile(const ASrcFileName: string);
begin
  if ASrcFileName <> '' then begin
    fCS.Acquire;
    try
      fSrcFiles.Add(ASrcFileName);
      fFilesEvent.SetEvent;
    finally
      fCS.Release;
    end;
  end;
end;

procedure TFileCopyThread.Execute;
var
  Handles: array[0..1] of THandle;
  Res: Cardinal;
  SrcFileName, DestFileName: string;
begin
  Handles[0] := fFilesEvent.Handle;
  Handles[1] := fShutdownEvent.Handle;
  while not Terminated do begin
    Res := WaitForMultipleObjects(2, @Handles[0], False, INFINITE);
    if Res = WAIT_OBJECT_0 + 1 then
      break;
    if Res = WAIT_OBJECT_0 then begin
      while not Terminated do begin
        fCS.Acquire;
        try
          if fSrcFiles.Count > 0 then begin
            SrcFileName := fSrcFiles[0];
            fSrcFiles.Delete(0);
          end else
            SrcFileName := '';
          if SrcFileName = '' then
            fFilesEvent.ResetEvent;
        finally
          fCS.Release;
        end;

        if SrcFileName = '' then
          break;
        DestFileName := fDestDir + ExtractFileName(SrcFileName);
        CopyFile(PChar(SrcFileName), PChar(DestFileName), True);
      end;
    end;
  end;
end;

function TFileCopyThread.IsCopyingFiles: boolean;
begin
  fCS.Acquire;
  try
    Result := (fSrcFiles.Count > 0)
      // last file is still being copied
      or (WaitForSingleObject(fFilesEvent.Handle, 0) = WAIT_OBJECT_0);
  finally
    fCS.Release;
  end;
end;

Чтобы использовать это в производственном коде, вам нужно будет добавить обработку ошибок, возможно, некоторые уведомления о ходе выполнения, а само копирование, вероятно, должно быть реализовано по-другому, но это должно помочь вам начать.

Отвечая на ваши вопросы:

я должен создать FileCopyThread в FormCreate основной программы (и позволить ему работать), это как-то замедлит работу программы?

Вы можете создать поток, он будет блокировать события и потреблять 0 циклов ЦП, пока вы не добавите файл для копирования. После того, как все файлы будут скопированы, поток снова заблокируется, поэтому сохранение его в течение всего времени выполнения программы не имеет отрицательного эффекта, кроме потребления некоторой памяти.

Могу ли я добавить обычное уведомление о событии в FileCopyThread (чтобы я мог отправить событие, как в свойстве onProgress: TProgressEvent, read fOnProgressEvent, запись fOnProgressEvent; с fi текущее количество файлов в списке и файл, который в настоящее время обрабатывается. Я хотел бы позвонить это при добавлении и до и после процедуры копирования

Вы можете добавлять уведомления, но для того, чтобы они были действительно полезными, они должны выполняться в контексте основного потока. Самый простой и уродливый способ сделать это - обернуть их методом Synchronize(). Посмотрите на демонстрацию Delphi Threads, как это сделать. Затем прочтите некоторые вопросы и ответы, найденные при поиске по запросу "[delphi] synchronize" здесь, на SO, чтобы увидеть, насколько этот метод имеет немало недостатков.

Однако я бы не стал реализовывать уведомления таким образом. Если вы просто хотите отобразить прогресс, нет необходимости обновлять это для каждого файла. Кроме того, у вас уже есть вся необходимая информация в потоке VCL, в том месте, куда вы добавляете файлы для копирования. Вы можете просто запустить таймер с Interval, скажем, 100, и заставить обработчик событий таймера проверять, занят ли поток и сколько файлов осталось скопировать. Когда поток снова заблокируется, вы можете отключить таймер. Если вам нужно больше или другая информация из потока, вы можете легко добавить больше потоковобезопасных методов в класс потока (например, вернуть количество ожидающих файлов). Я начал с минималистичного интерфейса, чтобы все было легко и просто, я использовал его только для вдохновения.

Комментарий к обновленному вопросу:

У вас есть этот код:

function TFileCopyThread.GetFileBeingCopied: string;
begin
  Result := '';
  if fFileBeingCopied <> '' then begin
    fCS.Acquire;
    try
      Result := fFileBeingCopied;
      fFilesEvent.SetEvent;
    finally
      fCS.Release;
    end;
  end;
end;

но с этим есть две проблемы. Во-первых, весь доступ к полям данных должен быть защищен, чтобы быть безопасным, а затем вы просто читаете данные, а не добавляете новый файл, поэтому нет необходимости устанавливать событие. Пересмотренный метод будет просто:

function TFileCopyThread.GetFileBeingCopied: string;
begin
  fCS.Acquire;
  try
    Result := fFileBeingCopied;
  finally
    fCS.Release;
  end;
end;

Также вы устанавливаете только поле fFileBeingCopied, но никогда не сбрасываете его, поэтому он всегда будет равен последнему скопированному файлу, даже если поток заблокирован. Вы должны установить эту строку пустой, когда был скопирован последний файл, и, конечно же, сделайте это, когда будет получена критическая секция. Просто переместите присвоение за блок if.

person mghie    schedule 17.01.2010
comment
Спасибо за ответ .. Я попробую и сообщу результат. - person Edelcom; 17.01.2010
comment
+1: Первая тестовая программа работает. Но еще два вопроса: 1 / следует ли мне создать FileCopyThread в FormCreate основной программы (и позволить ему работать), это как-то замедлит работу программы? и 2 / Могу ли я добавить обычное уведомление о событии в FileCopyThread (чтобы я мог отправить событие, как в property onProgress:TProgressEvent read fOnProgressEvent write fOnProgressEvent;, с fi текущим количеством файлов в списке и файлом, который в данный момент обрабатывается. Я хотел бы вызвать это при добавлении и перед и после процедуры копирования. - person Edelcom; 18.01.2010
comment
Ни один из примеров, которые я могу найти, похоже, не использует события Delphi, поэтому я предполагаю, что это невозможно. Сейчас я использую PostMessage. Буду признателен за любые дополнительные мысли. Но ответьте, пожалуйста, на пункт 1 / в моем предыдущем комментарии. Спасибо - person Edelcom; 18.01.2010
comment
@Edelcom: Вы можете создать поток при запуске приложения или позже, это не имеет значения. Поток не будет тратить впустую ни одного цикла ЦП, он полностью заблокирован, если не копировать файлы. Однако я бы создал его при необходимости, то есть когда нужно скопировать первый файл - так ваше приложение будет запускаться быстрее. Но в основном это вопрос стиля. Что касается реализации событий, вам, вероятно, следует задать новый вопрос, чтобы держать вещи более сфокусированными. - person mghie; 18.01.2010
comment
@mghie: Пожалуйста, посмотрите мой обновленный вопрос, который теперь содержит мой первый код, основанный на вашем коде. Пожалуйста, продолжайте давать любые комментарии или критику, которые вы хотите - это очень ценно. Я начну поток в начале моей программы создания сети, потому что разные части моего кода или пользователь могут начать копирование файлов в любое время - создание сайта, обновление изображений и так далее. - person Edelcom; 18.01.2010
comment
@mghie: Просто любопытно: установка значения Result тоже должна быть в критической секции? - person Edelcom; 18.01.2010
comment
@Edelcom: Нет, только данные, к которым можно получить доступ одновременно из нескольких потоков, должны быть защищены объектами синхронизации. Result допустимо только в контексте текущего потока, поэтому одновременный доступ невозможен. Однако у задания есть части для чтения и записи, и если одна из них нуждается в защите, то все задание должно быть защищено. Я действительно советую прочитать статью Харви (eonclash.com/Tutorials/Multithreading /MartinHarvey1.1/ToC.html), успех в многопоточности достигается только при хорошем понимании основ. - person mghie; 18.01.2010

Если вы несколько неохотно переходите к делу и имеете дело с TThread напрямую, как в mghie solution, альтернативой, которая может быть быстрее, является использование AsyncCalls Андреаса Хаусладена.

скелетный код:

procedure MoveFile(AFileName: TFileName; const DestFolder: string);
//------------------------------------------------------------------------------
begin
  if DestFolder > '' then
    if CopyFile(PChar(AFileName), PChar(IncludeTrailingPathDelimiter(DestFolder) + ExtractFileName(AFileName)), False) then
      SysUtils.DeleteFile(AFileName)
    else
      RaiseLastOSError;
end;

procedure DoExport;
//------------------------------------------------------------------------------
var
  TempPath, TempFileName: TFileName;
  I: Integer;
  AsyncCallsList: array of IAsyncCall;
begin
  // find Windows temp directory
  SetLength(TempPath, MAX_PATH);
  SetLength(TempPath, GetTempPath(MAX_PATH, PChar(TempPath)));

  // we suppose you have an array of items (1 per file to be created) with some info
  SetLength(AsyncCallsList, Length(AnItemListArray));
  for I := Low(AnItemListArray) to High(AnItemListArray) do
  begin
    AnItem := AnItemListArray[I];
    LogMessage('.Processing current file for '+ AnItem.NAME);
    TempFileName := TempPath + Format(AFormatString, [AnItem.NAME, ...]);
    CreateYourFile(TempFileName);
    LogMessage('.File generated for '+ AnItem.NAME);
    // Move the file to Dest asynchronously, without waiting
    AsyncCallsList[I] := AsyncCall(@MoveFile, [TempFileName, AnItem.DestFolder])
  end;

  // final rendez-vous synchronization
  AsyncMultiSync(AsyncCallsList);
  LogMessage('Job finished... ');
end;
person Francesca    schedule 17.01.2010
comment
Я решил не использовать AsyncCalls, потому что ввод-вывод должен быть сериализован, чтобы максимизировать пропускную способность, а планирование более чем одного потока на самом деле контрпродуктивно. Если файлы не перемещаются в разные места назначения, в этом случае несколько потоков могут улучшить ситуацию. - person mghie; 17.01.2010
comment
+1: Я пойду и попробую решение Thread (также потому, что я никогда раньше не работал с потоками, и это кажется хорошей причиной их попробовать), но спасибо за альтернативное решение. Я тоже попробую это решение (просто посмотреть, как оно работает). Я копирую файлы в разные папки в сети, хотя и не случайным образом (например, сначала веб-страницы на английском языке, затем веб-страницы на голландском языке, затем изображения и так далее ... вы понимаете). - person Edelcom; 18.01.2010
comment
@mghie. Вы можете установить количество потоков в ThreadPool в соответствии со своими потребностями и средой. - person Francesca; 18.01.2010

Хорошим началом для использования потоков является Delphi, которую можно найти на сайте Delphi about site

Чтобы ваше решение работало, вам нужна очередь заданий для рабочего потока. Можно использовать список строк. Но в любом случае вам нужно охранять очередь, чтобы только один поток мог писать в нее в любой момент. Даже если пишущий поток приостановлен.

Ваше приложение записывает в очередь. Так что должен быть защищенный метод записи.

Ваш поток читает и удаляет из очереди. Так что должен быть защищенный метод чтения / удаления.

Вы можете использовать критический раздел, чтобы убедиться, что только один из них имеет доступ к очереди в любой момент.

person Toon Krijthe    schedule 17.01.2010
comment
Спасибо за ответ, но я очень надеюсь на рабочий пример. - person Edelcom; 17.01.2010