Отправка данных из 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 с обменом сообщениями поверх него. Поток данных будет выглядеть так:
- Поток помещает данные в очередь.
- В теме опубликовано сообщение в главном окне темы. Какой мне плевать:)
- Вы обрабатываете сообщение о том, что данные доступны, и обрабатываете любые сообщения в очереди, как считаете нужным.
Код будет выглядеть примерно так:
очередь...
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 в главном контексте, чтобы избежать освобождения структуры данных, пока поток еще работает (и записывает в память).