Передать сообщение в поток delphi

Обновлено: 08.07.2024

Юрий Балыкин
дата публикации 15-07-2008 03:39

Данная статья предназначена для начинающих программистов, которые никогда не работали с потоками, и хотели бы узнать основы работы с ними. Желательно, чтоб читатель знал основы ООП и имел какой-нибудь опыт работы в Delphi. Для начала давайте определимся, что под словом "поток" я подразумеваю именно Thread, который еще имеет название "нить".

Нередко встречал на форумах мнения, что потоки не нужны вообще, любую программу можно написать так, что она будет замечательно работать и без них. Конечно, если не делать ничего серьёзней "Hello World" это так и есть, но если постепенно набирать опыт, рано или поздно любой начинающий программист упрётся в возможности "плоского" кода, возникнет необходимость распараллелить задачи. А некоторые задачи вообще нельзя реализовать без использования потоков, например работа с сокетами, COM-портом, длительное ожидание каких-либо событий, и т.д.

Всем известно, что Windows система многозадачная. Попросту говоря, это означает, что несколько программ могут работать одновременно под управлением ОС. Все мы открывали диспетчер задач и видели список процессов. Процесс - это экземпляр выполняемого приложения. На самом деле сам по себе он ничего не выполняет, он создаётся при запуске приложения, содержит в себе служебную информацию, через которую система с ним работает, так же ему выделяется необходимая память под код и данные. Для того, чтобы программа заработала, в нём создаётся поток. Любой процесс содержит в себе хотя бы один поток, и именно он отвечает за выполнение кода и получает на это процессорное время. Этим и достигается мнимая параллельность работы программ, или, как её еще называют, псевдопараллельность. Почему мнимая? Да потому, что реально процессор в каждый момент времени может выполнять только один участок кода. Windows раздаёт процессорное время всем потокам в системе по очереди, тем самым создаётся впечатление, что они работают одновременно. Реально работающие параллельно потоки могут быть только на машинах с двумя и более процессорами.

Для создания дополнительных потоков в Delphi существует базовый класс TThread, от него мы и будем наследоваться при реализации своих потоков. Для того, чтобы создать "скелет" нового класса, можно выбрать в меню File - New - Thread Object, Delphi создаст новый модуль с заготовкой этого класса. Я же для наглядности опишу его в модуле формы. Как видите, в этой заготовке добавлен один метод - Execute. Именно его нам и нужно переопределить, код внутри него и будет работать в отдельном потоке. И так, попробуем написать пример - запустим в потоке бесконечный цикл:

Запустите пример на выполнение и нажмите кнопку. Вроде ничего не происходит — форма не зависла, реагирует на перемещения. На самом деле это не так - откройте диспетчер задач и вы увидите, что процессор загружен по-полной. Сейчас в процессе вашего приложения работает два потока - один был создан изначально, при запуске приложения. Второй, который так грузит процессор - мы создали по нажатию кнопки. Итак, давайте разберём, что же означает код в Button1Click:

  • tpTimeCritical - критический
  • tpHighest - очень высокий
  • tpHigher - высокий
  • tpNormal - средний
  • tpLower - низкий
  • tpLowest - очень низкий
  • tpIdle - поток работает во время простоя системы

Думаю, теперь вам понятно, как создаются потоки. Заметьте, ничего сложного. Но не всё так просто. Казалось бы - пишем любой код внутри метода Execute и всё, а нет, потоки имеют одно неприятное свойство - они ничего не знают друг о друге. И что такого? - спросите вы. А вот что: допустим, вы пытаетесь из другого потока изменить свойство какого-нибудь компонента на форме. Как известно, VCL однопоточна, весь код внутри приложения выполняется последовательно. Допустим, в процессе работы изменились какие-то данные внутри классов VCL, система отбирает время у основного потока, передаёт по кругу остальным потокам и возвращает обратно, при этом выполнение кода продолжается с того места, где приостановилось. Если мы из своего потока что-то меняем, к примеру, на форме, задействуется много механизмов внутри VCL (напомню, выполнение основного потока пока "приостановлено"), соответственно за это время успеют измениться какие-либо данные. И тут вдруг время снова отдаётся основному потоку, он спокойно продолжает своё выполнение, но данные уже изменены! К чему это может привести - предугадать нельзя. Вы можете проверить это тысячу раз, и ничего не произойдёт, а на тысяча первый программа рухнет. И это относится не только к взаимодействию дополнительных потоков с главным, но и к взаимодействию потоков между собой. Писать такие ненадёжные программы конечно нельзя.

Вот мы и подошли к очень важному вопросу — синхронизации потоков.

Если вы создали шаблон класса автоматически, то, наверное, заметили комментарий, который дружелюбная Delphi поместила в новый модуль. Он гласит: "Methods and properties of objects in visual components can only be used in a method called using Synchronize". Это значит, что обращение к визуальным компонентам возможно только путём вызова процедуры Synchronize. Давайте рассмотрим пример, но теперь наш поток не будет разогревать процессор впустую, а будет делать что-нибудь полезное, к примеру, прокручивать ProgressBar на форме. В качестве параметра в процедуру Synchronize передаётся метод нашего потока, но сам он передаётся без параметров. Параметры можно передать, добавив поля нужного типа в описание нашего класса. У нас будет одно поле - тот самый прогресс:

Вот теперь ProgressBar двигается, и это вполне безопасно. А безопасно вот почему: процедура Synchronize на время приостанавливает выполнение нашего потока, и передаёт управление главному потоку, т.е. SetProgress выполняется в главном потоке. Это нужно запомнить, потому что некоторые допускают ошибки, выполняя внутри Synchronize длительную работу, при этом, что очевидно, форма зависает на длительное время. Поэтому используйте Synchronize для вывода информации - то самое двигание прогресса, обновления заголовков компонентов и т.д.

Вы наверное заметили, что внутри цикла мы используем процедуру Sleep. В однопоточном приложении Sleep используется редко, а вот в потоках его использовать очень удобно. Пример - бесконечный цикл, пока не выполнится какое-нибудь условие. Если не вставить туда Sleep мы будем просто нагружать систему бесполезной работой.

Теперь мы немного изменим, можно сказать даже упростим, реализацию метода Execute нашего потока:

Вот, в принципе, мы и рассмотрели основные способы работы с компонентами VCL из потоков. А как быть, если в нашей программе не один новый поток, а несколько? И нужно организовать работу с одними и теми же данными? Тут нам на помощь приходят другие способы синхронизации. Один из них мы и рассмотрим. Для его реализации нужно добавить в проект модуль SyncObjs.

Самый интересный способ, на мой взгляд — критические секции

Работают они следующим образом: внутри критической секции может работать только один поток, другие ждут его завершения. Чтобы лучше понять, везде приводят сравнение с узкой трубой: представьте, с одной стороны "толпятся" потоки, но в трубу может "пролезть" только один, а когда он "пролезет" - начнёт движение второй, и так по порядку. Еще проще понять это на примере и тем же ProgressBar'ом. Итак, запустите один из примеров, приведённых ранее. Нажмите на кнопку, подождите несколько секунд, а затем нажмите еще раз. Что происходит? ProgressBar начал прыгать. Прыгает потому, что у нас работает не один поток, а два, и каждый из них передаёт разные значения прогресса. Теперь немного переделаем код, в событии onCreate формы создадим критическую секцию:

У TCriticalSection есть два нужных нам метода, Enter и Leave, соответственно вход и выход из неё. Поместим наш код в критическую секцию:

Попробуйте запустить приложение и нажать несколько раз на кнопку, а потом посчитайте, сколько раз пройдёт прогресс. Понятно, в чем суть? Первый раз, нажимая на кнопку, мы создаём поток, он занимает критическую секцию и начинает работу. Нажимаем второй - создаётся второй поток, но критическая секция занята, и он ждёт, пока её не освободит первый. Третий, четвёртый - все пройдут только по-очереди.

Критические секции удобно использовать при обработке одних и тех же данных (списков, массивов) разными потоками. Поняв, как они работают, вы всегда найдёте им применение.

В этой небольшой статье рассмотрены не все способы синхронизации, есть еще события (TEvent), а так же объекты системы, такие как мьютексы (Mutex), семафоры (Semaphore), но они больше подходят для взаимодействия между приложениями. Остальное, что касается использования класса TThread, вы можете узнать самостоятельно, в help'е всё довольно подробно описано. Цель этой статьи - показать начинающим, что не всё так сложно и страшно, главное разобраться, что есть что. И побольше практики — самое главное опыт!

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Сама концепция потоков проста. Вы можете мыслить их как корабль, отправляющийся в дальнее плавание. На Марс, например. Вроде как и можно устроить сеанс связи с Хьюстоном, но это геморрой, поэтому нужно на корабль погрузить все до старта, и только после возврата вы сможете разобрать трофеи. Лучше всего - запустить и забыть. Даже если вы запустите 20 кораблей, то возвращаться они будут по одному. И на каждом будет написан его бортовой номер, чтобы вы могли их отличать.
Как-то так.
И точно потоки не помогут вам ничего рисовать на экране или двигать компоненты. Зато они могут все рассчитать для этого, создать битмапы и заполнить какие-то структуры данных. А вы уже в главном потоке все это будете использовать для вывода на экран.

Часто путают поток ОС с классом TThread
Это не одно и то же. TThread призван запомнить все данные, нужные в потоке, запустить поток в операционной системе, выполнить действие в нем и вернуть результат.

Жизнь потока состоит из трех этапов
1. создание и инициализация (это еще не поток с т.з. ОС)
2. запуск настоящего потока (вот тут настоящий поток ОС работает)
3. окончание работы потока
Обратите внимание! В пунктах 1 и 3 работа идет в главном потоке, там, где формы и пользовательский ввод.
Только 2-я часть уходит в автономное плавание, забирая с собой все переменные, которые вы ей насовали с собой в части 1

Начнем
1. Нужно создать своего наследника TThread, имеющего все нужные поля. Заполнить эти поля. Запустить поток.
Примечание.
Все дополнительные классы выносите в дополнительные юниты! Пусть класс TMyThread будет жить в UMyThread.pas
UMyThread мы должны прописать в uses у формы, откуда будет запускать свои потоки.
Юнит UMyThread ничего о форме знать НЕ ДОЛЖЕН!

Лирическое отступление
Класс TThread имеет замечательное свойство FreeOnTerminate. Если установить его в true, то не нужно будет хранить ссылку на созданный объект, чтобы потом удалить его вручную. Создали, запустили и забыли о нем. Память освободится сама. Сделать это лучше всего, переопределив конструктор нашего наследника, где и вписать FreeOnTerminate := true;
В том же конструкторе мы должны вызвать конструктор предка - класса TThread - с параметром true
Это значит, он не запустится сразу же, а даст нам сначала заполнить поля объекта, а мы потом его запустим командой Resume. (В свежих версиях Delphi вместо Resume нужно вызывать Start)

3. Главное. Как мы получим данные от потока?
У класса TThread есть обработчик OnTerminate. Если на него назначить нашу собственную процедуру, то она будет вызываться после Execute для каждого потока. Это удобно. Дважды удобно то, что OnTerminate работает уже в главном потоке и не надо ничего делать для синхронизации, можно прямо писать в компоненты, например выводить в мемо, или заполнять структуры данных, не боясь, что другие потоки тоже лезут туда. Все синхронизировано по факту. Поэтому это лучший метод возврата результата из потока.
Итак. Объявим метод формы с любым именем, лишь бы параметры были Sender: TObject

Для красоты и простоты переопределим конструктор нашего наследника, чтобы можно было указывать этот метод

Примечание.
Рекомендуется не заполонять систему своими потоками. 3000 потоков вашей программы почти с гарантией поставят систему на колени. Поэтому одновременно запустим N потоков. После окончания каждого одного будем запускать один следующий, пока не стартуем нужное нам количество. При этом активно всегда будет не более N потоков.

Итак, сценарий.
Запустить N потоков.
При завершении - проверять, нужно ли запустить еще, или нет.
Также проверять, если все потоки завершены, то подвести итог.
Кнопку запуска потоков сделаем неактивной до времени, когда закончится последний поток, чтобы нельзя было запустить процесс еще раз.
Реализация

Звезда активна
Звезда активна
Звезда активна
Звезда активна
Звезда активна

Перед разработчиком приложений Windows нередко встаёт вопрос, как обмениваться данными между двумя и более приложениями. Это бывает необходимо, например, если вы разрабатываете службу Windows и приложение для настройки и мониторинга за этой службой.

В данной статье, я хотел бы рассмотреть только один из перечисленных механизмов – именованные каналы (Named Pipes). Вообще существует два вида каналов: именованные и анонимные. Анонимные каналы обычно создаются родительским процессом и передаются дочернему процессу при его создании. У именованных каналов есть имя и любой процесс, знающий это имя, может подключиться к каналу. В любом случае, при работе с каналами у вас есть сервер (приложение создающее канал) и клиенты (приложения, подключающиеся к каналу).

Для работы с каналами в Windows есть системные функции, такие как CreateNamedPipe, TransactNamedPipe и другие. Но реализация Pipe-сервера и Pipe-клиента не лёгкая задача, поэтому я отправился на поиски готовых реализаций в Интернет.

Демонстрационные классы FWIOCompletionPipe

Первое, что я нашел решение FWIOCompletionPipes. Собственно это демонстрационная реализация сервера и клиента. В архиве вы найдёте юнит FWIOCompletionPipes.pas и примеры. В юните FWIOCompletionPipes.pas есть класс для создания сервера TFWPipeServer и класс клиента TFWPipeClient. Сразу скажу, что работу сервера я не проверял, а клиент пригодился, но об этом ниже.

Юнит Pipes

(Старая версия. Лучше использовать версию Pipes.pas (Win32 и Win64), см. ниже). В юните реализация классов TPipeServer (Pipe-сервер), TPipeClient (Pipe-клиент) и TPipeConsole (класс для запуска консольных приложений, управления ими и перехвата потока вывода). Работает только на платформе Win32. Юнит с моими правками для работы с Delphi до версии XE3. Функция TPipeConsole.Execute с моими правками. Источник здесь.

Есть вариант юнита c поддержкой всех версий Delphi и платформы Win64:

Юнит Pipes.pas с примерами, с runttime и designtime библиотеками и поддержкой платформы Win64. В юните Pipes.pas реализация классов TPipeServer (Pipe-сервер), TPipeClient (Pipe-клиент) и TPipeConsole (класс для запуска консольных приложений, управления ими и перехвата потока вывода). Должна работать с Delphi всех версий, но тестирование не проводилось. Функция TPipeConsole.Execute с моими правками. Источник здесь.

27.04.2017 Добавлены мои правки в функции TPipeConsole.Execute и TPipeConsole.Start, чтобы параметр CommandLine 100%-но не был константой, чтобы избежать AV, см. MSDN.

В юните есть класс-сервер TPipeServer, класс-клиент TPipeClient и даже есть приятный бонус - класс для запуска и управления консольным приложением с перехватом потока вывода TPipeConsole. Юнит лучше всего подключить к новому bpl-проекту и инсталлировать в среде Delphi. Тогда компоненты будут доступны в окне инструментов Tool Palette.

Демонстрационные классы для работы с именованными каналами Windows (Named Pipes) и пример.

А вот пример использования функции SendMessageToServer:

Позже, при тестировании, выяснилось, что в классе TFWPipeClient неправильно реализовано подключение к каналу, поэтому при активной работе с каналом часто возникает ошибка System Error. Code: 231. Все копии канала заняты. Поэтому мне пришлось переписать функцию SendMessageToServer отказавшись от использования класса TFWPipeClient:

Подведём итог. Используя вышеописанное решение, вы можете быстро наладить взаимодействие между несколькими процессами на одном компьютере под управлением Windows. Вы сможете отправлять большие объёмы информации и реализовать синхронное и асинхронное взаимодействие.

Комментарии

Понятно, тогда получается нужно определить почему после записи заголовка перестают писаться данные. И судя по всему уже при записи заголовка, несмотря на то что функция WriteFile возвращает true, происходит что-то не по плану, потомучто в серверной компоненте не вызывается функция QueuedWrite после записи заголовка в клиенте.

Понятно, тогда получается нужно определить почему после записи заголовка перестают писаться данные. И судя по всему уже при записи заголовка, несмотря на то что функция WriteFile возвращает true, происходит что-то не по плану, потомучто в серверной компоненте не вызывается функция QueuedWrite после записи заголовка в клиенте.

Result := GetOverlappedResult(FPipe, FOlapRead, FRcvRead, TRUE);

Попробуйте заменить код функции на следующий (у меня так работает):

// Reset pending read
FPendingRead := FALSE;

// Check the overlapped results
//Result := GetOverlappedResult(FPipe, FOlapRead, FRcvRead, TRUE);

bContinue := true;
while bContinue do
begin
bContinue := false;
Result := GetOverlappedResult(FPipe, FOlapRead, FRcvRead, FALSE);

// Handle failure
if not(Result) then begin
// Get the last error code
FErrorCode := GetLastError;
if (FErrorCode = ERROR_IO_INCOMPLETE) then begin
// Operation is still pending, allow while loop
bContinue := true;
FErrorCode := 0;
end
else if (FErrorCode = ERROR_HANDLE_EOF) then begin
FErrorCode := 0;
Result := true;
end
// Check for more data
else if (FErrorCode = ERROR_MORE_DATA) then begin
// Write the current data to the stream
FRcvStream.Write(FRcvBuffer^, FRcvSize);
// Determine how much we need to expand the buffer to
Result := PeekNamedPipe(FPipe, nil, 0, nil, nil, @FRcvSize);
// Check result
if Result then begin
// Determine if required size is larger than allocated size
if (FRcvSize > FRcvAlloc) then begin
// Realloc buffer
ReallocMem(FRcvBuffer, FRcvSize);
// Update allocated size
FRcvAlloc := FRcvSize;
end;
// Set overlapped fields
ClearOverlapped(FOlapRead);
// Read from the file again
Result := ReadFile(FPipe, FRcvBuffer^, FRcvSize, FRcvRead,
@FOlapRead);
// Handle error
if not(Result) then begin
// Set error code
FErrorCode := GetLastError;
// Check for pending again, which means our state hasn't changed
if (FErrorCode = ERROR_IO_PENDING) then begin
// Still a pending read
FPendingRead := TRUE;
// Success
Result := TRUE;
end;
end;
end
else
// Set error code
FErrorCode := GetLastError;
end;
end;
end;

// Check result and pending read flag
if Result and not(FPendingRead) then begin
// We have the full message
FRcvStream.Write(FRcvBuffer^, FRcvRead);
// Call the OnData
DoMessage;
end;
end;

Result := GetOverlappedResult(FPipe, FOlapRead, FRcvRead, TRUE);

Попробуйте заменить код функции на следующий (у меня так работает):


У меня хорошо работает. Попробуйте мои тестовые проекты (в архиве исходники и exe-шники): yadi.sk/d/eZF1Iog536qCYv

У меня хорошо работает. Попробуйте мои тестовые проекты (в архиве исходники и exe-шники): yadi.sk/d/eZF1Iog536qCYv

Создав поток с помощью API функции CreateThread и передав ему параметр, как можно в делфи принять или извлечь этот параметр?


3 ответа 3

Для этого есть специальный аргумент в данной функции - lpParameter. То есть можно передать указатель на переменную или инициализированную структуру, а в потоке использовать эти значения для совершения необходимых действий.

Передавать его просто - сделать операцию получения адреса. Получить его - присвоить адрес своей переменной и сделать разыменование адреса.

Читайте также: