Отправка данных из TThread в основной поток VCL

Я пишу некоторое программное обеспечение, которое общается с внешним оборудованием через DLL (перемещая некоторые двигатели и читая некоторые значения обратно). Звонки в dll блокируются и могут не возвращаться в течение порядка 10 секунд. Программное обеспечение выполняет сканирование путем перемещения оборудования, считывания и повторения нескольких точек. Одно сканирование может занять около 30 минут. В то время как сканирование выполняется, я бы хотел, чтобы графический интерфейс реагировал и действующий график (в MDI-потомке) входящих данных обновлялся в каждой точке. Многопоточность кажется очевидным выбором для этой проблемы.

Мой вопрос заключается в том, как лучше всего проделать это и поговорить с основным потоком VCL, чтобы обновить график во время сканирования?

В настоящее время у меня есть один потомок TThread, который выполняет 'логику сканирования', и массив значений типа double в публичном разделе var в ChildForm. Мне нужно заполнить этот массив из потока, но я не знаю, использовать ли Synchronize или CriticalSection или PostMessage или какой-либо другой метод. Каждый раз, когда добавляется новое значение, основной поток VCL должен обновлять график. Должен ли я действительно иметь промежуточный объект для данных, являющихся глобальными переменными, и как-то обращаться к нему из Thread и ChildForm отдельно?

4 ответа

Решение

Самый простой способ обновить GUI из потока - это использовать anonymous methods в сочетании с TThread.Synchronize а также 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;

Передача значений асинхронно часто требует "захвата" значения.

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;

Когда анонимный метод использует переменную, ссылка на переменную фиксируется. Это означает, что если вы измените значение переменной до выполнения анонимного метода, вместо него будет использовано новое значение. Отсюда и необходимость улавливать "ценность".

Более сложный пример можно найти здесь, synchronize-and-queue-with-parameters, от @UweRaabe,

Я считаю, что заполнение TThreadList из фонового потока, затем отправка сообщения в основной поток о том, что в списке появился новый элемент, затем обработка списка в основном потоке является простой и легко обслуживаемой.

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

Определите класс для показаний, создайте их экземпляры и добавьте их в список в фоновом потоке. Не забудьте освободить их в главном потоке, когда вы удалите их из списка.

Если вы хотите потратить немного больше, чем простой вызов Synchronize, который, кстати, блокирует основной поток, вы можете добавить простую очередь FIFO с обменом сообщениями поверх него. Поток данных будет выглядеть так:

  1. Поток помещает данные в очередь.
  2. В теме опубликовано сообщение в главном окне темы. Какой мне плевать:)
  3. Вы обрабатываете сообщение о том, что данные доступны, и обрабатываете любые сообщения в очереди, как считаете нужным.

Код будет выглядеть примерно так:

очередь...

const
  WM_DataAvailable = WM_USER + 1;

var
  ThreadSafeQueue: TThreadSafeQueue;

данные помещаются в очередь...

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

и обработка...

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;

Код написан без Delphi и проверок, поэтому он может содержать ошибки. Я показал пример, используя мою свободно доступную потокобезопасную очередь и TAnyValue. Вы можете найти оба здесь:

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

Также обратите внимание, что тогда я не проверял, действительно ли отправлено сообщение PostMessage. Вы должны проверить это в рабочем коде.

Используйте postmessage внутри вашей ветки и отправляйте сообщения на основной дескриптор формы. Зарегистрируйте одно (или несколько) пользовательских сообщений и напишите для них обработчик.

const WM_MEASURE_MESSAGE = WM_USER + 1;

Создайте класс потока, добавьте свойство MainFormHandle (Thandle или cardinal). Создание потока приостановлено, установите MainFormHandle с основным дескриптором формы, затем возобновите поток. Если у вас есть новая мера, присвойте data1 и data2 dword с некоторыми данными из меры, затем

PostMessage(fMainFormHandle,WM_MEASURE_MESSAGE,data1,data2);

В основной форме у вас есть обработчик сообщений:

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

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

Другие вопросы по тегам