0

I have some apps built with Delphi 2010 and XE4 that use Synchronize in a thread. I think Synchronize was introduced to Delphi in Delphi 2010. My thread operates very well so that is not the problem.

My question is: Is there any way to "Synchronize" with versions of Delphi prior to Delphi 2010 or to ask it in a different way, how do you update GUI controls in these earlier versions of Delphi without Synchronize?

The code shown below is a subset of the actual code to reduce the length of this post.

type
  { A TThread descendent for loading Images from a folder }
  TFolderLoadingThread = class(TThread)
  private
    { Private declarations }
    AspectRatio: double;
  protected
    { Protected declarations }
    procedure Execute; override;
  public
    { Public declarations }
    constructor Create(CreateSuspended: Boolean);
  end;

procedure TFolderLoadingThread.Execute;
{ Load images ImageEnView in the thread. }
begin
  inherited;
  { Free the thread onTerminate }
  FreeOnTerminate := True;
  if not Terminated then
  begin
    { Set the Progressbar.Max Value }
    **Synchronize**(
      procedure
      begin
         if iFileCount > 0 then
          Form1.Gauge1.MaxValue := iFileCount - 1;
 end);
end;
Arioch 'The
  • 15,799
  • 35
  • 62
Bill
  • 2,993
  • 5
  • 37
  • 71

2 Answers2

6

Synchronize is very old routine, but there are no anonymous procedures in Delphi versions prior to D2009. Synchronize was intended to call a method without parameters in these versions.

procedure TFolderLoadingThread.UpdateProgress;
begin
 if iFileCount > 0 then
          Form1.Gauge1.MaxValue := iFileCount - 1;
end;

in Execute:

do thead work...
Synchronize(UpdateProgress);

P.S. You have not to call Terminate in the Execute body

MBo
  • 77,366
  • 5
  • 53
  • 86
2
  1. Synchronize was there almost forever. I used it in Delphi 5. But that already was told above.

  2. You can also use Windows API (namely PostMessage) to do it, if you can afford the change happening "soon after", in other words if your thread does not have to stop and wait until GUI updated. For example if there is a long calculations, and you want to have a gauge "1234 of 5678 complete" - then there is little point to stop the calculation processes. Let the counter would be inaccurate, plus-minus a dozen. Why care ?

Now, how to implement it... For simplistic cases you can use direct Windows access. For example, if we have a windowed text label - the one inherited from TWinControl

Then we can bypass VCL and all its goodies and change the caption with a standard Windows API

You would also have to make a proper calculation of total numbers and messages to display. Yet this is not related to GUI and VCL. But hopefully you know about synchronization primitives, for example

AS. Yes, there is AtomicIncrement for you, i just show the framework in details.

There below i assume TMyThread to be one of the pool of workers, processing some common set of work items. It might be the only worker thread or one of many, it just does not know it, nor care about.

TThreadsCoordinator = class
   ...
 private
   CounterCS; TCriticalSection;

 public
   property CompleteItemsCount: integer 
            read FCompleteItemsCount;
   function IncCompleteItemsCount(const Delta: integer): integer;
end; 

...

function TThreadsCoordinator.IncCompleteItemsCount;
begin
  CounterCS.Acquire;
  try
    Inc(FCompleteItemsCount, Delta);
  finally
    CounterCS.Release;
  end;
  Result := FCompleteItemsCount;
end;

var GlobalCaptionBuffer: array[0..127] of char; 
// should be filled through with #0 before threads are started

procedure TMyThread.WorkItemComplete;
var s: string;
begin
 ...
 s := Format('Done: %d of %d', 
       [ Self.Coordinator.IncCompleteItemsCount( +1 ),
         Self.Coordinator.TotalItemsCount ] );

 StrCopy( @GlobalCaptionBuffer[0], PChar(s));

 if MyProgressForm1.Visible then // avoid ReCreateWnd inside .ShowModal
    PostMessage( MyProgressForm1.StaticText1.Handle, 
       WM_SETTEXT, 0, Integer(@GlobalCaptionBuffer[0]));
...
end;

This implementation - while being straight-forward - may suffer from synchronization issues. * you need some global buffer for the text, that would not be deleted before WM_SETTEXT would actually be received and executed. * you need to think what would happen, when the text in that buffer would be in process of updating when the label would read it simultaneously * David Heffernan below claims that in some conditions the Windows controls might be in process of destruction and re-creation (read: RecreateWnd method), thus unexpected consequences might happen. Personally - as long as that ProgressForm would only be controlled by WinGDI meeans avoiding all VCL goodies (such as changing combobox styles on the go, which does trigger RecreateWnd, but which is not possible by native WinGDI API) - i cannot see why that can happen. But David claims it can. Choose for yourself, if that suits you.

However for more complex tasks (or for using window-less text labels) you can resort to "message methods" - the foundation of VCL.

This implementation is more flexible (separating data generation and data visualiation) and arguable more reliable (see RecreateWnd remarks in comments). But it need more of a boilerplate code.

const WM_IncCounter = WM_USER + 10;

type TMyProgressForm = class(TForm)
private
   FCompleteItemsCount: integer;
   procedure IncCounter(var Msg: TMessage ); message WM_IncCounter;
...
end;

procedure TMyProgressForm.IncCounter(var Msg: TMessage );
var s: string;
begin
  Inc(FCompleteItemsCount, Msg.WParam);
  s := Format('Done: %d of %d', 
       [ FCompleteItemsCount, TotalItemsCount ] );
  Label1.Caption := s;
  ProgressBar1.Position := FCompleteItemsCount;
end;


procedure TMyThread.WorkItemComplete;
begin
  Inc(Self.UncommittedTicks);

  if MyProgressForm1.Visible then // avoid ReCreateWnd inside .ShowModal
  begin
    PostMessage( MyProgressForm1.Handle, WM_IncCounter, Self.UncommittedTicks, 0);
    Self.UncommittedTicks := 0;
  end;
end;
Arioch 'The
  • 15,799
  • 35
  • 62
  • PostMessage with `WM_SETTEXT` is wrong. Window re-creation breaks that. You can end up creating the window handle on the worker thread. Remember that `Handle` can lead to a call to `CreateWnd`. – David Heffernan Nov 21 '13 at 17:25
  • @DavidHeffernan i am assuming that progress window is ALREADY shown when the threads are running. Thus all the components do already have the handles - they were rendered on the screen. Well, i can re-write it to use http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Controls.TWinControl.WindowHandle instead (with >0 guard) – Arioch 'The Nov 21 '13 at 17:28
  • @Arioch'The It doesn't matter if it is already shown. Window re-creation can happen. And `WindowHandle` is not good either. There is no synchronisation and so you have a data race. Of course, you'll find it hard to manifest a fault, but that doesn't mean that the code is correct. You should post your message to a window allocated by, for instance, `AllocateHWnd`. – David Heffernan Nov 21 '13 at 17:39
  • @DavidHeffernan why TStaticText lying on TForm can be recreated ? it is not like i would change the combobox styles. If the race happens, then one or two messages would be posted to invalid handle and discarded. Big deal. – Arioch 'The Nov 22 '13 at 08:34
  • The parent could be re-created. Use AllocateHWnd for this. – David Heffernan Nov 22 '13 at 08:45
  • @DavidHeffernan when ? did not saw it even once. Why TForm - already visible before threads started and never hidden before they finished - can get recreated ? I mentioned your concerns, but i cannot justify for myself creating yet more boilerplate. Well, if to add even more boilerplate, then one should just use PostThreadMessage or maybe manage to use CromisIPC for in-process communications – Arioch 'The Nov 22 '13 at 08:48
  • PostThreadMessage is a bad plan. Doesn't play nice with modal message loops. – David Heffernan Nov 22 '13 at 08:54
  • @DavidHeffernan Isn't is what was used by synchronize? And why would messages posted to non-modal AllocateHWnd work better in modal loops case ? – Arioch 'The Nov 22 '13 at 08:55
  • @Arioch'The Look at `TApplication.WakeMainThread`. That does `PostMessage(Handle, WM_NULL, 0, 0)`. Note that `Handle` here is the stable `TApplication.Handle`. No re-creation. No information passed, just a null message to poke the main thread into life so that it can deal with the synchronize queue. – David Heffernan Nov 22 '13 at 09:10
  • @DavidHeffernan whell, i still don't see why TApplicaiton.Handle window is more stable than TModalProgressForm.Handle – Arioch 'The Nov 22 '13 at 11:28
  • 2
    @Arioch'The Because it is created once and once only. Take a look at that call to `RecreateWnd` in `ShowModal`. It's very hard in general to prove that a window handle will never be recreated. Which is why I don't bother trying to do so. `AllocateHWnd` on the other hand is well understood. – David Heffernan Nov 22 '13 at 11:56
  • @DavidHeffernan Interesting, thank you. So there really may be a corner case when the Window is updated before ti was shown and then might belong to a wrong thread. Seems check for Form.Visible should be done in advance... Well, but then i wonder you only was against WM_SETTEXT and not against the cutom-message example. – Arioch 'The Nov 22 '13 at 12:10
  • @DavidHeffernan Okay, added a guard. OTOH i never experienced this extreme corner case. Both because it needs a very special arrangement of settings, and because that is only needed for long processings, where the 1st message is posted far after the window got visible – Arioch 'The Nov 22 '13 at 12:16
  • The guard is no good. Because it runs in another thread and you have a race. Nothing you can do makes VCL access from another thread be safe. – David Heffernan Nov 22 '13 at 12:18
  • @DavidHeffernan How can reading a single byte only changed once have a race ? `FVisisble` is either true or false. – Arioch 'The Nov 22 '13 at 12:23
  • So do it your way then. You think it's better. Fine. – David Heffernan Nov 22 '13 at 12:25
  • @DavidHeffernan i think it's simplier. And if one needs a complex solution, he better just take OTL or Synchronize or anything ready and hope it would account for every possible faux pas (the trust that VCL developers are not always worth) – Arioch 'The Nov 22 '13 at 12:31
  • I'm having a hard time to understand why you introduce `TThreadsCoordinator`. The `TMyThread` is in control of the counter values and can post both current value and total items in the post message. No need to protect anything. – LU RD Nov 22 '13 at 12:56
  • @LURD It is not only the coordinator, it is also owner and task items dispatcher. It governs the pool of worker threads and assigns them parts of their common large task. Hence each thread only knows how many items it did complete, but he does not know about other threads (not even if they do exist). So, some entity that has links to ALL the threads is the one that can keep aggregate values – Arioch 'The Nov 22 '13 at 13:08
  • I suppose we differ in that I think AllocateHWnd is simple. – David Heffernan Nov 22 '13 at 13:11
  • @DavidHeffernan it requires a boilerplate to add the message dispatcher and handler. The point of those examples avoiding .Synchonize was "Less is better" approach, or maybe even "worse is better" – Arioch 'The Nov 22 '13 at 13:14
  • It's quicker to right the boiler plate than it is to prove that breaking the VCL threading model rules is OK. Especially when you upgrade compiler and have to check it all again because you relied on implementation details. Do it your way if you want. I prefer simple solutions. – David Heffernan Nov 22 '13 at 13:19
  • Thanks for the edit, it is more clear now. I would probably just had added the information on a threadsafe queue. – LU RD Nov 22 '13 at 13:36
  • @LURD and who and how would fetch it from it ? And did D7 had thread-safe queue ? Windows does. – Arioch 'The Nov 22 '13 at 13:54
  • @DavidHeffernan "implementation details" can be easily fixed for example by starting threads in OnShow (or enabling messaging in it). I really see little benefits in AllocateHWnd approach – Arioch 'The Nov 22 '13 at 13:56
  • It works. You've gone through three or four iterations trying to make your way work. You like the complex risky approach. That's your choice. – David Heffernan Nov 22 '13 at 13:58
  • @DavidHeffernan Well, there was no RecreateWnd in Delphi5 and i really did not knew they introduced it (i actually ain't very sure why the did). So thank you for highlighting it. Perhaps in future projects i'd have to delay calculations until OnShow for ad hoc minimalistic schemes. For proper schemes i'd better step further and just take advantage of some read-made implementation like OTL or maybe even CromisIPC is it can fit into. – Arioch 'The Nov 25 '13 at 08:30
  • @Arioch'The Window re-creation has been part of the VCL since Delphi 1. If AllocateHWnd is too complex for you then something like OTL may be a good idea. – David Heffernan Nov 25 '13 at 09:23
  • @DavidHeffernan it's not like over complex, it is just about anti-NIH. If i need a kind of a framework, then better just to re-use one. // I meant RecReateWnd as part of ShowModal sequence. AFAICT it is not there even in D7, actually it only appeared for PopupModes hence was not introduced before it – Arioch 'The Nov 25 '13 at 11:09