Пятница, 27.12.2024, 05:21 Приветствую вас Гость | Группа "Гости" 
Меню сайта

Категории раздела
Вирусология [40]
Статьи о вирусах
Системные [1]
Работа с системой
Примеры [45]
Приёмы, функции, процедуры
Ceти [1]
Работа с интернет
Шуточные программы [5]
Пишем шуточные программки
Остальное [6]
Всё что не вошло

Популярные статьи

Недавние темы

Опрос
На каком языке кодите?

Всего ответов: 780

Главная » Статьи » Delphi » Примеры

Работа с потоками в Delphi
Нередко встречал на форумах мнения, что потоки не нужны вообще, любую программу можно написать так, что она будет замечательно работать и без них. Конечно, если не делать ничего серьёзней "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"е всё довольно подробно описано. Цель этой статьи - показать начинающим, что не всё так сложно и страшно, главное разобраться, что есть что. И побольше практики - самое главное опыт!
Категория: Примеры | Добавил: dolphin (21.04.2011)
Просмотров: 18741 | Комментарии: 4 | Рейтинг: 3.1/13

Всего комментариев: 4
avatar
4 IgorChekun • 00:02, 30.12.2011
Но ведь полюбому будут системные потоки с большим приоритетом, даже при простое пк, ведь так? Даже взять, например, svchost процесс, приоритет средний.
avatar
3 dolphin • 22:04, 29.12.2011
Помойму это в рамках одной программы при создании нескольких потоков. Если поток с более высоким приоритетом приостановлен или закончен - начинается следующий с более низким
avatar
2 IgorChekun • 23:57, 28.12.2011
Спасибо, единственное, что хотелось спросить:
tpIdle - поток работает во время простоя системы
А когда это именно, что считается за простой системы, какоя-то минимальная нагрузка на проц?
avatar
1 kodecman • 18:37, 19.11.2011
спасибка
avatar
Профиль



Поиск

Наша кнопка
Вирусология, взгляд из Delphi

Статистика
Top.Mail.Ru Яндекс.Метрика Счетчик тИЦ и PR
Статистика материалов
Файлов: 457
Форум: 1175/8163
Коментариев: 770
Новостей: 29

Статистика пользователей
Всего: 385
За неделю: 0
Вчера: 0
Сегодня: 0
Всего онлайн: 1
Гостей: 1
Пользователей: 0

delphicode.ru © 2008 - 2024 Хостинг от uCoz