Глава 2. Создание потока в Delphi.

Содержание:

  • Предисловие с диаграммой.
  • Наш первый не-VCL поток .
  • Что именно делает эта программа?
  • Проблемы и сюрпризы.
  • Проблемы запуска.
  • Проблемы взаимодействия.
  • Проблемы завершения.


Предисловие с диаграммой.

До расмотрения деталей создания потока и выполнения его кода независимо от основного потока приложения необходимо разобраться в диаграмме, иллюстрирующей динамику выполнения потока. Это значительно нам поможет, когда мы начнем разрабатывать многопоточные программы. Рассмотрим пример 1

Выделить всёРазвернуть кодкод Pascal/Delphi
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
unit test;
 
interface
 
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;
 
type
 
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
 
var
  Form1: TForm1;
 
implementation
 
{$R *.DFM}
 
procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage('Hello World!');
end;
 
end.


.
Приложение имеет один исполняемый поток: основной поток VCL. Его работу можно проиллюстрировать диаграммой, показывающей состояние потока в течение времени выполнения. Ось времени направлена вниз. Описание этой диаграммы будет относиться и ко всем последующим диаграммам выполнения потоков.

--Resize_Images_Alt_Text--


Заметьте, что эта диаграмма не показывает деталей выполнения алгоритмов. Вместо этого она отображает порядок событий во времени и состояние потока приложения между этими событиями. Имеет значение не фактическое расстояние между разными точками на диаграмме, а их вертикальное упорядочение. Некоторые части этой диаграммы следует рассмотреть особенно подробно.
Поток приложения не выполняется непрерывно Могут быть длинные периоды времени, когда он не получает никаких внешних стимулов, и совсем не выполняет вычислений или действий. Память и ресурсы приложением заняты, и окно находится на экране, но CPU не исполняет кода.
Приложение запущено, и выполняется основной поток. Как только создано главное окно, работы больше нет, и поток попадает в часть кода VCL, которая называется цикл обработки сообщений, опрашивающий операционную систему о наличии сообщений. Если нет сообщений, требующих обработки, операционная система приостанавливает (suspend) поток.

  • В некоторый следующий момент, пользователь нажимает на кнопку, чтобы отобразить текстовое сообщение. Операционная система возобновляет (resume) основной поток и передает ему сообщение, указывающее, что кнопка нажата. Основной поток теперь снова активен.
  • Этот процесс resume - suspend происходит несколько раз. Для иллюстрации я ввел ожидание подтверждения пользователем закрытия окна сообщения, и ожидание нажатия кнопки закрытия. На практике могут быть получены и многие другие сообщения.


Наш первый не-VCL поток.

Хотя Win32 API обеспечивает исчерпывающую поддержку многопоточности, для создания и уничтожения потоков, в VCL имеется полезный класс, TThread, который предоставляет более высокоуровневый подход, значительно упрощает работу и помогает программисту избегать некоторых неприятных ловушек, в которые можно попасть при недостатке опыта. Я рекомендую использовать именно его. Система помощи Дельфи дает неплохое введение в создание класса потока, так что я не буду подробно рассказывать о последовательности действий для создания потока, за исключением того, что предложу выбрать пункт меню File| New... и затем Thread Object .
Этот пример содержит программу, которая вычисляет, является ли данное число простым. Она состоит из двух модулей, один с обычной формой, и один с объектом потока. Она более или менее работоспособна, но обладает несколькими неприятными особенностями, которые иллюстрируют основные проблемы, с которыми встречаются программисты, разрабатываюшие многопоточные приложения. Мы обсудим пути их преодоления позже. Модуль формы

Выделить всёРазвернуть кодкод Pascal/Delphi
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
unit PrimeForm;
 
interface
 
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;
 
type
  TPrimeFrm = class(TForm)
    NumEdit: TEdit;
    SpawnButton: TButton;
    procedure SpawnButtonClick(Sender: TObject);
  private
      { Private declarations }
  public
      { Public declarations }
  end;
 
var
  PrimeFrm: TPrimeFrm;
 
implementation
 
uses PrimeThread;
 
{$R *.DFM}
 
procedure TPrimeFrm.SpawnButtonClick(Sender: TObject);
 
var
  NewThread: TPrimeThrd;
 
begin
  NewThread := TPrimeThrd.Create(True);
  NewThread.FreeOnTerminate := True;
  try
    NewThread.TestNumber := StrToInt(NumEdit.Text);
    NewThread.Resume;
  except on EConvertError do
    begin
      NewThread.Free;
      ShowMessage('That is not a valid number!');
    end;
  end;
end;
 
end.


и модуль объекта потока.

Выделить всёРазвернуть кодкод Pascal/Delphi
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
unit PrimeThread;
 
interface
 
uses
  Classes;
 
type
  TPrimeThrd = class(TThread)
  private
    FTestNumber: integer;
  protected
    function IsPrime: boolean;
    procedure Execute; override;
  public
    property TestNumber: integer write FTestNumber;
  end;
 
implementation
 
uses SysUtils, Dialogs;
 
function TPrimeThrd.IsPrime: boolean;
 
var
  iter: integer;
 
begin
  result := true;
  if FTestNumber < then
  begin
    result := false;
    exit;
  end;
  if FTestNumber <= then
    exit;
  for iter := to FTestNumber - do
  begin
    if (FTestNumber mod iter) = then
    begin
      result := false;
      {exit;}
    end;
  end;
end;
 
procedure TPrimeThrd.Execute;
begin
  if IsPrime then
    ShowMessage(IntToStr(FTestNumber) + 'is prime.')
  else
    ShowMessage(IntToStr(FTestNumber) + 'is not prime.');
end;
 
end.



Что именно делает эта программа?

Всякий раз, когда нажата кнопка "Spawn", программа создает новый объект потока, инициализирует несколько его полей, затем запускает выполнение потока. В зависимости от величины входного числа, поток работает, вычисляя, простое ли это число, и как только вычисления завершаются, поток отображает сообщение, показывая, простое оно или нет. Эти потоки конкурируют между собой, и, независимо от того, одно- или многопроцессорная у вас машина, с точки зрения пользователя они выполняются одновременно. Кроме того, эта программа не ограничивает числа созданных потоков. В результате вы можете продемонстрировать истинный параллелизм так:

  • Поскольку я закомментировал оператор выхода в подпрограмме определения простого числа, время потраченное потоком, приблизительно пропорционально величине входного числа. У меня для аргумента порядка 2^24 поток до завершения работает около 10-20 секунд. Подберите число для обеспечения такой задержки на вашей машине.
  • Запустите программу, введите большое число, нажмите кнопку.
  • Сразу же введите небольшое число (например, 42) и нажмите кнопку снова. Вы увидите, что результат для маленького числа появится раньше результата для большого, несмотря на то, что сначала мы запустили поток для большого числа. Диаграмма иллюстрирует эту ситуацию.
--Resize_Images_Alt_Text--



Проблемы и сюрпризы.

На этом этапе появляется проблема синхронизации. Когда основной поток возобновляет выполнение (вызывает resume) "рабочего" потока, основной поток программы не может ничего знать о состоянии рабочего потока и наоборот. Вполне возможно, что рабочий поток может завершить свое выполнение прежде, чем в основном потоке VCL выполнится хоть один оператор. Фактически для маленьких чисел, расчет для которых займет менее чем 1/20 секунды, это весьма вероятно. Аналогично, рабочий поток не может ничего предполагать о состоянии основного потока. Остается лишь полагаться на планировщик Win32. Рассмотрим три основные проблемы: запуск, взаимодействие и завершение.

Проблемы запуска.

Delphi облегчает запуск потока. Перед началом исполнения порожденного потока часто нужно установить некоторое его начальное состояние. Создавая поток приостановленным (параметр конструктора потока), можно быть уверенным, что код потока не будет выполняться пока его не активируют. Это означает, что основной поток VCL может безопасно прочитать и модифицировать данные объекта TThread, гарантируя, что они будут правильными, когда порожденный поток начнет выполняться.
В данной программе свойства потока "FreeOnTerminate" и "TestNumber" установлены до начала выполнения. Если бы это не было сделано, то поведение потока должно было быть неопределенным. Если вы не хотите создавать поток приостановленным, то просто отодвигаете проблемы запуска до следующего этапа: проблемы взаимодействия.

Проблемы взаимодействия.

Эти проблемы появляются, если у вас есть два работающих потока, и вам нужно каким-то способом связываться между ними. Эта программа не затрагивает взаимодействие потоков. На данный момент достаточно отметить, что если вы не защищаете все операции над разделяемыми данными, ваша программа, вероятно, будет вести себя непредсказуемо. Если вы не обеспечиваете требуемую синхронизацию и управление параллельным доступом, недопустимо следующее:

  • Доступ к любой форме или общим ресурсам из двух потоков.
  • Доступ к потоко-незащищенным частям VCL из не-VCL потока.
  • Попытка делать графические операции из отдельного потока.


Даже такая простая операция, как доступ к общей целой переменной из двух потоков, может закончиться полным беспорядком, а несинхронизированный доступ к общим ресурсам или вызовы VCL приведут ко многим часам непростой отладки, значительной неразберихи, и возможно к обращению в ближайшую психиатрическую лечебницу. Пока вы не изучили подходящие методы в следующих главах, не делайте этого.
Есть ли хорошие новости? Вы можете делать все три вышеуказанные действия, если используете правильные механизмы для управления параллельным выполнением, и это не так уж и трудно! Мы рассмотрим простой путь разрешения вопросов взаимодействия через VCL в следующей главе, а более изящные (но и более сложные) методы позже.

Проблемы завершения.

Поток, подобно любому другому объекту Delphi, использует распределение памяти и других ресурсов, так что не должен вызывать удивления факт, что важно обращаться с завершением потока очень аккуратно, а наша программа этого не делает. Есть два возможных подхода к проблеме освобождения ресурсов.
Первый - позволить потоку решить все самому. Это главным образом используется для потоков, которые:
а) Передают результаты выполнения потока в основной поток VCL перед остановкой.
б) Не содержат ко времени завершения никакой информации, необходимой другому потоку.
В этих случаях программист может установить флаг "FreeOnTerminate" для объекта потока, и он корректно освободит ресурсы при своем завершении.
Второй подход - основной поток VCL должен прочитать данные из объекта рабочего потока после его завершения, а затем уничтожить его. Это описано в Главе 4.
Я не затрагивал проблем передачи результатов в основной поток, поскольку рабочий поток сам сообщает пользователю ответ путем вызова ShowMessage. При этом не используется связь с основным потоком VCL, и вызов ShowMessage можно рассматривать как потокобезопасный, так что работа VCL не нарушается. В результате я могу использовать первый метод, для разрушения потока и разрешить потоку самоуничтожиться. Несмотря на это, программа иллюстрирует одну неприятную особенность, проявляющуюся при саморазрушении потока:

--Resize_Images_Alt_Text--


Как можно заметить, могут произойти две вещи. Во-первых, мы можем попытаться выйти из программы, когда поток еще активен и ведет вычисления. Во-вторых - мы можем попытаться выйти из программы, когда поток приостановлен. Первый случай довольно благоприятный: приложение закрывается, не считаясь с потоком. Код завершения Delphi и Windows cделает все, как нужно. Второй вариант несколько хуже, поскольку поток приостанавливается где-то в недрах подсистемы обмена сообщениями Win32. При этом Delphi производит работу по очистке в обоих случаях. Тем не менее, принудительный выход из потока без учета того, в каком он состоянии - плохой стиль программирования. Например, рабочий поток может в это время вести запись в файл. Если пользователь выходит из программы до завершения процесса записи, то файл может быть поврежден. Вот почему правильнее, когда порожденные потоки завершают работу согласованно с основным потоком VCL, даже если не требуется передача данных: при этом возможно чистое завершение процесса и потока. В Главе 4 обсуждаются решения этой проблемы.

Сделать бесплатный сайт с uCoz