Информационный портал MSEVM
 Поиск

Главная > Программирование в Delphi 5 > Глава 13


Глава 13

Потоки и процессы

Работая с Delphi, нужно помнить: этот замечательный продукт не только упрощает разработку сложных приложении, он использует при этом все возможности операционной системы. Одна из возможностей, которую поддерживает Delphi, - это так называемые потоки или нити (threads).

Потоки позволяют в рамках одной программы решать несколько задач одновременно. Эта возможность есть во всех операционных системах для персональных компьютеров начиная с появления Windows NT.

Операционная система (ОС) предоставляет приложению некоторый интервал времени центрального процессора (ЦП) и в момент, когда приложение переходит к ожиданию сообщений или освобождает процессор, операционная система передает управление другой задаче. Теперь, когда компьютеры с более чем одним процессором резко упали в цене, а операционная система Windows NT может использовать наличие нескольких процессоров, пользователи действительно могут запускать одновременно более одной задачи. Планируя время  центрального процессора. Windows 95 или Windows NT распределяют его между потоками, а не между приложениями. Чтобы использовать все преимущества, обеспечиваемые несколькими процессорами в современных операционных системах, программист должен знать, как создавать потоки.

В этой главе рассматриваются следующие вопросы.

  • Что такое потоки
  • Разница между потоком и процессом
  • Преимущества потоков
  • Чего не нужно делать при использовании потоков
  • Планирование потоков в Win 32
  • Синхронизация потоков
  • Класс TThread в Delphi
  • Реализация многопоточного приложения

Обзор потоков

Потоки - это наборы команд, которые могут получать время процессора. Время процессора выделяется квантами. Квант времени - это минимальный интервал, в течение которого только один поток использует процессор.

Обратите внимание, что кванты выделяются не программам или процессам, а именно порожденным ими потокам. Как минимум, каждый процесс имеет хотя бы один (главный) поток, но операционные системы, начиная с Windows 95 и Windows NT позволяют запустить в рамках процесса произвольное число потоков.

Потоки дают современному программному обеспечению новые специфические возможности. К примеру, пакеты из состава MS Office задействуют по несколько потоков. Word может одновременно корректировать грамматику и печатать, при этом осуществляя ввод данных с клавиатуры и мыши; программа Excel способна выполнять фоновые вычисления и печатать. Потоки упрощают жизнь тем программистам, которые разрабатывают приложения в архитектуре клиент/сервер. Когда требуется обслуживание нового клиента, сервер может запустить специально для этого отдельный поток.

Узнать число потоков, запущенных приложением, в Windows NT и Windows 2000 можно при помощи утилиты Task Manager (Диспетчер задач). Для этого среди показателей, отображаемых в окне Processes, нужно выбрать параметр Thread Count. Так, в момент написания этих строк MS Word использовал 5 потоков, среда Delphi - 3.

Существуют две модели применения потоков - асимметричная и симметричная.

В рамках асимметричной модели потоки решают различные задачи и, как правило, не разделяют совместные ресурсы. Один из таких потоков отвечает за печать; другой обрабатывает сообщения от клавиатуры и мыши; третий заведует автоматическим сохранением документа пользователя.

В симметричной модели потоки выполняют одну и ту же работу, разделяют одни ресурсы и исполняют один код. Пример приложения с симметричными потоками - практически любая крупная клиент/серверная СУБД. Для обслуживания каждой транзакции запускается, как правило, отдельный новый поток. К приложению можно добавлять новые симметричные потоки по мере возрастания нагрузки (числа запросов).

Потоки и процессы

Когда мы говорим "программа", то обычно имеем в виду понятие, в терминологии операционной системы обозначаемое как "процесс". Процесс состоит из виртуальной памяти, исполняемого кода, потоков и данных. Процесс может содержать много потоков, но обязательно содержит по крайней мере один. Поток, как правило, имеет "в собственности" минимум ресурсов; он зависит от процесса, который и распоряжается виртуальной памятью, кодом, данными, файлами и другими ресурсами ОС.

Почему мы используем потоки вместо процессов, хотя, при необходимости, приложение может состоять и из нескольких процессов? Дело в том, что переключение между процессами - значительно более длительная операция, чем переключение между потоками. Другой довод в пользу применения потоков - то, что они специально задуманы для совместного использования ресурсов; разделить ресурсы между процессами (имеющими раздельное адресное пространство) не так-то просто.

Фоновые процедуры

Еще не столь давно программисты пытались эмулировать потоки, запуская процедуры внутри цикла обработки сообщений. Цикл обработки сообщений, или цикл ожидания - это особый фрагмент кода в программе, управляемой событиями. Он исполняется тогда, когда программа находит в очереди события, которые нужно обработать; если таковых нет, программа может выполнить в это время "фоновую процедуру". Такой способ имитации потоков весьма сложен, так как вынуждает программиста, во-первых, сохранять контекст фоновой процедуры, а во-вторых, определять момент, когда она вернет управление обработчику событий. Если такая процедура выполняется долго, то у пользователя может сложиться впечатление, что приложение перестало реагировать на внешние события. Использование потоков снимает проблему переключения контекста, теперь контекст (стек и регистры) сохраняет операционная система.

В Delphi возможность создать фоновую процедуру реализована через событие Onldle объекта Application:

property Onldle: TIdleEvent;

type TIdleEvent = procedure (Sender: TObject; var Done: Boolean) of object;

Все вышесказанное в полной мере относится к создаваемой вами процедуре-обработчику события Onldle. Чтобы сделать в фоновом режиме какую-то работу, следует разбить ее на кванты и выполнять понемногу - иначе приложение будет плохо реагировать на внешние воздействия.

Если задачи приложения можно разделить на различные подмножества: обработка событий, ввод/вывод, связь и другие; то потоки могут быть органично встроены в программное решение. Если разработчик может разделить большую задачу на несколько мелких, это только повысит переносимость кода и возможности его многократного использования.

Сделав приложение многопоточным, программист получает дополнительные возможности управления им. Например, через управление приоритетами потоков. Если один из них "притормаживает" приложение, занимая много процессорного времени, его приоритет может быть понижен.

Другое важное преимущество внедрения потоков - при возрастании "нагрузки" на приложение можно увеличить количество потоков и тем самым снять проблему.

Типичные ошибки при использовании потоков

Как и при использовании других сильнодействующих средств, в отношении потоков вы должны соблюдать определенные правила безопасности. Две типичные проблемы, с которыми программист может столкнуться при работе с потоками - это гонки (race conditions) и тупики (deadlocks).

Гонки

Ситуация гонок возникает, когда два или более потока пытаются получить доступ к общему ресурсу и изменить его состояние. Рассмотрим следующий пример. Пусть Поток 1 получил доступ к ресурсу и изменил его в своих интересах; затем активизировался Поток 2 и модифицировал этот же ресурс до завершения Потока 1. Поток 1 полагает, что ресурс остался в том же состоянии, в каком был до переключения. В зависимости от того, когда именно был изменен ресурс, результаты могут варьироваться - иногда код будет выполняться нормально, иногда нет. Программисты не должны строить никаких гипотез относительно порядка исполнения потоков, так как планировщик ОС может запускать и останавливать их в любое время. В качестве реального примера гонок рассмотрим связанный список, в который следующий фрагмент кода добавляет элементы. В качестве совместно используемого ресурса выступает процедура AddNode:

type pNode = ^Node;

Node = record Value: Integer;

NextNode: pNode;

end;

var HeadOfList : pNode;

procedure AddNode( Value : Integer ) ;

var

NewNode : pNode;

begin

GetMemf NewNode, Size0f( Node ));

NewNode^.Value := Value;

NewNode^.NextNode := HeadOfList;

HeadOfList := NewNode;

end;

В листинге приведен пример процедуры, добавляющей элемент в связанный список. Если два потока вызывают процедуру AddNode одновременно, возникает ситуация гонок. Если Поток 1 выполняет оператор NewNode^.NextNode := HeadOfList; Перед тем как поток 2 выполнит HeadOfList := NewNode;, результат будет плачевным, так как указатель NewNode". NextNode, относящийся к Потоку 1, на самом деле не указывает на вершину списка.

Тупики

Тупики имеют место, когда поток ожидает ресурс, который в данный момент принадлежит другому потоку. Рассмотрим пример: Поток 1 захватывает объект А и, для того чтобы продолжать работу, ждет возможности захватить объект Б. В то же время Поток 2 захватывает объект Б и ждет возможности захватить объект А. Развитие этого сценария заблокирует оба потока; ни один из них не будет исполняться. Возникновения как ситуаций гонок, так и тупиков можно избежать, если использовать приемы, обсуждаемые ниже в разделе "Средства синхронизации потоков" этой главы.

Приоритеты потоков

Интерфейс Win 32 API позволяет программисту управлять распределением времени между потоками; это распространяется и на приложения, написанные на Delphi. Операционная система планирует время процессора в соответствии с приоритетами потоков. Когда поток создается, ему назначается приоритет, соответствующий приоритету породившего его процесса. В свою очередь, процессы могут иметь следующие классы приоритетов.

  • Реального времени (Real time)
  • Высокий (High)
  • Нормальный (Normal)
  • Фоновый (Idle)

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

Класс с высоким приоритетом также применяется достаточно редко. Его использование ограничено процессами, которые должны завершаться за короткое время, чтобы не вызвать сбойной ситуации. Пример - процесс, который посылает сигналы внешнему устройству; причем устройство отключается, если не получит своевременный сигнал.

Большинство процессов запускается в рамках класса с нормальным приоритетом. Нормальный приоритет означает, что процесс не требует какого-либо специального внимания со стороны операционной системы.

Процессы с фоновым приоритетом запускаются лишь в том случае, если в очереди Диспетчера задач нет других процессов. Обычные виды приложений, использующие такой приоритет, - это программы-заставки и системные агенты (system agents). Программисты могут использовать фоновые процессы для организации завершающих операций и реорганизации данных. Примерами могут служить автосохранение документа или базы данных.

Приоритеты имеют численные значения от 0 до 31. Процесс, породивший поток, может впоследствии изменить его приоритет; в этой ситуации программист имеет возможность управлять скоростью отклика каждого потока.

Приоритет потока может отличаться от приоритета породившего его процесса на плюс-минус две единицы. Соответствующие величины показаны в табл. 13.1.

Таблица 13.1. Классы процессов и приоритеты их потоков

 

 

Низший

Пониженный

Нормаль-ный

Повышенный

Высший

Фоновый

2

3

4

5

6

Нормальный заднего плана

5

6

7

8

9

Нормальный переднего плана

7

8

9

10

11

Высокий

11

12

13

14

15

Реального времени

22

23

24

25

26

Как видно из таблицы, у программиста имеется широкий диапазон возможностей по управлению скоростью отклика. Потоки для обработки пользовательских событий (мышь, клавиатура) могут быть запущены с нормальным приоритетом и продолжать реагировать на события при наличии нескольких потоков, работающих с пониженным приоритетом.

Класс TThread

Delphi предоставляет программисту полный доступ к возможностям программирования интерфейса Win 32. Для чего же тогда фирма Inprise представила специальный класс для организации потоков? Вообще говоря, программист не обязан разбираться во всех тонкостях механизмов, предлагаемых операционной системой. Класс должен инкапсулировать и упрощать программный интерфейс; класс Tthread - прекрасный пример предоставления разработчику простого доступа к программированию потоков. Другая отличительная черта класса Tthread - это гарантия совместимости с библиотекой визуальных компонентов VCL. Без использования класса TThread во время вызовов VCL могут возникнуть ситуации гонок.

Нужно отдавать себе отчет, что с точки зрения операционной системы, поток - это ее объект. При создании он получает дескриптор и отслеживается ОС. Объект класса Tthread - это конструкция Delphi, соответствующая потоку ОС. Этот объект VCL создается до реального возникновения потока в системе и уничтожается после его исчезновения.

Изучение класса TThread начнем с конструктора:

constructor Create(CreateSuspended: Boolean);

В качестве аргумента он получает параметр CreateSuspended. Если его значение равно True, вновь созданный поток не начинает выполняться до тех пор, пока не будет сделан вызов метода Resume. В случае, если CreateSuspended имеет значение False, поток начинает исполнение и конструктор завершается.

destructor Destroy; override;

Деструктор Destroy вызывается, когда необходимость в созданном потоке отпадает. Деструктор завершает его и высвобождает все ресурсы, связанные

С Объектом TThread. procedure Resume; Метод Resume класса TThread вызывается, когда поток возобновляется после остановки, или если он был создан с параметром CreateSuspended равным

True. procedure Suspend;

Вызов метода suspend приостанавливает поток с возможностью повторного запуска впоследствии. Метод suspend приостанавливает поток вне зависимости от кода, исполняемого потоком в данный момент; выполнение продолжается с точки останова.

property Suspended: Boolean;

Свойство suspended позволяет программисту определить, не приостановлен ли поток. С помощью этого свойства можно также запускать и останавливать поток. Установив suspended в True, вы получите тот же результат, что и при вызове метода Suspend - приостановку. Наоборот, установка Suspended в False возобновляет выполнение потока, как и вызов метода Resume.

function Terminate: Integer;

Для окончательного завершения потока (без последующего запуска) существует метод Terminate; он останавливает поток и возвращает управление вызвавшему процессу только после того, как это произошло. Значение, возвращаемое функцией Terminate, соответствует состоянию потока. Примерами возможных состояний являются случай нормального завершения и случай, когда к моменту вызова Terminate поток уже завершился (или был завершен из другого потока).

Метод Terminate автоматически вызывается и из деструктора объекта TThread. В явном виде его, за редким исключением, вызывать не надо.

property Terminated: Boolean;

Свойство Terminated позволяет узнать, произошел ли уже вызов метода Terminate ИЛИ нет.

function WaitFor: Integer;

Метод WaitFor предназначен для синхронизации и позволяет одному потоку дождаться момента, когда завершится другой поток. Если вы внутри потока под именем FirstThread пишете код:

Code := SecondThread.WaitFor;

то это означает, что поток FirstThread останавливается до момента завершения потока SecondThread. Метод WaitFor возвращает код завершения ожидаемого потока.

Property Handle: THandle read FHandle;

Property ThreadID: THandle read FThreadID;

Свойства Handle и ThreadID дают программисту непосредственный доступ к потоку средствами Win 32 API. Если разработчик хочет обратиться к потоку и управлять им, минуя возможности класса TThread, значения Handle и ThreadID могут быть использованы в качестве аргументов функций Win 32 API. Например, если программист хочет перед продолжением выполнения приложения дождаться завершения сразу нескольких потоков, он должен вызвать функцию API waitForMultipleObjects; для ее вызова необходим массив дескрипторов потоков.

property Priority: TThreadPriority;

Свойство priority позволяет запросить и установить приоритет потоков. Приоритет определяет, насколько часто поток получает время процессора. Естественно, программист захочет выделить главному потоку в приложении большее время, а потоку, например, с фоновой проверкой орфографии - меньшее. Допустимыми значениями приоритета являются tpldie, tpLowest,

tpLower, tpNormal, tpHigher, tpHighest И tpTimeCritical.

Будьте осторожны, используя приоритеты tpHighest и tpTimeCritical. Оба они могут оказать влияние на работоспособность приложения, а последний - и на всю операционную систему.

 

procedure Synchronize(Method: TThreadMethod);

Этот метод относится к секции protected, то есть может быть вызван только из потомков TThread. Delphi предоставляет программисту метод Synchronize для безопасного вызова методов VCL внутри потоков. Во избежание ситуаций гонок, метод Synchronize дает гарантию, что к каждому объекту VCL одновременно имеет доступ только один поток. Аргумент, передаваемый в метод Synchronize, - это имя метода, который производит обращение к VCL; вызов Synchronize с этим параметром - это то же, что и вызов самого метода. Такой метод (класса TThreadMethod) не должен иметь никаких параметров и не должен возвращать никаких значений. К примеру, в основной форме приложения нужно предусмотреть функцию

procedure TMainForm.SyncShowMessage;

begin ShowMessage(IntToStr(List-1.Count));

//другие обращения к VCL

end;

а в потоке для показа сообщения писать не

ShowMessage(IntToStr(Listl.Count));

И даже не

MainFom. SyncShowMessage;

А только так:

Synchronize(MainForm.SyncShowMessage);

Производя любое обращение к объекту VCL из потока, убедитесь, что при этом используется метод Synchronize; в противном случае результаты могут оказаться непредсказуемыми.

Procedure Execute; virtual; abstract;

Это и есть главный метод объекта TThread. В его теле должен содержаться код, который и представляет собой собственно поток. Метод Execute класса TThread объявлен как абстрактный.

Если вы не вполне освоились с термином "абстрактный метод", уместным будет обращение к главе 1.

Переопределяя метод Execute, мы можем тем самым закладывать в новый потоковый класс то, что будет выполняться при его запуске. Если поток был создан с аргументом СreateSuspended, равным False, то метод Execute выполняется немедленно, в противном случае Execute выполняется после вызова метода Resume.

Если поток рассчитан на однократное выполнение каких-либо действий, то никакого специального кода завершения для него писать не надо. После выполнения метода Execute будет вызван деструктор, который сделает все необходимое.

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

procedure TMyThread.Execute;

begin repeat DoSomething;

Until Cancel-Condition or Terminated;

end;

Здесь canceicondition - ваше личное условие завершения потока (исчерпание данных, поступление на вход того или иного символа и т. п.), а свойство Terminated говорит о завершении потока извне (скорее всего, завершается породивший его процесс).

С завершением потока надо быть очень внимательным - если он зациклился и не реагирует на сигналы завершения, то зависнет все приложение.

Property ReturnValue: Integer;

Свойство ReturnValue позволяет узнать и установить значение, возвращаемое потоком при его завершении. Эта величина полностью определяется пользователем. По умолчанию поток возвращает ноль, но если программист захочет вернуть другую величину, то простая переустановка свойства Reiurnvalue внутри потока позволит получить эту информацию другим потокам. Это, к примеру, может пригодиться, если внутри потока возникли проблемы, или с помощью ReturnValue нужно вернуть число не прошедших орфографическую проверку слов.

На этом завершим подробный обзор класса TThread. Для более близкого знакомства с потоками и классом Delphi TThread создадим многопоточное приложение. Для этого нужно написать всего пару строк кода и несколько раз щелкнуть мышью.

Пример многопоточного приложения

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

Среди компонентов приложения - два регулятора для установки приоритетов потоков и две строки редактирования - для наблюдения за их изменением. Потоки будут выполнять некоторые простые вычисления, а главная программа - отображать, сколько вычислений в секунду выполнено в каждом потоке. Выполните следующие действия:

  1. Запустите среду Delphi.

Рис. 13.1. Внешний вид формы для приложения ThrdProj

    Рис. 13.2. Диалоговое окно New Items с выбранным объектом типа "поток"

    1. Откройте меню File и выберите пункт New Application.
    2. Расположите на форме две строки редактирования, два регулятора и один компонент типа TTimer, как показано на рис. 13.1. Поместите одну строку редактирования и один регулятор слева, а другую пару - справа.
    3. Откройте меню File и выберите пункт Save Project As. Сохраните модуль как Thrdunit, а проект - как Thrdproj.
    4. Откройте меню File и выберите пункт New. Затем выполните двойной щелчок на объекте типа поток (значок Thread Object). Откроется диалоговое окно New Items, показанное на рис. 13.2.
    5. Когда появится диалоговое окно для именования объекта поток, введите TSiriipieThread и нажмите клавишу <Enter> (рис. 13.3).

    Рис. 13.3. Диалоговое окно New Thread Object

    Delphi создаст шаблон для нового потока, который показан в листинге:

    unit Unit-1;

    interface uses

    Classes ;

    type

    TSimpleThread = class(TThread)

    private { Private declarations }

    protected procedure Execute; override;

    end;

    implementation

    {Важно: методы и свойства объектов из состава VCL могут быть

    использованы посредством метода под названием Synchronize, например, Synchronize(UpdateCaption);

    где UpdateCaption может выглядеть так:

    procedure ThrdProj Thread.UpdateCaption;

    begin

    Formi.Caption := 'Updated in a thread';

    end;

    }

    ( TSimpleThread } procedure TSimpleThread.Execute;

    Var

    begin

    { Код потока помещается здесь }

    end;

    end.

      1. Измените объявление класса TSimpleThread, чтобы включить в секцию public поле count. Поле count будет использовано, чтобы подсчитать, сколько вычислении в секунду выполняется в потоке:

    TSimpleThread = class(TThread) private

    { Private declarations }

    protected procedure Execute; override;

    public

    Count : Integer;

    end;

      1. Изменения, вносимые в модуль Execute, заключаются в том, чтобы подсчитать среднее значение десяти случайных чисел и затем увеличить на единицу значение count. Эти изменения показаны ниже:

    procedure TSimpleThread.Execute;

    Var

    I, Total, Avg : Integer;

    begin While True Do Begin Total := 0;

    For I := 1 To 10 Do

    Inc ( Total, Random( Maxint ));

    Avg := Avg Div 10;

    Inc( Count ) ;

    End;

    end;

      1. Откройте меню File и выберите пункт Save As. Сохраните модуль с потоком как Thrd.pas.
      2. Отредактируйте главный файл модуля ThrdUnit.раs, и добавьте модуль Thrd к списку используемых модулей. Он должен выглядеть так:

    uses

    Windows, Messages, SysUtils, Classes, Graphics, Controls,

    Forms, Dialogs, Thrd, ExtCtrls, StdCtrls, ComCtrls;

      1. В секции public формы TE'ormi добавьте следующую строку:

    Thread1, Thread2: TSimpleThread;

      1. Выполните двойной щелчок на свободном месте рабочей области формы, чтобы объявить два потока, которые будут использоваться программой; при этом создастся шаблон метода Formcreate. В этом методе произойдет создание потоков, присвоение им приоритетов и запуск. Поместите в шаблон Formcreate следующий код:

    procedure TFormI.FormCreate(Sender: TObject);

    begin

    Thread1 :=TSimpleThread.Create( False );

    Thread1.Priority := tpLowest;

    Thread2 := TSimpleThread.Create( False );

    Thread2.Priority := tpLowest;

    end;

      1. Выполните двойной щелчок на компоненте TTimer для создания пустого шаблона метода Timer. Этот метод будет автоматически вызываться каждую секунду, чтобы приложение могло отслеживать состояние потоков. Метод Timer должен выглядеть следующим образом:

    procedure TFormI.TimerlTimer(Sender: TObject);

    begin

    Editl.Text := IntToStrf Threadi.Count );

    Edit2.Text := IntToStrf Thread2.Count );

    Thread1.Count := 0;

    Thread2.Count := 0;

    end;

      1. Щелкните на левом регуляторе (TrackBarl) и выберите страницу Events в окне Object Inspector. Выполните двойной щелчок напротив имени метода onchange для создания шаблона метода, который будет вызываться каждый раз при изменении положения регулятора. Метод будет устанавливать регулятор в соответствии с приоритетом потока. Он должен содержать следующий код:

    procedure TForml.TrackBarlChange(Sender: TObject);

    Var

    I : Integer;

    Priority : TThreadPriority;

    begin

    Priority := tpLowest;

    For I := 0 To ( Sender as tTrackBar ).Position- 1

    Do inc( Priority ) ;

    If Sender = TrackBarl

    Then Threadi.Priority := Priority

    Else Thread2.Priority := Priority;

    end;

    1. Чтобы связать метод, созданный на шаге 14, со вторым регулятором, выберите TrackBar2 в окне Object Inspector, откройте комбинированный cписок события OnChange И выберите метод TrackBarlChange.
    2. Чтобы учесть прозвучавшее выше предупреждение о недопустимости приоритета, высшего чем tpHigher, максимальное положение регуляторов должно быть ограничено четырьмя. Выберите TrackBarl, затем, удерживая клавишу <Shift>, TrackBar2. Когда оба компонента будут выбраны, выберите в окне Object Inspector страницу properties и придайте свойству мах значение 4.

    Рис. 13.4. Выполняющееся приложение ThrdProj

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

    Средства синхронизации потоков

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

    MyCompThread := TComputationThread.Create( False );

    //пока поток считает, можно выполнить часть работы

    DoSoraeWork;

    //а теперь ждем завершения потока

    MyCompThread.WaitFor;

    Приведенная схема совершенно недопустима, если во время своей работы поток MyCompThread обращается к VCL посредством метода Synchronize. В этом случае поток ждет главный поток для обращения к VCL, а тот, в свою очередь, его - классический тупик!

    За "спасением" следует обратиться к программному интерфейсу Win 32. Он предоставляет богатый набор инструментов, которые могут понадобиться для организации совместной работы потоков.

    Главные понятия для понимания механизмов синхронизации - функции ожидания и объекты ожидания. В API предусмотрен ряд функций, позволяющих приостановить выполнение вызвавшего эту функцию потока вплоть до того момента, как будет изменено состояние какого-то объекта, называемого объектом ожидания. (Под этим термином здесь понимается не объект Delphi, а объект операционной системы.) Простейшая из этих функций - waitForsingleObject - предназначена для ожидания одного объекта.

    К возможным вариантам относятся четыре объекта, разработанных специально для синхронизации: событие (event), взаимное исключение (mutex), семафор (semaphore) и таймер (timer). К ним может быть добавлена критическая секция (critical section). Кроме них, можно ждать и других объектов, дескриптор которых используется в основном для других целей, но может применяться и для ожидания. К ним относятся: процесс, поток, оповещение об изменении в файловой системе (change notification) и консольный ввод.

    Перечисленные выше средства синхронизации в основном инкапсулированы в состав классов Delphi. У программиста есть две альтернативы. С одной стороны, в состав библиотеки VCL включен модуль syncobjs. pas, содержащий классы для события (TEvent) и критической секции (TCriticalSection).

    С другой, с Delphi поставляется отличный пример IPCDEMOS, который иллюстрирует проблемы взаимодействия процессов и содержит модуль ipcthrd. раз с аналогичными классами- для того же события, взаимного исключения (TMutex), а также совместно используемой памяти (TSharedMem).

    Перейдем к подробному описанию объектов, используемых для синхронизации.

    Событие

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

    Класс TEvent (модуль syncobjs.раз) имеет два метода: setEvent и ResetEvent, переводящих объект в активное и пассивное состояние, соответственно. Конструктор имеет следующий вид:

    constructor Create(EventAttributes: PSecurityAttributes; ManuaiReset, InitialState: Boolean; const Name: string);

    Здесь параметр InitialState - начальное состояние объекта, ManuaiReset - способ его сброса (перевода в пассивное состояние). Если этот параметр равен True, событие должно быть сброшено вручную. В противном случае событие сбрасывается в момент старта хотя бы одного потока, ждавшего данный объект.

    На третьем методе

    TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError);

    function WaitFor(Timeout: DWORD): TWaitResult;

    остановимся подробнее. Он дает возможность ожидать активизации события в течение Timeout миллисекунд. Как вы могли догадаться, внутри этого метода происходит вызов функции waitFotSingleObject. Типичных результа-тов на выходе WaitFor два - wrSignaled, если произошла активизация события, и wrTimeout, если за время тайм-аута ничего не произошло.

    Если нужно ждать сколь угодно (неопределенно) долго, следует установить параметр Timeout в значение infinite.

    Рассмотрим маленький пример. Включим в состав нового проекта объект типа TThread, наполнив его метод Execute следующим содержимым:

    Var res: TWaitResult;

    procedure TSimpleThread.Execute ;

    begin с :=TEvent.Create(nil,True,false, 'test');

    repeat

    e.ReSetEvent ;

    res := e.WaitFor(lOOOO);

    Synchronize(Showlnfo);

    until Terminated;

    e.Free ;

    end;

    procedure TSimpleThread.Showlnfo;

    begin

    ShowMessage(IntToStr(Integer(res)));

    end;

    А на главной форме разместим две кнопки - нажатие одной из них запускает поток, нажатие второй активизирует событие:

    procedure TFormI.ButtonlClick(Sender: TObject);

    begin TSimpleThread.Create(False);

    end;

    procedure TForml.Button2Click(Sender: TObject);

    begin e.SetEvent;

    end;

    Нажмем первую кнопку. Тогда появившийся на экране результат (метод Showlnfo) будет зависеть от того, была ли нажата вторая кнопка или истекли отведенные 10 секунд.

    Для работы с потоками используются не только события - некоторые процедуры операционной системы автоматически переключают их. К числу таких процедур относятся отложенный (overlapped) ввод/вывод и события, связанные с коммуникационными портами.

    Взаимные исключения

    Объект типа взаимное исключение (Mutex) позволяет только одному потоку в данное время владеть им. Если продолжать аналогии, то этот объект можно сравнить с эстафетной палочкой.

    Класс, инкапсулирующий взаимное исключение - Tmutex - находится в модуле ipcthrd.pas (пример IPCDEMOS). Конструктор:

    constructor Create(const Name: string);

    задает имя создаваемого объекта. Первоначально он не принадлежит никому. (Но функция API createMutex, вызываемая в нем, позволяет передать созданный объект тому потоку, в котором это произошло.) Далее метод:

    function Get(TimeOut: Integer): Boolean;

    производит попытку в течение Timeout миллисекунд завладеть объектом (в этом случае результат равен True). Если объект более не нужен, следует вызвать метод:

    function Release: Boolean;

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

    Критическая секция

    Работая в Delphi, программист может также использовать объект типа критическая секция (critical section). Критические секции подобны взаимным исключениям по сути, однако между ними существуют два главных отличия.

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

    Критические секции и взаимные исключения очень схожи. На первый взгляд, выигрыш от использования критической секции вместо взаимного исключения не очевиден. Критические секции, однако, более эффективны, чем взаимные исключения, так как используют меньше системных ресурсов. Взаимные исключения могут быть установлены на определенный интервал времени, по истечении которого выполнение продолжается; критическая секция всегда ждет столько, сколько потребуется.

    Возьмем класс TCriticalSection (модуль syncobjs.pas). Логика использования его проста - "держать и не пущать". В многопотоковом приложении создается и инициализируется общая для всех потоков критическая секция. Когда один из потоков достигает критически важного участка кода, он пытается захватить секцию вызовом метода Enter:

    MySection.Enter;

    try DoSomethingCritical;

    finally MySection.Leave;

    end;

    Когда другие потоки доходят до оператора захвата секции и обнаруживают, что она уже захвачена, они приостанавливаются вплоть до освобождения секции первым потоком путем вызова метода Leave. Обратите внимание, что вызов Leave помещен в конструкцию try.. finally - здесь требуется повышенная надежность. Критические секции являются системными объектами и подлежат обязательному освобождению - впрочем, как и остальные рассматриваемые здесь объекты.

    Семафор

    Семафор (semaphore) подобен взаимному исключению. Разница между ними в том, что семафор может управлять количеством потоков, которые имеют к нему доступ. Семафор устанавливается на предельное число потоков, которым доступ разрешен. Когда это число достигнуто, последующие потоки будут приостановлены, пока один или более потоков не отсоединятся от семафора и не освободят доступ.

    В качестве примера использования семафора рассмотрим случай, когда каждый из группы потоков работает с фрагментом совместно используемого пула памяти. Так как совместно используемая память допускает обращение к ней только определенного числа потоков, все прочие должны быть блокированы вплоть до момента, когда один или несколько пользователей пула откажутся от его совместного использования.

    Процесс. Порождение дочернего процесса

    Объект типа процесс (process) может быть использован для того, чтобы приостановить выполнение потока в том случае, если он для своего продолжения нуждается в завершении процесса. С практической точки зрения такая проблема встает, когда нужно в рамках вашего приложения исполнить приложение, созданное кем-то другим, или, к примеру, сеанс MS DOS.

    Рассмотрим, как, собственно, один процесс может породить другой. Вместо устаревшей и поддерживаемой только для совместимости функции winExec, перекочевавшей из прежних версий Windows, гораздо правильнее использовать более мощную:

    function CreateProcess(IpApplicationName: PChar; IpCommandLine: PChar;

    IpProcessAttributes, IpThreadAttributes: PSecurityAttributes;

    bInheritHandles: BOOL; dwCreationFlags: DWORD; IpEnvironment: Pointer;

    IpCurrentDirectory: PChar; const IpStartupInfo: TStartupInfo;

    var lpProce53lnfomation: TProcessInformation): BOOL;

    Первые два параметра ясны - это имя запускаемого приложения и передаваемые ему в командной строке параметры. Параметр dwCreationFiags содержит флаги, определяющие способ создания нового процесса и его будущий приоритет. Использованные в приведенном ниже примере флаги означают: create_new_console - будет запущено новое консольное приложение с отдельным окном; NORMAL_PRIORITY_CLASS - нормальный приоритет.

    Структура TStartupinfo содержит сведения о размере, цвете, положении окна создаваемого приложения. В нижеследующем примере используется поле wShowWindow: установлен флаг sw_shownormal, означающий визуализацию окна с нормальным размером.

    На выходе функции заполняется структура ipprocessinformation. В ней программисту возвращаются дескрипторы и идентификаторы созданного процесса и его первичного потока. Нам понадобится дескриптор процесса - в нашем примере создается консольное приложение, и затем происходит ожидание его завершения. "Просигналит" нам об этом именно объект Ipprocessinformation.hProcess.

    var

    IpStartupInfo: TStartupinfo;

    IpProceasInformation: TProcessInformation;

    begin FillChar(IpStartupInfo,Sizeof(IpStartupInfo),#0);

    IpStartupInfo.cb := Sizeof(IpStartupInfo);

    IpStartupInfo.dwFlags := STARTFJJSESHOWWINDOW;

    IpStartupInfo.wShowWindow := sw_shownormal;

    if not CreateProcess(nil,PCharfarj /?'), nil, nil, false,CREATE_NEW_CONSOLE or NORMAL_PRIORITY_CLASS, nil, nil,

    IpStartupInfo, Ipprocessinformation) then

    ShowMessage(IntToStr(GetLastError)) else

    begin WaitForSingleObject(IpProcessInfomation

    .hProcess,10000) ;

    CloseHandle(IpProcessInformation.hProcess) ;

    end;

    end;

    Поток

    Поток может ожидать другой поток, точно так же, как и другой процесс. Ожидание можно организовать с помощью функций API (как в только что рассмотренном примере), но удобнее это сделать при помощи метода

    TThread.Wait For.

    Консольный ввод

    Консольный ввод (console input) годится для потоков, которые должны ожидать отклика на нажатие пользователем клавиши на клавиатуре. Этот тип ожидания может быть использован в программе дуплексной связи (вроде Chat из состава Windows). Один поток при этом будет ожидать получения символов; второй - отслеживать ввод пользователя и затем отсылать набранный текст ожидающему приложению.

    Оповещение об изменении в файловой системе

    Этот вид объекта ожидания очень интересен и незаслуженно мало известен. Мы рассмотрели практически все варианты того, как один поток может подать сигнал другому. А как получить сигнал от операционной системы? Ну, например, о том, что в файловой системе произошли какие-то изменения? Такой вид оповещения позаимствован из ОС Unix и теперь доступен программистам, работающим с Win 32.

    Для организации мониторинга файловой системы нужно использовать три функции - FindFirstChangeNotification, FindNextChangeNotification И FindcioseChangeNotification. Первая из них возвращает дескриптор объекта файлового оповещения, который можно передать в функцию ожидания. Объект активизируется тогда, когда в заданной папке произошли те или иные изменения (создание или уничтожение файла или папки, изменение прав доступа и т. д.). Вторая - готовит объект к реакции на следующее изменение. Наконец, с помощью третьей функции следует закрыть ставший ненужным объект.

    Так может выглядеть код метода Execute потока, созданного для мониторинга:

    var DirName : string;

    procedure TSimpleThread.Execute;

    var r: Cardinal;

    fn : THandle;

    begin

    fn := FindFirstChangeNotification(pChar(DirName),

    True,FILE_NOTIFY_CHANGE_FILE_NAME) ;

    repeat r := WaitForSingleObject(fn,2000);

    if r = WAIT_OBJECT_0 then Forml.UpdateList;

    if not FindNextChangeNotification (fn) then break;

    until Terminated;

    FindCloseChangeNotification(fn) ;

    end;

    На главной форме должны находиться компоненты, нужные для выбора обследуемой папки, а также компонент TListBox, в который будут записываться имена файлов:

    procedure TFormI.ButtonlClick(Sender:

    TObject);

    var dir : string;

    begin

    if SelectDirectory(dir,[],0) then begin Editl.Text := dir;

    DirName := dir;

    end;

    end;

    procedure TFormI.UpdateList;

    var SearchRec: TSearchRec;

    begin

    ListBoxl.Clear;

    FindFirst(Editl.Text+'\*.*', faAnyFile, SearchRec);

    repeat ListBoxl.Items.Add(SearchRec.Name) ;

    until FindNext(SearchRec) <> 0;

    FindClose(SearchRec) ;

    end;

    Приложение готово. Чтобы оно стало полнофункциональным, предусмотрите в нем механизм перезапуска потока при изменении обследуемой папки.

    Локальные данные потока

    Интересная проблема возникает, если в приложении будет несколько одинаковых потоков. Как избежать совместного использования одних и тех же переменных несколькими потоками? Первое, что приходит на ум - добавить и использовать поля - потомка TThread, которые можно добавить при его создании. Каждый поток соответствует отдельному экземпляру объекта, и их данные пересекаться не будут. (Кстати, это одна из целей, ради которых разрабатывался TThread.) Но есть функции API, которые знать не знают об объектах Delphi и их полях и свойствах. Для поддержки разделения данных между потоками на нижнем уровне в язык Object Pascal введена специальная директива - threadvar, которая отличается от директивы описания переменных var тем, что применяется только к локальным данным потока. Следующее описание:

    Var

    datal: Integer;

    threadvar

    data2: Integer;

    означает, что переменная datal будет использоваться всеми потоками данного приложения, а переменная data2 будет у каждого потока своя.

    Как избежать одновременного запуска двух копий одного приложения

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

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

    Пример такого ресурса - общий блок в файле, отображаемом в память. Поскольку этот ресурс имеет имя, можно сделать его уникальным именно для вашего приложения:

    var UniqueMapping : THandle;

    begin

    UniqueMapping := CreateFileMapping($ffffffff,

    nil, PAGE_READONLY, 0, 32,'MyMap');

    if UniqueMapping - 0 then

    begin

    ShowMessage('Ошибка выделения памяти!');

    Halt;

    end

    else if GetLastError = ERROR_ALREADY_EXISTS then

    begin

    ShowMessage('Вторую копию задачи запускать нельзя!');

    Halt;

    end;

    //можно продолжать

    Application.Initialize;

    Примерно такие строки нужно вставить в начало текста проекта, до создания форм. Блок совместно используемой памяти выделяется в системном страничном файле (об этом говорит первый параметр, равный -1, см. описание функции CreateFiieMapping). его имя - МуМар. если при создании блока будет получен код ошибки error_already exists, это свидетельствует о наличии работающей копии приложения. В этом случае приложение завершается; в противном случае процесс инициализации продолжается.

    Резюме

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

    • Если потоки работают только с переменными, объявленными внутри их собственного класса, то ситуации гонок и тупиков крайне маловероятны. Другими словами, избегайте использования в потоках глобальных переменных и переменных других объектов.
    • Если вы обращаетесь к полям или методам объектов VCL, делайте это только посредством метода synchroize.
    • Не "пересинхронизируйте" ваше приложение, а не то оно будет работать как один-единственный поток. Избыточно синхронизированное приложение теряет все преимущества от наличия нескольких потоков, так как они будут постоянно останавливаться и ждать синхронизации.

    Дополнительную информацию по смежным темам можно найти в следующих главах:

    • глава 1 "Основы объектно-ориентированного программирования" расскажет больше об описании классов.
    • глава 4 "Обработка исключительных ситуаций" - это путеводитель по структурированной обработке ошибок.


    e-mail рассылки
    Радиолюбитель
    Подписаться письмом
    Найти DataSheet!





    Rambler's Top100