Файл: А. В. Гордеев А. Ю. Молчанов системное программное обеспечение электронный вариант книги издательства Питер СанктПетербург Челябинск юургу каф. Автоматика и управление 2002 2 Предисловие Настоящий учебник.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 12.01.2024
Просмотров: 1020
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
331
dows 9x, содержит в себе стандартные классы, позволяющие без особых усилий использовать многопоточные возможности этих ОС. Воспользуемся стандартным классом TThread. Объект, создаваемый на основе этого класса, можно охарактери- зовать следующими теперь уже очевидными для нас свойствами:
♦ каждый тред имеет свою, при необходимости уникальную, исполняемую часть;
♦ каждый тред для своего исполнения требует отдельного процессорного времени, то есть диспетчер задач принимает во внимание только приоритет треда;
♦ диспетчеризация выполнения тредов осуществляется операционной систе- мой и не требует вмешательства программиста;
♦ несколько тредов, принадлежащих одному процессу, могут использовать один и тот же ресурс (например, глобальную переменную). Как нам известно, тре- ды могут обращаться к полям другого треда из того же вычислительного процесса.
При этом программисту необходимо самостоятельно ограничивать доступ к этому ресурсу во избежание известных проблем.
Итак, пусть необходимо создать многопоточное приложение, схема взаимо- действия отдельных потоков в котором (в рамках единого вычислительного про- цесса) приведена на рис. 6.6.
Рис. 6.6. Схема №1 взаимодействия параллельно выполняющихся задач
Так, согласно этому рисунку, процесс А после своего завершения запускает задачи D, С и Е. Считаем, что задачи В, D и С завершаются примерно в одинаковое
332
время. По крайней мере, нам не известно, какой из потоков должен быть первым, а какой – последним. Однако по условиям задачи пусть поток F будет запускаться тем из перечисленных тредов, который завершается первым, но только после того,
как завершатся два оставшихся треда, приходящие в «точку синхронизации». На- конец, пусть задача G запускается последним закончившим работу потоком Е или
F.
Все указанные задачи создадим как потомки объекта TThread. Тексты всех программных модулей приведены в приложении А. Поскольку, согласно условию,
действия, выполняемые задачами, для нас не имеют значения, представим испол- няемую часть простейшим циклом с соответствующей задержкой. Для наглядности внутри цикла можно организовать вывод текущего состояния выполнения задачи в процентах на «строке состояния», для чего используем компонент TGauge. Благо- даря тому, что все семь тредов похожи (используют одни и те же методы) и отли- чаются только в части принятия решения о синхронизации, опишем организацию базового объекта-треда.
Базовый объект (TTreadProgress) является потомком объекта TTread. При этом он имеет следующие поля:
♦ имя треда;
♦ строка состояния треда;
♦ «длина» треда (время его работы в отсутствие конкурентов);
♦ текущее состояние треда;
♦ признак завершения треда;
♦ имя запустившего треда;
♦ строка для вывода сообщений в компонент TMemo.
В базовом объекте объявлены следующие процедуры:
♦ исполняемая часть;
♦ завершающая часть;
♦ процедура прорисовки строки состояния;
♦ процедура вывода сообщения;
♦ конструктор объекта.
333
Все треды (от А до G) являются потомками этого объекта и перекрывают единственный метод – процедуру завершения процесса. В исполняемой части зада- чи после завершения цикла задержки, имитирующего выполнение полезной рабо- ты, устанавливается признак завершения и вызывается процедура завершения за- дачи, которая и выполняет соответствующие действия.
Общую схему работы программы, реализующей задание, можно описать сле- дующим образом. Все задачи инициализируются соответствующей процедурой од- новременно, но в режиме ожидания запуска. В качестве параметров инициализации в создаваемый поток передаются его имя, длительность и имя запускающего объ- екта (если оно известно заранее). Сразу после инициализации запускаются задачи
А и В. Обе задачи сигнализируют об этом соответствующим сообщением. После своего завершения поток А запускает задачи (потоки) С, D и Е. Далее всё идет в соответствии с заданной блок-схемой. Задача, запускающая другую задачу, переда-
ёт ей свое имя, обращаясь непосредственно к полю этого объекта. Информацию о том, завершился тот или иной поток, можно получить, обратившись к соответст- вующему полю – признаку завершения задачи.
Естественно, что при подобной организации доступа к полям тредов вероятно возникновение разного рода критических ситуаций. Напомним, основная причина их возникновения заключена в том, что несколько задач (в нашем случае – пото- ков) реально имеют возможность обращения к общим ресурсам практически одно- временно, то есть с таким интервалом времени, за который этот ресурс не успеет изменить своё состояние. В результате задачи могут получать некорректные значе- ния, о чем мы уже немало говорили.
Каждый процесс имеет связь с так называемыми VCL-объектами – видимыми компонентами. В данном случае такими являются строка состояния TGauge и поле сообщений TMemo. Для того чтобы в процессе работы нескольких параллельно выполняющихся задач не возникало критических ситуаций с выводом информации на эти видимые на экране объекты, к ним необходимо обеспечить синхронизиро- ванный доступ. Это довольно легко достигается с помощью стандартного для объ- екта TThread метода Synchronize. Метод имеет в качестве параметра имя процеду-
334
ры, в которой производится вывод на VCL-объекты. При этом сама эта процедура нигде в программе не должна вызываться напрямую без использования метода
Synchronize. В нашей программе такими процедурами являются прорисовка строки состояния (Do Visual Progress) и вывод текстового сообщения (WriteToMemo). По- добное использование метода Synchronize обеспечивает корректную работу не- скольких параллельных процессов с VCL-объектами.
Однако метод Synchronize не помогает в случае совместного доступа к другим общим ресурсам. Поэтому необходимо применять другие средства для организа- ции взаимного исключения. Главная цель этих средств заключается в обеспечении монопольного доступа для каждой задачи к общим ресурсам, то есть пока один по- ток не закончил обращение к подобному ресурсу, другой не имеет право этот ре- сурс использовать.
В системе программирования Delphi для этой цели имеется довольно-таки простой в использовании и достаточно эффективный метод критической секции с помощью объекта TCriticalSection. Этот метод заключается в следующем:
♦ участок кода каждого потока, в котором производится обращение к общему ресурсу, заключается в «скобки» критической секции – используются методы Enter и Leave;
♦ если какой-либо тред уже находится внутри критической секции, то другой поток, который дошел до «открывающей скобки» Enter, не имеет права входить в критическую секцию до тех пор, пока первый поток находится в ней. Когда первый тред выйдет из критической секции, второй сможет войти в неё и, в свою очередь,
обратиться к критическому ресурсу.
Очевидно, что такой метод надежно обеспечивает задачам монопольный дос- туп к общим (критическим) ресурсам.
Метод критической секции имеет ряд преимуществ перед его аналогами. Так,
например, использование семафоров (Semaphore) сложнее в реализации. Другой метод взаимных исключений – mutex – в целом похож на метод критической сек- ции, но он требует больше системных ресурсов и имеет своё время тайм-аута, по
335
истечении которого ожидающий процесс может всё-таки войти в защищённый блок, в то время как в критической секции подобного механизма нет.
Текст всей программы с необходимыми комментариями приведен в приложе- нии А.
Пример создания комплекса параллельных взаимодействующих программ,
выступающих как самостоятельные вычислительные процессы
Теперь рассмотрим более сложную задачу: пусть параллельно выполняющие- ся задачи имеют статус полноценного вычислительного процесса, а не потока и,
кроме этого, организуем обращение к соответствующим системным механизмам непосредственно на уровне API. Схема взаимодействия программ для этого приме- ра изображена на рис. 6.7.
Рис. 6.7. Схема № 2 взаимодействия параллельно выполняющихся вычислитель- ных процессов
На этом рисунке программа с именем А в точке 1 порождает сразу четыре па- раллельно выполняющихся задачи (вычислительных процесса): В, С, I и J. Процес- сы В и С завершаются и должны запустить, в свою очередь, два параллельных про- цесса D и Е. В точке 2, в которой происходит синхронизация задач В и С, процессы
D и Е должны быть запущены той из задач В или С, которая закончит свое выпол- нение раньше, но только после момента окончания второй. Далее, в точке 3 необ- ходимо запустить программы G и Н. Запускает их первая завершившая свою рабо- ту программа (D, Е или I), но дождавшись завершения остальных программ. Точка
336 4 синхронизирует выполнение процессов F, G, Н и J, при этом программу К запус- кает последний из завершившихся процессов.
Для решения поставленной задачи будем использовать соответствующие ме- ханизмы, описанные нами выше (применительно к OS/2), а именно конвейер (pipe)
и семафоры. В нашей задаче через конвейер передается переменная типа
ParentInfo:
struct ParentInfo
{
char ParentName [15];
char LaunchTime [12];
int Number;
}
Parentlnfo – хранит имя программы-предка; LaunchTIme – содержит время запуска текущей программы программой-предком (время, когда программа-предок выполнила функцию DosExecPgm); переменная Number указывает, для какого числа программ предназначена переменная ParentInfo.
Читать из конвейера мы можем при помощи функции DorRead, но при этом уничтожается запись в конвейере, и мы обязаны перезаписывать данные в конвей- ер для других программ. Каждая программа читает данные из конвейера и затем уменьшает значение Number. Программа, установившая значение 1, является по- следней программой, прочитавшей данные из конвейера; она посылает сигнал предку о завершении инициализации необходимых ресурсов. Программа пишет в конвейер данные только в том случае, если именно она будет запускать следующие программы.
Для чтения и записи в конвейер необходимы переменные WriteHandle и
ReadHandle, указывающие на описатели записи и чтения в конвейер. Необходимо,
чтобы значения этих переменных, полученных в процессе А, были известны ос- тальным процессам для совместного использования. Для этого значения этих пе- ременных мы можем передать процессам-потомкам в качестве строк аргументов.
337
При попытке считывания информации из пустого конвейера процесс перево- дится в состояние ожидания. После записи информации в конвейер другим процес- сом ожидающий процесс считывает поступившую информацию и продолжает свою работу.
Поскольку программы, которые мы сейчас рассматриваем, созданы для опера- ционной системы OS/2, приведём краткое описание использованных механизмов,
которые имеют отличия от вышеописанных общих принципов и схем.
♦ Функция создания семафора:
DosCreateEventSem (“\\SEM\\PIPESEM”, &PipeSem, 1, 0);
где "\\SEM\\PIPESEM" – имя семафора, PipeSem – идентификатор семафора,
1 – параметр совместного использования семафора (DC_SEM_SHARED), 0 – за- резервированный системный параметр.
♦ Функция открытия семафора:
DosOpenEventSem ("\\SEM\\PIPESEM", &PipeSem);
где ''\\SEM\\PIPESEM" – имя семафора, PipeSem – идентификатор семафора.
♦ Функция установки семафора:
DosPostEventSem (PipeSem);
где PipeSem – идентификатор семафора.
♦ Функция сброса семафора:
DosResetEventSem (PipeSem, &NPost);
где PipeSem – идентификатор семафора, NPost – количество обращений к установке семафора с момента последнего сброса.
♦ Функция ожидания установки семафора:
DosPostEventSem (PipeSem, -1):
где PipeSem – идентификатор семафора, -1 – ожидание семафора до его уста- новки (положительное значение – это задержка в миллисекундах).
Для синхронизации процессов и обработки критических участков программ необходимы семафоры, к которым имеют доступ все работающие программы.
Программа-потомок может унаследовать семафор от программы-предка, которая создала или открыла необходимый семафор. Но это произойдет только тогда, когда
338
программа-предок дождётся момента открытия семафора в программе-потомке.
Это обеспечивается за счёт дополнительных семафоров ExitSem1, ExitSem2,
ExitSem3. Когда последняя программа-потомок прочитывает данные из конвейера
(работа с конвейером) и обнаруживает, что она последней прошла участок откры- тия уже созданных семафоров, она устанавливает необходимый семафор, принад- лежащий программе-предку. Программа-предок, ожидающая установки семафора,
завершает свою работу.
Для управления запуском и завершением программ также используются соот- ветствующие функции.
DosExecPgm (FailFile, sizeof( FailFile), 1, Argument, 0, ResCode,
"progr_b.exe") – функция запуска программы-потомка, где
♦ FailFile – буфер для помещения имени объекта, из-за которого возникла ошибка запуска программы, sizeof( FailFile) – размер буфера;
♦ 1 означает, что процесс-потомок следует выполнять асинхронно с процес- сом-предком, Argument – строки аргументов программы, 0 – строки среды,
ResCode – результирующие коды запуска программы, "progr_b.exe" – имя запус- каемой (планируемой на выполнение) программы;
♦ DosExit – функция завершения процесса (и всех его подпроцессов);
♦ DosSetPriority – установка приоритета программы (и всех его подпроцес- сов).
Каждую программу в соответствии с заданием создаём как независимую, то есть имеющую своё локальное адресное пространство. Благодаря этому перемен- ные в текстах программ, имеющие одинаковые имена (см. листинги в приложении
Б), фактически являются разными переменными. Программа с именем А является начальной, стартовой. Именно она создаёт те системные объекты, которыми потом пользуются её потомки для своей синхронизации. Имена системных объектов они получают в наследство при своём порождении.
Для совместного использования вычислительными процессами одного файла необходимо правильно его открыть в программе А. После корректного открытия или создания файла к нему могут обращаться все программы, имеющие идентифи-
339
катор открытого файла. Значение этого идентификатора передается запускающим- ся процессам в строке аргументов, сопровождающих вызов.
В соответствии с заданием каждая программа записывает в файл время своего запуска, имя процесса-предка и время завершения своей работы. Поскольку файл используется всеми программами и запись производится в строгой последователь- ности, часть программы, которая обеспечивает запись в файл, должна быть при- знана нами как критический участок программы (критический интервал). Алго- ритм обработки этого участка описан ниже. Если не пытаться регулировать доступ процессов к файлу, может возникнуть беспорядочная запись в файл. При регулиро- вании записи каждый процесс производит сразу все три записи в файл.
Опишем теперь алгоритм обработки критических участков программ – записи в файл и работы с конвейером.
♦ Критический участок – работа с конвейером.
Приведем фрагмент программы, обеспечивающий чтение из конвейера.
1: do { DosWaitEventSem (PipeSem, -1);
2: rc=DosResetEventSem (PipeSem, &NPost);
3: } while (rc!=0);
4: DosRead(ReadHandle,(PVOID)&OldInform,sizeof(Oldlnform),
BytesReaden);
5: DosPostEventSem(PipeSem);
Программа А создает семафор PipeSem, который затем используют все про- граммы для прохождения критической части программы, связанной с чтением и записью в конвейер.
В строке 1 программа ожидает установки семафора PipeSem; после того как этот семафор будет установлен, программа переходит к строке 2. Переменная rc возвращает код ошибки при попытке сбросить семафор в строке 2. Это сделано со следующей целью: если после завершения строки 1 будет выполняться другая про- грамма и она успеет сбросить семафор, то когда программа вновь продолжит своё
выполнение, она обнаружит в строке 2, что семафор уже сброшен и rc не будет равно нулю; затем цикл повторится. Так будет продолжаться до тех пор, пока rc не
340
станет равно нулю, то есть текущая программа первой сбросила семафор. Тогда в строке 4 программа считывает из конвейера данные и сигнализирует об освобож- дении ресурса в строке 5.
Для записи в конвейер используется аналогичный алгоритм.
♦ Критический участок –запись в файл.
Здесь использован такой же алгоритм, как и при работе с конвейером, но за- действован специально созданный для этого семафор FileSem. Теперь рассмотрим алгоритмы, решающие задачи синхронизации в каждой из точек нашей схемы (см.
рис. 6.7). Начнем с прохождения точки 2. Приведём фрагмент исходного текста программы (см. приложение Б).
1: rc=DosPostEventSem(Point1Sem) :
2: if (rc==0)
3: { DosWrite(WriteHandle, (PVOID)&NewInform, sizeof(NewInform),
&BytesWr1tten);
4: DosCreateEventSem("\\SEM32\\EXITSEM2", &ExitSem2,
DC_SEM_SHARED, 0);
5: DosExecPgm(FailFileb, sizeof(FailFileb), 1, Argument, 0, &ResCodeb,
"progr_d.exe");
6: DosExecPgm(FaiIFileb, sizeof(FailFileb), 1, Argument, 0, &ResCodeb,
"progr_e.exe");
7: DosExecPgm(FailFileb, sizeof(FailFileb), 1, Argument, 0, &ResCodeb,
"progr_f.exe");
8: DosWaitEventSem(ExitSem2, -1);
9: DosCloseEventSem(ExitSem2); }
В точке 2 программы D, Е, и F должны быть запущены процессом, который первым завершит свою работу. Для определения последовательности завершения работы программ (В или С) нами используется семафор Point1Sem. В строке 1 про- изводится установка данного семафора. Если значение rc не равно нулю, значит,
семафор уже установлен, то есть текущая программа не первой завершила свою работу. Если rc равно нулю, то текущая программа закончила работу первой и