Данная статья предназначена для начинающих программистов, которые
никогда не работали с потоками, и хотели бы узнать основы работы с ними.
Желательно, чтоб читатель знал основы ООП и имел какой-нибудь опыт
работы в Delphi. Для начала давайте определимся, что под словом "поток" я
подразумеваю именно Thread, который еще имеет название "нить".
Нередко встречал на форумах мнения, что потоки не нужны вообще, любую
программу можно написать так, что она будет замечательно работать и без
них. Конечно, если не делать ничего серьёзней "Hello World" это так и
есть, но если постепенно набирать опыт, рано или поздно любой начинающий
программист упрётся в возможности "плоского" кода, возникнет
необходимость распараллелить задачи. А некоторые задачи вообще нельзя
реализовать без использования потоков, например работа с сокетами,
COM-портом, длительное ожидание каких-либо событий, и т.д.
Всем
известно, что Windows система многозадачная. Попросту говоря, это
означает, что несколько программ могут работать одновременно под
управлением ОС. Все мы открывали диспетчер задач и видели список
процессов. Процесс - это экземпляр выполняемого приложения. На самом
деле сам по себе он ничего не выполняет, он создаётся при запуске
приложения, содержит в себе служебную информацию, через которую система с
ним работает, так же ему выделяется необходимая память под код и
данные. Для того, чтобы программа заработала, в нём создаётся поток.
Любой процесс содержит в себе хотя бы один поток, и именно он отвечает
за выполнение кода и получает на это процессорное время. Этим и
достигается мнимая параллельность работы программ, или, как её еще
называют, псевдопараллельность. Почему мнимая? Да потому, что реально
процессор в каждый момент времени может выполнять только один участок
кода. Windows раздаёт процессорное время всем потокам в системе по
очереди, тем самым создаётся впечатление, что они работают одновременно.
Реально работающие параллельно потоки могут быть только на машинах с
двумя и более процессорами.
Для создания дополнительных потоков в
Delphi существует базовый класс TThread, от него мы и будем
наследоваться при реализации своих потоков. Для того, чтобы создать
"скелет" нового класса, можно выбрать в меню File - New - Thread Object,
Delphi создаст новый модуль с заготовкой этого класса. Я же для
наглядности опишу его в модуле формы. Как видите, в этой заготовке
добавлен один метод - Execute. Именно его нам и нужно переопределить,
код внутри него и будет работать в отдельном потоке. И так, попробуем
написать пример - запустим в потоке бесконечный цикл:
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 передаётся метод нашего потока, но сам он
передаётся без параметров. Параметры можно передать, добавив поля
нужного типа в описание нашего класса. У нас будет одно поле - тот самый
прогресс:
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;
В
объявление класса формы добавим новый метод, а затем и его реализацию:
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'е
всё довольно подробно описано. Цель этой статьи - показать начинающим,
что не всё так сложно и страшно, главное разобраться, что есть что. И
побольше практики - самое главное опыт!
http://ukrsud.info Наш вебсайт - информационный ресурс, призванный помочь каждому человеку в получении надежных сведений по проблемам организации защиты своих законных интересов. http://ukrsud.info/reshenie-suda-po-isku-fizicheskogo-lica-263 - основного долга с учетом Мы осуществили обобщения судебной практики Украины. http://ukrsud.info/reshenie-suda-po-isku-gosudarstvennogo-27 - 22 Хозяйственного процессуального кодекса Украины Судебная практика включает организацию, процесс и итоги деятельности судебной системы по притворению в жизнь правосудия, но первее всего объектом ее исследования считается правоприменительная работа судов, анализ качества судебной работы. http://ukrsud.info/reshenie-suda-po-isku-o-priznanii-nedejstvitelnymi - «О Государственном комитете Благодаря применению современных поисковых технологий, у Вас появилась возможность быстро ознакомиться с существующей судебной практикой по схожим делам и произвести вывод, в чью пользу формируется современная украинская судебная практика. http://ukrsud.info/reshenie-suda-po-isku-fonda-kommunalnogo-2 - городского совета и ООО
Znakomye predostavili МНК URL: http://srub-dizain.ru/pilomaterial - obreznaya doska производителя Киржач и я сразу организовал заказ! Мне тогда срочно нужен был брусок для лестницы. В общем после этого я и повёз туда деньги! Там реально всё качественно сделано и профилированный брус естественной влажности не тяжело выбрать. Всему данному событию сопутствовала трудная навигация ресурса, которую быстро обнаружил.Очень круто конечно то, что это изготовитель пиломатериалов – но сайтец очень трудно понять.