3
votes

I'm writing some software that talks to external hardware via a dll (moving some motors and reading some values back). The calls to the dll are blocking and may not return for in the order of 10 seconds. The software performs a scan by moving the hardware, taking a reading and repeating for a number of points. One scan can take in the order of 30 minutes to complete. While the scan is running I would obviously like the GUI to be responsive and a live graph (in an MDI Child) of the incoming data to be updated at each point. Multithreading seems the obvious choice for this problem.

My question is, what is the best way to thread this and talk back to the main VCL thread to update the graph during a scan?

I currently have a single TThread descendant that performs the 'scan logic' and an array of doubles in the public var section of the ChildForm. I need to fill out this array from the thread but I don't know whether to use Synchronize or CriticalSection or PostMessage or some other method. Each time a new value is added, the main VCL thread needs to update the graph. Should I really have an intermediary object for the data that is a global var and access this from the Thread and the ChildForm separately somehow?

4
Lots of options. Who are we to say which is best? Synchronize will work and is by a distance the easiest option. Let the main thread own the data and have the worker thread send new data using Synchronize for the main thread to append and display. - David Heffernan
What I'm trying to get to is a scalable solution, ie what happens when my scan can be performed multiple times and each scan has extra properties (color, title, noPoints) etc - where should this logic live, inside the thread or should the thread be as simple as possible and just perform a single measurement even? - Steve Magness
Are all your requirements going to be specified in the comments? - David Heffernan
Sorry, I think I'm still a little unclear in my own head what the 'real' question is - it's a more general 'architecture' thing than a simple question. I rewrote the post a few times trying to work out what it was I needed to ask but I need to get the ball rolling somehow... - Steve Magness
You can use anonymous functions with TThread.Synchronize and TThread.Queue and make it quite maintainable and readable. I really don't fancy the very low-level approach suggested by Ghigo. - David Heffernan

4 Answers

6
votes

The simplest way to update the GUI from a thread is to use anonymous methods in conjunction with TThread.Synchronize and TThread.Queue.

procedure TMyThread.Execute;
begin
  ...
  Synchronize(  // Synchronous example
    procedure
    begin
      // Your code executed in main thread here 
    end
  );
  ...
  Queue( // Asynchronous example
    procedure
    begin
      // Your code executed in main thread here
    end
  );
end;

Passing values asynchronous often requires "capturing" a value.

procedure TMyThread.PassAValue(anInteger: Integer);
begin
  Queue(
    procedure
    begin
      // Use anInteger in main thread 
    end
  );
end;

procedure TMyThread.Execute;
var
  myInt: Integer;
begin
  ...
  PassAValue(myInt);  // Capture myInt
  ...
end;

When an anonymous method is using a variable, the reference to the variable is captured. This means that if you alter the variable value before the anonymous method is executed, the new value is used instead. Hence the need to capture the "value".

A more elaborate example can be found here, synchronize-and-queue-with-parameters, by @UweRaabe.

1
votes

If you want to invest a little more then a simple Synchronize call which by the way blocks the main thread, you can add a simple FIFO queue with messaging on top of it. The flow of data would be like this:

  1. The thread puts the data into the queue.
  2. The thread post a message to the main thread window. Which one I don't care :)
  3. You handle the message that data is available and process any messages in the queue as you see fit.

The code would look something like this:

the queue...

const
  WM_DataAvailable = WM_USER + 1;

var
  ThreadSafeQueue: TThreadSafeQueue;

the data is put into the queue...

procedure PutDataIntoQueue;
var
  MyObject: TMyObject;
begin
  MyObject := TMyObject.Create;
  ThreadSafeQueue.Enqueue(MyObject);
  PostMessage(FMainWindowHandle, WM_DataAvailable, 0, 0);
end;

and processing...

procedure ProcessDataInTheQueue(var Msg: TMessage); message WM_DataAvailable;

procedure ProcessDataInTheQueue(var Msg: TMessage);
var
  AnyValue: TAnyValue;
  MyObject: TMyObject;
begin
  while ThreadSafeQueue.Dequeue(AnyValue) do
  begin
    MyObject := TMyObject(AnyValue.AsObject);
    try
      // process the actual object as needed
    finally
      MyObject.Free
    end;
  end;
end;

The code is written without Delphi and checks so it can contain errors. I showed the example using my freely available thread safe queue and TAnyValue. You can find both here:

http://www.cromis.net/blog/downloads/

Also please note then I did not do any check if PostMessage was actually sent. You should check that in production code.

1
votes

I find that populating a TThreadList from the background thread, then posting a message to the main thread that there is a new item in the list, then processing the list in the main thread is simple and easily maintainable.

With this method, you could store as many readings as you wanted in the list, and every time the main thread received a message, it would simply process all the items in the list at once.

Define a class for the readings, instantiate them, and add them to the list in the background thread. Don't forget to free them in the main thread when you pop them off the list.

0
votes

Use postmessage inside you thread and send messages to main form handle. Register one (or more) custom messages and write a handler for them.

const WM_MEASURE_MESSAGE = WM_USER + 1;

Create a thread class, add a MainFormHandle property (Thandle or cardinal). Create thread suspended, set MainFormHandle with main form handle, then resume thread. When you have a new measure, assign data1 and data2 dword with some data from measure, then

PostMessage(fMainFormHandle,WM_MEASURE_MESSAGE,data1,data2);

In main form you have message handler:

procedure MeasureMessage(var msg: TMessage); message WM_MEASURE_MESSAGE;
begin
  // update graph here
  // msg.wparam is data1
  // msg.lparam is data2
end;

If you need to send much more data from thread to main form, you can create an appropriate structure in main context for the whole measurement data, pass a reference to thread, let the thread write data and use messages just to tell main form new data position (an array index, for example). Use TThread.Waitfor in main context to avoid freeing data structure while thread is still running (and writing into memory).