СОЗДАНИЕ И СИНХРОНИЗАЦИЯ МНОГОПОТОЧНЫХ...

26
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ ДАЛЬНЕВОСТОЧНЫЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ ШКОЛА ЕСТЕСТВЕННЫХ НАУК СОЗДАНИЕ И СИНХРОНИЗАЦИЯ МНОГОПОТОЧНЫХ ПРИЛОЖЕНИЙ МЕТОДИЧЕСКИЕ УКАЗАНИЯ И ЗАДАНИЯ ДЛЯ ВЫПОЛНЕНИЯ ЛАБОРАТОРНОЙ РАБОТЫ ПО ДИСЦИПЛИНЕ “СИСТЕМЫ РЕАЛЬНОГО ВРЕМЕНИ ” Разработала ст.преподаватель кафедры Информационных систем управления Елсукова Е.А Владивосток 2011

Upload: others

Post on 23-May-2020

31 views

Category:

Documents


0 download

TRANSCRIPT

МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ

ДАЛЬНЕВОСТОЧНЫЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ

ШКОЛА ЕСТЕСТВЕННЫХ НАУК

СОЗДАНИЕ И СИНХРОНИЗАЦИЯ

МНОГОПОТОЧНЫХ ПРИЛОЖЕНИЙ

МЕТОДИЧЕСКИЕ УКАЗАНИЯ И ЗАДАНИЯ

ДЛЯ ВЫПОЛНЕНИЯ ЛАБОРАТОРНОЙ РАБОТЫ

ПО ДИСЦИПЛИНЕ “СИСТЕМЫ РЕАЛЬНОГО ВРЕМЕНИ ”

Разработала ст.преподаватель кафедры

Информационных систем управления

Елсукова Е.А

Владивосток

2011

2

СОДЕРЖАНИЕ:

МНОГОЗАДАЧНОСТЬ И МНОГОПОТОЧНОСТЬ 3

СОЗДАНИЕ ПОТОКОВ СРЕДСТВАМИ API 3

СОЗДАНИЕ ПОТОКОВ С ПОМОЩЬЮ КЛАССА TTHREAD 6

ПРОБЛЕМЫ, ВОЗНИКАЮЩИЕ ПРИ ИСПОЛЬЗОВАНИИ ПОТОКОВ 11

СИНХРОНИЗАЦИЯ ПОТОКОВ С ПОМОЩЬЮ КРИТИЧЕСКОЙ СЕКЦИИ 12

СИНХРОНИЗАЦИЯ ПОТОКОВ С ПОМОЩЬЮ СЕМАФОРОВ И МЬЮТЕКСОВ 17

ЗАДАНИЯ ДЛЯ САМОСТОЯТЕЛЬНОЙ РАБОТЫ 22

ИСПОЛЬЗУЕМАЯ ЛИТЕРАТУРА: 23

ПРИЛОЖЕНИЕ 1 24

ПРИЛОЖЕНИЕ 2 25

3

ЛАБОРАТОРНАЯ РАБОТА

СОЗДАНИЕ И СИНХРОНИЗАЦИЯ МНОГОПОТОЧНЫХ ПРИЛОЖЕНИЙ

МНОГОЗАДАЧНОСТЬ И МНОГОПОТОЧНОСТЬ

Многозадачность (multitasking) – это способность операционной системы (ОС)

выполнять несколько программ одновременно. Для реализации многозадачности ОС

использует аппаратный таймер для выделения отрезков времени (time slices) каждому

из одновременно выполняемых процессов. Если эти отрезки времени достаточно малы,

и ПК не перегружен слишком большим числом программ, то пользователю кажется,

что все эти программы выполняются параллельно.

Многопоточность (multithreading) – это возможность программы самой быть

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

(threads), которые, как кажется, выполняются параллельно.

Процессорное время выделяется потокам квантами времени. ОС вытесняет поток,

когда истечет его квант или на очереди поток с большим приоритетом. Приоритеты

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

С каждым потоком связан контекст - структура, содержащая информацию о состоянии

потока, в том числе содержимое регистров процессора. (Более подробную информацию

можно получить, изучив структуру TComext в файле windows.pas из папки

..\borland\delpln4\source\rtl\win.) Вытеснение потока сопровождается

сохранением содержимого регистров в контексте, а получение потоком кванта времени –

восстановлением регистров из контекста.

Поток (Thread, нить выполнения) - исполняемая сущность процесса. Процесс

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

виртуальное адресное пространство, системные и пользовательские ресурсы,

выделенные программе, и одни или несколько потоков (нитей) выполнения. В каждом

процессе всегда неявно (незаметно для программиста и независимо от его воли)

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

программист создает сам. Потоки выполняются параллельно в виртуальном адресном

пространстве породившего их процесса и разделяют ресурсы этого процесса. В ОС

Windows единицей многозадачности является поток, а не процесс.

СОЗДАНИЕ ПОТОКОВ СРЕДСТВАМИ API

Использование библиотеки API Win32 - наиболее мощный и универсальный

способ работы с потоками на большинстве языков программирования . Имеющиеся в

Delphi специализированные классы являются надстройкой над соответствующими

функциями API.

Поток создается с помощью функции API CreateThread. Описание функции

и начальные значения параметров приведены в таблице 1, пример вызова - в листинге

1.

function CreateThread (

Attr : Pointer;. // Адрес атрибутов безопасности Stack: Dword; // Размер стека для потока Start: Pointer; // Начальной адрес потока

par: pointer; // Аргументы потока flag: Dword; // флаг создания var ID: Dword // Идентификатор потока

) : THandle; // Результат функции - дескриптор потока.

4

Таблица 1

Параметры функции CreateThread

Наименование Тип Назначение Описание

Attr Pointer Адрес атрибутов

безопасности

Обычно Attr = Nil

(соответствует атрибутам

безопасности по умолчанию)

Stack Dword Размер стека для

потока

Параметр задает размер стека

для потока. Если Stack = 0,

то размер стека совпадает с

размером стека основного

потока приложения.

Start Pointer Начальной адрес

потока

Основной параметр, через

него передается адрес

функции, вызываемой при

запуске потока.

Par Pointer Аргументы потока Указывает на структуру

(запись), содержащую

аргументы, передаваемые в

процедуру потока.

Если Par = Nil, то

аргументы отсутствуют

flag Dword Флаг создания Если flag,= 0, то поток сразу

начнет работу,

если flag = CREATESUSPENSED

(константа), то поток начнет

работу только после вызова

функции ResumeThread.

ID Dword Идентификатор

потока

Возвращает идентификатор

потока

Вызов функции происходит через параметр Start. Вызываемая функция

обязана возвращать результат типа Longint, иметь один параметр типа Pointer.

При описании функции необходимо указывать атрибут stdcafl (см. листинг 1),

определяющий стандартный для API способ вызова функции (запись аргументов в стек

в порядке справа-налево, очистка стека при завершении работы самой подпрограммой).

Функция function ResumeThread {hThread: THandle): Dword;

(Здесь hThread - дескриптор созданного потока.)

Приостановить поток можно с помощью функции:

function SuspendThread {hThread: THandle): Dword;

Чтобы досрочно завершить поток, следует вызвать функцию SuspendThread, а затем

– функцию: function CloseHandle(hThread: THandle): Dword;

Задание №1: Использование функций API Win32 для создания потока в Delphi.

В задании организуем вычисления традиционным способом и, для сравнения, с

помощью дополнительного потока.

1. Создать новый проект с кнопками «С потоком», «Без потока»,

«Приостановить», «Продолжить».

5

2. Создать функцию func и обработчики событий, как показано в листинге 1.

Внешний вид работающего приложения приведѐн на рис.1.

Листинг 1

unit Unitl;

interface uses

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

Dialogs, StdCtrls;

type

TForml = class(TForm)

Buttonl: TButton;

Button2: TButton;

Button3: TButton;

Button4 : TButton ;

procedure ButtonlCliek(Sender: TObject);

procedure Button2Click(Sender: TObject};

procedure Button3Ciick<Sender: TObject);

procedure Button4Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations. }

end;

var

Forml: TForml;

implementation

var hThread: THandle; // Дескриптор потока

ThreadID: DWord; // Идентификатор потока

{$R *.DFM}

// Эта функция далее будет выполнять вычисления внутри потока

function func (p: pointer): longint; stdcall;

var i: integer;

dc: h.Dc; //дескриптор контекста графического устройства

s: string;

begin

dc:=GetDc(form1.handle);//Получаем контекст формы

for i:=0 to 10000000 do begin

s := IntToStr(i); //Выводим в цикле число, используя функции API

textout(dc, 10, 10,pchar (s) , length <s));

end ;

ReleaseDC{Forml.handle, dc); //Освобождаем контекст

end;

procedure TForml.Button1Click(Sender: TObject);

begin

// Вызываем функцию Func в составе потока. Для зтого создаем

поток передаем в него адрес функции

hThread := CreateThread(Nil,0 , @Func ,Nil ,0 ,THreadID);

if hThread=0 then ShowMessage (‘No THread');

6

end;

procedure TForml.Button2Click(Sender: TObject);

begin

SuspendThread(hThread); // Приостанавливаем поток

end;

procedure TForml.Button3Click(Sender: TObject);

begin

ResumeThread(hThread); // Продолжаем поток

end;

procedure TForml.Button4Click(Sender: TObject);

begin

Func (nil); // Вызываем функцию обычным способом

end;

end.

Рис.1. Пример работы программы

Комментарии работы программы:

1. При нажатии кнопки «Создать поток» будут выполняться не один, а два

потока: основной поток обрабатывает сообщения, адресованные главной форме,

дополнительный - выводит числа. Если вызвать функцию Func(nil) напрямую (при

нажатии кнопки «Без потока»), то все операции будет выполнять основной поток,

поэтому программа будет ожидать окончания цикла («зависнет»). В это время окно не

реагирует на события, не позволяя себя ни переместить, ни закрыть. При нажатии

кнопки «Создать поток» окно программы будет вести себя обычным образом.

2. Для вывода строки текста вместо “обычных” визуальны: средств Delphi

использовалась функция API TexiOut. Это объясняется тем, что большинство

визуальных компонентов Delphi работают стабильно только в главном потоке

приложения. Если без компонентов Delphi в дополнительных потоках не обойтись, то

рекомендуется использовать специализированный класс TThread.

СОЗДАНИЕ ПОТОКОВ С ПОМОЩЬЮ КЛАССА TTHREAD

Для создания потоков пользователя используется базовый класс TThread.

Описание свойств и методов класса TThread приведено в таблице 2. Он инкапсулирует

функции API программирования потоков. Его основными преимуществами являются:

удобство использования, свойственное всем классам-надстройкам; наличие специального

7

метода Synchronize для корректного использования внутри потоков визуальных

компонентов VCL Delphi.

TThread = class

protected

procedure DoTerminate; virtual;

procedure Execute; virtual;

procedure Synchronize(Method: TThreadMethod);

property ReturnValue: Integer;

property Terminated: Boolean;

public

constructor Create(CreateSuspended: Boolean);

procedure Resume;

procedure Suspend;

procedure Terminate;

function WaitFor: LongWord;

property FreeOnTerminate: Boolean;

property Handle: Thandle;

property Priority: TthreadPriority;

property Suspended: Boolean;

property ThreadID: Thandle;

property OnTerminate: TnotifyEvent;

end;

Таблица 2

Свойства и методы класса TThread

Метод Описание метода

constructor Create

(CreateSuspensed:

Boolean);

Создает поток:

Если аргумент CreateSuspensed = False, созданный

поток немедленно начинает выполнение (управление

передается методу Execute).

Если CreateSuspensed = True, то- поток ожидает

вызова метода Resume

destructor Destroy:

override;

Завершает поток и освобождает все ресурсы, им

занятые. Вызывается автоматически при завершении

метода Execute

procedure Resume; Возобновляет поток после приостановки

procedure Suspend; Приостанавливает поток

property Suspended:

Boolean;

При записи True/ False приостанавливает /

возобновляет поток. При чтении показывает, не

приостановлен ли поток

procedure Terminate; Устанавливает свойство Terminated в True. При

использовании этого метода для завершения потока

метод Execute должен включать в себя проверку

свойства Terminated.

8

Метод Описание метода

property Terminated:

Boolean;

Показывает True, если ранее был вызван метод

Terminate.

function WaifcFor:

integer;

Приостанавливает текущий поток до завершения

заданного потока и возвращает код завершения,

Например, внутри потока T1 вызов

code:=T2.WaitFor приостанавливает T1 до

завершения T2.

property Handle:

THandle;

Дескриптор потока

property ThreadID:

THandle;

Идентификатор потока

property Priority:

TThreadPriority;

Приоритет потока. Значения приоритета приведены в

таблице 3.

procedure

Synchronize (Method:

TThreadMethod);

Метод используется для обращения к компонентам

VCL внутри потока. Указанный в качестве аргумента

метод, содержащий вызовы VCL, включается в

главный поток приложения.

procedure Execute;

virtua1; abstract;

Главный метод класса. Обязательно переопределяется,

после чего должен содержать код потока

property ReturnValue:

integer;

Код завершения потока. По умолчанию - ноль. Другие

значения могут быть присвоены, внутри потока по

усмотрению программиста

propertу OnТеrminate:

TNotifуEvent;

Событие, происходящее после завершения метода

Execute, но перед Destroy.

Таблица 3

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

Имя Значение

tpldle Поток получает квант времени только тогда, когда

система находится в состоянии простоя

tpLowest Приоритет на два пункта ниже нормального

tpLower Приоритет на одни пункта ниже нормального

tpNormal Норм а льный при оритет

tpHigher На один пункт выше нормы

tpHighest* На два пункта выше нормы

tpTimeCritical* Максимальный приоритет

* - назначение приоритетов tpHighest и tpTimeCritical следует проводить

осторожно, это может нарушить работу приложения.

Стандартный мастер модулей в Delphi автоматически создает модуль, содержащий

класс потока с указанным именем. Весь код, который необходимо вынести в

отдельный поток, помещается в метод класса Execute. Процесс, породивший поток

может гибко управлять его состоянием: приоритетом Priority; приостановить и

продолжить его исполнения, а так же досрочно завершить выполнение потока.

Из перечисленных в таблице 2 методов обязательными являются только методы

9

Create и Execute. Последний метод является абстрактным. Поэтому нельзя создать

экземпляр TThread, необходимо предварительно обязательно создать потомка класса

TThread, перекрыть в нем метод Execute, добавив в него необходимую функциональность. Общая структура приведена в приложении 1.

Задание 2: Создание и завершение потока с помощью методов класса TThread.

1. Организуйте на форме две кнопки и одну строку редактирования.

2. Запишите объявление класса TSimpleThread и переопределите его метод

Execute, как это показано на листинге 2.

3. В форме определите метод OutMessage.

Внешний вид работающего приложения приведѐн на рис.3.

Листинг 2

unit ThrdUnit;

interface

uses

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

Forms, Dialogs, ComCtrls, StdCtrls, ExtCtrls;

type

TForml = class(TFozm)

Editl: TEdit;

Button1; TButton;

Button2: TButton;

procedure ButtonlClick(Sender: TGbject);

procedure Button2Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Forml : TForml;

implementation

{$R *,DFM}

type // Объявление класса потока-потомка.

TSimpleThread = class(TThread)

procedure Execute; override;

public // Здесь удобно добавить любые поля и методы,. //которые

будут использоваться локально, внутри потока. Count: Integer;

// счетчик цикла

procedure OutMessage;

end ;

procedure TSimpleThread.Execute;

begin

// Содержание потока

while not Terminated do begin//Поток будет выполняться,

// пока не будет нажата кнопка Button2

Count := Count+1;

Synchronize(OutMessage); // Безопасный вывод

end;

10

end;

procedure TSimpleThread.OutMessage;

begin

Forml.Editl.Text:=IntToStr(count);// Обращение к VCL

end;

var Threadl: TSimpleThread; // Экземпляр класса «поток»

procedure TForml.Button1Click(Sender: TObject);

begin

Threadl: = TSimpleThread. Create (False);

//Создание потока

Threadl.Priority := tpLowest; // Приоритет

Threadl.Count : = 0; // Начальное значение счетчика

end.;

procedure TForml.Button2Click(Sender: TObject);

begin

Thread1.Terminate; // Завершаем бесконечный поток

end;

end.

Рис2. Пример работы программы

Комментарии работы программы:

1. Аргументом метода Synchronize является имя метода (но не процедуры),

который обращается к объектам VCL. Последний метод должен быть оформлен в виде

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

Editl был определен метод потока:

procedure TSimpleThread,OutMessage;

begin

FormI.Editl.Text := IntToStr(count);

End;

а внутри метода Execute потока происходил вызов Synchronize(OutMessage).

Процедура Synchronize (Method:TThreadMethod) необходима для синхронизации

дочернего потока с главным при вызове методов VCL. В данном примере метод

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

записи в объект Editl. Работающий главный поток позволяет компонентам,

расположенным в окне, обмениваться сообщениями; при этом может наступить

ситуация гонок, например, одновременное присваивание

Forml.Editl.Text := IntToStr(count);

и редактирование Editl другим потоком или пользователем, что в принципе

11

недопустимо. В данном примере это не имеет особого значения, но в ответственных

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

измените метод, добавив задержку на 1 секунду:

procedure TSimpleThread.OutMessage;

begin

Sleep (1000) ;

Forml.Editl.Text := IntToStr(count);

end;

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

приращениями счетчика невозможно ни отредактировать строку, ни переместить окно-

метод OutMessage выполняется в главном потоке и блокирует его остальные действия,

как это было в задании1 при нажатии кнопки «Без потока». (Временная задержка 1 сек.

нужна лишь, чтобы визуально заметить данный эффект).

Очевидно, метод OutMessage не рекомендуется загромождать вычислениями,

так как это сильно замедлит работу главного потока.

2. Чтобы создать поток, удобно воспользоваться имеющимся в Delphi шаблоном:

выбрать пункт меню File New Thread Object и ввести имя класса потока-

потомка TThread, например, TFirstThread. После этого появится автоматически

сгенерированный модуль с объявлением класса TFintThreacL который следует

должным образом видоизменить и подключить к вызывающему модулю обычной

директивой uses.

Задание №3

Доработайте предыдущий проект, сделав два независимых аналогичных потока Thread1,

Thread2, выводящих результаты в отдельные строки редактирования. Здесь можно

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

двух экземпляров одного класса TShnpteThread. Для ступенчатой регулировки

приоритетов используйте два компонента ТТrаскВаr (см. рис.3). Проект должен

показывать изменение скорости работы в зависимости от установленного приоритета.

Рис.3. Пример работы

программы

ПРОБЛЕМЫ, ВОЗНИКАЮЩИЕ ПРИ ИСПОЛЬЗОВАНИИ ПОТОКОВ

Современные приложения представляют собой сложный комплекс

взаимодействующих процессов. В одних решаются вопросы подготовки данных,

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

отладка такого многопоточного приложения требует от программиста высокой

квалификации. Если в ОС реализована вытесняющая многозадачность, то поток

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

потенциальную опасность.

Разделение данных между двумя и более потоками является общим случаем.

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

12

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

иногда – нет. ОС (модуль планирования) может переключать управление потоками

только между инструкциями машинного кода. Если простое целое число разделяется

между двумя потоками, то изменение этой переменной обычно осуществляется одной

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

Однако, предположим, что потоки разделяют несколько переменных или

сложные структуры данных. Часто эти сложные переменные или поля структур данных

должны быть согласованными между собой. Операционная система может прерывать

поток в середине процесса обновления этих переменных. В этом случае поток, который

затем использует эти переменные, будет иметь дело с несогласованными данными.В

результате возникла коллизия, и нетрудно представить себе, как такого рода ошибка

может привести к краху всей системы.

Одной из основных ошибок в многопоточных программах является так

называемое состояние гонки (race condition). Это случается, если программист считает,

что один поток закончит выполнение своих действий, например, подготовку каких -

либо данных, до того, как эти данные потребуются другому потоку.

К числу типичных проблем, возникающих в многопоточных приложениях,

относятся так называемые гонки и тупики. Состояние гонки (race condition) или конфликт

(collision) гонок происходит, когда два потока пытаются изменить одну и ту же область

памяти. Если не предпринять специальных мер синхронизации, то результат работы

приложения будет непредсказуемым и зависеть от скорости работы потоков (гонки).

Тупики возникают, когда потоки ожидают ресурсы, занимаемые друг другом.

Например, поток А держит ресурс 1, ожидая когда поток Б отдаст ему ресурс 2, Но поток

Б ресурс не отдает, так как ждет ресурса 1. В результате оба потока бездействуют.

Для того чтобы избежать таких ситуаций и скоординировать потоки,

операционным системам необходимы средства синхронизации. Все эти средства

основаны на блокировке доступа к ресурсу, пока этот ресурс занят каким-либо

потоком. В последнее время в этом направлении активно ведутся работы. Далее, более

подробно будут рассмотрены отдельные механизмы синхронизации в ОС Windows.

СИНХРОНИЗАЦИЯ ПОТОКОВ С ПОМОЩЬЮ КРИТИЧЕСКОЙ СЕКЦИИ

Одним из простых средств синхронизации потоков является критическая секция

(критический раздел). Критическая секция – это блок кода, содержащий обращения к

разделяемым данным, при его выполнении поток не может быть прерван.

Механизм критических секций основан на принципе взаимного исключения

(mutual exclusion). Только один поток может быть владельцем критического раздела в

каждый конкретный момент времени. Следовательно, один поток может войти в

критический раздел, установить значения полей структуры и выйти из критического

раздела. Другой поток, использующий эту структуру, также мог бы войти в

критический раздел перед осуществлением доступа к полям структуры, а затем выйти

из критического раздела. Критические секции выполняются быстро, но их недостатком

является то, что их можно использовать для синхронизации в рамках одного процесса.

Рассмотрим реализацию критической секции на базе функций API Win32 (как

обычно, в Delphi имеются аналогичные классы-надстройки).

Для использования критической секции достаточно создать структуру типа

TRTLCriticalSection и проинициализировать еѐ с помощью

InitializeCriticalSection(Sect). Перед входом в критическую секцию

проверяется глобальная запись типа TRTLCriticalSection (в которой отмечено,

13

выполняется ли критическая секция в данный момент времени каким-либо другим

потоком), если секция не занята, поток допускается к работе. Структуру записи

TRTLCriticalSection можно увидеть в файле

..\borland\delplii4\source\rtl\wm\wiudowi.pas).

TRTLCriticalSection обычно располагается в области глобальных

переменных, доступной всем потокам процесса. Так как каждый процесс работает в своем

адресном пространстве, то нельзя передавать адрес критической секции из одного

процесса в другой. Именно поэтому критические секции нельзя использовать для

синхронизации потоков, созданных разными процессами.

В начале критической секции необходимо вызвать подпрограмму API

EnterCritiсalSection (sect:TRTLCriticalSection);,которая проверяет

состояние записи sect и если в записи секция помечена как занятая другим потоком,

приостанавливает текущий поток, пока критическая секция не освободится.

В конце критической секции вызывается подпрограмма LeaveCritlcalSection

(sect:TRTLCriticalSection);,которая делает пометку в записи, что критическая

секция свободна, и другой поток в соответствии с приоритетом получает квант времени и

входит в критическую секцию.

Предварительно, перед использованием необходимо проинициализировать запись

sect с помощью процедуры InitializeCriticalSection(Sect);,а при

завершении использования – удалить процедурой

DeleteCriticalSection(Sect);.

В таблице 4 приведены средства, используемые при работе с критическими

секциями. В рассматриваемом примере первая операция выполняется в обработчике

события создания формы OnCreate. а вторая - в обработчике события удаления формы

из памяти OnDestroy.

Таблица 4

Функция Описание Возвращаемое

значение

procedure

InitializeCriticalSection

(Sect);

Инициализация критической

секции. Передаваемый

параметр – адрес структуры

типа

TRTLCriticalSection

Нет

procedure

DeleteCriticalSection

(Sect);

Удаление критической

секции. Передаваемый

параметр – адрес структуры

типа

TRTLCriticalSection

Нет

procedure

EnterCritiсalSection

(sect);

Вход в критическую секции.

Передаваемый параметр –

адрес структуры типа

TRTLCriticalSection

Нет

procedure

LeaveCritlcalSection

(sect);

Выход из критической

секции. Передаваемый

параметр – адрес структуры

типа

TRTLCriticalSection

Нет

14

Рекомендации по использованию критических секций

Возможно определение нескольких объектов типа критическая секция,

например, cs1 и cs2. Если в программе имеется четыре потока, и два первых

из них разделяют некоторые данные, то они могут использовать первый

объект критическая секция cs1 , а два других потока, разделяющих уже

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

При использовании в программе нескольких критических секций необходимо

соблюдать одинаковую последовательность входа и выхода в эти критические

секции, иначе возможны взаимные блокировки задач.

Аккуратно используйте критические разделы в главном потоке: если

вторичный поток проводит слишком много времени в его собственном

критическом разделе, то это может привести к "зависанию" главного потока

на слишком большой период времени.

Задание 4: Синхронизация доступа к разделяемой переменной с помощью

критической секции.

Рассмотрим пример, когда два потока обращаются к одной разделяемой переменной

GlobalData увеличивают и сразу уменьшают еѐ значение:

GlobalData := GlobalData+3;

GlobalData := GlobalData-3;

а результаты выводят в соответствующие компоненты ТМето.

1. Разместите на форме: два компонента Memol и Меmо2 - две метки с надписями

«Первый поток» и «Второй поток», кнопку Button1 «Выполнить»,

выключатель cbSect: TCheckBox с подписью «Использовать критическую

секцию».

Чтобы можно было сравнивать результаты, полученные с использованием

синхронизации и без синхронизации, в обработчике события OnClick для

выключателя запишите CritSect := cbSect.Checked

2. Инициализацию и удаление критической секции запишите, соответственно, в

обработчиках событий OnCreate и OnDestroy для формы.

3. Так оба потока идентичны, нерационально создавать два отдельных класса.

Поэтому опишите один класс TMyThread. переопределите его методы и

создайте два экземпляра этого класса, как показано на листинге 3.

Листинг 3

unit ThrdUnit;

interface

uses

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

Forms, Dialogs, ComCtris, StdCtrls, ExtCtrls;

type

TForml = class(TForm)

Buttonl: TBtitton ;

cbSect; TCheckBox;

Label1: TLabel;

Label2: TLabel;

Memo1: TMemo;

15

Memo2: TMemo;

procedure ButtonlClick(Sender: TObject);

procedure FormCreate(Sender: TObject);

procedure FormDestroy(Sender: TObject);

procedure cbSectClick(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Forml: TForml;

type

TMyThread = class(TThread)

private

{ Private declarations }

mem: TMemo;

// Б переменную mem при вызове конструктора потока будет // передаваться конкретный

экземпляр TMemo

protected

constructor Create (M: TMemo);// Переопределение

procedure Execute; override;

procedure OutMessage;

end ;

implementation

{$R *.DFM}

var CritSect; boolean; // Флаг «вкл/откл»

Sectl: TRTLCriticalSection; // Критическая секция

GlobalData: integer; // Глобальная переменная

constructor TMyThread.Create(m: TMemo);

begin //Переопределяем конструктор

Inherited Create(False);//Вызываем унаследованный

mem:= m; //Передаем, куда записывать числа

end;

procedure TMyThread.OutMessage;

begin // Добавление строки в редактор

Mem.Lines.add(IntToStr(GIobalData)) ;

end;

procedure TMyThread.Execute;

var j: integer;

begin

for j:=1 to 50 do begin

// Если механизм критической секции используется

if (CritSect) then EnterCriticalSection{Sectl);

// Вход в критическую секцию

GIobalData := GlobalData+3; //Изменение разделяемой переменной

16

Sleep (3); //Задержка на 3 мс для увеличения вероятности рассинхронизации

GIobalData := GlobalData-3; // Изменение разделяемой переменной

Synchronize (OutMessage);

// Выход из критической секции

if (CritSect) then LeaveCriticalSection(Sect1);

end;

end;

var Threadl, Thread2: TMyThrsad; //Экземпляры потоков

procedure TForml.Button1Click(Sender: TObject);

begin

GIobalData := 100;

Memol.Lines.Clear;

Memo2.Lines.Clear;

Thread1 := TMyThread.Create(Memol);// Старт потока1

Thread2 := TMyThread.Create(Memo2);// Старт потока2

end;

procedure TForml.FormCreate(Sender; TObject);

begin

InitializeCriticalSection(Sectl);

CritSect := false;

end;

procedure TForrm1.FormDestroy(Sender: TObject);

begin

DeleteCriticalSectioa(Sectl);

end;

procedure TForml.cbSectClick(Sender: TObject);

begin

CritSect:= cbSect.Checked;

end;

Рис.4. Пример работы программы без

использования синхронизации

Очевидный результат работы

потоков (GlobalData=100)

искажается эффектом «гонок».

17

Рис.5. Пример работы программы с

использованием критической секции

СИНХРОНИЗАЦИЯ ПОТОКОВ С ПОМОЩЬЮ СЕМАФОРОВ И

МЬЮТЕКСОВ

Существующее ограничение в использовании критических секций не позволяет

их применять для синхронизации потоков различных процессов. В таких случаях

необходимо применять объекты типа семафоры и мьютексы (mutex).

Семафоры и мьютексы во многом похожи на критические секции, методика их

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

Преимуществом по сравнению с критическими секциями является, прежде всего,

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

активными приложениями. Кроме того, при обращении к семафорам и мьютексам

можно указывать максимальное время ожидания доступа к ресурсу, что позволяет

предотвращать ситуацию «тупиков».

Семафоры-счетчики регулируют число потоков, которые могут использовать

ресурс. Исключающий семафор (mutex, мьютекс) допускает к ресурсу только один

поток. В таблицах 5,6 приведены функции, используемые для работы с мьютексами и

семаформи.

Объекты синхронизации mutex обеспечивают последовательное использование

ресурсов. Составное слово "mutex" происходит из словосочетания "mutual exclusion"

(“взаимное исключение”, “взаимоисключающий”), что точно отражает назначение

объектов. Требуется предотвратить возможность прерывания потока в программе до

тех пор, пока не будет выполнено работа с разделяемыми данными.

Организация последовательного доступа к ресурсам с использованием объектов

mutex возможна потому, что в каждый момент только одна задача может владеть этим

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

захвачен, должны ждать, например, с помощью функции WaitForSingleObject.

Когда какая-либо задача, принадлежащая любому процессу, становится владельцем

объекта mutex, последний переключается в неотмеченное состояние. Если же задача

"отказывается" от владения объектом mutex, его состояние становится отмеченным.

Перед использованием объекта mutex его нужно создать при помощи функции

CreateMutex.

function CreateMutex(

Attr: Pointer; // указатель на атрибут безопасности

18

flag; Boolean; // флаг создания

lpName : PChar // указатель на имя мьютекса

): THandle; // дескриптор

Стандартный вариант создания: hMutex := CreateMutex(Nil, False, Nil);

Для того чтобы объект mutex был доступен задачам, принадлежащим

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

mutex, задача может его открыть с помощью функции OpenMutex.

Мьютексы могут находиться в сигнальном/ отмеченном (если им не владеет ни

один поток, т.е. ресурс свободен) или несигнальном состоянии (поток владеет

мьютексом). Чтобы поток завладел мьютексом (т.е. получил доступ к ресурсу),

необходимо вызвать функцию ожидания WaitForSingleObject или

WaitForMultipleObjects:

function WaitForSingleObject (

hHandle: THandle; // дескриптор мьютекса

dwMilliseconds: DWord // время ожидания в мc

): DWord;

Параметр dwMilliseconds может принимать значение Infinite, что означает

бесконечный интервал ожидания.

При вызове функции WaitForSingleObject для объекта mutex, который

никому не принадлежит, функция сразу возвращает управление. При этом задача,

вызвавшая функцию WaitForSingleObject, становится владельцем объекта mutex.

Если теперь другая задача вызовет функцию WaitForSingleObject для этого же

объекта mutex, то она будет переведена в состояние ожидания до тех пор, пока первая

задача не "откажется от своих прав" на данный объект mutex .

Функция завершает работу, если мьютекс становится сигнальным или истекло

время ожидания. После этого поток, находившийся в состоянии ожидания мьютекса.

получает к нему доступ, а мьютекс, получивший нового владельца, перестает

сигнализировать.

После того, как поток выполнил участок кода, содержащий совместно

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

помощью функции API:

function ReleaseMutex (

hHandle: THandle; // дескриптор мьютекса ): Boolean;

При завершении работы мьютекс удаляется из памяти с помощью функции

CloseHandle, в которую передается дескриптор мьютекса.

Таблица 5

Функции для работы с объектами мьютекс

Функция Описание Возвращаемое

значение

function CreateMutex(

Attr: Pointer;

flagini; Boolean;

Создание объекта мьютекс.

указатель на атрибут

безопасности (защиты)

флаг создания – начальное

состояние:

True – поток, создающий

мьютекс будет владеть им

Дескриптор

(идентификатор

) объекта или

Null при

ошибке

19

Функция Описание Возвращаемое

значение

lpName : PChar ):

THandle;

сразу после создания;

False – после создания

объект не будет принадлежать

никакому потоку.

указатель на имя мьютекса

function OpenMutex (

FdwAccess: dword;

flagHerit; Boolean;

lpName : PChar ):

THandle;

Открытие объекта мьютекс.

Требуемый доступ:

EVENT_ALL_ACCESS –

указаны все возможные флаги

доступа;

SYNCHRONIZE – полученный

дескриптор можно

использовать во всех функциях

ожидания.

Флаг наследования:

True – дескриптор может

наследоваться другими

процессами;

False – наследование не

допускается.

указатель на имя мьютекса

Дескриптор

объекта или

Null при

ошибке

function ReleaseMutex (

hHandle: THandle; ):

Boolean;

Освободить мьютекс

дескриптор мьютекса True при

завершении без

ошибки,

иначе False.

В отличие от объектов mutex, которые используются для организации

последовательного доступа потоков к ресурсу, семафоры позволяют обеспечить доступ

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

пытающиеся получить доступ сверх установленного лимита, будут переведены при

этом в состояние ожидания до тех пор, пока ресурс, связанный с данным семафором

не освободит поток, получивший доступ к ресурсу раньше.

Примером области применения семафоров может послужить программное

обеспечение какого-либо аппаратного устройства, с которым может работать только

ограниченное количество задач. Все остальные задачи, пытающиеся получить доступ к

этому устройству, должны быть переведены в состояние ожидания, и находиться в нем

до тех пор, пока не завершит свою работу одна из задач, уже получившая доступ к

устройству.

Так же как и объект mutex. семафор может находиться в отмеченном и

неотмеченном состоянии. Приложение выполняет ожидание для семафора при помощи

таких функций, как WaitForSingleObject или WaitForMultipleObjects. Если

семафор находится в неотмеченном состоянии, задача, вызвавшая для него функцию

WaitForSingleObject, находится в состоянии ожидания. Когда же состояние

семафора становится отмеченным, работа задачи возобновляется.

Семафор представляет собой объект синхронизации, реализованный в виде

счетчика, начальное и максимальное значение которого задаются при создании

семафора. Значение этого счетчика уменьшается, когда задача вызывает для семафора

20

функции WaitForSingleObject или WaitForMultipleObjects, и

увеличивается при вызове функции ReleaseSemaphore.

Если значение счетчика семафора равно нулю, он находится в неотмеченном

состоянии. Если же это значение больше нуля, семафор переходит в отмеченное

состояние.

Пусть, например, приложение создало семафор, указав для него максимальное

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

равно двум. Если в этой ситуации несколько запускаемых по очереди задач будут

выполнять с помощью функции WaitForSingleObject ожидание семафора, то

первые две запущенные задачи будут работать, а все остальные перейдут в состояние

ожидания. Это связано с тем, что первые два вызова функции

WaitForSingleObject приведут к последовательному уменьшению значения

счетчика семафора до нуля, в результате чего семафор переключится в неотмеченное

состояние. Задача, запущенная третьей, вызовет функцию WaitForSingleObject

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

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

семафора. Ожидание продлится до тех пор, пока одна из первых двух задач не

освободит семафор, увеличив его счетчик на единицу вызовом функции

ReleaseSemaph&re. Сразу после этого будет запущена одна из задач, ожидающих

семафор.

Семафор создается функцией:

function CreateSemaphore (

Attr : Pointer; //указатель на атрибуты

InitialCount: longint; //Начальное число потоков, допущенных к

объекту

MaxCount: longint; //Максимальное число допускаемых потоков

ipName: PChar // указатель на имя семафоря

) : THandle; // дескриптор семафора

Семафор сигнализирует, если InitialComn больше нуля, и не подает сигнала, если

IninnlCoimt равен нулю.

Для доступа к объекту используется функция ожидания WaitForSingleObject или

WaitForMultipleObjects.

При завершении функции ожидания значение счетчика InitialCount

соответственно уменьшается, и поток получает доступ к ресурсу.

При завершении доступа к ресурсу необходимо вызвать функцию, увеличивающую

значение счетчика семафора:

function ReleaseSemaphore(

hHandle: THandle; // дескриптор семафора

ReleaseCount: longint // приращение счетчика

LP: Pointer // как правило, nil

): Boolean;

Не предусмотрено средств, с помощью которых можно было бы определить

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

текущего значения счетчика заключается в увеличении этого значения функцией

ReleaseSemaphore. Значение счетчика, которое было до увеличения, будет записано

в переменную, адрес которой передается функции ReleaseSemaphore через

параметр LP.

21

Если семафор используется только для синхронизации задач, созданных в

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

параметра lpName функции CreateSemaphore значение NULL. В том случае, когда

необходимо синхронизировать задачи разных процессов, следует определить имя

семафора. При этом один процесс создает семафор с помощью функции

CreateSemaphore, а второй открывает его, получая идентификатор для уже

существующего семафора.

Таблица 6

Функции для работы с семафорами

Функция Описание Возвращаемое

значение function CreateSemaphore

(

Attr : Pointer;

InitialCount: longint;

MaxCount: longint;

ipName: PChar ) :

THandle;

Создание семафора

указатель на атрибуты защиты

Начальное значение семафора

Максимальное значение

семафора – (Максимальное

число допускаемых потоков)

Указатель на имя семафора

Дескриптор

(идентификатор

) семафора или

Null при

ошибке

function OpenSemaphore

(

FdwAccess: dword;

flagHerit; Boolean;

lpName : PChar ):

THandle;

Открытие семафора.

Требуемый доступ:

Флаг наследования:

True – дескриптор может

наследоваться другими

процессами;

False – наследование не

допускается.

указатель на имя семафора

Дескриптор

семафора или

Null при

ошибке

function

ReleaseSemaphore(

hHandle: THandle; ):

ReleaseCount: longint

LP: Pointer

Boolean;

Освободить семафор

(увеличить его значение)

дескриптор семафора

приращение счетчика

как правило, nil

True при

завершении без

ошибки,

иначе False.

Задание 5:Замените в задании 4 критические секции а) мьютексами, б) семафорами.

22

ЗАДАНИЯ ДЛЯ САМОСТОЯТЕЛЬНОЙ РАБОТЫ

1. Создать два потока, один из которых заполняет Memo случайными числами до

остановки процесса пользователем, а второй осуществляет вычисление очередного числа

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

вычисления целого числа изменить надпись на кнопке и перейти к вычислению квадратного

корня.

2. Создать два потока, один из которых выводит в Memo 200 строк, заполняя их

соответствующими номеру строки числами, а второй вычисляет значение функции в точке.

Выбор функции реализовать с помощью ComboBox. а значение аргумента вводить в редакторе,

/*Для заполнения Меню предусмотреть индикатор процесса.*/

3. Вычислить приближенное значение определенного интеграла с помощью метода

прямоугольников, метода Симпсона. Выбор функции реализовать с помощью ComboBox. а

границы отрезка задавать в редакторе. /*Предусмотреть возможность графического

отображения. */

4. Вычислить приближенное значение определенного интеграла с помощью метода

трапеций. Разные потоки осуществляют вычисления с различным шагом. Выбор функции

реализовать с помощью ComboBox. а границы отрезка задавать в редакторе.

/*Предусмотреть возможность графического отображения. */

5. Решить нелинейное уравнение с одним неизвестным методами половинного

деления, методом хорд, методом Ньютона. Для каждого метода свой поток. /*

Предусмотреть возможность графического отображения. */

6. Пользователь задает 2 функции. Реализовать потоки для построения графиков

функций по задаваемому числу точек с возможностью задания приоритета потока. Точки

пересечения выделить другим цветом.

7. Задан массив целых чисел. Осуществить сортировку массива методами пузырька,

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

графического отображения: каждому числу массива поставить в соответствие линию

пропорциональной длины; при сортировке линии также меняются местами.

8. Задан массив целых чисел. Осуществить сортировку массива методами пузырька,

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

графического отображения: каждому числу массива поставить в соответствие линию

пропорциональной длины; при сортировке линии также меняются местами.

9. Задается функция. В нескольких окнах построить графики функции по

задаваемому числу точек с возможностью задания приоритета потока.

10.Описать движение шарика в прямоугольной области с отражением от границ

под случайным углом. Предусмотреть возможность запуска нескольких шариков.

23

ИСПОЛЬЗУЕМАЯ ЛИТЕРАТУРА:

1. Рудалев В.Г., Крыжановская Ю.А. Разработка многопоточных приложений.

Методические указания к курсу ”Системное и прикладное программное обеспечение”. –

Воронеж, Изд-во Воронежского государственного университета, 2001. - 19 с.

2. Александровский А.Д. Delphi 5.0. Разработка корпоративных приложений. -М.:

ДМК. 2000. -512 с.

3. Гордеев А.В. Операционные системы: Учебник для вузов. - 4-е изд., перераб. И

доп. – СПб.:Питер, 2009. – 470c.

4. Бэкон Д., Харрис Т. Операционные системы: Параллельные и распределенные

системы. – СПб.:Питер, Киев: Издательская группа BHV, 2009. – 800с.

5. Столлингс В. Операционные системы – М: Издательский дом “Вильямс”, 2010. –

848с.

24

ПРИЛОЖЕНИЕ 1 unit Unit1;

interface

uses

Classes;

type

TSamples = class(TThread)

private

{ Private declarations }

protected

procedure Execute; override;

end;

implementation

{ Подсказка Delphi по поводу Synchronize:

Important: Methods and properties of objects in VCL can

only be used in a method called using Synchronize, for

example,

Synchronize(UpdateCaption);

and UpdateCaption could look like,

procedure Samples.UpdateCaption;

begin

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

end; }

{ Samples }

procedure TSamples.Execute;

begin

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

end;

end.

25

ПРИЛОЖЕНИЕ 2

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

Объект критический раздел

Чтобы использовать функции для работы с критическими разделами, необходимо

определить объект типа критический раздел, который является глобальной переменной

типа CRITICAL_SECTION. Например, CRITICAL_SECTION CS ;

Тип данных CRITICAL_SECTION является структурой, но ее поля используются только

внутри Windows. Объект типа критический раздел сначала должен быть инициализирован

одним из потоков программы с помощью функции:

InitializeCriticalSection (&cs);

Эта функция создает объект критический раздел с именем cs. В документации

содержится следующее предупреждение: "Объект критический раздел не может быть

перемещен или скопирован. Процесс также не должен модифицировать объект, а должен

обращаться с ним, как с "черным ящиком"".

После инициализации объекта критический раздел поток входит в критический раздел,

вызывая функцию:

EnterCriticalSection (&cs) ;

В этот момент поток становится владельцем объекта. Два различных потока не могут быть

владельцами одного объекта критический раздел одновременно. Следовательно, если

один поток вошел в критический раздел, то следующий поток, вызывая функцию

EnterCriticalSection с тем же самым объектом

критический раздел, будет задержан внутри функции. Возврат из функции произойдет

только тогда, когда первый поток покинет критический раздел, вызвав функцию:

LeaveCriticalSection (&cs);

В этот момент второй поток, задержанный в функции EnterCriticalSection, станет

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

Когда объект критический раздел больше не нужен вашей программе, его можно удалить

с помощью функции:

DeleteCriticalSection (&cs);

Это приведет к освобождению всех ресурсов системы, задействованных для поддержки

объекта критический раздел.

Локальная память потока

Глобальные переменные в многопоточных программах (так же как и любая выделенная

память) разделяются между всеми потоками в программе. Локальные статические

переменные функций также разделяются между всеми потоками, использующими эту

функцию. Локальные автоматические переменные в функции являются уникальными для

каждого потока, потому что они хранятся в стеке, а каждый поток имеет свой

собственный стек.

Может возникнуть необходимость иметь постоянную область памяти, уникальную для

каждого потока. Например, функция strtok языка С, требует такого типа память, но

средствами С она не поддерживается. В ОС Windows есть функции, работающие с этой

памятью, которая называется локальной памятью потока (thread local storage, TLS).

26

Первичный поток вызывает функцию TLsAlloc для получения значения индекса:

dwTlsIndex = TIsAlloc () ;

Он может храниться в глобальной переменной или может быть передан функции потока в

параметре-структуре.

Функция потока начинается с выделения памяти для структуры данных и с вызова

функции TIsSetValue, используя индекс, полученный ранее:

TIsSetValue (dwTlsIndex, GlobalAlloc (GPTR, sizeof (DATA))) ;

Это действие устанавливает соответствие указателя с конкретным потоком и конкретным

индексом в потоке. Теперь, любая функция, которой нужно использовать этот указатель

(включая саму базовую функцию потока), может использовать код, подобный такому:

PDATA pdata ;

pdata = (PDATA) TIsGetValue (dwTlsIndex) ;

Теперь она может изменять значения pdata->a и pdata->b. Перед завершением функции

потока необходимо освободить захваченную память: GlobalFree (TIsGetValue

(dwTlsIndex));

Когда все потоки, использующие эти данные будут завершены, первичный поток

освобождает индекс: TIsFree (dwTlsIndex) ;

Организация локальной памяти потока

Во-первых, функция TIsAlloc могла бы просто выделить блок памяти (длиной 0 байт) и

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

вызове функции TIsSetValue с этим индексом блок памяти увеличивается на 8 байт с

помощью функции GlobalReAlloc. В этих 8 байтах хранятся идентификатор потока,

вызывающего функцию, полученный с помощью функции GetCurrentThreadID, и

указатель, переданный функции TIsSetValue. Функция TIsGetValue просто

использует идентификатор потока для поиска в таблице, а затем возвращает указатель.

Функция TZsFree освобождает блок памяти.