HHIDE_DUMP
Гость
H
HHIDE_DUMP
Гость
В этом уроке мы рассмотрим возможность разработки многопоточных приложений в Delphi. Для начала давайте разберем, для чего же нам нужны эти самые потоки, что они из себя представляют.
Принцип работы
Для того, чтобы реализовать многозадачность, операционная система выделяет каждому приложению определенное количество процессорного времени, которое она старается распределить по заданным приоритетам. Таким образом производительность каждой такой задачи/потока ограничивается операционной системой в рамках своего процессорного времени. Для того, чтобы выполнять какие-либо действия параллельно (псевдопараллельно), мы можем создавать дополнительные потоки, и в них выполнять те самые действия. Если грамотно распределить какие-либо сложные операции в нашем приложении, то это поможет значительно увеличить производительность ПО.
Реализация. Класс TThread
В модуле Classes в Delphi существует специальный класс TThread, предназначенный для создания потоков. Не забудьте подключить модуль Classes. Рассмотрим создание простого потока. Для этого нам потребуется написать свой класс, который будет наследоваться от класса TThread. Делать мы это будем в разделе type нашего модуля.
1 type
2 TMyThread = class(TThread) // Описываем класс нашего нового потока
3 private
4 { Private declarations }
5 protected
6 procedure Execute; override; // В методе Execute будет находиться непосредственно тело самого потока -
7 // код, который будет в нем обрабатываться.
8 end;
Теперь установим курсор на наше описание метода Execute, нажмем Ctrl+Shift+C, чтобы перейти к реализации этого метода. Мы увидим следующий код:
1 procedure TMyThread.Execute;
2 begin
3
4 end;
В теле этой процедуры мы и должны будем написать код, который будет обрабатываться в нашем потоке.
Вот таким несложным способом мы можем реализовать наш поток. Осталось только разобраться с тем, как его можно использовать. Создадим экземпляр нашего потока TMyThread:
1 var MyFirstThread: TMyThread;
2 begin
3 MyFirstThread := TMyThread.Create(False);
4 end;
В нашем случае, мы передали конструктору Create нашего потока параметр false. Это означает, что наш поток будет запущен сразу после создания. Если передать True, то поток будет запущен тогда, когда мы вызовем у него метод Resume. Сразу после запуска, поток начнет выполнять программный код метода Execute.
Потокам можно устанавливать их приоритет, от которого будет зависеть количество процессорного времени, которое выделит ОС на данный поток. Другими словами, мы можем настраивать производительность этого потока относительно других. Делается это при помощи свойства Priority у нашего потока:
1 MyFirstThread.Priority := tpNormal;
Существует 7 уровней приоритета для потоков:
Также существует полезное свойство FreeOnTerminate, если установить ему значение True, то поток будет автоматически уничтожен, как только будет завершен программный код метода Execute.
Для выполнения каких-либо циклических операций внутри потока, удобно использовать следующую конструкцию:
1 procedure TMyThread.Execute;
2 begin
3 while true do
4 begin
5 {Do something}
6 end;
7 end;
Содержимое цикла While потока будет обрабатываться, пока внутри цикла не будет вызван break, или пока поток не будет завершен вручную.
Ну, и наконец, уничтожить поток можно методом Terminate:
1 MyFirstThread.Terminate;
Соответственно существует метод Terminated, который возвращает true, если поток был завершен/уничтожен.
Бывает полезно использовать метод Suspend, для того, чтобы временно приостановить выполнение потока, после чего возобновить работу потока можно будет при помощи метода Resume. Чтобы узнать, приостановлен ли поток, можно использовать метод Suspended, который возвращает true, если поток приостановлен.
Синхронизация
Одной из самых главных проблем в работе с потоками является невозможность обращаться к каким-либо данным из двух потоков одновременно.
Вероятнее всего такое одновременное обращение приведет к ошибке Access Violation.
Самым типичным примером возникновения такой ошибки является обращение к GUI (к визуальным компонентам формы) из нового потока. Дело заключается в том, что GUI отрисовывается и постоянно обрабатывается в главном потоке вашего приложения, в котором также обрабатывается и другой ваш основной код. Так вот если к GUI мы обратимся еще из какого-то потока, то возникнет ошибка, т.к. GUI уже занят другим потоком. Одним из самых распространенных решений этой проблемы является синхронизация, о которой мы сейчас и поговорим.
Синхронизация позволяет выполнить какой-либо метод в главном потоке приложения, вызвав его при этом в другом потоке.
Запомните! Все методы, которые вы будете вызывать из тела потока, будут вызываться и обрабатываться в этом потоке. И только для вызова метода в главном потоке, используется синхронизация.
Для реализации примера синхронизации создадим метод ChangeGUI, в котором мы будем обращаться к нашему GUI, и соответственно, который будет выполняться в главном потоке, а вызываться из нашего нового. Также добавим в наш поток поле с названием num, в котором мы будем хранить число и постоянно увеличивать его внутри тела потока (для примера).
1 type
2 TMyThread = class(TThread) // Описываем класс нашего нового потока
3 private
4 { Private declarations }
5 num: integer;
6 protected
7 procedure ChangeGUI; // Метод, в котором мы будем обращаться к GUI, и который будет обрабатываться в главном потоке.
8 procedure Execute; override; // В методе Execute будет находиться непосредственно тело самого потока -
9 // код, который будет в нем обрабатываться.
10 end;
11 ...
12 procedure TMyThread.ChangeGUI;
13 begin
14 Form2.Label1.Caption := IntToStr(num); // Выводим новое значение num
15 end;
16 procedure TMyThread.Execute;
17 begin
18 while True do
19 begin
20 if num = 1000000 then
21 num := 0;
22 Inc(num); // Увеличиваем num на единицу
23 Synchronize(ChangeGUI); // Та самая синхронизация! Изменяем надпись Label1 в главном потоке
24 end;
25 end;
26 procedure TForm2.FormCreate(Sender: TObject);
27 var
28 MyFirstThread: TMyThread;
29 begin
30 MyFirstThread := TMyThread.Create(False); // Создаем наш поток
31 MyFirstThread.Priority := tpLower; // Устанавливаем приоритет
32 end;
Как вы уже заметили из приведенного выше листинга, для того, чтобы вызвать какой-либо метод потока в главном потоке, необходимо передать этот метод в качестве параметра для метода Synchronize. Если вызывать метод не через Synchronize, то я уверен, что ваша программа долго не проживет Скорее всего возникнет какой-то конфликт либо в главном потоке, либо в нашем новом, в результате чего программа завершит свое выполнение с ошибкой.
Принцип работы
Для того, чтобы реализовать многозадачность, операционная система выделяет каждому приложению определенное количество процессорного времени, которое она старается распределить по заданным приоритетам. Таким образом производительность каждой такой задачи/потока ограничивается операционной системой в рамках своего процессорного времени. Для того, чтобы выполнять какие-либо действия параллельно (псевдопараллельно), мы можем создавать дополнительные потоки, и в них выполнять те самые действия. Если грамотно распределить какие-либо сложные операции в нашем приложении, то это поможет значительно увеличить производительность ПО.
Реализация. Класс TThread
В модуле Classes в Delphi существует специальный класс TThread, предназначенный для создания потоков. Не забудьте подключить модуль Classes. Рассмотрим создание простого потока. Для этого нам потребуется написать свой класс, который будет наследоваться от класса TThread. Делать мы это будем в разделе type нашего модуля.
1 type
2 TMyThread = class(TThread) // Описываем класс нашего нового потока
3 private
4 { Private declarations }
5 protected
6 procedure Execute; override; // В методе Execute будет находиться непосредственно тело самого потока -
7 // код, который будет в нем обрабатываться.
8 end;
Теперь установим курсор на наше описание метода Execute, нажмем Ctrl+Shift+C, чтобы перейти к реализации этого метода. Мы увидим следующий код:
1 procedure TMyThread.Execute;
2 begin
3
4 end;
В теле этой процедуры мы и должны будем написать код, который будет обрабатываться в нашем потоке.
Вот таким несложным способом мы можем реализовать наш поток. Осталось только разобраться с тем, как его можно использовать. Создадим экземпляр нашего потока TMyThread:
1 var MyFirstThread: TMyThread;
2 begin
3 MyFirstThread := TMyThread.Create(False);
4 end;
В нашем случае, мы передали конструктору Create нашего потока параметр false. Это означает, что наш поток будет запущен сразу после создания. Если передать True, то поток будет запущен тогда, когда мы вызовем у него метод Resume. Сразу после запуска, поток начнет выполнять программный код метода Execute.
Потокам можно устанавливать их приоритет, от которого будет зависеть количество процессорного времени, которое выделит ОС на данный поток. Другими словами, мы можем настраивать производительность этого потока относительно других. Делается это при помощи свойства Priority у нашего потока:
1 MyFirstThread.Priority := tpNormal;
Существует 7 уровней приоритета для потоков:
- tpIdle — Самый низкий уровень, поток выполнятся во время «простоя» системы.
- tpLowest
- tpLower
- tpNormal
- tpHigher
- tpHighest
- tpTimeCritical — Самый высокий уровень. Приоритет исполняемого потока равноценен приоритету ядра ОС.
Также существует полезное свойство FreeOnTerminate, если установить ему значение True, то поток будет автоматически уничтожен, как только будет завершен программный код метода Execute.
Для выполнения каких-либо циклических операций внутри потока, удобно использовать следующую конструкцию:
1 procedure TMyThread.Execute;
2 begin
3 while true do
4 begin
5 {Do something}
6 end;
7 end;
Содержимое цикла While потока будет обрабатываться, пока внутри цикла не будет вызван break, или пока поток не будет завершен вручную.
Ну, и наконец, уничтожить поток можно методом Terminate:
1 MyFirstThread.Terminate;
Соответственно существует метод Terminated, который возвращает true, если поток был завершен/уничтожен.
Бывает полезно использовать метод Suspend, для того, чтобы временно приостановить выполнение потока, после чего возобновить работу потока можно будет при помощи метода Resume. Чтобы узнать, приостановлен ли поток, можно использовать метод Suspended, который возвращает true, если поток приостановлен.
Синхронизация
Одной из самых главных проблем в работе с потоками является невозможность обращаться к каким-либо данным из двух потоков одновременно.
Вероятнее всего такое одновременное обращение приведет к ошибке Access Violation.
Самым типичным примером возникновения такой ошибки является обращение к GUI (к визуальным компонентам формы) из нового потока. Дело заключается в том, что GUI отрисовывается и постоянно обрабатывается в главном потоке вашего приложения, в котором также обрабатывается и другой ваш основной код. Так вот если к GUI мы обратимся еще из какого-то потока, то возникнет ошибка, т.к. GUI уже занят другим потоком. Одним из самых распространенных решений этой проблемы является синхронизация, о которой мы сейчас и поговорим.
Синхронизация позволяет выполнить какой-либо метод в главном потоке приложения, вызвав его при этом в другом потоке.
Запомните! Все методы, которые вы будете вызывать из тела потока, будут вызываться и обрабатываться в этом потоке. И только для вызова метода в главном потоке, используется синхронизация.
Для реализации примера синхронизации создадим метод ChangeGUI, в котором мы будем обращаться к нашему GUI, и соответственно, который будет выполняться в главном потоке, а вызываться из нашего нового. Также добавим в наш поток поле с названием num, в котором мы будем хранить число и постоянно увеличивать его внутри тела потока (для примера).
1 type
2 TMyThread = class(TThread) // Описываем класс нашего нового потока
3 private
4 { Private declarations }
5 num: integer;
6 protected
7 procedure ChangeGUI; // Метод, в котором мы будем обращаться к GUI, и который будет обрабатываться в главном потоке.
8 procedure Execute; override; // В методе Execute будет находиться непосредственно тело самого потока -
9 // код, который будет в нем обрабатываться.
10 end;
11 ...
12 procedure TMyThread.ChangeGUI;
13 begin
14 Form2.Label1.Caption := IntToStr(num); // Выводим новое значение num
15 end;
16 procedure TMyThread.Execute;
17 begin
18 while True do
19 begin
20 if num = 1000000 then
21 num := 0;
22 Inc(num); // Увеличиваем num на единицу
23 Synchronize(ChangeGUI); // Та самая синхронизация! Изменяем надпись Label1 в главном потоке
24 end;
25 end;
26 procedure TForm2.FormCreate(Sender: TObject);
27 var
28 MyFirstThread: TMyThread;
29 begin
30 MyFirstThread := TMyThread.Create(False); // Создаем наш поток
31 MyFirstThread.Priority := tpLower; // Устанавливаем приоритет
32 end;
Как вы уже заметили из приведенного выше листинга, для того, чтобы вызвать какой-либо метод потока в главном потоке, необходимо передать этот метод в качестве параметра для метода Synchronize. Если вызывать метод не через Synchronize, то я уверен, что ваша программа долго не проживет Скорее всего возникнет какой-то конфликт либо в главном потоке, либо в нашем новом, в результате чего программа завершит свое выполнение с ошибкой.