На этом шаге мы рассмотрим
некоторые средства синхронизации, предоставляемы средой программирования Delphi.
Как нам уже известно, метод Synchronize нужен для того, чтобы обеспечить корректное обращение дополнительного потока и главного потока приложения к одним и тем же ресурсам. Иными словами, синхронизировать их. Но может возникнуть ситуация, когда синхронизация потребуется для двух дополнительных потоков. Очень часто на практике возникают конфликты между потоками, обращающимися к разделяемым ресурсам, а также к глобальным переменным. Типичными ситуациями, при которых требуется синхронизация потоков, являются гонки и тупики.
Ситуация гонок возникает, когда два и более потока пытаются получить доступ к общему ресурсу и изменить его состояние. Например, пусть поток A получил доступ к ресурсу и изменил его в своих интересах; затем активизировался поток Б и модифицировал этот же ресурс до завершения потока А. Поток А полагает, что ресурс остался в том же состоянии, в каком был до переключения. В зависимости от того, когда именно был изменен ресурс, результаты могут варьироваться - иногда код будет выполняться нормально, иногда нет. Но программист не должен строить никаких гипотез относительно порядка исполнения потоков, т.к. планировщик операционной системы может запускать их и останавливать в любое время. Приведем простой пример ситуации гонок. Пусть два потока выполняют одновременно следующий код:
Inc(i); if i=Value then DoSomething;
Здесь i - глобальная переменная, доступная для обоих потоков. Допустим, поток А инкрементировал значение переменной i и хочет проверить ее значение для выполнения тех или иных условий. Но тут активизировался поток Б, который еще увеличивает значение переменной i. В результате поток А может "проскочить" мимо условия, которое, казалось бы, должно было быть выполнено.
Другими случаями взаимодействия потоков, требующими синхронизации, являются тупики. Ситуации тупиков имеют место, когда поток ожидает ресурс, который в данный момент принадлежит другому потоку. Рассмотрим пример. Допустим, поток 1 захватывает ресурс А, и для того, чтобы продолжить работу, ждет возможности захватить ресурс Б. В тоже время поток 2 захватывает ресурс Б и ждет возможности захватить ресурс А. Развитие этого сценария заблокирует оба потока, и ни один из них не будет выполняться. Ресурсами могут выступать любые совместно используемые объекты системы - файлы, массивы в памяти, устройства ввода/вывода и т.п. Чтобы избежать ситуаций гонок и тупиков, существуют различные приемы, и среда программирования Delphi предоставляет возможность реализации этих приемов.
Наиболее простым в понимании способом синхронизации является критическая секция. Код, расположенный в критической секции может выполняться только одним потоком. В принципе код ни как не выделяется, а происходит обращение к коду через критическую секцию. В начале кода находится функция входа в секцию, а по завершению его выход из секции. Если секция занята потоком, то остальные потоки ждут, пока критическая секция не освободится. В библиотеке VCL критические секции представлены классом TCriticalSection. Для начала рассмотрим методы этого класса.
Метод | Описание |
---|---|
procedure Acquire; override; | Привязывает критическую секцию к вызывающему потоку. |
constructor Create; | Создает объект критической секции. |
destructor Destroy; override; | Уничтожает объект критической секции. |
procedure Enter; | Блокирует прочие потоки, когда вызывающий поток заходит в критическую секцию. |
procedure Leave; | Позволяет другим потокам использовать критическую секцию. |
procedure Release; override; | Освобождает критическую секцию. |
Здесь нужно уточнить, что методы Enter и Leave содержат вызовы методов Acquire и Release соответственно. Поэтому можно сказать, что эти соответствующие пары методов (Enter и Acquire, Leave и Release) выполняют одни и те же действия.
Использовать критические секции довольно просто. Рассмотрим небольшой пример. Допустим, имеется функция, увеличивающая значения элементов глобального массива на единицу.
function IncElem; var i: byte; begin for i:=1 to 10 do Inc(mas[i]); end;
Если эту функцию будут использовать несколько потоков, то может возникнуть конфликт по данным. Чтобы этого не произошло, будем использовать критическую секцию. Для начала следует подключить модуль SyncObjs.pas, содержащий реализацию класса TCriticalSection. Теперь мы можем описать глобальную переменную Section.
Var Section: TCriticalSection;
Затем создадим объект критической секции с помощью метода Create.
Section:=TCriticalSection.Create;
Далее используем критическую секцию следующим образом:
procedure IncElem; var i: byte; begin Section.Enter; for i:=1 to 100 do Inc(mas[i]); Section.Leave; end;
Что будет происходить, когда потоки начнут выполнять данную процедуру? Когда поток встретит в процедуре Section.Enter, он проверит, не занята ли секция другим потоком, и, если она свободна, начнет выполнять код, пока не встретит вызов метода Section.Leave. После использования критической секции, когда она уже больше не нужна, ее необходимо уничтожить.
Section.Free;
Вообще, критических секций может быть несколько. Поэтому при использовании нескольких функции, в которых могут быть конфликты по данным,надо для каждой функции создавать свою критическую секцию. Вход и выход из критической секции не обязательно должны находиться в одной функции. Но нужно внимательно следить за тем, чтобы поток имел выход из критической секции, иначе остальные потоки так и не получат к ней доступ.
Другой способ синхронизации потоков основывается на использовании событий (event). Объект типа событие может принимать одно из двух состояний: активное или пассивное. Когда событие находится в активном состоянии, его видят многие потоки одновременно. В результате такой объект можно использовать для управления работой сразу многих потоков. В библиотеке VCL события представлены классом TEvent. Методы класса TEvent приведены в таблице 2.
Метод | Описание |
---|---|
constructor Create(EventAttributes: PSecurityAttributes; ManualReset, InitialState: Boolean; const Name: string); | Создает объект класса TEvent, представляющий объект события. |
procedure ResetEvent; | Переводит объект события в пассивное состояние. |
procedure SetEvent; | Переводит объект события в активное состояние. |
function WaitFor(Timeout: DWORD): TWaitResult; | Заставляет ждать, пока другой поток или процесс не пошлют сигнал об активизации объекта событие. |
Рассмотрим некоторые методы поподробнее.
Этот метод может, как генерировать новый объект события, так и обеспечивать
доступ к уже существующему объекту.
- EventAttributes - определяет дескриптор безопасности для нового события.
Если данный параметр равен nil, то событие получает дескриптор,
установленный по умолчанию.
- ManualReset - определяет ручной или автоматический сброс
состояния события. Если значение данного параметра true,
то нужно использовать метод ResetEvent для ручного
перевода события в пассивное состояние. Если значение false, то Windows
автоматически осуществит такой перевод.
- InitialState - определяет начальное состояние события (активное или пассивное).
Если значение параметра true, то состояние активное иначе пассивное.
- Name - определяет имя события для обращения к нему из других потоков.
Имя может содержать любые символы, кроме "\". Имя чувствительно к регистру букв.
В случае, когда имя повторяет имя созданного события, происходит
обращение к уже существующему событию.
Данный метод относится к так называемым wait-функциям (функциям, приостанавливающим
выполнение потока). Он позволяет приостановить выполнение потока на
заданное время, устанавливаемое параметром Timeout,
либо до активации события. Этот параметр воспринимается как количество миллисекунд.
Если требуется ожидать активации события неопределенный интервал времени, то следует
установить параметр Timeout в значение INFINITE (данная константа определена в модуле Windows.pas).
Метод WaitFor возвращает результат типа TWaitResult. Объекты этого типа могут принимать следующие значения:
- wrSignaled - означает, что событие было активировано до истечения времени;
- wrTimeout - означает, что время, определенное параметром Timeout
истекло до активации события;
- wrAbandoned - означает, что объект события был уничтожен до истечения времени;
- wrError - означает, что во время ожидания произошла ошибка. Код произошедшей
ошибки содержится в свойстве LastError.
Теперь воспользуемся предыдущем примером для синхронизации потоков событием. Для начала опишем глобальную переменную (не забываем подключить модуль SyncObjs.pas).
Var Event: TEvent;
Далее создаем объект события.
Event:=TEvent.Create(nil,false,true,'');
Теперь помещаем следующий код в процедуру:
procedure IncElem; var i: byte; begin Event.WaitFor(INFINITE); Event.ResetEvent; for i:=1 to 100 do Inc(mas[i]); Event.SetEvent; end;
Итак, как же будет работать эта процедура? Первый поток, начавший ее выполнение, не будет остановлен методом WaitFor, так как изначально объект события был создан в активном состоянии. Далее произойдет сброс события, и поток начнет выполнять цикл. Другой поток, начав выполнять эту процедуру, будет остановлен методом WaitFor, так как первый поток перевел объект события в пассивное состояние. Когда первый поток выполнит цикл, он переведет событие в активное состояние, и второй поток продолжит работу.
На следующем шаге мы начнем рассматривать применение потоков в приложениях.