1

I need to log some data and preferrably copy files using a thread, but by using the code below, but it simply freezes my app.

if I understand the whole XE7 Parallell library correctly TThread.Queue and TThread.Synchronize are supposed to sync with the main thread, but in my case the whole app freezes.

What am I doing wrong?

procedure TCopyDeviceContent.StartCopy;
var
  OK: boolean;
begin
  OK := false;

  //  showmessage('fFiles.Count = '+inttostr(fFiles.Count));

  if fFiles.Count = 0 then
  begin
    NotifyDone;
    exit;
  end;

  TParallel.For(0, fFiles.Count-1,
    procedure (Value: Integer)
    begin
      TThread.Queue(TThread.CurrentThread, //Here is where it freezes
        procedure
        begin
          Log('Setting fCurrentFile to '+fFiles.Strings[value]);
        end
      );

      sleep(1000);

      fCurrentFile := fFiles.Strings[value];
      Log('Triggering fOnBeforeProcess');
      if assigned(fOnBeforeProcess) then fOnBeforeProcess(self);

      Log('Does file exist?');
      if FileExists(fFiles.Strings[value]) = true then
      begin
        Log('Yes!');
        Log('Trying to copy file to Windows temp folder.');
        try
          TFile.Copy(fFiles.Strings[value], GetEnvironmentVariable('TEMP'));
        finally
          OK := true;
        end;

        if OK = true then
        begin
          Log('Success!');
          OK := false;

          Log('Does file exist in Windows temp folder?');
          if FileExists(GetEnvironmentVariable('TEMP')+ExtractFileName(fFiles.Strings[value])) then
          begin
            Log('Yes');
            Log('Trying to copy file from Windows temp folder to final destination: '+DestPath+DateToStr(Now)+'\'+ExtractFileName(fFiles.Strings[value]));
            try
              TFile.Move(GetEnvironmentVariable('TEMP')+ExtractFileName(fFiles.Strings[value]),
              DestPath+DateToStr(Now)+'\'+ExtractFileName(fFiles.Strings[value]));
            finally
              fFilesOK.Add(fFiles.Strings[value]);
              Log('Sucess!');
            end;
          end;    
        end
        else
        begin
          fFilesFailed.Add(fFiles.Strings[value]);
          Log('Failed copying to Windows temp folder!');
        end;
      end;
      inc(fProgress);
      NotifyProgress;
      Log('File copy success. Moving on to next file if available...');
    end
  );

  NotifyDone;

  if fFilesFailed.Count > 0 then NotifyError;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Aid Vllasaliu
  • 85
  • 2
  • 11
  • Don't pass the thread when calling TThread.Queue. Use the overloaded call without this option. See [Using the Delphi XE7 Parallel Library](http://stackoverflow.com/a/26939797/576719). – LU RD Mar 10 '15 at 14:25
  • Correction: code inside `TThread.Queue` is executed AFTER the thread is finnished. `TThread.Synchronize`is what I need for my implementation. – Aid Vllasaliu Mar 10 '15 at 14:25
  • @LURD there isno overloaded call without the option. Where do I find it? – Aid Vllasaliu Mar 10 '15 at 14:28
  • `TThread.Queue(nil, Aproc);` should work. – LU RD Mar 10 '15 at 14:36
  • `TThread.Queue` with **`nil`** assigned to the first parameter works, but after reading more about this, it turns out that any code inside `TThread.Queue` is executed after the thread is done. I don't need that, I need to log every step of the process as it is happening, hence why `TThread.Synchronize` is more suitable. – Aid Vllasaliu Mar 10 '15 at 14:40
  • @LURD by the way, what is "Aproc"? – Aid Vllasaliu Mar 10 '15 at 14:41
  • It's short for an anonymous procedure. – LU RD Mar 10 '15 at 14:43
  • _code inside TThread.Queue is executed AFTER the thread is finnished._ Not necessarily true. The code will execute **eventually**, the docs never stated what i've said. – EProgrammerNotFound Mar 10 '15 at 16:04
  • @EProgrammerNotFound you are correct! – Aid Vllasaliu Mar 10 '15 at 16:33
  • @AidVllasaliu to say that the code in TThread.Queue is exectued after the thread finishes is plain wrong. – iamjoosy Mar 10 '15 at 16:54
  • 3
    Code passed to `TThread.Queue()` is executed whenever the main thread gets around to executing it. It has nothing to do with the lifetime of the thread that is calling `Queue()`. The thread may still be running, or it may have terminated. The point is `Queue()` is **asynchronous**, it exits immediately and does not wait for the code to execute. `TThread.Synchronize()` does wait. – Remy Lebeau Mar 10 '15 at 17:41
  • What does Log() procedure do? Updates the UI, writes to file? – iPath ツ Mar 10 '15 at 18:54
  • In **this special case** the queued event is executed after the `TParallel.For` just because it is a blocking operation and waits until everything is done. `TThread.Sychronize` should cause a deadlock here – Sir Rufo Mar 10 '15 at 19:09
  • @iPathツ `log()` simply adds a line of string to a `TMemo` – Aid Vllasaliu Mar 10 '15 at 19:34
  • @AidVllasaliu does the Log() method use TThread.Synchronize/queue? – iPath ツ Mar 10 '15 at 20:38
  • @iPathツ Look at my answer, that explains why the queued messages are processed **afterwards** – Sir Rufo Mar 10 '15 at 20:43
  • @iPathツ nope, the Log() method is located in the main thread. in a completely different unit actually. – Aid Vllasaliu Mar 10 '15 at 21:10
  • 1
    @AidVllasaliu a method like your Log() method cannot be "located" in the main thread. It can be **called** from any thread including the main thread. That is it is executed in the context of a thread. In your example you mainly call the Log() method from the context of the current for-worker thread and not from the main thread. The only way to call it in the context of the Main Thread is to use Synchronize/Queue methods. The only difference between the two methods is that Synchronize is a blocking function and Queue is not blocking function. – iPath ツ Mar 10 '15 at 22:21
  • @iPathツ Well, I am referring to my code which is working now, thanks to Edin and LURD. Their answer is working properly, and I have replaced their `Form1.Memo1.....` line with my `Log()` method. – Aid Vllasaliu Mar 10 '15 at 23:43

2 Answers2

9

TParallel.For performs a threaded execution of the iteration events, but itself is a blocking method. So for this you will have to be careful with the synchronization, if you start this from the main thread.

The use of TThread.Queue works safely but as you already noticed, all the queued events are processed after TParallel.For has finished - in fact, after leaving the method and return to idle.

The use of TThread.Synchronize will cause a dead lock, if you use it in the iteration events and start TParallel.For from the main thread.

Here is a little application showing the difference using

  • CopyFiles
  • ParallelCopyFiles
  • AsyncCopyFiles calls CopyFiles from a task
  • AsyncParallelCopyFiles calls ParallelCopyFiles from a task

And I assume AsyncParallelCopyFiles is the one you are looking for.

Within the Async... methods it is safe to use TThread.Synchronize - if you do not wait for task inside the main thread.

unit Form.Main;

interface

uses
  System.IOUtils,
  System.Threading,
  System.Types,

  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TLogMsg = record
  private
    FMsg: string;
    FThreadID: Cardinal;
    FOccurred: TDateTime;
  public
    class operator implicit( a: string ): TLogMsg;
    class operator implicit( a: TLogMsg ): string;

    constructor Create( const AMsg: string );
    function ToString: string;

    property Msg: string read FMsg;
    property ThreadID: Cardinal read FThreadID;
    property Occurred: TDateTime read FOccurred;
  end;

type
  TForm1 = class( TForm )
    ListBox1: TListBox;
    RadioGroup1: TRadioGroup;
    Button1: TButton;
    procedure Button1Click( Sender: TObject );
  private
    FTask: ITask;
    procedure ThreadSafeLog( ALogMsg: TLogMsg );
  public
    procedure CopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean );
    procedure ParallelCopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean );

    function AsyncCopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean; ACallback: TThreadProcedure ): ITask;
    function AsyncParallelCopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean; ACallback: TThreadProcedure ): ITask;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}
{ TForm1 }

// *** ATTENTION ***
// ParallelCopyFiles will cause a dead lock without USE_QUEUE
// but you still can try yourself ...
//
{$DEFINE USE_QUEUE}
//
// *****************

procedure TForm1.ThreadSafeLog( ALogMsg: TLogMsg );
begin
{$IFDEF USE_QUEUE}
  TThread.Queue
{$ELSE}
  TThread.Synchronize
{$ENDIF}
    ( nil,
      procedure
    begin
      ListBox1.Items.Add( ALogMsg );
    end );
end;

procedure TForm1.CopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean );
var
  LSource, LDestination: string;
begin
  ThreadSafeLog( 'CopyFiles - ENTER' );
  for LSource in AFiles do
    begin
      LDestination := TPath.Combine( ADestPath, TPath.GetFileName( LSource ) );
      ThreadSafeLog( 'Copy ' + LSource );
      TFile.Copy( LSource, LDestination, Overwrite );
    end;
  ThreadSafeLog( 'CopyFiles - EXIT' );
end;

procedure TForm1.ParallelCopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean );
begin
  ThreadSafeLog( 'ParallelCopyFiles - ENTER' );
  TParallel.&For( Low( AFiles ), High( AFiles ),
    procedure( AIndex: Integer )
    var
      LSource, LDestination: string;
    begin
      LSource := AFiles[AIndex];
      LDestination := TPath.Combine( ADestPath, TPath.GetFileName( LSource ) );
      ThreadSafeLog( 'Copy ' + LSource );
      TFile.Copy( LSource, LDestination, Overwrite );
    end );
  ThreadSafeLog( 'ParallelCopyFiles - EXIT' );
end;

function TForm1.AsyncCopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean; ACallback: TThreadProcedure ): ITask;
begin
  ThreadSafeLog( 'AsyncCopyFiles - ENTER' );
  Result := TTask.Run(
    procedure
    begin
      CopyFiles( AFiles, ADestPath, Overwrite );
      TThread.Synchronize( nil, ACallback );
    end );
  ThreadSafeLog( 'AsyncCopyFiles - EXIT' );
end;

function TForm1.AsyncParallelCopyFiles( AFiles: TStringDynArray; ADestPath: string; Overwrite: Boolean; ACallback: TThreadProcedure ): ITask;
begin
  ThreadSafeLog( 'AsyncParallelCopyFiles - ENTER' );
  Result := TTask.Run(
    procedure
    begin
      ParallelCopyFiles( AFiles, ADestPath, Overwrite );
      TThread.Synchronize( nil, ACallback );
    end );
  ThreadSafeLog( 'AsyncParallelCopyFiles - EXIT' );
end;

procedure TForm1.Button1Click( Sender: TObject );
var
  LFiles: TStringDynArray;
  LDestPath: string;
begin
  ListBox1.Clear; // Clear the log destination

  LFiles := TDirectory.GetFiles( TPath.GetDocumentsPath, '*.*' );
  LDestPath := TPath.Combine( TPath.GetDocumentsPath, '_COPYTEST_' );
  TDirectory.CreateDirectory( LDestPath );

  case RadioGroup1.ItemIndex of
    0:
      CopyFiles( LFiles, LDestPath, True );
    1:
      ParallelCopyFiles( LFiles, LDestPath, True );
    2:
      begin
        Button1.Enabled := False;
        AsyncCopyFiles( LFiles, LDestPath, True,
          procedure
          begin
            Button1.Enabled := True;
          end );
      end;
    3:
      begin
        Button1.Enabled := False;
        AsyncParallelCopyFiles( LFiles, LDestPath, True,
          procedure
          begin
            Button1.Enabled := True;
          end );
      end;
  end;
end;

{ TLogMsg }

constructor TLogMsg.Create( const AMsg: string );
begin
  FMsg := AMsg;
  FThreadID := TThread.CurrentThread.ThreadID;
  FOccurred := Now;
end;

class operator TLogMsg.implicit( a: string ): TLogMsg;
begin
  Result := TLogMsg.Create( a );
end;

class operator TLogMsg.implicit( a: TLogMsg ): string;
begin
  Result := a.ToString;
end;

function TLogMsg.ToString: string;
begin
  Result := Format( '$%8.8x [%s] %s', [FThreadID, FormatDateTime( 'hh:nn:ss.zzz', FOccurred ), FMsg] );
end;

end.

UPDATED

I just extend the log message with more information about the thread and the occurred time of the message

Sir Rufo
  • 18,395
  • 2
  • 39
  • 73
  • Are you sure about the deadlock if Parallel.For is started from the main thread with calls to synchronize? I haven't used the threading library, but my own implementation puts the overhead of Parallel.For in a thread of its own. – LU RD Mar 10 '15 at 20:54
  • @LURD That is the reason for the compiler switch. You will get a deadlock and I would be very surprised if not. How can you synchronize a thread with the mainthread if the mainthread is waiting for this thread to finish? You can't – Sir Rufo Mar 10 '15 at 21:05
  • @LURD TParallel.ForWorker in PPL creates separate task for actual wotrk but ends with RootTask.Start.Wait(). So TParallel.For is a blocking function. – iPath ツ Mar 10 '15 at 22:10
  • @SirRufo +100 for the explanation and examples. I think your post should be selected as an answer :) – iPath ツ Mar 10 '15 at 22:12
  • @iPathツ, yes I looked that up and confirmed the deadlock. I was expecting a wait with a call to CheckSynchronize or similar to avoid the deadlock, but there is no such thing. – LU RD Mar 10 '15 at 22:40
  • Thanks for this fully commented explanation of why i was having a deadlock in my TParallel.For sleeped loop ^^ – Darkendorf Dec 24 '15 at 08:07
  • @SirRufo, you have `ThreadSafeLog` defined as `procedure TForm1.ThreadSafeLog( ALogMsg: TLogMsg );`. Then later you call it as `ThreadSafeLog( 'ParallelCopyFiles - EXIT' );`. Does it compile? – Interface Unknown Jul 03 '17 at 10:05
  • @InterfaceUnknown Yes, it does compile – Sir Rufo Jul 03 '17 at 10:12
  • @SirRufo, thank you. I probably missed something... Will learn the code deeper. – Interface Unknown Jul 03 '17 at 10:15
  • 1
    @InterfaceUnknown The magic will happen because of the implicit operator of TLogMsg. – Sir Rufo Jul 03 '17 at 10:17
1

If goal is just to copy files without freezing UI thread i would just use something like this:

procedure TCopyDeviceContent.StartCopy;
var
 aTask: ITask;
begin
 aTask := TTask.Create (procedure ()
   begin
      // Copy files here  
      TThread.Synchronize(nil,procedure
                  begin
                     //Interact with UI  
                     Form1.Memo1.Lines.Add(‘Begin Execution’);
                  end);
   end);
 aTask.Start;
end;

Inside task procedure just copy files as you would do it usually, i am not sure if copying using multiple threads will help you.

In case you need interact with UI you need to switch back to UI thread, you can use TThread.Synchronize.

LU RD
  • 34,438
  • 5
  • 88
  • 296
Edin Omeragic
  • 1,948
  • 1
  • 23
  • 26