Работа с потоками в Delphi
Данная статья предназначена для начинающих программистов, которые
никогда не работали с потоками, и хотели бы узнать основы работы с
ними. Желательно, чтоб читатель знал основы ООП и имел какой-нибудь
опыт работы в Delphi. Для начала давайте определимся, что под словом
"поток" я подразумеваю именно Thread, который еще имеет название
"нить".
Нередко встречал на форумах мнения, что потоки не нужны вообще, любую
программу можно написать так, что она будет замечательно работать и без
них. Конечно, если не делать ничего серьёзней "Hello World" это так и
есть, но если постепенно набирать опыт, рано или поздно любой
начинающий программист упрётся в возможности "плоского" кода, возникнет
необходимость распараллелить задачи. А некоторые задачи вообще нельзя
реализовать без использования потоков, например работа с сокетами,
COM-портом, длительное ожидание каких-либо событий, и т.д.
Всем
известно, что Windows система многозадачная. Попросту говоря, это
означает, что несколько программ могут работать одновременно под
управлением ОС. Все мы открывали диспетчер задач и видели список
процессов. Процесс - это экземпляр выполняемого приложения. На самом
деле сам по себе он ничего не выполняет, он создаётся при запуске
приложения, содержит в себе служебную информацию, через которую система
с ним работает, так же ему выделяется необходимая память под код и
данные. Для того, чтобы программа заработала, в нём создаётся поток.
Любой процесс содержит в себе хотя бы один поток, и именно он отвечает
за выполнение кода и получает на это процессорное время. Этим и
достигается мнимая параллельность работы программ, или, как её еще
называют, псевдопараллельность. Почему мнимая? Да потому, что реально
процессор в каждый момент времени может выполнять только один участок
кода. Windows раздаёт процессорное время всем потокам в системе по
очереди, тем самым создаётся впечатление, что они работают
одновременно. Реально работающие параллельно потоки могут быть только
на машинах с двумя и более процессорами.
Для создания
дополнительных потоков в Delphi существует базовый класс TThread, от
него мы и будем наследоваться при реализации своих потоков. Для того,
чтобы создать "скелет" нового класса, можно выбрать в меню File - New -
Thread Object, Delphi создаст новый модуль с заготовкой этого класса. Я
же для наглядности опишу его в модуле формы. Как видите, в этой
заготовке добавлен один метод - Execute. Именно его нам и нужно
переопределить, код внутри него и будет работать в отдельном потоке. И
так, попробуем написать пример - запустим в потоке бесконечный цикл:
TNewThread = class(TThread) private { Private declarations } protected procedure Execute; override; end;
var Form1: TForm1;
implementation
{$R *.dfm}
{ TNewThread }
procedure TNewThread.Execute; begin while true do {ничего не делаем}; end;
procedure TForm1.Button1Click(Sender: TObject); var NewThread: TNewThread; begin NewThread:=TNewThread.Create(true); NewThread.FreeOnTerminate:=true; NewThread.Priority:=tpLower; NewThread.Resume; end; Запустите
пример на выполнение и нажмите кнопку. Вроде ничего не происходит -
форма не зависла, реагирует на перемещения. На самом деле это не так -
откройте диспетчер задач и вы увидите, что процессор загружен
по-полной. Сейчас в процессе вашего приложения работает два потока -
один был создан изначально, при запуске приложения. Второй, который так
грузит процессор - мы создали по нажатию кнопки. Итак, давайте
разберём, что же означает код в Button1Click:
NewThread:=TNewThread.Create(true); тут
мы создали экземпляр класса TNewThread. Конструктор Create имеет всего
один параметр - CreateSuspended типа boolean, который указывает,
запустить новый поток сразу после создания (если false), или дождаться
команды (если true).
New.FreeOnTerminate := true; свойство
FreeOnTerminate определяет, что поток после выполнения автоматически
завершится, объект будет уничтожен, и нам не придётся его уничтожать
вручную. В нашем примере это не имеет значения, так как сам по себе он
никогда не завершится, но понадобится в следующих примерах.
NewThread.Priority:=tpLower; Свойство
Priority, если вы еще не догадались из названия, устанавливает
приоритет потока. Да да, каждый поток в системе имеет свой приоритет.
Если процессорного времени не хватает, система начинает распределять
его согласно приоритетам потоков. Свойство Priority может принимать
следующие значения:
- tpTimeCritical - критический
- tpHighest - очень высокий
- tpHigher - высокий
- tpNormal - средний
- tpLower - низкий
- tpLowest - очень низкий
- tpIdle - поток работает во время простоя системы
Ставить высокие приоритеты потокам не стоит, если этого не требует задача, так как это сильно нагружает систему.
NewThread.Resume; Ну и собственно, запуск потока.
Думаю,
теперь вам понятно, как создаются потоки. Заметьте, ничего сложного. Но
не всё так просто. Казалось бы - пишем любой код внутри метода 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 передаётся метод нашего потока, но
сам он передаётся без параметров. Параметры можно передать, добавив
поля нужного типа в описание нашего класса. У нас будет одно поле - тот
самый прогресс:
TNewThread = class(TThread) private Progress: integer; procedure SetProgress; protected procedure Execute; override; end; ...
procedure TNewThread.Execute; var i: integer; begin for i:=0 to 100 do begin sleep(50); Progress:=i; Synchronize(SetProgress); end; end;
procedure TNewThread.SetProgress; begin Form1.ProgressBar1.Position:=Progress; end; Вот
теперь ProgressBar двигается, и это вполне безопасно. А безопасно вот
почему: процедура Synchronize на время приостанавливает выполнение
нашего потока, и передаёт управление главному потоку, т.е. SetProgress
выполняется в главном потоке. Это нужно запомнить, потому что некоторые
допускают ошибки, выполняя внутри Synchronize длительную работу, при
этом, что очевидно, форма зависает на длительное время. Поэтому
используйте Synchronize для вывода информации - то самое двигание
прогресса, обновления заголовков компонентов и т.д.
Вы наверное
заметили, что внутри цикла мы используем процедуру Sleep. В
однопоточном приложении Sleep используется редко, а вот в потоках его
использовать очень удобно. Пример - бесконечный цикл, пока не
выполнится какое-нибудь условие. Если не вставить туда Sleep мы будем
просто нагружать систему бесполезной работой.
Надеюсь, вы поняли
как работает Synchronize. Но есть еще один довольно удобный способ
передать информацию форме - посылка сообщения. Давайте рассмотрим и
его. Для этого объявим константу:
const PROGRESS_POS = WM_USER+1; В объявление класса формы добавим новый метод, а затем и его реализацию:
TForm1 = class(TForm) Button1: TButton; ProgressBar1: TProgressBar; procedure Button1Click(Sender: TObject); private procedure SetProgressPos(var Msg: TMessage); message PROGRESS_POS; public { Public declarations } end; ...
procedure TForm1.SetProgressPos(var Msg: TMessage); begin ProgressBar1.Position:=Msg.LParam; end; Теперь мы немного изменим, можно сказать даже упростим, реализацию метода Execute нашего потока:
procedure TNewThread.Execute; var i: integer; begin for i:=0 to 100 do begin sleep(50); SendMessage(Form1.Handle,PROGRESS_POS,0,i); end; end; Используя
функцию SendMessage, мы посылаем окну приложения сообщение, один из
параметров которого содержит нужный нам прогресс. Сообщение становится
в очередь, и согласно этой очереди будет обработано главным потоком,
где и выполнится метод SetProgressPos. Но тут есть один нюанс:
SendMessage, как и в случае с Synchronize, приостановит выполнение
нашего потока, пока основной поток не обработает сообщение. Если
использовать PostMessage этого не произойдёт, наш поток отправит
сообщение и продолжит свою работу, а уж когда оно там обработается -
неважно. Какую из этих функций использовать - решать вам, всё зависит
от задачи.
Вот, в принципе, мы и рассмотрели основные способы
работы с компонентами VCL из потоков. А как быть, если в нашей
программе не один новый поток, а несколько? И нужно организовать работу
с одними и теми же данными? Тут нам на помощь приходят другие способы
синхронизации. Один из них мы и рассмотрим. Для его реализации нужно
добавить в проект модуль SyncObjs.
Критические секцииРаботают
они следующим образом: внутри критической секции может работать только
один поток, другие ждут его завершения. Чтобы лучше понять, везде
приводят сравнение с узкой трубой: представьте, с одной стороны
"толпятся" потоки, но в трубу может "пролезть" только один, а когда он
"пролезет" - начнёт движение второй, и так по порядку. Еще проще понять
это на примере и тем же ProgressBar'ом. Итак, запустите один из
примеров, приведённых ранее. Нажмите на кнопку, подождите несколько
секунд, а затем нажмите еще раз. Что происходит? ProgressBar начал
прыгать. Прыгает потому, что у нас работает не один поток, а два, и
каждый из них передаёт разные значения прогресса. Теперь немного
переделаем код, в событии onCreate формы создадим критическую секцию:
var Form1: TForm1; CriticalSection: TCriticalSection;
...
procedure TForm1.FormCreate(Sender: TObject); begin CriticalSection:=TCriticalSection.Create; end; У
TCriticalSection есть два нужных нам метода, Enter и Leave,
соответственно вход и выход из неё. Поместим наш код в критическую
секцию:
procedure TNewThread.Execute; var i: integer; begin CriticalSection.Enter; for i:=0 to 100 do begin sleep(50); SendMessage(Form1.Handle,PROGRESS_POS,0,i); end; CriticalSection.Leave; end; Попробуйте
запустить приложение и нажать несколько раз на кнопку, а потом
посчитайте, сколько раз пройдёт прогресс. Понятно, в чем суть? Первый
раз, нажимая на кнопку, мы создаём поток, он занимает критическую
секцию и начинает работу. Нажимаем второй - создаётся второй поток, но
критическая секция занята, и он ждёт, пока её не освободит первый.
Третий, четвёртый - все пройдут только по-очереди.
Критические
секции удобно использовать при обработке одних и тех же данных
(списков, массивов) разными потоками. Поняв, как они работают, вы
всегда найдёте им применение.
В этой небольшой статье
рассмотрены не все способы синхронизации, есть еще события (TEvent), а
так же объекты системы, такие как мьютексы (Mutex), семафоры
(Semaphore), но они больше подходят для взаимодействия между
приложениями. Остальное, что касается использования класса TThread, вы
можете узнать самостоятельно, в help'е всё довольно подробно описано.
Цель этой статьи - показать начинающим, что не всё так сложно и
страшно, главное разобраться, что есть что. И побольше практики - самое
главное опыт!
|