Sending data from TThread to main VCL Thread

2020-06-27 09:26发布

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条回答
霸刀☆藐视天下
2楼-- · 2020-06-27 09:31

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).

查看更多
何必那么认真
3楼-- · 2020-06-27 09:32

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.

查看更多
▲ chillily
4楼-- · 2020-06-27 09:38

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.

查看更多
Lonely孤独者°
5楼-- · 2020-06-27 09:52

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.

查看更多
登录 后发表回答