Общий обзор потоков
Инициализация потоков
Приоритеты потоков
Поведение потока при завершении его работы
Пример создания многопоточного приложения в Kylix
Использование главного CLX-потока
Координация потоков
В этой главе мы рассмотрим проблему одновременного выполнения нескольких задач
внутри одного приложения. Вы научитесь использовать объекты потоков и координировать
их работу.
Первое определение, которое мы рассмотрим, - это многопоточностъ. Многопоточность
используется для:
обхода медленных процессов. Когда используется только один поток, приложение
может приостановить свое выполнение на то время, пока им завершается какой-либо
медленный процесс (доступ к диску, связь с другим компьютером по сети и др.).
Центральный процессор компьютера в данный момент находится в режиме ожидания
и практически не выполняет никаких команд. С использованием многопоточности
ваше приложение может продолжать выполнение других потоков, пока один из потоков
ожидает завершения медленного процесса;
организации поведения приложения. Благодаря использованию потоков вы можете
организовать выполнение частей приложения так, как вам захочется. Например,
вы можете для каждой задачи приложения (если каждой задаче выделен свой поток)
распределить приоритеты выполнения. Таким образом, задача, имеющая наибольший
приоритет, будет занимать больше процессорного времени, что очень важно для
решения критических задач;
поддержки мультипроцессорной обработки. Если в компьютере, на котором запущено
многопоточное приложение, имеется несколько процессоров, то можно значительно
увеличить скорость выполнения вашего приложения, направляя на каждый процессор
свой поток.
Примечание
Linux - это многопоточная операционная система с поддержкой архитектуры Intel
MP. Процессы в Linux представляют собой отдельные задачи. Каждый процесс запускается
в собственном виртуальном адресном пространстве и не способен взаимодействовать
с другими процессами. Исключение составляют лишь процессы, обеспечивающие механизмы
защиты Linux, и процессы ядра операционной системы.
Общий обзор потоков
Поток (Thread) - это объект операционной системы, заключенный в процесс и реализующий
какую-либо задачу. Любое приложение (процесс) содержит несколько потоков (как
минимум, один, который называется основным, стандартным. В большинстве приложений
вы можете использовать объект потока, который позволяет вам использовать потоки
в ваших приложениях. Объекты потоков инкапсулируют в себе основные свойства
и методы, необходимые для написания многопоточных приложений.
Итак, любой поток - это объект, получающий определенное процессорное время.
Всякое приложение Linux является процессом операционной системы.
Примечание
Объекты потоков не позволяют вам управлять атрибутами безопасности или размером
стека ваших потоков. Для того чтобы контролировать их, вам необходимо использовать
функцию BeginThread, которая рассматривается далее.
Для того чтобы использовать объекты потоков в вашем приложении, вам нужно создать
потомок класса TThread.
Класс TThread был создан для облегчения написания приложений с несколькими потоками.
Он гарантирует совместимость при работе с библиотекой визуальных компонентов
(CLX) Kylix.
Вообще, при создании многопоточных приложений необходимо следовать приведенным
ниже рекомендациям:
остерегайтесь создавать слишком много потоков - это может привести к большой
загруженности операционной системы и процессора;
используйте синхронизацию в случае, когда несколько потоков пытаются получить
доступ к одному ресурсу;
большинство методов, которые обращаются к объектам CLX и изменяют содержимое
формы, должны вызываться из главного CLX-потока или использовать объект синхронизации.
Определение объекта TThread находится в модуле classes и имеет вид, приведенный
в листинге 14.1.
Листинг 14.1. Объект TThread
TThread = class
private
FHandle: THandle;
FThreadID: THandle;
FTerminated: Boolean;
FSuspended: Boolean;
FFreeOnTerminate: Boolean;
FFinished: Boolean;
FReturnValue: Integer;
FOnTerminate: TNotifyEvent;
FMethod: TThreadMethod;
FSynchronizeException: TObject;
procedure CallOnTerminate;
function GetPriority: TThreadPriority;
procedure SetPriority(Value: TThreadPriority);
procedure SetSuspended(Value: Boolean);
protected
procedure DoTerminate; virtual;
procedure Execute; virtual; abstract;
procedure Synchronize(Method: TThreadMethod);
property ReturnValue: Integer read FReturnValue write FReturnValue;
property Terminated: Boolean read FTerminated;
public
constructor Create(CreateSuspended: Boolean);
destructor Destroy; override;
procedure Resume;
procedure Suspend;
procedure Terminate;
function WaitFor: LongWord;
property FreeOnTerminate: Boolean read FFreeOnTerminate write treeOnTerminate
;
property Handle: THandle read FHandle;
property Priority: TThreadPriority read GetPriority write SetPriority;
property Suspended: Boolean read FSuspended write SetSuspended; property ThreadID:
THandle read FThreadID;
property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
end;
Из вышеприведенного листинга можно определить, что объект TThread является
прямым потомком объекта TObject, следовательно, он не является визуальным компонентом.
Его метод Execute - абстрактный. Поэтому сам объект TThread тоже является абстрактным,
и вы не сможете создать экземпляр этого класса. Таким образом, вам придется
создавать классы - потомки данного класса для работы с потоками.
Для создания потомка класса TThread выберите в главном меню Kylix команду File/New,
затем в появившемся окне (рис. 14.1) выберите пиктограмму Thread Object.
Рис. 14.1. Добавление объекта потока в проект с помощью диалогового окна New Items
В появившемся диалоговом окне напишите имя для вашего нового объекта потока. После всего этого Kylix создаст новый модуль и в окне редактора кода появится новая закладка.
Примечание
В отличие от большинства диалоговых окон IDE Kylix, которые требуют ввода имени
класса, диалоговое окно создания нового объекта потока не добавляет автоматически
букву "т" перед названием нового класса потока. Поэтому желательно
самостоятельно правильно называть новые потоки, например
TMyThread.
Если вы проделали все вышеописанное и назвали новый объект потока TMyThread,
то в новом модуле, сгенерированном Kylix, вы можете увидеть код - заготовку
для вашего нового объекта потока (листинг 14.2).
Листинг 14.2. Заготовка для нового объекта потока
unit Unitl;
interface
uses
Classes;
type
TMyThread = class(TThread)
private
{Private declarations }
protected
procedure Execute; override;
end;
ilementation
{Важно: Методы и свойства объектов CLX могут быть использованы только с
помощью вызова метода синхронизации, например
Synchronize(UpdateCaption); и UpdateCaption должно выглядеть
следующим образом:
procedure TMyThread.UpdateCaption;
begin
Forml.Caption := 'Обновлено внутри потока';
end;
{TMyThread }
procedure TMyThread.Execute;
begin
{ Разместите код потока здесь }
end;
end.
В автоматически сгенерированном файле модуля вы можете:
инициализировать поток;
заполнить метод Execute объекта потока, разместив там функции и процедуры;
написать код гарантированного разрушения потока (например, строку FreeOnTerminate:=True;).
Инициализация потоков
Если вы хотите написать код инициализации для вашего нового объекта потока,
вам необходимо добавить новый конструктор в описание вашего нового класса потока,
после чего вы можете добавлять код инициализации вместо кода реализации класса.
Здесь вы можете также указать, какой приоритет вы устанавливаете для данного
потока, а также, как должен вести себя данный поток по завершении своей работы.
Приоритеты потоков
Приоритеты потоков указывают операционной системе, сколько процессорного времени
выделяется для данного потока. Для критических задач можно установить наивысший
приоритет, для менее значимых - более низкий приоритет.
Для установки значения приоритета у потоков имеется свойство Priority. Данное
свойство может принимать произвольное целое значение. Используется как бы шкала
значений приоритетов от самого низкого до наивысшего. В Linux-приложениях свойство
Priority всегда должно принимать целое числовое значение. Наибольшее числовое
значение указывает на самый высокий приоритет выполнения данного потока. Диапазон
значений, которые может принимать свойство Priority, зависит от значения свойства
policy. Это свойство доступно только для Linux-приложений и может принимать
значения, перечисленные в табл. 14.1.
Таблица 14.1. Значения свойства Policy
Значение
|
Тип значения
|
Возможные числовые значения для свойства
Priority
|
SCHED_RR
|
Real Time
|
0-99
|
SCHED_FIFO
|
Real Time
|
0-99
|
SCHED_OTHER
|
Regular
|
0
|
Примечание
Значения SCHED_PR и SCHED_FIFO могут быть установлены только в том случае, когда
вы вошли в Linux под администратором (root).
Для Windows-приложений свойство Priority может принимать значения, перечисленные
в табл. 14.2.
Таблица 14.2. Значения свойства Priority для Windows
Значение приоритета | Приоритет | Соответствующее числовое значение |
TpIdle | Данный поток выполняется, когда система не занята и не выполняются никакие другие потоки. Windows не будет прекращать работу других потоков для выполнения потока, имеющего приоритет tpldle |
-15
|
TpLowest | Низший приоритет выполнения. Данный поток занимает минимум процессорного времени |
-2
|
TpLower | Низкий приоритет. Данный поток занимает немного
больше процессорного времени, чем имеющий приоритет tpLowest |
-1
|
TpNormal | Нормальный приоритет. Все потоки по умолчанию имеют приоритет tpNormal |
0
|
TpHigher | Высокий приоритет. Данный поток имеет приоритет выше нормального |
1
|
TpHighest | Высший приоритет. Данный поток имеет приоритет выше, чем tpHigher |
2
|
TpTimeCritical | Наивысший приоритет. Поток с данным приоритетом занимает максимум процессорного времени |
15
|
Примечание
Использование высших и наивысших приоритетов может привести к замедлению работы
других потоков. Применение данных видов приоритетов целесообразно использовать
в случае, когда возникает острая необходимость в скорейшем выполнении одного
из процессов.
Поведение потока при завершении его работы
Обычно при завершении своей работы поток просто освобождается. Однако иногда
бывает необходимо, чтобы завершение работы и освобождение потока было согласовано
с другими потоками. Например, вы можете ожидать какое-либо значение, возвращаемое
одним потоком, перед выполнением другого потока. Для реализации этого вы не
должны освобождать первый поток, пока второй не получит значение, возвращаемое
первым. Для управления завершением работы потока существует свойство потока
FreeOnTerminate. По умолчанию данное свойство установлено в true. При этом поток
освобождается по завершении своей работы. Если же установить данное свойство
в false, то вы можете сами завершить работу потока.
Кроме того, в Kylix имеется возможность прекратить выполнение одного потока
подачей команды о прекращении из другого потока. Когда один поток пытается прекратить
работу другого потока, он вызывает метод Terminate. В результате, свойство Terminate
потока будет установлено в true, что можно проверить во время выполнения метода
Execute (листинг 14.3).
Листинг 14.3. Проверка прекращения работы потока :
procedure TMyThread.Execute;
begin
while not Terminated do
{выполнять какие-либо задачи};
end;
Пример создания многопоточного приложения
в Kylix
Теперь настало время создания простого многопоточного приложения.
Мы создадим приложение, которое состоит из трех потоков (главного CLX-потока
и двух потоков - потомков класса TThread). Для начала запустим Kylix и выберем
в его главном меню пункт File/New Application, после чего разместим на главной
форме приложения Form1 поле для редактирования Editl, индикатор хода выполнения
работы ProgressBarl и системный таймер Timer 1. В результате должна получиться
форма, похожая на представленную на рис. 14.2.
Рис. 14.2. Форма приложения
Теперь добавим новый объект потока через пункт главного меню Kylix File/New/Thread
Object. Введем имя для нового потомка класса TThread, например TMyThreadi. Kylix
автоматически добавит модуль Unit2 в наше приложение. В описание объекта TMyThreadl
добавим раздел public, в котором опишем глобальную переменную count.
Далее запишем в метод Execute объекта TMyThreadl код, который должен выполняться
в потоке. Пусть это будет код, который генерирует случайные числа и присваивает
их глобальной переменной count. Для того чтобы генерация случайных чисел была
бесконечной, зациклим ее с помощью так нарываемого "бесконечного цикла".
В итоге модуль Unit2 должен выглядеть так как показано на листинге 14.4.
Листинг 14.4. Модуль первого потока
unit Unit2;
interface
uses
Classes;
type
TMyThreadl = class(TThread) private
{ Private declarations }
protected
procedure Execute; override;
public
count:integer; // Добавили переменную Count
end;
implementation
{ TMyThreadl }
procedure TMyThreadl.Execute;
begin
while true do
begin
count:=random(maxint);
end;
end;
end.
Теперь создадим второй объект потока, который должен заполнять индикатор хода
работы ProgressBarl.
По аналогии с первым объектом потока при помощи главного меню Kylix создадим
объект потока с именем TMyThread2. Во вновь добавленный модуль Unit3 включим
глобальную переменную prcount, после чего в процедуре Execute объекта TMyThread2
запишем код, представленный в листинге 14.5, также зациклив его.
Листинг 14.5. Модуль второго потока
unit Unit3;
interface
uses Classes;
type
TMyThread2 = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
public
prcount:integer; // Добавили переменную prcount
end;
implementation
{TMyThread2 }
procedure TMyThread2.Execute;
begin
wile true do
begin
prcount:=prcount+l;
if prcount>100 then prcount:=0;
end;
end;
end.
Теперь сохраним наш проект и модули под теми именами, которые нам предложит
Kylix (Projectl, Unitl, Unit2, Unit3).
Добавим в модуле Unitl в описание класса формы в разделе private объекты потоков
Threadl - потомок класса TMyThreadl и Thread2 - потомок класса TMyThread2.
Далее дважды щелкнем на любом свободном пространстве формы Form1. При этом откроется
"заготовка" для метода создания формы FormCreate. В обработчике FormCreate
напишем код, представленный в листинге 14.6.
Здесь мы создаем объект потока с использованием конструктора create и устанавливаем
приоритеты потоков (tpLowest и tpNormai).
Запишем в блоке uses модуля Unitl используемые модули Unit2 и unit3.
Нам осталось отразить результаты вычислений, производимых потоками на форме.
Для этого создадим обработчик события onTimer, дважды щелкнув на объекте Timer1.
Запишем в обработчик события OnTimer код, представленный в листинге 14.6.
Листинг 14.6. Главный модуль многопоточного приложения
unit Unitl;
interface
uses
SysUtils, Types, Classes, Variants, QGraphics, QControls, QForms, QDialogs,
QTypes, QExtCtrls, QComCtrls, QStdCtrls, unit2, unit3;
type
TForml = class(TForm)
Editl: TEdit;
ProgressBarl: TProgressBar;
Timerl: TTimer;
procedure FormCreate(Sender: TObject);
procedure TimerlTimer(Sender: TObject);
private
{ Private declarations }
threadl:tmythreadl;
thread2:tmythread2;
public
{ Public declarations }
end;
var
Forml: TForml;
implementation
{$R *.xfm}
procedure TForml.FormCreate(Sender: TObject);
begin
threadl:=tmythreadl.create(false);
thread2:=tmythread2.create(false);
end;
procedure TForml.TimerlTimer(Sender: TObject);
begin
editl.Text:=inttostr(threadl.count);
progressbarl.Position:=thread2.prcount;
end;
end.
Наше первое приложение, которое использует потоки, готово. Теперь, если мы запустим приложение с помощью главного меню Kylix: Run/Run, то сможем увидеть, как два созданных нами потока успешно работают (рис. 14.3).
Рис. 14.3. Результат работы многопоточного приложения
Использование главного CLX-потока
Когда вы применяете в своем приложении объекты из библиотеки визуальных компонентов,
то вам нужно иметь в виду, что свойства и методы объектов из CLX не гарантируют
потоковую безопасность.
В случае, когда все объекты обращаются к своим свойствам и выполняют вой методы
в одном потоке, вам не нужно беспокоиться о том, что объекты будут создавать
друг другу помехи.
Для использования главного CLX-потока создайте отдельную процедуру, Которая
выполняет необходимые действия. А затем вызовите эту процедуру вашего потокового
модуля (внутри метода Execute), применив синхронизацию. Например, как представлено
в листинге 14.7.
Листинг 14.7. Использование синхронизации
procedure TMyThread.PushTheButton;
begin
Buttonl.Click;
end;
procedure TMyThread.Execute;
begin
…
Synchronize(PushTheButton);
…
end;
Метод синхронизации защитит ваше приложение от ситуации "гонки"
Ситуация "гонки" возникает, когда два потока или более пытаются получить
доступ к общему ресурсу и изменить его состояние.
Примечание
Метод синхронизации не может использоваться в консольных приложениях Для защиты
одновременного доступа к CLX-объектам в консольных приложениях вы должны использовать
другие методы, такие как критические секции.
В некоторых случаях вы можете обходиться без метода синхронизации, например,
если:
компоненты доступа к данным (Data access) являются потокобезопасными в том случае,
когда каждый поток обращается к собственной базе данных;
Примечание
Когда вы используете компоненты доступа к данным, вы должны применять синхронизацию
в том случае, если устанавливаете связь между компонентами доступа к данным
(например, свойство DataSet в объекте DataSource). Но вы можете не прибегать
к синхронизации при обращении к данным таблицы базы данных.
объекты для работы с графикой являются потокобезопасными. Это такие классы,
как TFont, TPen, TBrush, TBitmap, TDrawing и TIcon;
вместо объектов списков (List), которые не являются потокобезопасными, вы можете
использовать потокобезопасный потомок объекта TList - TThreadList.
Координация потоков
При работе с потоками, на этапе первоначального знакомства с ними, неизбежны
ошибки. Особенно неприятны ошибки, связанные с конфликтами потоков, обращающихся
к разделяемым ресурсам, а также глобальным переменным. Также иногда бывает необходимо,
чтобы потоки работали слаженно. Например, после выполнения какого-либо события
в одном потоке вам необходимо, чтобы возникало какое-либо постоянное событие
в другом потоке. Все это можно объединить общим названием координации потоков.
Рассмотрим, как можно эффективно управлять потоками для достижения каких-либо
задач.
Для начала определим, какие способы хранения локальных переменных потока нам
предоставляет Kylix. Таких способов три.
Хранение локальных переменных в стеке потока. Так как любой поток приложения
получает свой стек, он будет иметь собственные локальные переменные.
Сохранение локальных переменных в объекте потомка класса Tthread.
Хранение локальных переменных на уровне операционной системы с использованием
в описании локальных переменных слова threadvar.
Первый способ является самым простым, очевидным и самым эффективным. Доступ
к локальным переменным, расположенным в стеке потока, самый быстрый.
Рассмотрим два других способа хранения локальных переменных потока.
Второй способ проще и эффективнее чем третий (листинг 14.8).
Листинг 14.8. Объявление локальных переменных в потомке класса TThread
type
TMyThreadl = class(TThread)
private
i,j,k,l: integer; // локальные переменные потока типа integer
a,b,c: char; // локальные переменные потока типа char
…
end;
Эффективность объявления локальных переменных в потомке класса Thread очевидна,
т. к. доступ к любому полю объекта осуществляется очень быстро (примерно в 10-11
раз быстрее, чем при использовании описания threadvar).
Третий способ хранения локальных переменных (с помощью threadvar) служит для
создания в потоках локальных копий глобальных переменных.
Глобальные переменные могут использоваться всеми потоками приложения, при этом
может возникнуть ситуация, когда при изменении глобальной переменной одним потоком
происходит изменение этой же глобальной переменной другим потоком. В результате
значение, установленное первым потоком, бесследно исчезает, приводя к нежелательным
последствиям (в данном примере произойдет обработка ошибочного значения глобальной
переменной первым потоком). Для исключения такой ситуации Kylix предлагает средство
умения локальных данных потоков (thread-local storage). С помощью данного средства
можно создавать локальные копии глобальных переменных для любого потока. При
этом требуется всего лишь заменить одно слово var при объявлении глобальных
переменных на слово threadvar:
threadvar
X: integer;
Итак, прочитав эту главу, вы узнали некоторые приемы работы с потоками научились
создавать многопоточные приложения. Это вам особенно пригодится при переносе
приложений из среды Windows в Linux.