cout << «мягкий предел для размера файлов: " << R_limit_values.rlim_cur < В листинге 3.4 мягкий предел для размера файлов устанавливается равным 2000 байт, а жесткий предел — максимально возможному значению. Функции setrlimit передаются значения RLIMIT_FSIZE и R_limit, а функции getrlimit — значения RLIMIT_FSIZE и R_limit_values. После их выполнения на экран выводится установленное значение мягкого предела. Функция getrusage возвращает информацию об использовании ресурсов вызывающим процессом. Она также возвращает информацию о сыновнем процессе, завершения которого ожидает вызывающий процесс. Параметр who может иметь следующие значения: RUSAGE_SELF RUSAGE_CHILDREN Если параметру who передано значение RUSAGE_SELF, то возвращаемая информация будет относиться к вызывающему процессу. Если же параметр who содержит значение RUSAGE_CHILDREN, то возвращаемая информация будет относиться к потомку вызывающего процесса. Если вызывающий процесс не ожидает завершения своего потомка, информация, связанная с ним, отбрасывается (не учитывается). Возвращаемая информация передается через параметр r_usage, который указывает на структуру rusage. Эта структура содержит члены, перечисленные и описанные в табл. 3.7. При успешном выполнении функция возвращает число 0, в противном случае — число -1. Таблица 3.7. Члены структуры rusage struct timeval ru_utime Время,потраченное пользователем struct timeval ru_sutime Время,использованное системой long ru_maxrss Максимальный размер, установленный для резидентной программы long ru_maxixrss Размер разделяемой памяти long ru_maxidrss Размер неразделяемой области данных long ru_maxisrss Размер неразделяемой области стеков long ru_minflt Количество запросов на страницы long ru_maj flt Количество ошибок из-за отсутствия страниц long ru_nswap Количество перекачек страниц long ru_inblock Блочные операции по вводу данных long ru_oublock Блочные операции операций по выводу данных long ru_msgsnd Количество отправленных сообщений long ru_msgrcv Количество полученных сообщений long ru_nsignals Количество полученных сигналов long ru_nvcsw Количество преднамеренных переключений контекста long ru_nivcsw Количество принудительных переключений контекста
Асинхронные и синхронные процессы
Асинхронные процессы выполняются независимо один от другого. Это означает, что процесс А будет выполняться до конца безотносительно к процессу В. Между асинхронными процессами могут быть прямые родственные («родитель-сын») отношения, а могут и не быть. Если процесс А создает процесс В, они оба могут выполняться независимо, но в некоторый момент родитель должен получить статус завершения сыновнего процесса. Если между процессами нет прямых родственных отношений, у них может быть общий родитель. Асинхронные процессы могут выполняться последовательно, параллельно или с перекрытием. Эти сценарии изображены на рис. 3.12. В ситуации 1 до самого конца выполняется процесс А, затем процесс В и процесс С выполняются до самого конца. Это и есть последовательное выполнение процессов. В ситуации 2 процессы выполняются одновременно. Процессы А и В - активные процессы. Во время выполнения процесса А процесс В находится в состоянии ожидания. В течение некоторого интервала времени оба процесса пребывают в ждущем режиме. Затем процесс В «просыпается», причем раньше процесса А, а через некоторое время «просыпается» и процесс А, и теперь оба процесса выполняются одновременно. Эта ситуация показывает, что асинхронные процессы могут выполняться одновременно только в течение определенных интервалов времени. В ситуации 3 выполнение процессов А и В перекрывается. Рис. 3.12. Возможные сценарии асинхронных и синхронных процессов Асинхронные процессы могут совместно использовать такие ресурсы, как файлы или память. Это может потребовать (или не потребовать) синхронизации или взаимодействия при разделении ресурсов. Если процессы выполняются последовательно (ситуация 1), то они не потребуют никакой синхронизации. Например, все три процесса, А, В и С, могут разделять некоторую глобальную переменную. Процесс А (перед тем как завершиться) записывает значение в эту переменную, затем процесс В во время своего выполнения считывает данные, хранимые в этой переменной и (перед тем как завершиться) записывает в нее «свое» значение. Затем во время своего выполнения процесс С считывает данные из этой переменной. Но в ситуациях 2 и 3 процессы могут попытаться одновременно модифицировать эту переменную, поэтому здесь не обойтись без синхронизации доступа к ней. Мы определяем синхронные процессы как процессы с перемежающимся выполнением, когда один процесс приостанавливает свое выполнение до тех пор, пока не з аверш ится другой - Например, процесс А, родительский, при выполнении создает процесс В, сыновний. Процесс А приостанавливает свое выполнение до тех пор, пока не завершится процесс В. После завершения процесса В его выходной код помещается в таблицу процессов. Тем самым процесс А уведомляется о завершении процecca В. Процесс А может продолжить выполнение, а затем завершиться или завершиться немедленно. В этом случае выполнение процессов А и В является синхронизированным. Сценарий синхронного выполнения процессов А и В (для сравнения с асинхронным) также показан на рис. 3.12.
Создание синхронных и асинхронных процессов с помощью функций fork , exec , system и posix_spawn
Функции fork , fork-exec и posix_spawn позволяют создавать асинхронные процессы. При использовании функции fork дублируется образ родительского процесса. После создания сыновнего процесса эта функция возвращает родителю (через параметр) идентификатор (PID) процесса-потомка и (обычным путем) число 0, означающее, что создание процесса прошло успешно. При этом родительский процесс не приостанавливается; оба процесса продолжают выполняться независимо от инструкции, следующей непосредственно за вызовом функции fork . При создании сыновнего процесса посредством fork-exec-комбинации его образ инициализируется с помощью образа нового процесса. Если функция exec выполнилась успешно (т.е. успешно прошла инициализация), она не возвращает родительскому процессу никакого значения. Функции posix_spawn создают образы сыновних процессов инициализируют их. Помимо идентификатора (PID), возвращаемого (через параметр) функцией posix_spawn родительскому процессу, обычным путем возвращается значение, служащее индикатором успешного порождения процесса. После выполнения функции posix_spawn оба процесса выполняются одновременно. Функция system позволяет создавать синхронные процессы. При этом создается оболочка, которая выполняет системную команду или запускает выполняемый файл. В этом случае родительский процесс приостанавливается до тех пор, пока не завершится сыновний процесс и функция system не возвратит значение.
Функция wait
Асинхронный процесс, вызвав функцию wait , может приостановить выполнение до тех пор, пока не завершится сыновний процесс. После завершения сыновнего процесса ожидающий родительский процесс считывает статус завершения своего потомка, чтобы не допустить создания процесса- «зомби». Функция wait получает статус завершения из таблицы процессов. Параметр status указывает на ту область, которая содержит статус завершения сыновнего процесса. Если родительский процесс имеет не один, а несколько сыновних процессов и некоторые из них уже завершились, функция wait считывает из таблицы процессов статус завершения только для одного сыновнего процесса. Если информация о статусе окажется доступной еще до вып олнения функции wait , эта функция завершится немедленно. Если родительский процесс не имеет ни одного потомка, эта функция возвратит код ошибки. Функцию wait можно использовать также в том случае, когда вызывающий процесс должен ожидать до тех пор, пока не получит сигнал, чтобы затем выполнить определенные действия по его обработке. Синопсис #include pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); Функция waitpid аналогична функции wait за исключением того, что она принимает дополнительные параметры pid и options. Параметр pid задает множество сыновних процессов, для которых считывается статус завершения. Другими словами, значение параметра pid определяет, какие процессы попадают в это множество. pid > 0 Единственный сыновний процесс. pid = 0 Любой сыновний процесс, групповой идентификатор которого совпадает с идентификатором вызывающего процесса. pid < -1 Любые сыновние процессы, групповой идентификатор которых равен абсолютному значению pid. pid = -1 Любые сыновние процессы. Параметр options определяет, как должно происходить ожидание процесса, и может принимать одно из значений следующих констант, определенных в заголовке WCONTINUED Сообщает статус завершения любого продолженного сыновнего процесса (заданного параметром pid), о статусе которого не было доложено с момента продолжения его выполнения. WUNTRACED Сообщает статус завершения любого остановленного сыновнего процесса (заданного параметром pid), о статусе которого не было доложено с момента его останова. WNOHANG Вызывающий процесс не приостанавливается, если статус завершения заданного сыновнего процесса недоступен. Эти константы могут быть объединены с помощью логической операции ИЛИ и переданы в качестве параметра options (например, WCONTINUED | WUNTRACED). Обе эти функции возвращают идентификатор (PID) сыновнего процесса, для которого получен статус завершения. Если значение, содержащееся в параметре status, равно числу 0, это означает, что сыновний процесс завершился при таких условиях: • процесс вернул значение 0 из функции main ; • процесс вызвал некоторую версию функции exit с аргументом 0; • процесс был завершен, поскольку завершился последний поток процесса. В табл. 3.8 перечислены макросы, которые позволяют вычислить значение статуса завершения. Таблица З.8. Макросы, которые позволяют вычислить значение статуса завершения WIFEXITED Приводится к ненулевому значению, если статус был возвращен нормально завершенным сыновним процессом WEXITSTATUS Если значение WIFEXITED оказывается ненулевым, то оцениваются младшие 8 бит аргумента status, переданного завершенным сыновним процессом функции _exit или exit , либо значения, возвращенного функцией main WIFSIGNALED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который завершился, поскольку ему был послан сигнал, но этот сигнал не был перехвачен WTERMSIG Если значение WIFSIGNALED оказывается ненулевым, то оценивается номер сигнала, который послужил причиной завершения сыновнего процесса WIFSTOPPED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который в данный момент остановлен WSTOPSIG Если значение WIFSTOPPED оказывается ненулевым, то оценивается номер сигнала, который послужил причиной останова сыновнего процесса WIFCONTINUED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который продолжил выполнение после сигнала останова, принятого от блока управления заданиями
Разбиение программы на задачи
Рассматривая разбиение программы на несколько задач, вы делаете первый шаг к внесению параллелизма в свою программу. В однопроцессорной среде параллелизм реализуется посредством Различают два уровня параллельной обработки в приложении или системе: уровень процессов и уровень потоков. Параллельная обработка на уровне потоков носит название При декомпозиции программы на функции обычно используется нисходящий принцип, а при разделении на объекты — восходящий. При этом необходимо определить, какие функции или объекты лучше реализовать в виде отдельных программ или подпрограмм, а какие — в виде потоков. Подпрограммы должны выполняться операционной системой как процессы. Отдельные подпрограммы, или процессы, выполняют задачи, порученные проектировщиком ПО. Задачи, на которые будет разделена программа, могут выполняться параллельно, причем здесь можно выделить следующие три способа реализации параллелизма. 1. Выделение в программе одной основной задачи, которая создает некоторое количество подзадач. 2. Разделение программы на множество отдельных выполняемых файлов. 3. Разделение программы на несколько задач разного типа, отвечающих за создание других подзадач только определенного типа. Эти способы реализации параллелизма отображены на рис. 3.13. Например, эти методы реализации параллелизма можно применить к программе визуализации. Под визуализацией будем понимать процесс перехода от представления трехмерного объекта в форме записей базы данных в двухмерную теневую графическую проекцию на поверхность отображения (экран дисплея). Изображение представляется в виде теневых многоугольников, повторяющих форму объекта. Этапы визуализации показаны на рис. 3.14. Визуализацию можно разбить на ряд отдельных задач. 1. Установить структуру данных для сеточных моделей многоугольников. 2. Применить линейные преобразования. 3. Отбраковать многоугольники, относящиеся к невидимой поверхности. 4. Выполнить растеризацию. 5. Применить алгоритм удаления скрытых поверхностей. 6. Затушевать отдельные пиксели. Первая задача состоит в представлении объекта в виде массива многоугольников, в котором каждая вершина многоугольника описывается в трехмерной мировой системе координат. Вторая задача — применить линейные преобразования к сеточной модели многоугольников. Эти преобразования используются для позиционирования объектов на сцене и создания точки обзора или поверхности отображения (области, которая видима наблюдателю с его точки обзора). Третья задача — отбраковать невидимые поверхности объектов на сцене. Это означает удаление линий, принадлежащих тем частям объектов, которые невидимы с точки обзора. Четвертая задача — преобразовать модель вершин в набор координат пикселей. Пятая задача — удалить любые скрытые поверхности. Если сцена содержит взаимодействующие объекты, например, когда одни объекты заслоняют другие, то скрытые (передними объектами) поверхности должны быть удалены. Шестая задача - наложить на поверхности изображения тень. Рис. 3.13. Способы разбиения программы на отдельные задачи Рис. 3.14. Этапы визуализации Решение каждой задачи представляется в виде отдельного выполняемого файла. Первые три задачи (Taskl, Task2 и Task3) выполняются последовательно, а остальные три (Task4, Task5 и Task6)— параллельно. Реализация первого способа создания программы визуализации приведена в листинге 3.5. // Листинг 3.5. Использование способа 1 для создания процессов #include #include #include #include #include #include int main(void) { posix_spawnattr_t Attr; posix_spawn_file_actions_t FileActions; char *const argv4[] = {«Task4»,...,NULL}; char *const argv5[] = {«Task5'\...,NULL}; char *const argv6[] = {«Task6»,...,NULL}; pid_t Pid; int stat; // Выполняем первые три задачи синхронно, system(«Taskl . . . ") ; system(«Task2 . . . ") ; system(«Task3 . . . ") ; //иниииализируем структуры posix_spawnattr_init(&Attr); posix_spawn_file_actions_init(&FileActions); // execute last 3 tasks asynchronously posix_spawn(&Pid,«Task4»,&FileActions,&Attr,argv4,NULL); posix_spawn(&Pid,«Task5»,&FileActions,&Attr,argv5,NULL); posix_spawn(&Pid,«Task6»,&FileActions,&Attr,argv6,NULL); // like a good parent, wait for all your children wait (&stat); wait (&stat); wait (&stat); return(0); } В листинге 3.5 из функции main с помощью функции system( ) вызываются на выполнение задачи Task1, Task2 и Task3. Каждая из них выполняется синхронно с родительским процессом. Задачи Task4, Task5 и Task6 выполняются асинхронно родительскому процессу благодаря использованию функций posix__spawn( ). Многоточие (... ) используется для обозначения файлов, требуемых задачам. Родительский процесс вызывает три функции wait , и каждая из них ожидает завершения одной из задач (Task4, Task5 или Task6). Используя второй способ, программу визуализации можно запустить из сценария командной оболочки. Преимущество этого сценария состоит в том, что он позволяет использовать все команды и операторы оболочки. В нашей программе визуализации для управления выполнением задач используются метасимволы & и &&. Task1 ... && Task2 ... && Task3 Task4 . . . & Task5 . . . & Task6 Здесь благодаря использованию метасимвола && задачи Task1, Task2 и Task3 выполняются последовательно при условии успешного выполнения предыдущей задачи. Задачи же Task4, Task5 и Task6 выполняются одновременно, поскольку использован метасимвол &. Приведем некоторые метасимволы, применяемые при разделении команд в средах UNIX/Linux, и способы выполнения этих команд. && Каждая следующая команда будет выполняться только в случае успешного выполнения предыдущей команды. || Каждая следующая команда будет выполняться только в случае неудачного выполнения предыдущей команды. ; Команды должны выполняться последовательно. & Все команды должны выполняться одновременно. При использовании третьего способа задачи делятся по категориям. При декомпозиции программы следует разобраться, можно ли в ней выделить различные категории задач. Например, одни задачи могут «отвечать» за интерфейс пользователя, т.е. его создание, ввод данных, вывод данных и пр. Другим задачам поручаются вычисления, управление данными и пр. Такой подход весьма полезен не только при проектировании программы, но и при ее реализации. В нашей программе визуализации мы можем разделить задачи по следующим категориям: • • • • Разбиение задач по категориям позволяет нашей программе приобрести более общий характер. Процессы при необходимости создают другие процессы, предназначенные для выполнения действий только определенной категории. Например, если нашей программе предстоит визуализировать лишь один объект, а не всю сцену, то нет никакой необходимости порождать процесс, который выполняет удаление скрытых поверхностей; вполне достаточно будет удаления невидимых поверхностей (одного объекта). Если объект не нужно затенять, то нет необходимости порождать задачу, выполняющую наложение тени; обязательным остается лишь линейное преобразование при решении задачи растеризации. Для запуска программы с использованием третьего способа можно использовать родительский процесс или сценарий оболочки. Родительский процесс может определить, какой нужен тип визуализации, и передать соответствующую информацию каждому из специализированных процессов, чтобы они «знали», какие процессы им следует порождать. Эта информация может быть также перенаправлена каждому из специализированных процессов из сценария оболочки. Реализация третьего способа представлена в листинге 3.6. // Листинг 3.6. Использование третьего метода для // создания процессов. Задачи запускаются из // родительского процесса #include #include #include #include #include #include int main(void) { posix_spawnattr_t Attr; posix_spawn_file_actions_t FileActions; pid_t Pid; int stat; //••• system(«Task1 ...»);// Выполняется безотносительно к типу используемой визуализации. //определяем, какой нужен тип визуализации. Это можно // затем сообщаем о результате другим задачам с помощью // аргументов. char *const argv4[] = {«TaskType4»,...,NULL}; char *const argv5[] = {«TaskType5»,...,NULL}; char *const argv6[] = {«TaskType6»,...,NULL} system(«TaskType2 . . . "); system(«TaskType3 . . . "); // Инициализируем структуры. posix_spawnattr_init(&Attr) ; posix_spawn_file_actions_init (&FileActions) ; posix_spawn(&Pid, «TaskType4», &FileActions,&Attr,argv4, NULL); posix_spawn(&Pid, «TaskType5», &FileActions,&Attr,argv5, NULL); if(Y){ posix_spawn(&Pid,«TaskType6»,&FileActions,&Attr, argv6,NULL); } // Подобно хорошему родителю, ожидаем возвращения // своих «детей». wait(&stat); wait(&stat); wait(&stat); return(0); } // Все TaskType-задачи должны быть аналогичными. //.. . int main(int argc, char *argv[]){ int Rt; //. . . if(argv[1] == X){ // Инициализируем структуры. posix_spawn(&Pid,«TaskTypeX»,&FileActions,&Attr,..., NULL); else{ // Инициализируем структуры. //.. • posix_spawn(&Pid,«TaskTypeY», &FileActions,&Attr, ...,NULL); } wait(&stat); exit(0); } В листинге 3.6 тип каждой задачи (а следовательно, и тип порождаемого процесса) определяется на основе информации, передаваемой от родительского процесса или сценария оболочки.
Линии видимого контура
Порождение процессов, как показано в листинге 3.7, возможно с помощью функций, вызываемых из функции main . // Листинг 3.7. Стержневая ветвь программы, из которой // вызывается функция, порождающая процесс int main(int argc, char *argv[]) { Rt = funcl(X, Y, Z); //.. . } // Определение функции. int funcl(char *M, char *N, char *V) { //.. . char *const args[] = {«TaskX»',M,N,V,NULL}; Pid = fork; if(Pid == 0) { exec(«TaskX»,args); } if(Pid > 0) { //.. . } wait(&stat); } В листинге 3.7 функция funcl вызывается с тремя аргументами. Эти аргументы передаются порожденному процессу. Процессы также могут порождаться из методов, принадлежащих объектам. Как показано в листинге 3.8, объекты можно объявить в любом процессе. my_pbject MyObject; //-•• // Объявление и определение класса. class my_object { public: //... int spawnProcess(int X); //... }; int my_object::spawnProcess(int X) { //.. . // posix__spawn или system //.. . } Как показано в листинге 3.8, объект может создавать любое количество процессов из любого метода.
Резюме
Параллелизм в С++-программе достигается за счет ее разложения на несколько процессов или несколько потоков. Процесс- это «единица работы», создаваемая операционной системой. Если программа- это артефакт (продукт деятельности) разработчика, то процесс - это артефакт операционной системы. Приложение может состоять из нескольких процессов, которые могут быть не связаны с какой-то конкретной программой. Операционные системы способны управлять сотнями и даже тысячами параллельно загруженных процессов. Некоторые данные и атрибуты процесса хранятся в блоке управления процессами (process control block - PCB), или БУП, используемом операционной системой для идентификации процесса. С помощью этой информации операционная система Управляет процессами. Многозадачность (выполнение одновременно нескольких процессов) реализуется путем переключения контекста. Текущее состояние выполняемого процесса и его контекст сохраняются в БУП-блоке, что позволяет успешно возобновить этот процесс в следующий раз, когда он будет назначен центральному процессору. Занимая процессор, процесс пребывает в состоянии выполнения, а когда он ожидает использования ЦП, - то в состоянии готовности (ожидания). Получить информацию о процессах, выполняющихся в системе, можно с помощью утилиты ps. Процессы, которые создают другие процессы, вступают с ними в «родственные» (отцы- и -дети) отношения. Создатель процесса называется родительским, а созданный процесс — сыновним. Сыновние процессы наследуют от родительских множество атрибутов. «Святая обязанность» родительского процесса — подождать, пока сыновний не покинет систему. Для создания процессов предусмотрены различные системные функции: fork , fork-exec , system и posix_spawn . Функции fork, fork-exec и posix_spawn создают процессы, которые являются асинхронными, в то время как функция system создает сыновний процесс, который является синхронным по отношению к родительскому. Асинхронные родительские процессы могут вызвать функцию wait , после чего «синхронно» ожидать, пока сыновние процессы не завершатся или пока не будут считаны коды завершения для уже завершившихся сыновних процессов. Программу можно разбить на несколько процессов. Эти процессы может породить родительский процесс, либо они могут быть запущены из сценария оболочки как отдельные выполняемые программы. Специализированные процессы могут при необходимости порождать другие процессы, предназначенные для выполнения действий только определенного типа. Порождение процессов может быть осуществлено как из функций, так и из методов.
Разбиение C++ программ на множество потоков
Работу любой последовательной программы можно разделить между несколькими подпрограммами. Каждой подпрограмме назначается конкретная задача, и все эти задачи выполняются одна за другой. Вторая задача не может начаться до тех пор, пока не завершится первая, а третья — пока не закончится вторая и т.д. Описанная схема прекрасно работает до тех пор, пока не будут достигнуты границы производительности и сложности. В одних случаях единственное решение проблемы производительности — найти возможность выполнять одновременно более одной задачи. В других ситуациях работа подпрограмм в программе настолько сложна, что имеет смысл представить эти подпрограммы в виде мини-программ, которые выполняются параллельно внутри основной программы. В главе 3 были представлены методы разбиения одной программы на несколько процессов, каждый из которых выполняет отдельную задачу. Такие методы позволяют приложению в каждый момент времени выполнять сразу несколько действий. Однако в этом случае каждый процесс имеет собственные адресное пространство и ресурсы. Поскольку каждый процесс занимает отдельное адресное пространство, то взаимодействие между процессами превращается в настоящую проблему. Для обеспечения связи между раздельно выполняемыми частями общей программы нужно реализовать такие средства межпроцессного взаимодействия, как каналы, FIFO-очереди (с дисциплиной обслуживания по принципу «первым пришел — первым обслужен») и переменные среды. Иногда нужно иметь одну программу (которая выполняет несколько задач одновременно), не разбивая ее на множество мини-программ. В таких обстоятельствах можно использовать потоки. Потоки позволяют одной программе состоять из параллельно выполняемых частей, причем все части имеют доступ к одним и тем же переменным, константам и адресному пространству в целом. Потоки можно рассматривать как мини-программы в основной программе. Если программа разделена на несколько процессов, как было показано в главе 3 , то с выполнением каждого отдельного процесса связаны определенные затраты системных ресурсов. Для потоков требуется меньший объем затрат системных ресурсов. Поэтому потоки можно рассматривать как
Определение потока
Под потоком подразумевается часть выполняемого кода в UNIX- или Linux-процессе, которая может быть регламентирована определенным образом. Затраты вычислительных ресурсов, связанные с созданием потока, его поддержкой и управлением, у операционной системы значительно ниже по сравнению с аналогичными затратами для процессов, поскольку объем информации отдельного потока гораздо меньше, чем у процесса. Каждый процесс имеет Рис. 4.1. Потоки выполнения многопоточного процесса
Контекстные требования потока
Все потоки одного процесса существуют в одном и том же адресном пространстве. Все ресурсы, принадлежащие процессу, разделяются между потоками. Потоки не владеют никакими ресурсами. Ресурсы, которыми владеет процесс, совместно используются всеми потоками этого процесса. Потоки разделяют дескрипторы файлов и файловые указатели, но каждый поток имеет собственные программный указатель, набор регистров, состояние и стек. Все стеки потоков находятся в стековом разделе своего процесса. Раздел данных процесса совместно используется потоками процесса. Поток может считывать (и записывать) информацию из области памяти своего процесса. Когда основной поток записывает данные в память, то любые сыновние потоки могут получить к ним доступ. Потоки могут создавать другие потоки в пределах того же процесса. Все потоки в одном процессе считаются Потоки — это выполняемые части программы, которые соревнуются за использование процессора с потоками того же самого или других процессов. В многопроцессорной системе потоки одного процесса могут выполняться одновременно на различных процессорах. Однако потоки конкретного процесса выполняются только на процессоре, который назначен этому процессу. Если, например, процессоры 1, 2 и 3 назначены процессу А, а процесс А имеет три потока, то любой из них может быть назначен любому процессору. В среде с одним процессором потоки конкурируют за его использование. Параллельность же достигается за счет
Сравнение потоков и процессов
У потоков и процессов есть много общего. Они имеют идентификационный номер (id), состояние, набор регистров, приоритет и привязку к определенной стратегии планирования. Подобно процессам, потоки имеют атрибуты, которые описывают их для операционной системы. Эта информация содержится в информационном блоке потока, подобном информационному блоку процесса. Потоки и сыновние процессы разделяют ресурсы родительского процесса. Ресурсы, открытые родительским процессом (в его основном потоке), немедленно становятся доступными всем потокам и сыновним процессам. При этом никакой дополнительной инициализации или подготовки не требуется. Потоки и сыновние процессы независимы от родителя (создателя) и конкурируют за использование процессора. Создатель процесса или потока управляет своим потомком, т.е. он может отменить, приостановить или возобновить его выполнение либо изменить его приоритет. Поток или процесс может изменить свои атрибуты и создать новые ресурсы, но не может получить доступ к ресурсам, принадлежащим другим процессам. Однако между потоками и процессами есть множество различий.
Различия между потоками и процессами
Основное различие между потоками и процессами состоит в том, что каждый процесс имеет собственное адресное пространство, а потоки — нет. Если процесс создает множество потоков, то все они будут содержаться в его адресном пространстве. Вот почему они так легко разделяют общие ресурсы, и так просто обеспечивается взаимодействие между ними. Сыновние процессы имеют собственные адресные пространства и копии разделов данных. Следовательно, когда процесс-потомок изменяет свои переменные или данные, это не влияет на данные родительского процесса. Если необходимо, чтобы родительский и сыновний процессы совместно использовали данные, нужно создать общую область памяти. Для передачи данных между родителем и потомком используются такие механизмы межпроцессного взаимодействия, как каналы и FIFO-очереди. Потоки одного процесса могут передавать информацию и связываться друг с другом путем непосредственного считывания и записи общих данных, которые доступны родительскому процессу.
Потоки, управляющие другими потоками
В то время как процессы могут управлять другими процессами, если между ними установлены отношения типа «родитель-потомок», потоки одного процесса считаются равноправными и находятся на одном уровне, независимо от того, кто кого создал. Любой поток, имеющий доступ к идентификационному номеру (id) некоторого другого потока, может отменить, приостановить, возобновить выполнение э того потока либо изменить его приоритет. Отмена основного потока приведет к завершению всех потоков процесса, т.е. к ликвидации процесса. Любые изменения, внесенные в основной поток, могут повлиять на все потоки процесса. При изменении приоритета процесса все его потоки, которые унаследовали этот приоритет, должны также изменить свои приоритеты. Сходства и различия между потоками и процессами сведены в табл. 4.1. Таблица 4.1. Сходства и различия между потоками и процессами • Оба имеют идентификационный номер (id), состояние, набор регистров, приоритет и привязку к определенной стратегии планирования • И поток, и процесс имеют атрибуты, которые описывают их особенности для операционной системы • Как поток, так и процесс имеют информационные блоки • Оба разделяют ресурсы с родительским процессом • Оба функционируют независимо от родительского процесса • Их создатель может управлять потоком или процессом • И поток, и процесс могут изменять свои атрибуты • Оба могут создавать новые ресурсы • Как поток, так и процесс не имеют доступа к ресурсам другого процесса • Потоки разделяют адресное пространство процесса, который их создал; процессы имеют собственное адресное пространство • Потоки имеют прямой доступ к разделу данных своего процесса; процессы имеют собственную копию раздела данных родительского процесса • Потоки могут напрямую взаимодействовать с другими потоками своего процесса; процессы должны использовать специальный механизм межпроцессного взаимодействия для связи с «братскими» процессами • Потоки почти не требуют системных затратна поддержку процессов требуются значительные затраты системных ресурсов • Новые потоки создаются легко; новые процессы требуют дублирования родительского процесса • Потоки могут в значительной степени управлять потоками того же процесса; процессы управляют только сыновними процессами • Изменения, вносимые в основной поток (отмена, изменение приоритета и т.д.), могут влиять на поведение других потоков процесса; изменения, вносимые в родительский процесс, не влияют на сыновние процессы
Преимущества использования потоков
При управлении подзадачами приложения использование потоков имеет ряд преимуществ. • Для переключения контекста требуется меньше системных ресурсов. • Достигается более высокая производительность приложения. • Для обеспечения взаимодействия между задачами не требуется никакого специального механизма. • Программа имеет более простую структуру.
Переключение контекста при низкой (ограниченной) доступности процессора
При организации процесса для выполнения возложенной на него функции может оказаться вполне достаточно одного основного потока. Если же процесс имеет множество параллельных подзадач, то их асинхронное выполнение можно обеспечить с помощью нескольких потоков, на переключение контекста которых потребуются незначительные затраты системных ресурсов. При ограниченной доступности процессора или при наличии в системе только одного процессора параллельное выполнение процессов потребует существенных затрат системных ресурсов в связи с необходимостью обеспечить переключение контекста. В некоторых ситуациях контекст процессов переключается только тогда, когда процессору последовательно назначаются потоки из разных процессов. Под системными затратами подразумеваются не только системные ресурсы, но и время, требуемое на переключение контекста. Но если система содержит достаточное количество процессоров, то переключение контекста не является проблемой.
Возможности повышения производительности приложения
Создание нескольких потоков повышает производительность приложения. При использовании одного потока запрос к устройствам ввода-вывода может остановить весь процесс. Если же в приложении организовано несколько потоков, то пока один из них будет ожидать удовлетворения запроса ввода-вывода, другие потоки, которые не зависят от заблокированного, смогут продолжать выполнение. Тот факт, что не все потоки ожидают завершения операции ввода-вывода, означает, что приложение в целом не заблокировано ожиданием, а продолжает работать.
Простая схема взаимодействия между параллельно выполняющимися потоками
Потоки не требуют специального механизма взаимодействия между подзадачами. Потоки могут напрямую передавать данные другим потокам и получать данные от них, что также способствует экономии системных ресурсов, которые при использовании нескольких процессов пришлось бы направлять на настройку и поддержку специальных механизмов взаимодействия. Потоки же используют общую память, выделяемую в адресном пространстве процесса. Процессы также могут взаимодействовать через общую память, но они имеют раздельные адресные пространства, и поэтому такая общая память должна быть вне адресных пространств обоих взаимодействующих процессов. Этот подход увеличит временные и пространственные расходы системы на поддержку и доступ к общей памяти. Схема взаимодействия между потоками и процессами показана на рис. 4 .2 .
Упрощение структуры программы
Потоки можно использовать, чтобы упростить структуру приложения. Каждому потоку назначается подзадача или подпрограмма, за выполнение которой он отвечает. Поток должен независимо управлять выполнением своей подзадачи. Каждому потоку можно присвоить приоритет, отражающий важность выполняемой им задачи Для приложения. Такой подход позволяет упростить поддержку программного кода.
Недостатки использования потоков
Простота доступности потоков к памяти процесса имеет свои недостатки. • Потоки могут легко разрушить адресное пространство процесса. • Потоки необходимо синхронизировать при параллельном доступе (для чтения или записи) к памяти. • Один поток может ликвидировать целый процесс или программу. • Потоки существуют только в рамках единого процесса и, следовательно, не являются многократно используемыми. Рис. 4.2. Взаимодействие между потоками одного процесса и взаимодействие между несколькими процессами
Потоки могут легко разрушить адресное пространство процесса
Потоки могут легко разрушить информацию процесса во время «гонки» данных, если сразу несколько потоков получат доступ для записи одних и тех же данных. При использовании процессов это невозможно. Каждый процесс имеет собственные данные, и другие процессы не в состоянии получить к ним доступ, если не сделать это специально. Защита информации обусловлена наличием у процессов отдельных адресных пространств. Тот факт, что потоки совместно используют одно и то же адресное пространство, делает данные незащищенными от искажения. Например, процесс имеет три потока — А, В и С. Потоки А и В записывают информацию в некоторую область памяти, а поток С считывает из нее значение и использует его для вычислений. Потоки А и В могут попытаться одновременно записать информацию в эту область памяти. Поток В может перезаписать данные, записанные потоком А, еще до того, как поток С получит возможность считать их. Поведение этих потоков должно быть синхронизировано таким образом, чтобы поток С мог считать данные, записанные потоком А, до того, как поток В их перезапишет. Синхронизация защищает данные от перезаписи до их использования. Тема синхронизации потоков рассматривается в главе 5.
Один поток может ликвидировать целую программу
Поскольку потоки не имеют собственного адресного пространства, они не изолированы. Если поток стал причиной фатального нарушения доступа, это может привести к завершению всего процесса. Процессы изолированы друг от друга. Если процесс разрушит свое адресное пространство, проблемы ограничатся этим процессом. Процесс может допустить нарушение доступа, которое приведет к его завершению, но все остальные процессы будут продолжать выполнение. Это нарушение не окажется фатальным для всего приложения. Ошибки, связанные с некорректностью данных, могут не выйти за рамки одного процесса. Но ошибки, вызванные некорректным поведением потока, как правило, гораздо серьезнее ошибок, допущенных процессом. Потоки могут стать причиной ошибок, которые повлияют на все адресное пространство всех потоков. Процессы защищают свои ресурсы от беспорядочного доступа со стороны других процессов. Потоки же совместно используют ресурсы со всеми остальными потоками процесса. Поэтому поток, разрушающий ресурсы, оказывает негативное влияние на процесс или программу в целом.
Потоки не могут многократно использоваться другими программами
Потоки зависят от процесса, в котором они существуют, и их невозможно от него отделить. Процессы отличаются большей степенью независимости, чем потоки. Приложение можно так разделить на задачи, порученные процессам, что эти процессы можно оформить в виде модулей, которые возможно использовать в других приложениях. Потоки не могут существовать вне процессов, в которых они были созданы и, следовательно, они не являются повторно используемыми. Преимущества и недостатки потоков сведены в табл. 4.2.
Анатомия потока
Образ потока встраивается в образ процесса. Как было описано в главе 3, процесс имеет разделы программного кода, данных и стеков. Поток разделяет разделы кода и данных с остальными потоками процесса. Каждый поток имеет собственный стек, выделенный ему в стековом разделе адресного пространства процесса. Размер потокового стека устанавливается при создании потока. Если создатель потока не определяет размер его стека, то система назначает размер по умолчанию. Размер, устанавливаемый по умолчанию, зависит от конкретной системы, максимально возможного количества потоков в процессе, размера адресного пространства, выделяемого процессу, и пространства, используемого системными ресурсами. Размер потокового стека должен быть достаточно большим для любых функций, вызываемых потоком, любого кода, который является внешним по отношению к процессу (например, это Может быть библиотечный код), и хранения локальных переменных. Процесс с несколькими потоками должен иметь стековый раздел, который будет вмещать все стеки его потоков. Адресное пространство, выделенное для процесса, ограничивает раз-Мер стека, ограничивая тем самым размер, который может иметь каждый поток. На Рис.4.3 показана схема процесса, который содержит два потока. Как показано на рис. 4.3, процесс содержит два потока А и В, и их стеки располо жены в стековом разделе процесса. Потоки выполняют различные функции: поток А вы п олняет функцию func1, а поток В - функцию func2. Рис. 4.3. Схема процесса, содержащего два потока (SP — указатель стека, PC — счетчик команд) Таблица 4.2. Преимущества и недостатки потоков Для переключения контекста требуется меньше системных ресурсов Потоки способны повысить производительность приложения Для обеспечения взаимодействия между потоками никакого специального механизма не требуется Благодаря потокам структуру программы можно упростить Для параллельного доступа к памяти (чтения или записи данных) требуется синхронизация Потоки могут разрушить адресное пространство своего процесса Потоки существуют в рамках только одного процесса, поэтому их нельзя повторно использовать
Атрибуты потока
Атрибуты процесса содержат информацию, которая описывает процесс для операционной системы. Операционная система использует эту информацию для управления процессами, а также для того, чтобы отличать один процесс от другого. Процесс совместно использует со своими потоками практически все, включая ресурсы и переменные среды. Разделы данных, раздел программного кода и все ресурсы связаны с процессом, а не с потоками. Все, что нужно для функционирования потока, определяется и предоставляется процессом. Потоки же отличаются один от другого идентификационным номером (id), набором регистров, определяющих состояние потока, его приоритетом и стеком. Именно эти атрибуты формируют уникальность каждого потока. Как и при использовании процессов, информация о потоках хранится в структурах данных и возвращается функциями, поддерживаемыми операционной системой. Например, часть информации о потоке содержится в структуре, именуемой Идентификационный номер (id) потока — это уникальное значение, которое идентифицирует каждый поток во время его существования в процессе. Приоритет потока определяет, каким потокам предоставлен привилегированный доступ к процессору в выделенное время. Под состоянием потока понимаются условия, в которых он пребывает в любой момент времени. Набор регистров для потока включает программный счетчик и указатель стека. Программный счетчик содержит адрес инструкции, которую поток должен выполнить, а указатель стека ссылается на вершину стека потока. Библиотека потоков POSIX определяет объект атрибутов потока, инкапсулирующий свойства потока, к которым его создатель может получить доступ и модифицировать их. Объект атрибутов потока определяет следующие компоненты: • область видимости; • размер стека; • адрес стека; • приоритет; • состояние; • стратегия планирования и параметры. Объект атрибутов потока может быть связан с одним или несколькими потоками. При использовании этого объекта поведение потока или группы потоков определяется профилем. Все потоки, которые используют объект атрибутов, приобретают все свойства, определенные этим объектом. На рис. 4.3 показаны атрибуты, связанные с каждым потоком. Как видите, оба потока (А и В) разделяют объект атрибутов, но они поддерживают свои отдельные идентификационные номера и наборы регистров. После того как объект атрибутов создан и инициализирован, его можно использовать в любых обращениях к функциям создания потоков. Следовательно, можно создать группу потоков, которые будут иметь «малый стек и низкий приоритет» или «большой стек, высокий приоритет и состояние открепления». Открепленный (detached) поток — это поток, который не синхронизирован с другими потоками в процессе. Иначе говоря, не существует потоков, которые бы ожидали до тех пор, пока завершит выполнение открепленный поток. Следовательно, если уж такой поток существует, то его ресурсы (а именно id потока) немедленно принимаются на повторное использование. [8] Для установки и считывания значений этих атрибутов предусмотрены специальные методы. После создания потока его атрибуты нельзя изменить до тех пор, пока он существует. Атрибут области видимости описывает, с какими потоками конкретный поток конкурирует за обладание системными ресурсами. Потоки соперничают за ресурсы в рамках двух областей видимости: Таблица 4.3. Члены объекта атрибутов потока detachstate int pthread_attr_ setdetachstate (pthread_attr_t *attr, int detachstate); Атрибут detachstate определяет, является ли новый поток открепленным. Если это соответствует истине, то его нельзя объединить ни с каким другим потоком guardsize int pthread_attr_ setguardsize (pthread_attr_t *attr, size_t guardsize) Атрибут guardsize позволяет управлять размером защитной области стека нового потока. Он создает буферную зону размером guardsize на переполненяемом конце стека inheritsched int pthread_attr_ setinheritsched (pthread_attr_t *attr, int inheritsched) Атрибут inheritsched определяет, как будут установлены атрибуты планирования для нового потока, т.е. будут ли они унаследованы от потока-создателя или установлены атрибутным объектом param int pthread_attr_ setschedparam (pthread_attr_t *restrict attr, const struct sched_param *restrict param); schedpolicy int pthread_attr_ setschedpolicy (pthread_attr_t *attr, int policy); contentionscope int pthread_attr_ setscope (pthread_attr_t *attr, int contentionscope); stackaddr int pthread_attr_ setstackaddr (pthread_attr_t *attr, void *stackaddr); int pthread_attr_ setstack (pthread_attr_t *attr, void *stackaddr, size_t stacksize)j stacksize int pthread_attr_ setstacksize (pthread_attr_t *attr, size_t stacksize), int pthread_attr_ setstack (pthread_attr_t *attr, void *stackaddr, size_t stacksize)j Атрибут param— это структура, которую можно использовать для установки приоритета нового потока Атрибут schedpolicy определяет стратегию планирования создаваемого потока Атрибут contentionscope определяет, с каким множеством потоков будет соперничать создаваемый поток за использование процессорного времени. Область видимости процесса означает, что поток будет состязаться со множеством потоков одного процесса, а область видимости системы означает, что поток будет состязаться с потоками в масштабе всей системы (т.е. сюда входят потоки других процессов) Атрибуты stackaddr и stacksize определяют базовый адрес и минимальный размер (в байтах) стека, выделяемого для создаваемого потока, соответственно Атрибут stackaddr определяет базовый адрес стека, выделяемого для создаваемого потока Атрибут stacksize определяет минимальный размер стека в байтах, выделяемого для создаваемого потока
Планирование потоков
Когда подходит время для выполнения процесса, процессор занимает один из его оков. Если процесс имеет только один поток, то именно он (т.е. основной поток) назначается процессору. Если процесс содержит несколько потоков и в системе есть достаточно е количе ство процессоров, то процессорам назначаются все потоки. Потоки соперничают-за процессор либо со всеми потоками из активного процесса системы, либо только с потоками из одного процесса. Потоки помещаются в очереди готовых потоков, отсортированные по значению их приоритета. Потоки с одинаковым приоритетом назначаются процессорам в соответствии с некоторой стратегией планирования. Если система не содержит достаточного количества процессоров, поток с более высоким приоритетом может выгрузить поток, выполняющийся в данный момент. Если новый активный поток принадлежит тому же процессу, что и выгруженный, возникает переключение контекста потоков. Если же новый активный поток «родом» из другого процесса, то сначала происходит переключение контекста процессов, а затем — контекста потоков.
Состояния потоков
Потоки имеют такие же состояния и переходы между ними (см. главу 3), как и процессы. Диаграмма состояний, показанная на рис. 4.4, — это копия диаграммы, изображенной на рис. 3.4 из главы 3. (Вспомним, что процесс может пребывать в одном из четырех состояний: готовности, выполнения, останова и ожидания, или блокирования.) Состояние потока — это режим или условия, в которых поток существует в данный момент. Поток находится в состоянии готовности (работоспособности), когда он готов к выполнению. Все готовые к работе потоки помещаются в очереди готовности, причем в каждой такой очереди содержатся потоки с одинаковым приоритетом. Когда поток выбирается из очереди готовности и назначается процессору, он (поток) переходит в состояние выполнения. Поток снимается с процессора, если его квант времени истек, или если перешел в состояние готовности поток с более высоким приоритетом. Выгруженный поток снова помещается в очередь готовых потоков. Поток пребывает в состоянии ожидания, если он ожидает наступления некоторого события или завершения операции ввода-вывода. Поток прекращает выполнение, получив сигнал останова, и остается в этом состоянии до тех пор, пока не получит сигнал продолжить работу. Рис. 4.4. Состояния потоков и переходы между ними При получении этого сигнала поток переходит из состояния останова в состояние готовности. Переход потока из одного состояния в другое является своего рода сигналом о наступлении некоторого события. Переход конкретного потока из состояния готовности в со стояние выполнения происходит потому, что система выбрала именно его для выполнения, т.е. поток Один поток может определить состояние всего процесса. Состояние процесса с одним потоком синонимично состоянию его основного потока. Если его основной поток находится в состоянии ожидания, значит, и весь процесс находится в состоянии ожидания. Если основной поток выполняется, значит, и процесс выполняется. Что касается процесса с несколькими потоками, то для того, чтобы утверждать, что весь процесс находится в состоянии ожидания или останова, необходимо, чтобы все его потоки пребывали в состоянии ожидания или останова. Но если хотя бы один из его потоков активен (т.е. готов к выполнению или выполняется), процесс считается активным.
Планирование потоков и область конкуренции
Область конкуренции потоков определяет, с каким множеством потоков будет соперничать рассматриваемый поток за использование процессорного времени. Если поток имеет область конкуренции уровня процесса, он будет соперничать за ресурсы с потоками того же самого процесса. Если же поток имеет системную область конкуренции, он будет соперничать за процессорный ресурс с равными ему по правам потоками (из одного с ним процесса) и с потоками других процессов. Пусть, например, как показано на рис. 4.5, существуют два процесса в мультипроцессорной среде, которая включает три процессора. Процесс А имеет четыре потока, а процесс В — три. Для процесса А «расстановка сил» такова: три (из четырех его потоков) имеют область конкуренции уровня процесса, а один— уровня системы. Для процесса В такая «картина»: два (из трех его потоков) имеют область конкуренции уровня процесса, а один— уровня системы. Потоки процесса А с процессной областью конкуренции соперничают за процессор А, а потоки процесса В с такой же (процессной) областью конкуренции соперничают за процессор С. Потоки процессов А и В с системной областью конкуренции соперничают за процессор В. ПРИМЕЧАНИЕ: потоки при моделировании их реального поведения в приложении Должны иметь системную область конкуренции.
Стратегия планирования и приоритет
Стратегия планирования и приоритет процесса принадлежат основному потоку. Каждый поток (независимо от основного) может иметь собственную стратегию планирования и приоритет. Потокам присваиваются целочисленные значения приоритета, к оторые лежат в диапазоне между заданными минимальным и максимальным значения- м и. Схема приоритетов используется при определении, какой поток следует назначить п роцессору: поток с более высоким приоритетом выполняется раньше потока с более н изким приоритетом. После назначения потокам приоритетов задачам, которые тре буют немедленного выполнения или ответа от системы, предоставляется необходимое процессорное время. В операционной системе с приоритетами выполняющийся поток снимается с процессора, если в состояние готовности переходит поток с более высоким приоритетом, обладающий при этом тем же уровнем области конкуренции. Например, как показано на рис. 4.5, потоки с процессной областью конкуренции соревнуются за процессор с потоками того же процесса, имеющими такой же уровень области конкуренции. Процесс А имеет два потока с приоритетом 3, и один из них назначен процессору. Как только поток с приоритетом 2 заявит о своей готовности, активный поток будет вытеснен, а процессор займет поток с более высоким приоритетом. Кроме того, в процессе В есть два потока (процессной области конкуренции) с приоритетом 1 (приоритет 1 выше приоритета 2). Один из этих потоков назначается процессору. И хотя другой поток с приоритетом 1 готов к выполнению, он не вытеснит поток с приоритетом 2 из процесса А, поскольку эти потоки соперничают за процессор в рамках своих процессов. Потоки с системной областью конкуренции и более низким приоритетом не вытесняются ни одним из потоков из процессов А или В. Они соперничают за процессорное время только с потоками, имеющими системную область конкуренции. Рис. 4.5. Планирование потоков (с процессной и системной областями конкуренции) в мультипроцессорной среде Как упоминалось в главе 3, очереди готовности организованы в виде отсортированных списков, в которых каждый элемент представляет собой уровень приоритета. Под уровнем приоритета понимается очередь потоков с одинаковым значением приоритета. Все потоки одного уровня приоритета назначаются процессору с использованием стратегии планирования: FIFO (сокр. от °Д Другими» стратегиями планирования подразумеваются уже рассмотренные, но с небольшими вариациями. Например, FIFO-стратегия может быть изменена таким разом, чтобы позволить разблокировать потоки, выбранные случайно.
Изменение приоритета потоков
Приоритеты потоков следует менять, чтобы ускорить выполнение потоков, от которых зависит выполнение других потоков. И, наоборот, этого не следует делать ради того, чтобы какой-то конкретный поток получил больше процессорного времени. Это может изменить общую производительность системы. Потоки с более высоким классом приоритета получают больше процессорного времени, чем потоки с более низким классом приоритета, поскольку они выполняются чаще. Потоки с более высоким приоритетом практически монополизируют процессор, не выделяя потокам с более низким приоритетом такое ценное процессорное время. Эта ситуация получила название информационного Гарантировать, что конкретный процесс или поток будет выполняться до его полного завершения, — все равно что присвоить ему самый высокий приоритет. Однако реализация такой стратегии может повлиять на общую производительность системы. Такие привилегированные потоки могут нарушить взаимодействие программных компонентов через сетевые средства коммуникации, вызвав потерю данных. На потоки, которые управляют интерфейсом пользователя, может быть оказано чрезмерно большое влияние, выраженное в замедлении реакции на использование клавиатуры, мыши или экрана. В некоторых системах пользовательским процессам или потокам не назначается более высокий приоритет, чем системным процессам. В противном случае системные процессы или потоки не смогли бы реагировать на критические изменения в системе. Поэтому большинство пользовательских процессов и потоков попадают в категорию программных компонентов с нормальным (средним) приоритетом.
Ресурсы потоков
Потоки используют большую часть своих ресурсов вместе с другими потоками из того же процесса. Собственные ресурсы потока определяют его контекст. Так, в контекст потока входят его идентификационный номер, набор регистров (включающих указатель стека и программный счетчик) и стек. Остальные ресурсы (процессор, память и файловые дескрипторы), необходимые потоку для выполнения его задачи, он должен разделять с другими потоками. Дескрипторы файлов выделяются каждому процессу в отдельности, и потоки одного процесса соревнуются за доступ к этим дескрипторам. Что касается памяти, процессора и других глобально распределяемых ресурсов, то за доступ к ним потоки конкурируют с другими потоками своего процесса, а также с потоками других процессов. Поток при выполнении может запрашивать дополнительные ресурсы, например, файлы или мьютексы, но они становятся доступными для всех потоков процесса. Существуют ограничения на ресурсы, которые может использовать один процесс. Таким образом, все потоки в общей сложности не должны превышать предельный объем ресурсов, выделяемых процессу. Если поток попытается расходовать больше ресурсов, чем предусмотрено предельным объемом, формируется сигнал о том, что достигнут предельный объем ресурсов для данного процесса. Потоки, которые используют ресурсы, должны следить за тем, чтобы эти ресурсы не оставались в нестабильном состоянии после их аннулирования. Поток, который открыл файл или создал мьютекс, может завершиться, оставив этот файл открытым или мьютекс заблокированным. Если приложение завершилось, а файл не был закрыт надлежащим образом, это может привести к его разрушению или потере данных. Завершение потока после блокировки мьютекса надежно запирает доступ к критическому разделу, который находится под контролем этого мьютекса. Перед завершением поток должен выполнить некоторые действия очистительно-восстановительного характера, чтобы Н е допустить возникновения нежелательных ситуаций.
Модели создания и функционирования потоков
Цель потока— выполнить некоторую работу от имени процесса. Если процесс содержит несколько потоков, каждый поток выполняет некоторые подзадачи как части общей задачи, выполняемой процессом. Потокам делегируется работа в соответствии с конкретной стратегией, которая определяет, каким образом реализуется делегирование работы. Если приложение моделирует некоторую процедуру или объект, то выбранная стратегия должна отражать эту модель. Используются следующие распространенные модели: • делегирование («управляющий-рабочий»); • сеть с равноправными узлами; • конвейер; • «изготовитель-потребитель». Каждая модель характеризуется собственной Возможны задачи, для успешного решения которых следует комбинировать перечисленные выше модели. В главе 3 мы рассматривали процесс визуализации. За-Дачи 1, 2 и 3 выполнялись последовательно, а задачи 4, 5 и 6 могли выполняться параллельно. Все задачи можно выполнить различными потоками. Если необходимо визуализировать несколько изображений, потоки 1, 2 и 3 могут сформировать конвейер. По завершении потока 1 изображение передается потоку 2, в то время к ак поток 1 может выполнять свою работу над следующим изображением. После буферизации изображений потоки 4, 5 и 6 могут реализовать параллельную обработку. Модель функционирования потоков представляет собой часть структурирования па раллелизма в приложении, в котором каждый поток может выполняться на отдельном процессоре. Модели функционирования потоков (и их краткое описание) приведены в табл. 4.4. Таблица 4.4. Модели функционирования потоков Центральный поток («управляющий») создает потоки («рабочие»), назначая каждому из них задачу. Управляющий поток может ожидать до тех пор, пока все потоки не завершат выполнение своих задач Все потоки имеют одинаковый рабочий статус. Такие потоки называются Конвейерный подход применяется для поэтапной обработки потока входных данных. Каждый этап — это поток, который выполняет работу на некоторой совокупности входных данных. Когда эта совокупность пройдет все этапы, обработка всего потока данных будет завершена Поток-«изготовитель» готовит данные
Модель делегирования
В модели делегирования один поток («управляющий») создает потоки («рабочие») и назначает каждому из них задачу. Управляющему потоку нужно ожидать до тех пор, пока все потоки не завершат выполнение своих задач. Управляющий поток делегирует задачу, которую каждый рабочий поток должен выполнить, путем задания некоторой функции. Вместе с задачей на рабочий поток возлагается и ответственность за ее выполнение и получение результатов. Кроме того, на этапе получения результатов возможна синхронизация действий с управляющим (или другим) потоком. Управляющий поток может создавать рабочие потоки в результате запросов, обращенных к системе. При этом обработка запроса каждого типа может быть делегирована рабочему потоку. В этом случае управляющий поток выполняет некоторый цикл событий. По мере возникновения событий рабочие потоки создаются и на них тут же возлагаются определенные обязанности. Для каждого нового запроса, обращенного к системе, создается новый поток. При использовании такого подхода процесс может превысить предельный объем выделенных ему ресурсов или предельное количество потоков. В качестве альтернативного варианта управляющий поток может создать пул потоков, которым будут переназначаться новые запросы. Управляющий поток создает во время инициализации некоторое количество потоков, а затем каждый поток приостанавливается до тех пор, пока не будет добавлен запрос в их очередь. По мере размещения запросов в очереди управляющий поток сигнализирует рабочему о необходимости обработки запроса. Как только поток справится со своей задачей, он извлекает из очереди следующий запрос. Если в очереди больше нет доступных запросов, поток приостанавливается до тех пор. пока управляющий поток не просигналит ему о появлении очередного задания в очереди. Если все рабочие потоки должны разделять одну очередь, то их можно программировать на обработку запросов только определенного типа. Если тип запроса в очереди не совпадает с типом запросов, на обработку которых ориентирован данный поток, то он может снова приостановиться. Главная цель управляю-потока — создать все потоки, поместить задания в очередь и «разбудить» рабочие потоки, когда эти задания станут доступными. Рабочие потоки справляются о наличии запроса в очереди, выполняют назначенную задачу и приостанавливаются сами, если для них больше нет работы. Все рабочие и управляющий потоки выполняются параллельно. Описанные два подхода к построению модели делегирования представлены для сравнения на рис. 4.6.
Модель с равноправными узлами
Если в модели делегирования есть управляющий поток, который делегирует задачи рабочим потокам, то в
Модель конвейера
Модель конвейера подобна ленте сборочного конвейера в том, что она предполагает наличие потока элементов, которые обрабатываются поэтапно. На каждом этапе отдельный поток выполняет некоторые операции над определенной совокупностью входных данных. Когда эта совокупность данных пройдет все этапы, обработка всего входного потока данных будет завершена. Этот подход позволяет обрабатывать несколько входных потоков одновременно. Каждый поток отвечает за получение промежуточных результатов, делая их доступными для следующего этапа (или следующего потока) конвейера Последний этап (или поток) генерирует результаты работы конвейера в целом. По мере того как входные данные проходят по конвейеру, не исключено, что некоторые их порции придется буферизировать на определенных этапах, пока потоки еще занимаются обработкой предыдущих порций. Это может вызвать торможение конвейера, если окажется, что обработка данных на каком-то этапе происходит медленнее, чем на других. При этом образуется отставание в работе. Чтобы предотвратить отставание, можно для «слабого» этапа создать дополнительные потоки. Все этапы конвейера должны быть уравновешены по времени, чтобы ни один этап не занимал больше времени, чем другие. Для этого необходимо всю работу распределить по конвейеру равномерно. Чем больше этапов в конвейере, тем больше должно быть создано потоков обработки. Увеличение количества потоков также может способствовать предотвращению отставаний в работе. Модель конвейера представлена на рис. 4.8.
Модель «изготовитель-потребитель»
В модели «изготовитель-потребитель» существует поток-«изготовитель», который готовит данные, потребляемые потоком-«потребителем». Данные сохраняются в блоке памяти, разделяемом между потоками «изготовителем» и «потребителем». Поток-изготовитель» должен сначала приготовить данные, которые затем поток-^потребитель» получит. Такому процессу необходима синхронизация. Если поток-изготовитель» будет поставлять данные гораздо быстрее, чем поток-«потребитель» сможет их потреблять, поток-«изготовитель» несколько раз перезапишет результаты, полученные им ранее, прежде чем поток-«потребитель» успеет их обработать. Но если поток-«потребитель» будет принимать данные гораздо быстрее, чем поток-изготовитель» сможет их поставлять, поток-«потребитель» будет либо снова обрабатывать уже обработанные им данные, либо попытается принять еще не подготовленные данные. Модель «изготовитель-потребитель» представлена на рис. 4.9.
Модели SPMD и МРМD для потоков
В каждой из описанных выше моделей потоки вновь и вновь выполняют одну и ту задачу на различных наборах данных или им назначаются различные задачи для выполнения на различных наборах данных. Эти потоковые модели используют схемы (Single-Program, Multiple-Data — одна программа, несколько потоков данных) и MPMD (Multiple-Programs, Multiple-Data — множество программ, множество потоков данных). Эти схемы представляют собой модели параллелизма, которые делят программы на потоки инструкций и данных. Их можно использовать для описания типа работы, которую реализуют потоковые модели с использованием параллелизма. В контексте нашего изложения материала модель MPMD лучше представить как модель MTMD (Multiple-Threads, Multiple-Data— множество потоков выполнения, множество потоков данных). Эта модель описывает систему с различными потоками выполнения (thread), которые обрабатывают различные наборы данных, или Как модель делегирования, так и модель равноправных потоков могут использовать модели параллелизма STMD и MTMD. Как было описано выше, пул потоков может выполнять различные подпрограммы для обработки различных наборов данных. Такое поведение соответствует модели MTMD. Пул потоков может быть также настроен на выполнение одной и той же подпрограммы. Запросы (или задания), отсылаемые системе, могут представлять собой различные наборы данных, а не различные задачи. И в этом случае поведение множества потоков, реализующих одни и те же инструкции, но на различных наборах данных, соответствует модели STMD. Модель равноправных потоков может быть реализована в виде потоков, выполняющих одинаковые или различные задачи. Каждый поток выполнения может иметь собственный поток данных или несколько файлов сданными, предназначенных для обработки каждым потоком. В модели конвейера используется МТМГ>модель параллелизма. На разных этапах выполняются различные виды обработки, поэтому в любой момент времени различные совокупности входных данных будут находиться на различных этапах выполнения. Модельное представление конвейера было бы бесполезным, если бы на каждом этапе выполнялась одна и та же обработка. Модели STMD и MTMD представлены на рис. 4.10.
Введение в библиотеку Pthread
Библиотека Pthread предоставляет API-интерфейс для создания и управления потоками в приложении. Библиотека Pthread основана на стандартизированном интерфейсе программирования, который был определен комитетом по выпуску стандартов IEEE в стандарте POSIX 1003.1с. Сторонние фирмы-изготовители придерживаются стандарта POSIX в реализациях, которые именуются библиотеками потоков Pthread или POSIX. Рис. 4.10. Модели параллелизма STMD и MTMD Библиотека Pthread содержит более 60 функций, которые можно разделить на следующие категории. 1. Функции управления потоками. 1.1. Конфигурирование потоков. 1.2. Отмена потоков. 1.3. Стратегии планирования потоков. 1.4. Доступ к данным потоков. 1.5. Обработка сигналов. 1.6. Функции доступа к атрибутам потоков. 1.6.1. Конфигурирование атрибутов потоков. 1.6.2. Конфигурирование атрибутов, относящихся к стекам потоков. 1.6.3. Конфигурирование атрибутов, относящихся к стратегиям планирования потоков. 2. Функции управления мьютексами. 2.1. Конфигурирование мьютексов. 2.2. Управление приоритетами. 2.3. Функции доступа к атрибутам мьютексов. 2.3.1. Конфигурирование атрибутов мьютексов.« 2.3.2. Конфигурирование атрибутов, относящихся к протоколам мьютексов. 2.3.3. Конфигурирование атрибутов, относящихся к управлению приоритетами мьютексов. 3. Функции управления условными переменными. 3.1. Конфигурирование условных переменных. 3.2. Функции доступа к атрибутам условных переменных. 3.2.1. Конфигурирование атрибутов условных переменных. 3.2.2. Функции совместного использования условных переменных. Библиотека Pthread может быть реализована на любом языке, но для соответствия стандарту POSIX она должна быть согласована со стандартизированным интерфейсом. Библиотека Pthread — не единственная реализация потокового API-интерфейса Существуют другие реализации, созданные сторонними фирмами-производителями аппаратных и программных средств. Например, среда Sun поддерживает библиотеку Pthread и собственный вариант библиотеки потоков Solaris. В этой главе мы рассмотрим некоторые функции библиотеки Pthread, которые реализуют управление потоками.
Анатомия простой многопоточной программы
Любая простая многопоточная программа должна состоять из основного потока и функций, которые будут выполнять другие потоки. Выбранная для реализации модель создания и функционирования потоков определяет, каким образом в программе будут созда ваться потоки и как будет осуществляться управление ими. Потоки создаются по принципу «все и сразу» или при определенных условиях. Пример простой многопоточной программы, в которой реализована модель делегирования, представлен в листинге 4.1. // Листинг 4.1. Использование модели делегирования в // простой многопоточной программе #include #include void *task1(void *X) //define task to be executed by ThreadA { //... cout << «Thread A complete» << endl; } void *task2(void *X) //define task to be executed by ThreadB { //... cout << «Thread B complete» << endl; } int main(int argc, char *argv[]) { pthread_t ThreadA,ThreadB; // declare threads pthread_create(&ThreadA,NULL,task1,NULL); // create threads pthread_create(&ThreadB,NULL,task2,NULL); // additional processing pthread_join(ThreadA,NULL); // wait for threads pthread_join(ThreadB,NULL); return(0); } В листинге 4.1 делается акцент на определении набора инструкций для основного потока. Основным в данном случае является управляющий поток, который объявляет два рабочих потока ThreadA и ThreadB. С помощью функции pthread_create эти два потока связываются с задачами, которые они должны выполнить (taskl и task2). Здесь (ради простоты примера) эти задачи всего лишь отп равляют сообщение в стандартный выходной поток, но понятно, что они могли бы быть запрограммированы на нечто более полезное. При вызове функции pthread_create потоки немедленно приступают к выполнению назначенных им задач. Работа функции pthread_join аналогична работе функции wait для процессов. Основной поток ожидает до тех пор, пока не завершатся оба рабочих потока. Диаграмма последовательностей, соответствующая листингу 4.1, показана на рис. 4.11. Обратите внимание на то, что происходит с потоками выполнения при вызове функций pthread_create и pthread_join . На рис.4.11 показано, что вызов функции pthread_create является причиной разветвления, или образования «вилки» в основном потоке выполнения, в результате чего образуются два дополнительных «ручейка» (по одному для каждой задачи), которые выполняются параллельно. Функция pthread_create завершается сразу же после создания потоков. Эта функция предназначена для создания асинхронных потоков. Это означает, что, как рабочие, так и основной поток, выполняют свои инструкции независимо друг от друга. Функция pthread_join заставляет основной поток ожидать до тех пор, пока все рабочие потоки завершатся и «присоединятся» к основному.
Компиляция и компоновка многопоточных программ
Все многопоточные программы, использующие библиотеку потоков POSIX, должны включать заголовок: < Для компиляции многопоточного приложения в средах UNIX или Linux с помощью компиляторов командной строки g++ или gcc необходимо скомпоновать его с библиотекой Pthreads. Для задания библиотеки используйте опцию -l. Так, команда Законченные программы, представленные в этой книге, сопровождаются профилем.
Создание потоков
Библиотека Pthreads используется для создания, поддержки и управления потоками многопоточных программ и приложений. При создании многопоточной программы потоки могут создаваться на любом этапе выполнения процесса, поскольку это — динамические образования. Функция pthread_create создает новый поток в адресном пространстве процесса. Параметр thread указывает на дескриптор, или идентификатор (id), создаваемого потока. Новый поток будет иметь атрибуты, заданные объектом attr. Созданный поток немедленно приступит к выполнению инструкций, заданных параметром start_routine с использованием аргументов, заданных параметром arg. При успешном создании потока функция возвращает его идентификатор (id), значение которого сохраняется в параметре thread. Синопсис #include int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg) ; Если параметр attr содержит значение NULL, новый поток будет использовать атрибуты, действующие по умолчанию. В противном случае новый поток использует атрибуты, заданные параметром attr при его создании. Если же значение параметра attr изменится после того, как поток был создан, это никак не отразится на его атрибутах. При завершении параметра-функции start_routine завершается и поток, причем так, как будто была вызвана функция pthread_exit с использованием в качестве статуса завершения значения, возвращаемого функцией start_routine. При успешном завершении функция возвращает число 0 . В противном случае по-не создается, и функция возвращает код ошибки. Если в системе отсутствуют ресурсы для создания потока, или в процессе достигнут предел по количеству возможных потоков, выполнение функции считается неудачным. Неудачным оно также будет в случае, если атрибут потока задан некорректно или если инициатор вызова потока не имеет разрешения на установку необходимых атрибутов потока. Приведем примеры создания двух потоков с заданными по умолчанию атрибутами: pthread_create(&threadA,NULL, taskl,NULL) ; pthread_create(&threadB,NULL, task2, NULL) ; Это — два вызова функции pthread_create из листинга 4 .1. Оба потока создаются с атрибутами, действующими по умолчанию. В программе 4 .1 отображен основной поток, который передает аргумент из командной строки в функции, выполняемые потоками. // Программа 4.1 #include #include #include int main(int argc, char *argv[]) { pthread_t ThreadA,ThreadB; int N; if(argc != 2) { cout << «error» << endl; exit (1); } N = atoi(argv[l]); pthread_create(&ThreadA,NULL, taskl,&N); pthread_create(&ThreadB,NULL, task2, &N); cout « «Ожидание присоединения потоков.» « endl; pthread_join(ThreadA,NULL) ; pthread_join(ThreadB,NULL); return (0) ; }; В программе 4 .1 показано, как основной поток может передать аргументы из командной строки в каждую из потоковых функций. Число в командной строке имеет строковый тип. Поэтому в основном потоке аргумент сначала преобразуется в целочисленное значение, и только после этого результат преобразования передается при каждом вызове функции pthread_create посредством ее последнего аргумента. В программе 4.2 представлена каждая из потоковых функций. // Программа 4.2 void *task1(void *X) { int *Temp; Temp = static_cast for(int Count = 1;Count < *Temp;Count++){ cout << «work from thread A: " << Count << " * 2 = " << Count * 2 << endl; } cout << «Thread A complete» << endl; } void *task2(void *X) { int *Temp; Temp = static_cast for(int Count = 1;Count < *Temp;Count++){ cout << «work from thread B: " << Count << " + 2 = " << Count + 2 << endl; } cout << «Thread B complete» << endl; } В программе 4.2 функции taskl и task2 выполняют цикл, количество итераций которого равно числу, переданному каждой функции в качестве параметра. Одна функция увеличивает переменную цикла на два, вторая — умножает ее на два, а затем каждая из них отправляет результат в стандартный поток вывода данных. По выходу из цикла каждая функция выводит сообщение о завершении выполнения потока. Инструкции по компиляции и выполнению программ 4.1 и 4.2 содержатся в профиле программы 4.1. [ Профиль программы 4.1 Имя программы •program4-12.cc * Описание Принимает целочисленное значение из командной строки и передает функциям: потоков. Каждая функция выполняет цикл, в котором переменная цикла увеличивается (в одной функции на два, а в другой в два раза), а затем результат отсылается в стандартный поток вывода данных. Код основного потока выполнения приведен в программе 4.1, а код функций — в программе 4.2. Требуемая библиотека libpthread Требуемые заголовки Инструкции по компиляции и компоновке программ с++ -о program4-12 program4-12.cc -lpthread Среда для тестирования SuSE Linux 7.1, gcc 2.95.2, Инструкции по выполнению ./program4-12 34 Примечания Эта программа требует задания аргумента командной строки. В этом разделе был приведен пример передачи функции потока лишь одного аргумента. Если необходимо передать функции потока несколько аргументов, создайте структуру (struct) или контейнер, содержащий все требуемые аргументы, и передайте функции потока указатель на эту структуру.
Получение идентификатора потока
Как упоминалось выше, процесс разделяет все свои ресурсы с потоками, используя лишь собственное адресное пространство. Потокам в собственное пользование выделяются весьма небольшие их объемы. Идентификатор потока (id) — это один из ресурсов, уникальных для каждого потока. Чтобы узнать свой идентификатор, потоку необходимо вызвать функцию pthread_self . Синопсис #include pthread_t pthread_self(void) Эта функция аналогична функции getpid для процессов. При создании потока его идентификатор возвращается его создателю или вызывающему потоку. Однако идентификатор потока не становится известным созданному потоку автоматически. Но если уж поток обладает собственным идентификатором, он может передать его (предварительно узнав его сам) другим потокам процесса. Функция pthread_self возвращает идентификатор потока, не определяя никаких кодов ошибок. Вот пример вызова этой функции: pthread_t ThreadId; ThreadId = pthread_self; Поток вызывает функцию pthread_self, а значение, возвращаемое ею (идентификатор потока), сохраняет в переменной ThreadId типа pthread_t.
Присоединение потоков
Функция pthread_join используется для присоединения или воссоединения потоков выполнения в одном процессе. Эта функция обеспечивает приостановку выполнения вызывающего потока до тех пор, пока не завершится заданный поток. По своему Действию эта функция аналогична функции wait , используемой процессами. Эту функцию может вызвать создатель потока, после чего он будет ожидать до тех пор, пока не завершится новый (созданный им) поток, что, образно говоря, можно назвать воссоединением потоков выполнения. Функцию pthread_join могут также вызывать равноправные потоки, если потоковый дескриптор является глобальным. Это позволяет любому потоку соединиться с любым другим потоком выполнения в процессе. Если вызы вающий поток аннулируется до завершения заданного (для присоединения) потока,этот заданный поток не станет открепленным (detached) потоком (см. следующий раздел) Если различные равноправные потоки одновременно вызовут функцию pthread_join для одного и того же потока, его дальнейшее поведение не определено. Синопсис #include int pthread_join(pthread_t thread, void **value_ptr); Параметр thread представляет поток, завершения которого ожидает вызывающий поток. При успешном выполнении этой функции в параметре value_ptr будет записан статус завершения потока. Статус завершения — это аргумент, передаваемый при вызове функции pthread_exit завершаемым потоком. При неудачном выполнении эта функция возвратит код ошибки. Функция не будет выполнена успешно, если заданный поток не является присоединяемым, т.е. создан как открепленный. Об успешном выполнении этой функции не может быть и речи, если заданный поток попросту не существует. Функцию pthread_join необходимо вызывать для всех присоединяемых потоков. После присоединения потока операционная система сможет снова использовать память, которую он занимал. Если присоединяемый поток не был присоединен ни к одному потоку или если поток, который вызывает функцию присоединения, аннулируется, то заданный поток будет продолжать использовать свою память. Это состояние аналогично зомбированному процессу, в которое переходит сыновний процесс, когда его родитель не принимает статус завершения потомка, и этот «беспризорный» сыновний процесс продолжает занимать структуру в таблице процессов.
Создание открепленных потоков
Синопсис #include int pthread_detach(pthread_t thread thread); При успешном выполнении эта функция возвращает число 0 , в противном случае— код ошибки. Функция pthread_detach не будет успешной, если заданный поток уже откреплен или поток, заданный параметром thread, не был обнаружен. Вот пример открепления уже существующего присоединяемого потока: pthread_create(&threadA,NULL,taskl,NULL); pthread_detach(threadA); При выполнении этих строк кода поток threadA станет открепленным. Чтобы создать открепленный поток (в противоположность динамическому откреплению потока) необходимо установить атрибут detachstate в объекте атрибутов потока и использовать этот объект при создании потока.
Использование объекта атрибутов
Объект атрибутов инкапсулирует атрибуты потока или группы потоков. Он используется для установки атрибутов потоков при их создании. Атрибутный объект потока имеет тип pthread_attr_t. Он представляет собой структуру, позволяющую хранить следующие атрибуты: • размер стека потока; • местоположение стека потока; • стратегия планирования, наследование и параметры; • тип потока: открепленный или присоединяемый; • область конкуренции потока. Для типа pthread_attr_t предусмотрен ряд методов, которые могут быть вызваны для установки или считывания каждого из перечисленных выше атрибутов (см. табл. 4.3). Для инициализации и разрушения атрибутного объекта потока используются функции pthread_attr_init и pthread_attr_destroy соответственно. Синопсис #include int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread attr__t *attr) ; Функция pthread_attr_init инициализирует атрибутный объект потока с помощью стандартных значений, действующих для всех этих атрибутов. Параметр attr представляет собой указатель на объект типа pthread_attr_t. После инициализации attr-объекта значения его атрибутов можно изменить с помощью функций, перечисленных в табл. 4.3. После соответствующей модификации атрибутов значение attr используется в качестве параметра при вызове функции создания потока pthread_create. При успешном выполнении эта функция возвращает число 0, в противном случае — код ошибки. Функция pthread_attr_init завершится неуспешно, если для создания объекта в системе недостаточно памяти. Функцию pthread_attr_destroy можно использовать для разрушения объекта типа pthread_attr_t, заданного параметром attr. При обращении к этой функ ц ии будут удалены любые скрытые данные, связанные с этим атрибутным объектом потока. При успешном выполнении эта функция возвращает число 0, в противном случае - код ошибки.
Создание открепленных потоков с помощью объекта атрибутов
После инициализации объекта потока его атрибуты можно модифицировать. Дл я установки атрибута detachstate атрибутного объекта используется функция pthread__attr_setdetachstate. Параметр detachstate описывает поток как открепленный или присоединяемый. Синопсис #include int pthread_attr_setdetachstate( pthread_attr_t *attr, int *detachstate ); int pthread_attr_getdetachstate( const pthread_attr_t *attr, int *detachstate ); Параметр detachstate может принимать одно из следующих значений: PTHREAD_CREATE_DETACHED PTHREAD_CREATE_JOINABLE Значение PTHREAD_CREATE_DETACHED « превращает» все потоки, которые используют этот атрибутный объект, в открепленные, а значение PTHREAD_CREATE_JОINABLE — в присоединяемые. Значение PTHREAD_CREATE_JOINABLE атрибут detachstate принимает по умолчанию. При успешном выполнении функция pthread_attr_setdetachstate возвращает число 0 , в противном случае — код ошибки. Эта функция выполнится неуспешно, если значение параметра detachstate окажется недействительным. Функция pthread_attr_getdetachstate возвращает значение атрибута detachstate атрибутного объекта потока. При успешном выполнении эта функция возвращает значение атрибута detachstate в параметре detachstate и число 0 обычным способом. При неудаче функция возвращает код ошибки. В листинге 4.2 показано, как открепляются потоки, созданные в программе4.1. В этом примере при создании одного из потоков используется объект атрибутов. // Листинг 4.2. Использование атрибутного объекта для // создания открепленного потока int main(int argc, char *argv[]) { pthread_t ThreadA,ThreadB; pthread_attr_t DetachedAttr; int N; if(argc != 2) { cout « «Ошибка» << endl; exit (1); } N = atoi(argv[1]); pthread_attr_init(&DetachedAttr); pthread_attr_setdetachstate(&DetachedAttr,PTHREAD_CREATE_DETACHED); pthread_create(&ThreadA,NULL, task1, &N); pthread_create(&ThreadB,&DetachedAttr,task2 , &N); cout << «Ожидание присоединения потока А.» << endl; pthread_join(ThreadA,NULL); return (0) ; } В листинге 4.2 объявляется атрибутный объект DetachedAttr, для инициализации которого используется функция pthread_attr_init. После инициализации этого объекта вызывается функция pthread_attr_detachstate, которая изменяет свойство detachstate («присоединяемость»), установив значение PTHREAD_CREATE_DETACHED («открепленность»). При создании потока ThreadB с помощью функции pthread_create в качестве ее второго аргумента используется модифицированный объект DetachedAttr. Для потока ThreadB вызов функции pthread_join не используется, поскольку открепленные потоки присоединить невозможно.
Управление потоками
Создавая приложение с несколькими потоками, можно по-разному организовать их выполнение, использование ими ресурсов и состязание за ресурсы. Управление потоками по большей части осуществляется путем установки стратегий планирования и значений приоритета. Эти факторы влияют на эффективность потока. Кроме них, эффективность потока также определяется тем, как потоки состязаются за ресурсы: в рамках одного процесса либо в масштабе всей системы. Стратегию планирования, приоритет и область конкуренции потока можно установить с помощью объекта атрибутов потока. Поскольку потоки совместно используют ресурсы, доступ к ним необходимо синхронизировать. Эту тему мы кратко затронем в этой главе и более подробно— в главе 5. К вопросам синхронизации также относятся и такие: где и как завершаются и аннулируются потоки.
Завершение потоков
Выполнение потока может быть прервано по разным причинам: • в результате выхода из процесса с возвращаемым им статусом завершения (или без него); • в результате собственного завершения и предоставления статуса завершения; • в результате аннулирования другим потоком в том же адресном пространстве. Завершаясь, функция присоединения потока pthread_join возвращает вызывающему потоку статус завершения, передаваемый функции pthread_exit, которая была вызвана завершающимся потоком. Если завершающийся поток не обращался к функции pthread_exit , то в качестве статуса завершения будет использовано значение, возвращаемое этой функцией, если оно существует; в противном случае статус завершения равен значению NULL. [9] Воз можна ситуация, когда одному потоку необходимо завершить другой поток в том же процессе. Например, приложение может иметь поток, который контролирует работу других потоков. Если окажется, что некоторый поток «плохо себя ведет», или больше не нужен, то ради экономии системных ресурсов, возможно, его нужно завершить. Завершающийся поток может окончиться немедленно или отложить завершение до тех пор, пока не достигнет в своем выполнении некоторой логической точки. При этом вполне вероятно, что такой поток (прежде чем завершиться) должен выполнить некоторые действия очистительно-восстановительного характера. Поток имеет также возможность отказаться от завершения. Для завершения вызывающего потока используется функция pthread_exit Значение value_ptr передается потоку, который вызывает функцию pthread_join для этого потока. Еще не выполненные процедуры, связанные с «уборкой», будут выполнены вместе с деструкторами, предусмотренными для потоковых данных. Никакие ресурсы, используемые потоками, при этом не освобождаются. Синопсис #include int pthread_exit(void *value_ptr); При завершении последнего потока в процессе завершается сам процесс со статусом завершения, равным 0. Эта функция не может вернуться к вызывающему потоку и не определяет никаких кодов ошибок. Для отмены выполнения некоторого потока по инициативе потока из того же адресного пространства используется функция pthread__cancel . Отменяемый поток задается параметром thread. Синопсис #include int pthread_cancel(pthread_t thread); Обращение к функции pthread_cancel представляет собой запрос аннулировать поток. Этот запрос может быть удовлетворен немедленно, с отсрочкой или проигнорирован. Когда произойдет аннулирование (и произойдет ли оно вообще), зависит от типа аннулирования и состояния потока, подлежащего этой кардинальной операции. Для удовлетворения запроса на отмену потока предусмотрен процесс аннулирования, который происходит асинхронно (т.е. не совпадает по времени) по отношению к выходу из функции pthread_cancel и ее возврату в вызывающий поток. Если потоку нужно выполнить «уборочные» задачи, они обязательно выполняются. После выполнения последней такой задачи-обработчика вызываются деструкторы потоковых объектов, если таковые предусмотрены, и только после этого поток завершается. В этом и состоит процесс аннулирования потока. При успешном выполнении функция pthread_cancel возвращает число 0 , в противном случае — код ошибки. Эта функция не выполнится успешно, если параметр thread не соответствует ни одному из существующих потоков. Некоторые потоки могут потребовать принять меры безопасности против преждевременного их аннулирования. Внесение в потоковую функцию средств безопасности может предотвратить возникновение некоторых нежелательных ситуаций. Потоки разделяют общие данные, и (в зависимости от используемой потоковой модели) один поток может обрабатывать данные, которые должны быть переданы другому потоку для последующей обработки. Пока поток обрабатывает данные, он является их единственным обладателем благодаря блокированию мьютекса, связанного с этими данными. Если поток, имеющий заблокированный мьютекс, аннулируется до его освобождения, возникает взаимоблокировка. Для того чтобы снова использовать данные, их следует привести в определенное состояние. Если поток отменяется до освобождения мьютекса, могут возникнуть нежелательные условия. Другими словами, в зависимости от типа обработки, которую выполняет поток, его аннулирование должно происходить тогда, когда это безопасно. Об опасных и безопасных периодах «знает» только сам поток, и поэтому только он может предотвратить свое аннулирование в опасные периоды. Следовательно, круг потоков, которые можно аннулировать, должен быть ограничен потоками, которые не относятся к числу «жизненно важных» или которые не имеют блокировок ресурсов. Кроме того, аннулирование может быть отсрочено до тех пор, пока не будут выполнены «жизненно важные» действия. Для определения состояния готовности к аннулированию и типа аннулирования вызывающего потока используются функции p thread_setcancelstate pthread_setcanceltype. Функция pthread_setcancelstate устанавливает вызывающий поток в состояние, заданное параметром state, и возвращает предыдущее состояние в параметре oldstate. Синопсис #include int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldtype); Параметры state и oldstate могут принимать такие значения: PTHREAD_CANCEL_DISABLE PTHREAD_CANCEL_ENABLE Значение PTHREAD_CANCEL_DISABLE определяет состояние, в котором поток будет игнорировать запрос на аннулирование, а значение PTHREAD_CANCEL_ENABLE - состояние, в котором поток «согласится» выполнить соответствующий запрос (это состояние по умолчанию устанавливается для каждого нового потока). При успешном выполнении функция возвращает число 0 , в противном случае — код ошибки. Функция pthread_setcancelstate не может выполниться успешно, если переданное значение параметра state окажется недействительным. Функция pthread_setcanceltype устанавливает для вызывающего потока тип аннулирования, заданный параметром type, и возвращает предыдущее значение типа в параметре oldtype. Параметры type и oldtype могут принимать такие значения: PTHREAD_CANCEL_DEFFERED PTHREAD_ASYNCHRONOUS Значение PTHREAD_CANCEL_DEFFERED определяет тип аннулирования, при котором поток откладывает завершение до тех пор, пока он не достигнет точки, в котором его аннулирование возможно (этот тип по умолчанию устанавливается для каждого нового потока). Значение PTHREAD_CANCEL_ASYNCHRONOUS определяет тип аннулирования, при котором поток завершается немедленно. При успешном выполнении функция возвращает число 0 , в противном случае— код ошибки. Функция pthread_setcanceltype не может выполниться успешно, если переданное ей значение параметра type окажется недействительным. Функции pthread_setcancelstate и pthread_setcanceltype используются вместе для установки отношения вызывающего потока к потенциальному запросу на аннулирование. Возможные комбинации значений состояния и типа аннулирования перечислены и описаны в табл. 4 .5. Таблица 4.5. Комбинации значений состояния и типа аннулирования
Точки аннулирования потоков
Если удовлетворение запроса на аннулирование потока откладывается, значит, оно произойдет позже, когда это делать «безопасно», т.е. когда оно не попадает на период выполнения некоторого критического кода, блокирования мьютекса или пребывания данных в некотором «промежуточном» состоянии. Вне этих «опасных» разделов кода потоков вполне можно устанавливать точки аннулирования. Точка аннулирования — это контрольная точка, в которой поток проверяет факт существования каких-либо ждущих (отложенных) запросов на аннулирование и, если таковые имеются, разрешает завершение. Точки аннулирования можно пометить с помощью функции pthread_testcancel • Эта функция проверяет наличие необработанных запросов на аннулирование. Если они есть, она активизирует процесс аннулирования в точке своего вызова. В противном случае функция продолжает выполнение потока без каких-либо последствий. Вызов этой функции можно разместить в любом месте кода потока, которое считается безопасным для его завершения. Синопсис #include void pthread_testcancel(void) Программа 4.3 содержит функции, которые вызывают функции pthread_setcancelstate, pthread_setcanceltype и pthread_testcancel, связанные с установкой типа аннулирования потока и состояния готовности к аннулированию. #include #include void *task1(void *X) { int OldState; // disable cancelability pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,&OldState); for(int Count = 1;Count < 100;Count++) { cout << «thread A is working: " << Count << endl; } } void *task2(void *X) { int OldState,OldType; // enable cancelability, asynchronous pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&OldState); pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,&OldType); for(int Count = 1;Count < 100;Count++) { cout << «thread B is working: " << Count << endl; } } void *task3(void *X) { int OldState,OldType; // enable cancelability, deferred pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&OldState); pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,&OldType); for(int Count = 1;Count < 1000;Count++) { cout << «thread C is working: " << Count << endl; if((Count%100) == 0){ pthread_testcancel; } } } В программе 4.3 каждая задача устанавливает свое условие аннулирования. В задаче task1 аннулирование потока запрещено, поскольку вся она состоит из критического кода, который должен быть выполнен до конца. В задаче task2 аннулирование потока разрешено. Обращение к функции pthread_setcancelstate является необязательным, поскольку все новые потоки имеют статус разрешения аннулирования. Тип аннулирования здесь устанавливается равным значению PTHREAD_CANCEL_ASYNCHRONOUS Это означает, что после поступления запроса на аннулирование поток немедленно запустит соответствующую процедуру, независимо от того, на какой этап его выполнения придется этот запрос. А поскольку этот поток установил для себя именно такой тип аннулирования, значит, он не выполняет никакого «жизненно важного» кода. Например, вызовы системных функций должны попадать под категорию опасных для аннулирования, но в задаче task2 таких нет. Там выполняется лишь цикл, который будет работать до тех пор, пока не поступит запрос на аннулирование. В задаче task3 аннулирование потока также разрешено, а тип аннулирования устанавливается равным значению PTHREAD_CANCEL_DEFFERED. Эти состояние и тип аннулирования действуют по умолчанию для новых потоков, следовательно, обращения к функциям pthread_setcancelstate и pthread_setcanceltype здесь необязательны. Критический код этого потока здесь может спокойно выполняться после установки состояния и типа аннулирования, поскольку процедура завершения не стартует до вызова функции pthread_testcancel . Если не будут обнаружены ждущие запросы, поток продолжит свое выполнение до тех пор, пока не встретит очередные обращения к функции pthread_testcancel (если таковые предусмотрены). В задаче task3 функция pthread_cancel вызывается только после того, как переменная Count без остатка разделится на число 100 . Код, расположенный между точками аннулирования потока, не должен быть критическим, поскольку он может не выполниться. Программа 4.4 содержит управляющий поток, который делает запрос на аннулирование каждого рабочего потока. // Программа 4.4 int main(int argc, char *argv[]) { pthread_t Threads[3]; void *Status; pthread_create(&(Threads[0]),NULL,taskl, NULL); pthread_create(&(Threads[l]), NULL,task2,NULL); pthread_create(&(Threads[2]),NULL,task3,NULL); pthread_cancel(Threads[0]); pthread_cancel(Threads[l]); pthread_cancel(Threads[2]); for(int Count = 0;Count < 3;Count++) { pthread_join(Threads[Count],&Status); if(Status == PTHREAD_CANCELED){ cout << «Поток» << Count << " аннулирован.» << endl; } else{ cout « «Поток» << Count << " продолжает выполнение.» << endl; } } return (0) ; } Управляющий поток в программе 4.4 сначала создает три рабочих потока, затем делает запрос на аннулирование каждого из них. Управляющий поток для каждого рабочего потока вызывает функцию pthread_join. Эта функция завершается успешно даже тогда, когда она пытается присоединить поток, который уже завершен, функция присоединения в этом случае просто считывает статус завершения завершенного потока. И такое поведение весьма кстати, поскольку поток, который сделал запрос на аннулирование, и поток, вызвавший функцию pthread_join , могут оказаться совсем разными потоками. Мониторинг функционирования всех рабочих потоков может оказаться единственной задачей того потока, который «по совместительству» и аннулирует потоки. Опрашивать же статус завершения потоков с помощью функции pthread_join может совершенно другой поток. Этот тип информации используется для получения статистической оценки того, какой из потоков наиболее эффективен. В рассматриваемой нами программе все это делает один управляющий поток: в цикле он и присоединяет рабочие потоки, и проверяет их статус завершения. Поток Threads[0] не аннулирован, поскольку он имеет запрет на аннулирование, в то время как два остальных потока были аннулированы. Статус завершения аннулируемого потока может иметь, например, значение PTHREAD_CANCELED. Профили программ 4.3 и 4.4 представлены в разделе «Профиль программы 4.2». Профиль программы 4.2 Имя программы program4-34. cc ; Описание Демонстрирует аннулирование потоков. Три потока имеют различные типы состояния аннулирования. Каждый поток выполняет цикл. Состояние и тип аннулирования определяет количество итераций цикла и то, будет ли цикл выполняться вообще. Основной поток определяет статус завершения каждого , рабочего потока. Требуемая библиотека libpthread Тр ебуемые заголовки Инструкции по компиляции и компоновке программ с++ -о program4-34 program4-34.сс -lpthread Среда для тестирования SuSE Linux 7.1, gcc 2.95.2. И нс трукции по выполнению ./program4-34 В функциях, определенных пользователем, используются точки аннулирования отмеченные обращением к функции pthread_testcancel. Библиотека Pthread определяет в качестве точек аннулирования выполнение других функций. Эти функции блокируют вызывающий поток, а заблокированному потоку аннулирование не грозит. Вот эти функции библиотеки Pthread: pthread_testcancel pthread_cond_wait pthread_timedwait pthread_join Если поток, пребывающий в состоянии отсроченного аннулирования, имеет ждущий запрос на аннулирование, то при вызове одной из перечисленных выше функций библиотеки Pthread будет инициирована процедура аннулирования. Некоторые из системных функций, претендующих на роль точек аннулирования, перечислены в табл. 4.6. Таблица 4.6. Системные POSIX-функции, претендующие на роль точек аннулирования accept nanosleep sem_wait aio_suspend open send clock_nanosleep pause sendmsg close poll sendto connect pread sigpause creat pthread_cond_timedwait sigsuspend fcntl pthread_cond_wait sigtimedwait fsync pthread_join sigwait getmsg putmsg sigwaitinfo lockf putpmsg sleep mq_receive pwrite system mq_send read usleep mq_timedreceive readv wait mq_timedsend recvfrom waitpid msgrcv recvmsg write msgsnd select writev msync sem_timedwait Несмотря на то что эти функции безопасны для отсроченного аннулирования потоков, они могут не быть таковыми для асинхронного аннулирования. Асинхронное аннулирование во время вызова библиотечной функции, которая не является асин хронно-безопасной, может привести к тому, что библиотечные данные останутся не в надлежащем состоянии. Библиотека выделит память от имени потока, и, когда поток будет аннулирован, продолжит удерживать «за собой» эту память. Для других библиотечных и системных функций, которые не являются безопасными для аннулирования (асинхронного или отсроченного), возможно, имеет смысл написать код, препятствующий завершению потока путем установки категорического запрета на аннулирование или использование отсроченного аннулирования до тех пор, пока эти функции не будут выполнены.
Очистка перед завершением
Поток, «позволивший» себя аннулировать, прежде чем завершиться, обычно должен выполнить некоторые заключительные действия. Так, нужно закрыть файлы, привести разделяемые данные в надлежащее состояние, снять блокировки или освободить занимаемые ресурсы. Библиотека Pthread определяет механизм поведения каждого потока «в последние минуты своей жизни». С каждым потоком связывается стек очистительно-восстановительных операций (cleanup stack), который содержит указатели на процедуры (или функции), предназначенные для выполнения во время аннулирования потока. Для того чтобы поместить в этот стек указатель на процедуру, предусмотрена функция pthread_cleanup_push . Синопсис #include void pthread_cleanup_push(void (*routine)(void *),void *arg); void pthread cleanup pop(int execute); Параметр routine представляет собой указатель на функцию, помещаемый в стек завершающих процедур. Параметр arg содержит аргумент, передаваемый этой routine -функции, которая вызывается при завершении потока с помощью функции pthread_exit , когда поток «покоряется» запросу на аннулирование или явным образом вызывает функцию pthread__cleanup_pop с ненулевым значением параметра execute. Функция, заданная параметром routine, не возвращает никакого значения. Функция pthread_cleanup_pop удаляет указатель routine -функции из вершины стека завершающих процедур вызывающего потока. Параметр execute может принимать значение 1 или 0. Если его значение равно 1, поток выполняет routine- функцию, даже если он при этом и не завершается. Поток продолжает свое выполнение с инструкции, расположенной за вызовом функции pthread_cleanup_pop. Если значение параметра execute равно 0, указатель извлекается из вершины стека потока без выполнения routine -функции. Необходимо позаботиться о том, чтобы для каждой функции занесения в стек (push) существовала функция извлечения из стека (pop) в пределах одной и той же лексической области видимости. Например, для функции funcA обязательно выполнение cleanup -обработчика при ее нормальном завершении или аннулировании: void *funcA(void *X) { int *Tid; Tid = new int; // do some work //... pthread_cleanup_push(cleanup_funcA,Tid); // do some more work //... pthread_cleanup_pop(0); } Здесь функция funcA( ) помещает указатель на обработчик cleanup_funcA( ) в стек завершающих процедур путем вызова функции pthread_cleanup_push . Каждому обращению к этой функции должно соответствовать обращение к функции pthread_cleanup_pop. Если функции извлечения указателя из стека (pop- функции) передается значение 0, то извлечение из стека состоится, но без выполнения обработчика. Обработчик будет выполнен лишь при аннулировании потока, выполняющего функцию funcA. Для функции funcB также требуется cleanup -обработчик: void *funcB(void *X) { int *Tid; Tid = new int; // do some work //... pthread_cleanup_push(cleanup_funcB,Tid); // do some more work //... pthread_cleanup_pop(1); } Здесь функция funcB помещает указатель на обработчик cleanup_funcB в стек завершающих процедур. Отличие этого примера от предыдущего состоит в том, что функции pthread_cleanup_pop передается параметр со значением 1, т.е. после извлечения из стека указателя на обработчик этот обработчик будет тут же выполнен. Необходимо отметить, что выполнение обработчика в данном случае состоится «при любой погоде», т.е. и при аннулировании потока, который обеспечивает выполнение функции funcB( ), и при обычном его завершении. Обработчики- «уборщики», cleanup_funcA и cleanup_funcB , — это обычные функции, которые можно использовать для закрытия файлов, освобождения ресурсов, разблокирования мьютексов и пр.
Управление стеком потока
Адресное пространство процесса делится на раздел кода, раздел статических данных, свободную память и раздел стеков. Стекам потоков выделяется область из стекового раздела процесса. Стек потока предназначен для хранения Предположим, что поток А (см. рис. 4.12) выполняет функцию task1 , которая создает некоторые локальные переменные, выполняет заданную обработку, а затем вызывает функцию taskX . При этом для функции task1 создается стековый фрейм, который помещается в стек потока. Функция taskX выполняет «свои» действия, создает локальные переменные, а затем вызывает функцию taskC . Нетрудно догадаться, что стековый фрейм, созданный для функции taskX , также помещается в стек. Функция taskC вызывает функцию taskY и т.д. Каждый стек должен иметь достаточно большой размер, чтобы поместить всю информацию, необходимую для выполнения всех функций потока, а также цепочки других подпрограмм, которые будут вызваны потоковыми функциями. Размером и местоположением стека потока управляет операционная система, но для установки и считывания этой информации предусмотрены методы, которые определены в объекте атрибутов потока. Рис. 4.12. Стековые фреймы, сгенерированные потоками Функция pthread_attr_getstacksize( ) возвращает минимальный размер стека, устанавливаемый по умолчанию. Параметр attr определяет объект атрибутов потока, из которого считывается стандартный размер стека. При успешном выполнении функция возвращает значение 0, а стандартный размер стека, выраженный в байтах, coxpaняется в параметре stacksize. В случае неудачи функция возвращает код ошибки. Функция pthread_attr_setstacksize устанавливает минимальный размер стека. Параметр attr определяет объект атрибутов потока, для которого устанавливается размер стека. Параметр stacksize содержит минимальный размер стека, выраженный в байтах. При успешном выполнении функция возвращает значение 0 , в противном случае - код ошибки. Функция завершается неудачно, если значение параметра stacksize оказывается меньше значения PTHREAD_MIN_STACK или превышает системный минимум. Вероятно, значение PTHREAD_STACK_MIN будет меньше минимального размера стека, возвращаемого функцией p thread_attr_getstacksize. Прежде чем увеличивать минимальный размер стека потока, следует поинтересоваться значением, возвращаемым функцией p thread_attr_getstacksize р азмер ст ека фиксируется, чтобы его расширение во время выполнения программы ограничивалось рамками фиксированного пространства стека, установленного во время компиляции. Синопсис #include void pthread_attr_getstacksize( const pthread_attr_t *restrict attr, void **restrict stacksize); void pthread_attr_setstacksize(pthread_attr_t *attr, void *stacksize); Местоположение стека потока можно установить и прочитать с помощью функций pthread_attr_setstackaddr и pthread_attr_getstackaddr. Функция pthread_attr_setstackaddr для потока, создаваемого с помощью атрибутного объекта, заданного параметром attr, устанавливает базовый адрес стека равным адресу, заданному параметром stackaddr. Этот адрес stackaddr должен находиться в пределах виртуального адресного пространства процесса. Размер стека должен быть не меньше минимального размера стека, задаваемого значением PTHREAD_STACK_MIN. При успешном выполнении функция возвращает значение 0 , в противном случае — код ошибки. Функция pthread_attr_getstackaddr считывает базовый адрес стека для потока, создаваемого с помощью атрибутного объекта потока, заданного параметром attr. Считанный адрес сохраняется в параметре stackaddr. При успешном выполнении функция возвращает значение 0 , в противном случае — код ошибки. Синопсис #include void pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr); void pthread_attr_getstackaddr(const pthread_attr_t *restrict attr, void **restrict stackaddr); Атрибуты стека (размер и местоположение) можно установить с помощью одной функции. Функция pthread_attr_setstack устанавливает как размер, так и адрес стека для потока, создаваемого с помощью атрибутного объекта, заданного параметром attr. Базовый адрес стека устанавливается равным адресу, заданному параметром stackaddr, а размер стека — равным значению параметра stacksize. Функция pthread_attr_getstack считывает размер и адрес стека для потока, создаваемого с помощью атрибутного объекта, заданного параметром attr. При успешном выполнении функция возвращает значение 0 , в противном случае — код ошибки. Если считывание атрибутов стека прошло успешно, адрес будет записан в параметр stackaddr, а размер— в параметр stacksize. Функция pthread_setstack выполнится неудачно, если значение параметра stacksize окажется меньше значения PTHREAD_STACK_MIN или превысит некоторый предел, определяемый реализацией. Синопсис #include void pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); void pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t stacksize); В листинге 4.3 показан пример установки размера стека для потока, создаваемого с помощью атрибутного объекта. // Листинг 4.3. Изменение размера стека для потока pthread_attr_getstacksize(&SchedAttr, &DefaultSize) ; if(DefaultSize < Min_Stack_Req){ SizeOffset = Min_Stack_Req - DefaultSize; NewSize = DefaultSize + SizeOffset; pthread_attr_setstacksize(&Attrl, (size_t)NewSize); } В листинге 4.3 сначала из атрибутного объекта потока считывается размер стека, действующий по умолчанию. Затем, если окажется, что этот размер меньше желаемого минимального размера стека, вычисляется разность между сравниваемыми размерами, после чего значение этой разности (смещение) суммируется с размером стека, используемым по умолчанию. Результат суммирования становится новым минимальным размером стека для этого потока. ПРИМЕЧАНИЕ: установка размера и местоположения стека может сделать вашу программу непереносимой. Размер и местоположение стека, устанавливаемые на одной платформе, могут оказаться неподходящими для использования в качестве размера и местоположения стека для другой платформы.
Установка атрибутов планирования и свойств потоков
Подобно процессам, потоки выполняются независимо один от другого. Каждый поток назначается процессору для выполнения его задачи. Для каждого потока определяется стратегия планирования и приоритет, которые предписывают, как и когда именно он будет назначен процессору. Стратегия планирования и приоритет потока (или группы потоков) у станав ливаются с помощью объекта атрибутов и следующих функций: pthread_attr_setinheritsched pthread_attr_setschedpolicy pthread_attr_setschedparam Для получения информации о характере выполнения потока используются следующие функции: pthread_attr_getinheritsched pthread_attr_getschedpolicy pthread_attr_getschedparam Синопсис #include #include void pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched); void pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); void pthread_attr_setschedparam(pthread_attr_t *restrict attr, const struct sched_param *restrict param); Функции pthread_attr_setinheritsched, pthread_attr_setschedpolicy и pthread_attr_setschedparam используются для установки стратегии планирования и приоритета потока. Функция pthread_attr_setinheritsched позволяет определить, как будут устанавливаться атрибуты планирования потока: путем наследования от потока-создателя или в соответствии с содержимым объекта атрибутов. Параметр inheritsched может принимать одно из следующих значений. PTHREAD_INHERIT_SCHED Атрибуты планирования потока должны быть унаследованы от потока-создателя, при этом любые атрибуты планирования, определяемые параметром attr, будут игнорироваться. PTHREAD_EXPLICIT_SCHED Атрибуты планирования потока должны быть установлены в соответствии с атрибутами планирования, хранимыми в объекте, заданном параметром attr. Если параметр inheritsched получает значение PTHREAD_EXPLICIT_SCHED, то функция pthread_attr_setschedpolicy используется для установки стратегии планирования, а функция pthread_attr_setschedparam — установки приоритета. Функция pthread_attr_setschedpolicy устанавливает член объекта атрибутов потока (заданного параметром attr), «отвечающий» за стратегию планирования потока. Параметр policy может принимать одно из следующих значений, определенных в заголовке SCHED_FIFO Стратегия планирования типа FIFO (первым прибыл, первым обслужен), при которой поток выполняется до конца. SCHED_RR Стратегия циклического планирования, при которой каждый поток назначается процессору только в течение некоторого кванта времени- SCHED_OTHER Стратегия планирования другого типа (определяемая реализацией)-Для любого нового потока эта стратегия планирования принимается по умолчанию. Функ ция pthread_attr_setschedparam используется для установки членов атрибутного объекта (заданного параметром attr), связанных со стратегией планирования Параметр param представляет собой структуру, которая содержит эти члены. Структура sched_param включает по крайней мере такой член данных: struct sched_param { int sched_priority; //... }; Возможно, эта структура содержит и другие члены данных, а также ряд функций, предназначенных для установки и считывания минимального и максимального значений приоритета, атрибутов планировщика и пр. Но если для задания стратегии планирования используется либо значение SCHED_FIFO, либо значение SCHED_RR, то в структуре sched_param достаточно определить только член sched_priority. Чтобы получить минимальное и максимальное значения приоритета, используйте функции sched_get_priority_min и sched_get_priority_max . Синопсис #include int sched_get_priority_max(int policy); int sched_get_priority_min(int policy); Обеим функциям в качестве параметра policy передается значение, определяющее выбранную стратегию планирования, для которой нужно установить значения приоритета, и обе функции возвращают соответствующее значение приоритета (минимальное и максимальное) для заданной стратегии планирования. Как установить стратегию планирования и приоритет потока с помощью атрибутного объекта, показано в листинге 4.4. // Листинг 4.4. Использование атрибутного объекта потока #define Min_Stack_Req 3000000 pthread_t ThreadA; pthread_attr_t SchedAttr; size_t DefaultSize,SizeOffset,NewSize; int MinPriority,MaxPriority,MidPriority; sched_param SchedParam; int main(int argc, char *argv[]) { //... // initialize attribute object pthread_attr_init(&SchedAttr); // retrieve min and max priority values for scheduling policy MinPriority = sched_get_priority_max(SCHED_RR); MaxPriority = sched_get_priority_min(SCHED_RR); // calculate priority value MidPriority = (MaxPriority + MinPriority)/2; // assign priority value to sched_param structure SchedParam.sched_priority = MidPriority; // set attribute object with scheduling parameter pthread_attr_setschedparam(&Attr1,&SchedParam); // set scheduling attributes to be determined by attribute object pthread_attr_setinheritsched(&Attr1,PTHREAD_EXPLICIT_SCHED); // set scheduling policy pthread_attr_setschedpolicy(&Attr1,SCHED_RR); // create thread with scheduling attribute object pthread_create(&ThreadA,&Attr1,task2,Value); } В листинге 4.4 стратегия планирования и приоритет потока ThreadA устанавливаются с использованием атрибутного объекта SchedAttr. Выполним следующие действия. 1. Инициализируем атрибутный объект. 2. Считаем минимальное и максимальное значения приоритета для стратегии планирования. 3. Вычислим значение приоритета. 4. Запишем значение приоритета в структуру sched_param. 5. Установим атрибутный объект. 6. Обеспечим установку атрибутов планирования с помощью атрибутного объекта. 7. Установим стратегию планирования. 8. Создадим поток с помощью атрибутного объекта. Последовательное выполнение этих действий позволяет установить стратегию планирования и приоритет потока до его создания. Для динамического изменения стратегии планирования и приоритета используйте функции pthread_setschedparam и pthread_setschedprio. Синопсис #include int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); int pthread_getschedparam( pthread_t thread, int *restrict policy, struct sched_param *restrict param); int pthread_setschedprio(pthread_t thread, int prio); Функция pthread_setschedparam устанавливает как стратегию планирования, так и приоритет потока без использования атрибутного объекта. Параметр Таблица4.7. Условия потенциального неудачного завершения функций установки стратегии планирования и приоритета pthread_getschedparam • Параметр thread не ссылается на существующий поток pthread_setschedparam • Некорректен параметр policy или один из членов структуры, на которую указывает параметр param • Параметр policy или один из членов структуры, на которую указывает параметр param, содержит значение, которое не поддерживается в данной среде • Вызывающий поток не имеет соответствующего разрешения на установку значений приоритета или стратегии планирования для заданного потока • Параметр thread не ссылается на существующий поток • Данная реализация не позволяет приложению заменить один из параметров планирования заданным значением pthread_setschedprio • Параметр prio не подходит к стратегии планирования заданного потока • Параметр prio имеет значение, которое не поддерживается в данной среде • Вызывающий поток не имеет соответствующего разрешения на установку приоритета для заданного потока • Параметр thread не ссылается на существующий поток • Данная реализация не позволяет приложению заменить значение приоритета заданным Функция pthread_setschedprio используется для установки значения приоритета выполняемого потока, идентификатор которого задан параметром thread В результате выполнения этой функции текущее значение приоритета будет заменено значением параметра prio. При успешном выполнении функция возвращает число 0 в противном случае — код ошибки. При неуспешном выполнении функции приоритет потока изменен не будет. Условия, при которых эта функция может завершиться неуспешно, также перечислены в табл. 4.7. ПРИМЕЧАНИЕ: к изменению стратегии планирования или приоритета выполняемого потока необходимо отнестись очень осторожно. Это может непредсказуемым образом повлиять на общую эффективность приложения. Потоки с более высоким приоритетом будут вытеснять потоки с более низким, что приведет к зависанию либо к тому, что поток будет постоянно выгружаться с процессора и поэтому не сможет завершить выполнение.
Установка области конкуренции потока
Область конкуренции потока определяет, какое множество потоков с одинаковыми стратегиями планирования и приоритетами будут состязаться за использование процессора. Область конкуренции потока устанавливается его атрибутным объектом. Синопсис #include int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope); int pthread_attr_getscope( const pthread_attr_t *restrict_attr, int *restrict contentionscope) ; Функция pthread_attr_setscope устанавливает член объекта атрибутов потока (заданного параметром attr), связанный с областью конкуренции. Область конкуренции потока будет установлена равной значению параметра contentionscope, который может принимать следующие значения. PTHREAD_SCOPE_SYSTEM Область конкуренции системного уровня PTHREAD_SCOPE_PROCESS Область конкуренции уровня процесса Функция pthread_attr_getscope возвращает атрибут области конкуренции из объекта атрибутов потока, заданного параметром attr. При успешном выполнении значение области конкуренции сохраняется в параметре contentionscope. Обе функции при успешном выполнении возвращают число 0 , в противном случае — код ошибки-
Использование функции sysconf
Знание пределов, устанавливаемых системой на использование ресурсов, позволит вашему приложению эффективно управлять ресурсами. Например, максимальное количество потоков, приходящихся на один процесс, составляет верхнюю границу числа рабочих потоков, которое может быть создано процессом. Функция sysconf используется для получения текущего значения конфигурируемых системных пределов или опций Синопсис #include #include Параметр name - это запрашиваемая системная переменная. Функция возвращает значения, соответствующие стандарту POSIX IEEE Std. 1003.1-2001 для заданных системных переменных. Эти значения можно сравнить с константами, определенными вашей реализацией стандарта, чтобы узнать, насколько они согласуются между собой. Для ряда системных переменных существуют константы-аналоги, относящиеся к потокам, процессам и семафорам (см. табл. 4.8). Если параметр name не действителен, функция sysconf возвращает число -1 и устанавливает переменную errno, свидетельствующую об ошибке. Однако для заданного параметра name предел может быть не определен, и функция может возвращать число -1 как действительное значение. В этом случае переменная errno не устанавливается. Необходимо отметить, что неопределенный предел не означает безграничность ресурса Это просто означает, что не определен максимальный предел и (при условии доступности системных ресурсов) могут поддерживаться более высокие предельные значения. Рассмотрим пример вызова функции sysconf : if(PTHREAD_STACK_MIN == (sysconf(_SC_THREAD_STACK_MIN))){ //... } Значение константы PTHREAD_STACK_MIN сравнивается со значением, возвращаемым функцией sysconf , вызванной с параметром _SC_THREAD_STACK_MIN. Таблица4 .8. Системные переменные и соответствующие им символьные константы _SC_THREADS _POSIX_THREADS Поддерживает потоки _SC_THREAD_ATTR_ STACKADDR _POSIX_THREAD_ATTR_ STACKADDR Поддерживает атрибут адреса стека потока _SC_THREAD_ATTR_ STACKSIZE _POSIX_THREAD_ATTR_ STACKSIZE Поддерживает атрибут размера стека потока _SC_THREAD_STACK_ MIN PTHREAD_STACK_MIN Минимальный размер стека потока в байтах _SC_THREAD_THREADS_MAX PTHREAD_THREADS MAX Максимальное количество потоков на процесс _SC_THREAD_KEYS_MAX PTHREAD_KEYS_MAX Максимальное количество ключей на процесс _SC_THREAD_PRIO_INHERIT _POSIX_THREAD_PRIO_ INHERIT Поддерживает опцию наследования приоритета _SC_THREAD_PRIO _POSIX THREAD_PRIO Поддерживает опцию приоритета потока _SC_THREAD_PRIORITY_ SCHEDULING _POSIX_THREAD_PRIORITY_SCHEDULING Поддерживает опцию планирования приоритета потока _SC_THREAD_PROCESS_SHARED _POSIX_THREAD_PROCESS_SHARED Поддерживает синхронизацию на уровне процесса _SC_THREAD_SAFE_ FUNCTIONS _POSIX_THREAD_SAFE_FUNCTIONS Поддерживает функции безопасности потока _SC_THREAD_ DESTRUCTOR_ ITERATIONS _PTHREAD_THREAD_DESTRUCTOR_ITERATIONS Определяет количество попыток, направленных на разрушение потоковых данных при завершении потока _SC_CHILD_MAX CHILD_MAX Максимальное количество процессов, разрешенных для UID _SC_PRIORITY_ SCHEDULING _POSIX_PRIORITY_ SCHEDULING Поддерживает планирование процессов _SC_REALTIME_ SIGNALS _POSIX_REALTIME_SIGNALS Поддерживает сигналы реального времени _SC_XOPEN_REALTIME_THREADS _XOPEN_REALTIME_ THREADS Поддерживает группу потоковых средств реального времени X/Open POSIX _SC_STREAM_MAX STREAM_MAX Определяет количество потоков данных, которые один процесс может открыть одновременно _SC_SEMAPHORES _POSIX_SEMAPHORES Поддерживает семафоры _SC_SEM_NSEMS_MAX SEM_NSEMS_MAX Определяет максимальное количество семафоров, которое может иметь процесс _SC_SEM_VALUE_MAX SEM_VALUE_MAX Определяет максимальное значение, которое может иметь семафор _SC_SHARED_MEMORY_ OBJECTS _POSIX_SHARED_MEMORY_OBJECTS Поддерживает объекты общей памяти
Управление критическими разделами
Параллельно выполняемые процессы (или потоки в одном процессе) могут совместно использовать структуры данных, переменные или отдельные данные. Разделение глобальной памяти позволяет процессам или потокам взаимодействовать друг с другом и получать доступ к общим данным. При использовании нескольких процессов разделяемая глобальная память является внешней по отношению к ним. Внешнюю структуру данных можно использовать для передачи данных или команд между процессами. Если же необходимо организовать взаимодействие потоков, то они могут иметь доступ к структурам данных или переменным, являющимся частью одного и того же процесса, которому они принадлежат. Если существуют процессы или потоки, которые получают доступ к разделяемым модифицируемым данным, структурам данных или переменным, то все эти данные находятся в критической области (или разделе) кода процессов или потоков. Критический раздел кода — это та его часть, в которой обеспечивается доступ потока или процесса к разделяемому блоку модифицируемой памяти и обработка соответствующих данных. Отнесение раздела кода к критическому можно использовать для управления состоянием «гонок». Например, создаваемые в программе два потока, поток А и поток В, используются для поиска нескольких ключевых слов во всех файлах системы. Поток А просматривает текстовые файлы в каждом каталоге и записывает нужные пути в списочную структуру данных TextFiles, а затем инкрементирует переменную FileCount. Поток В выделяет имена файлов из списка TextFiles, декрементирует переменную FileCount, после чего просматривает файл на предмет поиска в нем заданных ключевых слов. Файл, который их содержит, переписывается в другой файл, и инкрементируется еще одна переменная FoundCount. К переменной FoundCount поток А доступа не имеет. Потоки А и В могут выполняться одновременно на отдельных процессорах. Поток А выполняется до тех пор, пока не будут просмотрены все каталоги, в то время как поток В просматривает каждый файл, путь к которому выделен из переменной TextFiles. Упомянутый список поддерживается в отсортированном порядке, и в любой момент его содержимое можно отобразить на экране. Здесь возможна масса проблем. Например, поток В может попытаться выделить имя файла из списка TextFiles до того, как поток А его туда поместит. Поток В может попытаться декрементировать переменную SearchCount до того, как поток А её инкрементирует, или же оба потока могут попытаться модифицировать эту переменную одновременно. Кроме того, во время сортировки элементов списка TextFiles поток А может попытаться записать в него имя файла, или поток В будет в это время пытаться выделить из него имя файла для выполнения своей задачи. Описанные проблемы—это примеры условий «гонок», при которых несколько потоков (или процессов) пытаются одновременно модифицировать один и тот же блок общей памяти. Если потоки или процессы одновременно лишь читают один и тот же блок памяти, условия «гонок» не возникают. Они возникают в случае, когда несколько процессов или потоков одновременно получают доступ к одному и тому же блоку памяти, и по крайней мере один из этих процессов или потоков делает попытку модифицировать данные. Раздел кода становится критическим, когда он делает возможными одновременные попытки изменить один и тот же блок памяти. Один из способов защитить к ритический раздел — разрешить только монопольный доступ к блоку памяти. Для управления условиями «гонок» можно использовать такой механизм блокировки, как взаимо - исключающий семафор , или Блокирование мьютекса // Вход в критический раздел. // Доступ к разделяемой модифицируемой памяти. // Выход из критического раздела. Деблокирование мьютекса Класс pthread_mutex_t позволяет смоделировать мьютексный объект. Прежде, чем объект типа pthread_mutex_t можно будет использовать, его необходимо инициализировать. Для инициализации мьютекса используется функция pthread_mutex_init. Инициализированный мьютекс можно заблокировать деблокировать и разрушить с помощью функций pthread_mutex_lock, pthread_mutex_unlock и pthread_mutex_destroy соответственно. В программе 4.5 содержится функция, которая выполняет поиск текстовых файлов, а в программе 4.6 — функция, которая просматривает каждый текстовый файл на предмет содержания в нем заданных ключевых слов. Каждая функция выполняется потоком. Основной поток реализован в программе 4.7. Эти программы реализуют модель «изготовитель-потребитель» для делегирования задач потокам. Программа4.5 содержит поток-«изготовитель», а программа 4.6 — поток-«потребитель». Критические разделы выделены в них полужирным шрифтом. // Программа 4.5 1 int isDirectory(string FileName) 2 { 3 struct stat StatBuffer; 4 5 lstat(FileName.c_str,&StatBuffer); 6 if((StatBuffer.st_mode & S_IFDIR) == -1) 7 { 8 cout << «could not get stats on file» << endl; 9 return(0); 10 } 11 else{ 12 if(StatBuffer.st_mode & S_IFDIR){ 13 return(1); 14 } 15 } 16 return(0); 17 } 18 19 20 int isRegular(string FileName) 21 { 22 struct stat StatBuffer; 23 24 lstat(FileName.c_str,&StatBuffer); 25 if((StatBuffer.st_mode & S_IFDIR) == -1) 26 { 27 cout << «could not get stats on file» << endl; 28 return(0); 29 } 30 else{ 31 if(StatBuffer.st_mode & S_IFREG){ 32 return(1); 33 } 34 } 35 return(0); 36 } 37 38 39 void depthFirstTraversal(const char *CurrentDir) 40 { 41 DIR *DirP; 42 string Temp; 43 string FileName; 44 struct dirent *EntryP; 45 chdir(CurrentDir); 46 cout << «Searching Directory: " << CurrentDir << endl; 47 DirP = opendir(CurrentDir); 48 49 if(DirP == NULL){ 50 cout << «could not open file» << endl; 51 return; 52 } 53 EntryP = readdir(DirP); 54 while(EntryP != NULL) 55 { 56 Temp.erase; 57 FileName.erase; 58 Temp = EntryP->d_name; 59 if((Temp != ".») && (Temp != "..»)){ 60 FileName.assign(CurrentDir); 61 FileName.append(1,'/'); 62 FileName.append(EntryP->d_name); 63 if(isDirectory(FileName)){ 64 string NewDirectory; 65 NewDirectory = FileName; 66 depthFirstTraversal(NewDirectory.c_str); 67 } 68 else{ 69 if(isRegular(FileName)){ 70 int Flag; 71 Flag = FileName.find(".cpp»); 72 if(Flag > 0){ 73 pthread_mutex_lock(&CountMutex); 74 FileCount++; 75 pthread_mutex_unlock(&CountMutex); 76 pthread_mutex_lock(&QueueMutex); 77 TextFiles.push(FileName); 78 pthread_mutex_unlock(&QueueMutex); 79 } 80 } 81 } 82 83 } 84 EntryP = readdir(DirP); 85 } 86 closedir(DirP); 87 } 88 89 90 91 void *task(void *X) 92 { 93 char *Directory; 94 Directory = static_cast 95 depthFirstTraversal(Directory); 96 return(NULL); 97 98 } Программа 4.6 содержит поток-«потребитель», который выполняет ных ключевых слов. // Программа 4.6 1 void *keySearch(void *X) 2 { 3 string Temp, Filename; 4 less 5 6 while(!Keyfile.eof && Keyfile.good) 7 { 8 Keyfile >> Temp; 9 if(!Keyfile.eof){ 10 KeyWords.insert(Temp); 11 } 12 } 13 Keyfile.close; 14 15 while(TextFiles.empty) 16 { } 17 18 while(!TextFiles.empty) 19 { 20 pthread_mutex_lock(&QueueMutex); 21 Filename = TextFiles.front; 22 TextFiles.pop; 23 pthread_mutex_unlock(&QueueMutex); 24 Infile.open(Filename.c_str); 25 SearchWords.erase(SearchWords.begin,SearchWords.end); 26 27 while(!Infile.eof && Infile.good) 28 { 29 Infile >> Temp; 30 SearchWords.insert(Temp); 31 } 32 33 Infile.close; 34 if(includes(SearchWords.begin,SearchWords.end, KeyWords.begin,KeyWords.end,Comp)){ 35 Outfile << Filename << endl; 36 pthread_mutex_lock(&CountMutex); 37 FileCount--; 38 pthread_mutex_unlock(&CountMutex); 39 FoundCount++; 40 } 41 } 42 return(NULL); 43 44 } Программа 4.7 содержит основной поток для потоков модели «изготовитель-потребитель», реализованных в программах 4.5 и 4.6. // Программа 4.7 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 9 pthread_mutex_t QueueMutex = PTHREAD_MUTEX_INITIALIZER; 10 pthread_mutex_t CountMutex = PTHREAD_MUTEX_INITIALIZER; 11 12 int FileCount = 0; 13 int FoundCount = 0; 14 15 int keySearch(void); 16 queue 17 set 18 set 19 ifstream Infile; 20 ofstream Outfile; 21 ifstream Keyfile; 22 string KeywordFile; 23 string OutFilename; 24 pthread_t Thread1; 25 pthread_t Thread2; 26 27 void depthFirstTraversal(const char *CurrentDir); 28 int isDirectory(string FileName); 29 int isRegular(string FileName); 30 31 int main(int argc, char *argv[]) 32 { 33 if(argc != 4){ 34 cerr << «need more info» << endl; 35 exit (1); 36 } 37 38 Outfile.open(argv[3],ios::app||ios::ate); 39 Keyfile.open(argv[2]); 40 pthread_create(&Thread1,NULL,task,argv[1]); 41 pthread_create(&Thread2,NULL,keySearch,argv[1]); 42 pthread_join(Thread1,NULL); 43 pthread_join(Thread2,NULL); 44 pthread_mutex_destroy(&CountMutex); 45 pthread_mutex_destroy(&QueueMutex); 46 47 cout << argv[1] << " contains " << FoundCount << " files that contains all keywords.» << endl; 48 return(0); 49 } С помощью мьютексов доступ к разделяемой памяти для чтения или записи данных разрешается получить только одному потоку. Для гарантии безопасности работы функций, определенных пользователем, можно использовать и другие механизмы и методы, которые реализуют одну из моделей PRAM: • EREW (монопольное чтение и монопольная запись) • CREW (параллельное чтение и монопольная запись) • ERCW (монопольное чтение и параллельная запись) • CRCW (параллельное чтение и параллельная запись) Мьютексы используются для реализации EREW-алгоритмов, которые рассматриваются в главе 5.
Безопасность использования потоков и библиотек
Климан (Klieman), Шах (Shah) и Смаалдерс (Smaalders) утверждали: «Функция или набор функций могут сделать поток безопасным или реентерабельным (повторно-входимым), если эти функции могут вызываться не одним, а несколькими потоками без предъявления каких бы то ни было требований к вызывающей части выполнить определенные действия»(1996) При разработке многопоточного приложения программист должен обеспечить безопасность параллельно выполняемых функций. Мы уже обсуждали безопасность функций, определенных пользователем, но без учета того, что приложение часто вызывает функции из системных библиотек или библиотек, созданных сторонними производителями. Одни такие функции и/или библиотеки безопасны для потоков, а другие — нет. Если функция небезопасна, это означает, что в ней используется хотя бы одна статическая переменная, осуществляется доступ к глобальным данным и/или она не является реентерабельной. Известно, что статические переменные поддерживают свои значения между вызовами функции. Если некоторая функция содержит статические переменные, то для ее корректного функционирования требуется считывать (и/или изменять) их значения. Если же к такой функции будут обращаться несколько параллельно выполняемых потоков, возникнут условия «гонок». Если функция модифицирует глобальную переменную, то каждый из нескольких потоков, вызывающих функцию, может попытаться модифицировать эту глобальную переменную. Возникновения условий «гонок» также не миновать, если не синхронизировать множество параллельных доступов к глобальной переменной. Например, несколько параллельных потоков могут выполнять функции, которые устанавливают переменную errno. Для некоторых потоков, предположим, эта функция не может выполниться успешно, и переменная errno устанавливается равной сообщению об ошибке [10], в то время как другие потоки выполняются успешно. Если реализация компилятора не обеспечивает потоковую безопасность поддержки переменной errno, то какое сообщение получит поток при проверке состояния переменной errno? Блок кода считается getgrgid_r getgrnam_r getpwuid_r sterror_r strtok_r readdir_r rand_r ttyname_r Если функция получает доступ к незащищенным глобальным переменным, содержит статические модифицируемые переменные или нереентерабельна, то такая функция считается небезопасной для потока. Системные библиотеки или библиотеки созданные сторонними производителями, могут иметь различные версии своих стандартных библиотек. Одна версия предназначена для однопоточных приложений, а другая — для многопоточных. Если предполагается разрабатывать многопоточное приложение, программист должен использовать многопоточные версии нужной ему библиотеки. Некоторые среды требуют не компоновки многопоточных приложений с многопоточной версией библиотеки, а лишь определения макросов, что позволяет объявить реентерабельные версии функций. Такое приложение будет затем компилироваться как безопасное для выполнения потоков. Во всех ситуациях использовать многопоточные версии функций попросту невозможно. В отдельных случаях многопоточные версии конкретных функций недоступны для данного компилятора или среды. Иногда один интерфейс функции не в состоянии сделать ее безопасной. Кроме того, программист может столкнуться с увеличением числа потоков в среде, которая изначально использовала функции, предназначенные для функционирования в однопоточной среде. В таких условиях обычно используются мьютексы. Например, программа имеет три параллельно выполняемых потока. Два из них, thread1 и thread2, параллельно выполняют функцию funcA, которая не является безопасной для одновременной работы потоков. Третий поток, thread3, выполняет функцию funcB . Для решения проблемы, связанной с функцией funcA , возможно, достаточно заключить в защитную оболочку мьютекса доступ к ней со стороны потоков threadl и thread2: thread1 thread2 thread3 { { { lock lock funcB funcA funcA } unlock unlock } } При реализации таких защитных мер к функции funcA в любой момент времени может получить доступ только один поток. Но проблемы на этом не исчерпываются. Если обе функции funcA и funcB небезопасны для выполнения потоками, они могут обе модифицировать глобальные или статические переменные. И хотя потоки thread1 и thread2 используют мьютексы для функции funcA , поток thread3 может выполнять функцию funcB одновременно с любым из остальных потоков. В такой ситуации вполне вероятно возникновение условий «гонок», поскольку функции funcA и funcB могут модифицировать одну и ту же глобальную или статическую переменную. Проиллюстрируем еще один тип условий «гонок», возникающих при использовании библиотеки Если неизвестно, какие функции из библиотеки являются безопасными, а какие -нет, программист может воспользоваться одним из следующих вариантов действий. • Ограничить использование всех опасных функций одним потоком. • Не использовать безопасные функции вообще. • Собрать все потенциально опасные функции в один набор механизмов синхронизации. Еще один вариант — создать интерфейсные классы для всех опасных функций, которые должны использоваться в многопоточном приложении, т.е. опасные функции инкапсулируются в одном интерфейсном классе. Такой интерфейсный класс может быть скомбинирован с соответствующими объектами синхронизации с помощью наследования или композиции и использован специализированным классом. Такой подход устраняет возможность возникновения условий «гонок».
Разбиение программы на несколько потоков
Выше в этой главе мы рассматривали делегирование работы в соответствии с конкретной стратегией или потоковой моделью. Итак, используются следующие распространенные модели: • делегирование («управляющий-рабочий»"); • сеть с равноправными узлами; • конвейер; • «изготовитель-потребитель». Каждая модель характеризуется собственной
Использование модели делегирования
Мы рассмотрели два подхода к реализации модели делегирования при разделении мы на потоки. Вспомним: в // Листинг 4.5. Подход 1: скелет программы реализации //... pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER int AvailableThreads pthread_t Thread[Max_Threads] void decrementThreadAvailability(void) void incrementThreadAvailability(void) int threadAvailability(void); // boss thread { //... if(sysconf(_SC_THREAD_THREADS_MAX) > 0){ AvailableThreads = sysconf(_SC_THREAD_THREADS_MAX) } else{ AvailableThreads = Default } int Count = 1; loop while(Request Queue is not empty) if(threadAvailability){ Count++ decrementThreadAvailability classify request switch(request type) { case X : pthread_create(&(Thread[Count])...taskX...) case Y : pthread_create(&(Thread[Count])...taskY...) case Z : pthread_create(&(Thread[Count])...taskZ...) //... } } else{ //free up thread resources } end loop } void *taskX(void *X) { // process X type request incrementThreadAvailability return(NULL) } void *taskY(void *Y) { // process Y type request incrementThreadAvailability return(NULL) } void *taskZ(void *Z) { // process Z type request decrementThreadAvailability return(NULL) } В листинге 4.5 управляющий поток динамически создает поток для обработки каждого нового запроса, который поступает в систему. Однако существует ограничение на количество потоков (максимальное число потоков), которое можно создать в процессе. Для обработки threadAvailability incrementThreadAvailability decrementThreadAvailability В листинге 4.6 содержится псевдокод реализации этих функций. // Листинг 4.6. Функции, которые управляют возможностью // создания потоков void incrementThreadAvailability(void) { //... pthread_mutex_lock(&Mutex) AvailableThreads++ pthread_mutex_unlock(&Mutex) } void decrementThreadAvailability(void) { //... pthread_mutex_lock(&Mutex) AvailableThreads— pthread_mutex_unlock(&Mutex) } int threadAvailability(void) { //... pthread_mutex_lock(&Mutex) if(AvailableThreads > 1) return 1 else return 0 pthread_mutex_unlock(&Mutex) } Ф ункция threadAvailability возвратит число 1, если максимально допустимое количество потоков для процесса еще не достигнуто. Эта функция опрашивает глобальную переменную ThreadAvailability, в которой хранится число потоков, еще доступных для процесса. Управляющий поток вызывает функцию decrementThreadAvailability, которая декрементирует эту глобальную переменную до создания им рабочего потока. Каждый рабочий поток вызывает функцию incrementThreadAvailability, которая инкрементирует глобальную переменную ThreadAvailability до начала его выполнения. Обе функции содержат обращение к функции pthread_mutex_lock до получения доступа к этой глобальной переменной и обращение к функции pthread_mutex_unlock после него. Если максимально допустимое количество потоков превышено, управляющий поток может отменить создание потока, если это возможно, или породить другой процесс, если это необходимо. Функции taskX, taskY и taskZ выполняют код, предназначенный для обработки запроса соответствующего типа. Другой подход к реализации модели делегирования состоит в создании управляющим потоком пула потоков, которым (вместо создания под каждый новый запрос нового потока) переназначаются новые запросы. Управляющий поток во время инициализации создает некоторое количество потоков, а затем каждый созданный поток приостанавливается до тех пор, пока в очередь не будет добавлен новый запрос. Управляющий поток для выделения запросов из очереди по-прежнему использует цикл событий. Но вместо создания нового потока для обслуживания очередного запроса, управляющий поток уведомляет уже существующий поток о необходимости обработки запроса. Этот подход к реализации модели делегирования представлен в листинге 4.7. // Листинг 4.7. Подход 2: скелет программы реализации . модели управляющего и рабочих потоков pthread_t Thread[N] // boss thread { pthread_create(&(Thread[1]...taskX...); pthread_create(&(Thread[2]...taskY...); pthread_create(&(Thread[3]...taskZ...); //... loop while(Request Queue is not empty get request classify request switch(request type) { case X : enqueue request to XQueue signal Thread[1] case Y : enqueue request to YQueue signal Thread[2] case Z : enqueue request to ZQueue signal Thread[3] //... } end loop } void *taskX(void *X) { loop suspend until awaken by boss loop while XQueue is not empty dequeue request process request end loop until done { void *taskY(void *Y) { loop suspend until awaken by boss loop while YQueue is not empty dequeue request process request end loop until done } void *taskZ(void *Z) { loop suspend until awaken by boss loop while (ZQueue is not empty) dequeue request process request end loop until done } //.. . В листинге 4.7 управляющий поток создает N рабочих потоков (по одному для каждого типа задачи). Каждая задача связана с обработкой запросов некоторого типа В цикле событий управляющий поток извлекает запрос из очереди запросов, определяет его тип, ставит его в очередь запросов, соответствующую типу, а затем оправляет сигнал потоку, который обрабатывает запросы из этой очереди. Функции потоков также содержат циклы событий. Поток приостанавливается до тех пор, пока не получит сигнал от управляющего потока о существовании запроса в его очереди. После «пробуждения» (уже во внутреннем цикле) поток обрабатывает все запросы до тех пор, пока его очередь не опустеет.
Использование модели сети с равноправными узлами
В Листинг 4.8. Скелет программы реализации модели равноправных потоков pthread_t Thread[N] // initial thread { pthread_create(&(Thread[1]...taskX...); pthread_create(&(Thread[2]...taskY...); pthread_create(&(Thread[3]...taskZ...); //... } void *taskX(void *X) { loop while (Type XRequests are available) extract Request process request end loop return(NULL) } В модели равноправных потоков каждый поток отвечает за собственный входной поток данных. Входные данные могут быть выделены из базы данных, файла и т.п.
Использование модели конвейера
В модели конве йера поток входных данных обрабатывается поэтапно. На каждом этапе некоторая порция работы (часть входного потока данных) обрабатывается одним потоком выполнения, а затем передается для обработки следующему. Каждая порция входных данных переходит на очередной этап обработки до тех пор, пока не будет завершена вся обработка. Такой подход позволяет обрабатывать несколько входных потоков данных одновременно. Каждый поток выполнения отвечает за достижение пром ежуточного результата, делая его доступным для следующего этапа (т.е. следующего потока конвейера). Скелет программы реализации модели конвейера представлен в листинге 4.9. // Листинг 4.9. Скелет программы реализации модели конвейера //... pthread_t Thread[N] Queues[N] // initial thread { place all input into stage1's queue pthread_create(&(Thread[1]...stage1...); pthread_create(&(Thread[2]...stage2...); pthread_create(&(Thread[3]...stage3...); //... } void *stageX(void *X) { loop suspend until input unit is in queue loop while XQueue is not empty dequeue input unit process input unit enqueue input unit into next stage's queue end loop until done return(NULL) } В листинге 4.9 объявляется N очередей для N этапов. Начальный поток помещает все порции входных потоков в очередь первого этапа, а затем создает все потоки, необходимые для выполнения всех этапов. Каждый этап содержит свой цикл событий. Поток выполнения находится в состоянии ожидания до тех пор, пока в его очереди не появится порция входных данных. Внутренний цикл продолжается до опустения соответствующей очереди. Порция входных данных извлекается из очереди, обрабатывается, а затем помещается в очередь следующего этапа обработки (следующего потока выполнения).
Использование модели «изготовитель-потребитель»
В модели «изготовитель-потребитель» поток- «изготовитель» готовит данные, «потребляемые» потоком-«потребителем» (причем таких потоков-«потребителей" может быть несколько). Данные хранятся в блоке памяти, разделяемом всеми потока, как изготовителем, так и потребителями. В листинге 4.10 представлен скелет программы реализации модели «изготовитель-потребитель» (эта модель также использовалась в программах 4.5, 4.6 и 4.7). Листинг 4.10. Скелет программы реализации модели «изготовитель-потребитель» pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER pthread_t Thread[2] Queue // initial thread { pthread_create(&(Thread[1]...producer...); pthread_create(&(Thread[2]...consumer...); //... } void *producer(void *X) { loop perform work pthread_mutex_lock(&Mutex) enqueue data pthread_mutex_unlock(&Mutex) signal consumer //... until done } void *consumer(void *X) { loop suspend until signaled loop while(Data Queue not empty) pthread_mutex_lock(&Mutex) dequeue data pthread_mutex_unlock(&Mutex) perform work end loop until done } // В листинге 4.9 начальный поток создает оба потока: «изготовителя» и «потребителя». Поток- «изготовитель» содержит цикл, в котором после выполнения некоторых действий блокируется мьютекс для совместно используемой очереди, чтобы поместить в нее подготовленные для потребителя данные. После этого «изготовитель» деблокирует мьютекс и посылает сигнал потоку- «потребителю» о том, что ожидаемые им данные уже находятся в очереди. Поток- «изготовитель» выполняет итерации цикла до тех пор, пока не будет выполне
Создание многопоточных объектов
Модели делегирования, равноправных потоков, конвейера и типа «изготовитель» - «потребитель» предлагают деление программы на несколько потоков с помощью функций. При использовании объектов функции-члены могут создавать потоки выполнения нескольких задач. Потоки используются для выполнения кода от имени объекта: посредством отдельных функций и функций-членов. В любом случае потоки объявляются в рамках объекта и создаются одной из функций-членов (например, конструктором). Потоки могут затем выполнять некоторые независимые функции (функции, определенные вне объекта), которые вызывают функции-члены глобальных объектов. Это — один из способов создания многопоточного объекта. Пример многопоточного объекта представлен в листинге 4.10. // Листинг 4.11 . Объявление и определение многопоточного // объекта #include #include #include void *taskl(void *); void *task2(void *); class multithreaded_object { pthread_t Threadl, Thread2; public: multithreaded_object(void); int cl(void); int c2(void); //.. . ); multithreaded_object::multithreaded_object(void) { //. . . pthread_create(&Threadl, NULL, taskl, NULL); pthread_create(&Thread2 , NULL, task2 , NULL); pthread_join(Threadl, NULL); pthread_join(Thread2 , NULL); //. . . } int multithreaded_object::cl(void) { // Выполнение действий, return(1); } int multithreaded_object::c2(void) { // Выполнение действий, return(1); } multithreaded_object MObj; void *taskl(void *) { //... MObj.cl ; return(NULL) ; } void *task2(void *) { //... M0bj.c2; return(NULL) ; } В листинге 4.11 в классе multithread_object объявляются два потока. Они создаются и присоединяются к основному потоку в конструкторе этого класса. Поток Thread1 выполняет функцию task1 , а поток Thread2 — функцию task2 . Функции taskl и task2 затем вызывают функции-члены глобального объекта MObj.
Резюме
В последовательной программе всю нагрузку можно разделить между отдельными подпрограммами таким образом, чтобы выполнение очередной подпрограммы было возможно только после завершения предыдущей. Существует и другая организация программ, когда, например, вся работа выполняется в виде мини-программ в рамках основной программы, причем эти мини-программы выполняются параллельно основной. Такие мини-программы могут быть реализованы как процессы или потоки. Если в реализации используются процессы, то каждый процесс должен иметь собственное адресное пространство, а если процессы должны взаимодействовать между собой, то такая реализация требует обеспечения механизма межпроцессного взаимодействия. Для потоков, разделяющих адресное пространство одного процесса, не нужны специальные методы взаимодействия. Но для защиты совместно используемой памяти (чтобы не допустить возникновения условий «гонок») необходимо использоватьтакие механизмы синхронизации, как мьютексы. Существует ряд моделей, которые можно использовать для делегирования работы потокам и управления их созданием и аннулированием. В модели делегирования один поток (управляющий) создает другие потоки (рабочие) и назначает им задачи. Управляющий поток ожидает до тех пор, пока каждый рабочий поток не завершит свою задачу. При использовании модели равноправных узлов есть один поток, который изначально создает все потоки, необходимые для выполнения всех задач, причем этот поток считается рабочим потоком, поскольку он не осуществляет никакого делегирования. Все потоки в этой модели имеют одинаковый статус. Применяя модель конвейера, программу можно охарактеризовать как сборочную линию, в которой входной поток (поток входных данных) обрабатывается поэтапно. На каждом этапе поток обрабатывает некоторую порцию входных элементов. Порция входных элементов перемещается от одного потока выполнения к следующему до тех пор, пока не завершится вся предусмотренная обработка. На последнем этапе работы конвейера формируются его результаты, т.е. последний поток отвечает за формирование конечных результатов программы. В модели «изготовитель-потребитель» поток- «изготовитель» готовит данные, «потребляемые» потоком-«потребителем». Данные хранятся в блоке памяти, разделяемом всеми потока ми: как изготовителем, так и потребителями. При использовании объектов функции члены могут создавать потоки для выполнения нескольких задач. Объекты можно создавать с многопоточной направленностью. В этом случае потоки объявляются в самом объекте. Функция-член может создать поток, который выполняет независимую функцию, а она (в свою очередь) вызывает одну из функций-членов объекта. Для создания и управления потоками многопоточного приложения можно использовать библиотеку Pthread. Библиотека Pthread опирается на стандартизированный программный интерфейс, предназначенный для создания и поддержки потоков Этот интерфейс определен комитетом стандартов IEEE в стандарте POSIX 1003.1с Сторонние производители при создании своих продуктов должны придерживаться этого стандарта POSIX.
Синхронизация параллельно выполняемых задач
Во всех компьютерных системах ресурсы ограничены. Ведь любой объем памяти конечен, как и количество устройств ввода-вывода, портов, аппаратных прерываний и процессоров. Если в среде ограниченных аппаратных ресурсов приложение состоит из нескольких процессов и потоков, то эти составляющие должны конкурировать за память, периферийные устройства и процессорное время. Когда и как долго процесс или поток будет использовать системные ресурсы, определяет операционная система. При использовании приоритетного планирования операционная система может прерывать выполняющийся процесс или поток, чтобы удовлетворить все остальные процессы и потоки, соревнующиеся за системные ресурсы. Процессам и потокам приходится также соперничать за программные ресурсы и ресурсы данных. Примерами программных ресурсов служат разделяемые библиотеки (которые предоставляют в общее пользование набор процедур или функций для процессов и потоков), а также приложения, программы и утилиты. При совместном использовании программных ресурсов в памяти содержится только одна копия программного кода. Под Синхронизация также необходима для координации порядка выполнения параллельных задач. Примером может служить модель «изготовитель-потребитель», которая рассмотрена в главе 4. «Изготовитель» обязательно начинает выполняться до «потребителя», но не обязательно завершается до него. Подобные задачи нуждаются в синхронизации Синхронизация данных
Координация порядка выполнения потоков
Предположим, у нас есть три параллельно выполняющихся потока — А, В и С. Все они участвуют в обработке списка. Список необходимо отсортировать, выполнить в нем операции поиска и вывода результатов. Каждому потоку назначается отдельная задача. Так, поток А должен отобразить результаты поиска, В — отсортировать список, а С — провести поиск. Сначала список необходимо отсортировать, затем выполнить несколько параллельных операций поиска, а уж потом отобразить результаты. Если задачи, выполняемые потоками, не синхронизировать надлежащим образом, то поток А может попытаться отобразить еще не сгенерированные результаты, что нарушит Сначала поток В должен отсортировать список, затем эстафета управления передается «мно
Взаимоотношения между синхронизируемыми задачами
Существует четыре основных типа отношений синхронизации между любыми двумя потоками в одном процессе или между любыми двумя процессами в одном приложении: старт-старт (CC), финиш-старт (ФС), старт-финиш (СФ) и финиш-финиш (ФФ). С помощью этих основных типов отношений можно охарактеризовать координацию задач между потоками и процессами. UML-диаграмма видов деятельности для каждого типа отношений синхронизации показана на рис. 5.2. Рис. 5.1. Диаграмма видов деятельности для задач сортировки списка, поиска и отображения результатов поиска
Отношения типа старт-старт (CC)
В отношениях синхронизации типа старт-старт одна задача не может начаться до тех пор, пока не начнется другая. Одна задача может начаться раньше другой, нo не позже. Предположим, у нас есть программа, которая реализует инкарнацию (воплощение). Инкарнация «материализуется» в виде говорящей головы, созданной, разумеется, компьютерной программой. Инкарнация обеспечивает своего рода «одушевление» программ. Программа, которая реализует «одушевление», имеет несколько потоков. Здесь нас в первую очередь интересует поток А, который «отвечает» за анимацию результата, и поток В, который управляет звуком, или голосом, говорящей головы. Мы хотим создать иллюзию синхронизации звука и движений рта. В идеале они должны происходить абсолютно одновременно. При наличии нескольких процессоров оба потока могут начинаться одновременно. Эти потоки связаны отношением типа старт - старт. В соответствии с условиями временной синхронизации допускается, чтобы поток А начинался немного раньше потока В (именно немного — иначе будет нарушена иллюзия одновременности), но поток В не может начаться раньше потока А. Голос должен ожидать анимацию, а не наоборот. Совершенно нежелательно услышать голос до того, как зашевелятся губы (если это не синхронное озвучивание). Рис. 5.2. Возможные отношения синхронизации между задачами А и В
Отношения типа финиш-старт (ФС)
В отношениях синхронизации типа
Отношения типа старт-финиш (СФ)
Отношения типа Отношения типа старт-финиш обычно предполагают существование информационной зависимости между задачами. При информационной зависимости для корректной работы потоков или процессов необходимо обеспечить межпоточное или межпроцессное взаимодействие. Например, поток поиска данных в списке сгенерирует некорректные результаты, если не будет выполнена сортировка списка. И поток-«потребитель» не получит файлы для обработки, если поток-«производитель» не подготовит их для потребителя.
Отношения типа финиш-финиш (ФФ)
В отношениях синхронизации типа
Синхронизация доступа к данным
Существует разница между данными, раздел Именно блок памяти, разделяемый между потоками внутри одного и того же адресного пространства, и блок памяти, раздел Синхронизация данных необходима для управления состоянием «гонок», а также для того, чтобы позволить параллельным потокам или процессам безопасно получить доступ к блоку памяти. Синхронизация данных позволяет управлять считыванием и модификацией данных в блоке памяти. В многопоточной среде параллельный доступ к общей памяти, глобальным переменным и файлам обязательно должен быть синхронизирован. Что касается программного кода задачи, то синхронизация данных необходима в тех его блоках, где делается попытка получить доступ к блоку памяти, глобальным переменным или файлам, разделяемым с другими параллельно выполняемыми процессами или потоками. Такие блоки кода называются
Модель РРАМ
Модель PRAM (Parallel Random-Access Machine — машина с параллельным произвольным доступом) — это упрощенная модель с N процессорами, обозначаемыми P 1 , Р 2 , Р 5 , ... Р n , которые разделяют одну глобальную память. Все процессоры одновременно получают доступ для чтения и записи к совместно используемой глобальной памяти. Каждый из этих теоретических процессоров может получить доступ к разделяе Рис. 5.3. Память, разделяемая между потоками и процессами
Параллельный и исключающий доступ к памяти
Алгоритмы параллельного и исключаю • исключаю • параллельное чтение и исключающая запись (concurrent read and exclusive write-CREW); • исключаю • параллельное чтение и параллельная запись (concurrent read and concurrent write-CRCW). Эти алгоритмы можно рассматривать как стратегии доступа, реализуемые задачами, которые совместно используют данные (рис. 5.4). Алгоритм EREW подразу Для этих четырех типов алгоритмов требуются различные уровни и типы синхронизации. Их диапазон довольно широк: от стратегии доступа, реализация которой требует минимальной синхронизации, до стратегии доступа, реализация которой требует максимальной синхронизации. Наша задача— реализовать эти стратегии, поддерживая целостность данных и удовлетворительную производительность системы. EREW — самая простая для реализации стратегия, поскольку она предполагает, по сути, только последовательную обработку. На первый взгляд самой простой может показаться стратегия CRCW, но она таит в себе массу трудностей. А ведь это только кажется, что если к памяти можно получить доступ без ограничений, то в ней и речь не идет о какой бы то ни было стратегии. Все как раз наоборот: CRCW — самая трудная для реализации стратегия, которая требует максимальной синхронизации.
Что такое семафоры
Операции по управлению семафором
Как упоминалось выше, к семафору можно получить доступ только с помощью специальных операций, подобных тем, которые выполняются с объектами. Это операции декремента, P, и инкремента, V. Если объект Mutex представляет собой семафор, то логика реализации операций P (Mutex) и V (Mutex) P(Mutex) if (Mutex > 0) { Mutex--; } else { Блокирование по объекту Mutex; } V(Mutex) if(Oчepeдь доступа к объекту Mutex не пуста){ Передача объекта Мьютекс следующей задаче; } else { Mutex++; } Реализация зависит от конкретной систе Операции с семафором могут иметь другие имена: Операци Операци Значение семафора зависит от его типа. Стандарт POSIX определяет несколько типов семафоров. Эти семафоры испо Таблица 5 .1. Типы семафоров, определенные стандартом POSIX Процессы или потоки Механизм, используемый для реализации взаимного исключения в критическом разделе кода Процессы или потоки Механизм, используемый д я реализации стратегии доступа для чтения и записи среди потоков Процессы или потоки Механизм, используемый д я уведом ения потоков о том, что произош о событие. Событийный мьютекс остается заблокированным потоком до тех пор, пока не будет получен соответствующий сигнал Процессы или потоки Аналогичен событийному мьютексу, но включает несколько событий или условий Операционные системы, которые не противоречат спецификации Single UNIX Specification или стандарту POSIX Standard, поддерживают реализацию семафоров, которые являются частью библиотеки libpthread (соответствующие функции объявлены в заголовке pthread. h).
Мьютексные семафоры
Стандарт POSIX определяет мьютексный семафор, используемый потоками и процессами, как объект типа pthread_mutex_t. Этот мьютекс обеспечивает базовые операции, необходимые для функционирования практического механизма синхронизации: • инициализация; • запрос на монопольное использование; • отказ от монопольного использования; • тестирование монопольного использования; • разрушение. Функции класса pthread_mutex_t, которые используются для выполнения этих базовых операций, перечислены в табл. 5.2. Во время Таблица 5.2. Фу Инициализация int pthread_mutex_init( pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER; Запрос на монопольное использование #include int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout); Отказ от монопольного использо вания int pthread_mutex_unlock(pthread_mutex_t *mutex); Тестирование монопольно Разрушение int pthread_mutex_destroy( pthread_mutex_t *mutex); Подобно потокам, мьютекс библиотеки Pthread имеет атрибутный объект (он рассматривается ниже), который инкапсулирует все атрибуты мьютекса. Этот атрибутный объект можно передать функции инициализации, в результате чего будет создан мьютекс с атрибутами, заданными с помощью этого объекта. Если при инициализации атрибутный объект не используется, мьютекс будет инициализирован значениями, действую pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER;[11] Этот метод менее затратный, но в нем не предусмотрено проверки ошибок. Мьютекс может иметь или не иметь владельца. Операция При выполнении
Использование мьютексного атрибутного объекта
Мьютексный объект типа pthread_mutex_t можно использовать вместе с атрибутным объектом подобно атрибутно Таблица 5.3. Функции доступа к мьютексному атрибу int pthread_mutexattr_init (pthread_mutexattr_t * attr); Инициализирует мьютексный атрибутный объект, заданный параметром attr, значениями, действующими по умолчанию для всех атрибутов, определяемых реализацией int pthread_mutexattr_destroy (pthread_mutexattr_t * attr); Разрушает мьютексный атрибутный объект, заданный параметром attr, в результате чего он становится неинициализированным. Его можно инициализировать повторно с помощью функции pthread_mutexattr_init int pthread_mutexattr_setprioceiling (pthread_mutexattr_t * attr, int prioceiling); Устанавливает и возвращает атрибут предельного приоритета мьютекса, заданного параметром attr. Параметр prioceiling содержит значение предельного приоритета мьютекса. Атрибут prioceiling определяет минимальный уровень приоритета, при котором еще выполняется критический раздел, защищаемый мьютексом. Значения, которые попадают в этот диапазон приоритетов, определяются стратегией планирования SCHED_FIFO int pthread_mutexattr_getprioceiling (const pthread_mutexattr_t * restrict attr, int *restrict prioceiling); int pthread_mutexattr_setprotocol (pthread_mutexattr_t * attr, protocol int protocol); Устанавливает и возвращает атрибут протокола мьютекса, заданного параметром attr. Параметр protocol может содержать следующие значения: PTHREAD_PRIO_NONE (на приоритет и стратегию планирования потока владение мьютексом не оказывает вли ни int pthread_mutexattr_getprotocol (const pthread_mutexattr_t * restrict attr, int *restrict protocol); (при таком протоколе поток, блокирующий другие потоки с более высокими приоритетами, благодаря владению таким мьютексом будет выполняться с самым высоким приоритетом из приоритетов потоков, ожидающих освобождения любого из мьютексов, которыми владеет данный поток); PTHREAD_PRIO_PROTECT (при таком протоколе потоки, владеющие таким мьютексом, будут выполняться при наивысших предельных значениях приоритетов всех мьютексов, которыми владеют эти потоки, независимо оттого, заблокированы ли другие потоки по каким-то из этих мьютексов) int pthread_mutexattr_setpshared (pthread_mutexattr_t * attr, int pshared); Устанавливает и возвращает атрибут process-shared мьютексного атрибутного объекта, заданного параметром attr. Параметр pshared может содержать следующие значения: int pthread_mutexattr_getpshared (const pthread_mutexattr_t * restrict attr, int *restrict pshared); PTHREAD_PROCESS_SHARED (разрешает разделять мьютекс с любыми потоками, которые имеют доступ к выделенной для этого мьютекса памяти, даже если эти потоки принадлежат различным процессам); PTHREAD_PROCESS_PRIVATE (мьютекс разделяется между потоками одного и того же процесса) int pthread_mutexattr_settype (pthread_mutexattr_t* attr, int type); Устанавливает и возвращает атрибут мьютекса мьютексного атрибутного объекта, заданного параметром . Атрибут мьютекса позволяет определить, будет ли мьютекс распознавать взаимоблокировку, проверять ошибки и т.д. Параметр может содержать такие значения: int PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_RECURSIVE PTHREAD_MUTEX_ERRORCHECK PTHREAD_MUTEX_NORMAL pthread_mutexattr_gettype (const pthread_mutexattr_t * restrict attr, int *restrict type); Самый большой интерес представляет установка атрибута, связанного с тем, каким должен быть мьютекс: закрытым или разделяемым.
Использование мьютексных семафоров для управления критическими разделами
Мьютексы используются для управления критическими разделами процессов и потоков, чтобы предотвратить возникновение условий «гонок». Мьютексы позволяют избежать условий «гонок», реализуя последовательный доступ к критическому разделу. Рассмотрим код листинга5.1. В нем демонстрируется выполнение двух потоков. Для защиты их критических разделов и используются мьютексы. // Листинг 5.1. Использование мьютексов для защиты // критических разделов потоков // . . . pthread_t ThreadA, ThreadB; pthread_mutex_t Mutex,-pthread_mutexattr_t MutexAttr; void *task1(void *X) { pthread_mutex_lock(&Mutex); // Критический раздел кода. pthread_mutex_unlock(&Mutex); return(0) ; } void *task2 (void *X) { pthread_mutex_lock(&Mutex) ; // Критический раздел кода. pthread_mutex_unlосk (Μ t ex) ; return(0) ; } int main(void) { //... pthread_mutexattr_init (&MutexAttr) ; pthread_mutex_init (&Mutex, &MutexAttr) ; //Устанавливаем атрибуты мьютекса. pthread_create(&ThreadA, NULL, taskl, NULL) ; pthread_create(&ThreadB,NULL, task2,NULL) ; //... return(0) ; } В листинге 5.1 потоки ThreadA и ThreadB содержат критические разделы, защищаемые с помощью объекта Mutex. В листинге 5.2 демонстрируется, как можно использовать мьютексы для защиты критических разделов процессов. // Листинг 5.2. Использование мьютексов для зашиты // критических разделов процессов //... int Rt; pthread_mutex_t Mutexl ; pthread_mutexattr_t MutexAttr; int main(void) { //... pthread_mutexattr_init (&MutexAttr); pthread_mutexattr_setpshared( &MutexAttr, PTHREAD_PROCESS_SHARED ) ; pthread_mutex_init (&Mutexl, &MutexAttr) ; if((Rt = fork) == 0){ // Сыновний процесс. pthread_mutex_lock(&Mutexl); // Критический раздел. pthread_mutex_unlock(&Mutexl) ; } else{ // Родительский процесс, pthread_mutex_lock(&Mutexl); // Критический раздел. pthread_mutex_unlock(&Mutexl) ; } //.. . return(0); } Рис. 5.5. Закрытые и разделяемые мьютексы Важно отметить, что в листинге 5.2 при вызове следующей функции мьютекс инициализируется как разделяемый: pthread_mutexattr_setpshared(&MutexAttr,PTHREAD_PROCESS_SHARED); Установка этого атрибута равным значению PTHREAD_PROCESS_SHARED позволяет объекту Mutex стать разделяемым между потоками различных процессов. После вызова функции fork сыновний и родительский процессы могут защищать свои критические разделы с помощью объекта Mutex. Критические разделы этих процессов могут содержать некоторые ресурсы, разделяемые обоими процессами.
Блокировки для чтения и записи
Мьютексные семафоры позволяют управлять критическими разделами, обеспечивая последовательный вход в эти разделы. В любой момент времени вход в критический раздел разрешается только одному потоку или процессу. Реализуя блокировки для чтения и записи, можно разрешить вход в критический раздел сразу нескольким потокам, если они намерены лишь считывать данные из разделяемой памяти. Следовательно, блокировкой для чтения может владеть любое количество потоков. Но если сразу несколько потоков должны записывать или модифицировать данные общей памяти, то доступ для этого будет предоставлен только одному потоку. Другими словами, никаким другим потокам не будет разрешено входить в критический раздел, если одному потоку предоставлен монопольный доступ для записи в разделяемую память. Такой подход может оказаться полезным, если приложения чаще считывают данные, чем записывают их. Если в приложении создается множество потоков, организация взаимно исключающего доступа может оказаться излишней предосторож Блокировки для чтения и записи и Различие между обычными мьютексами и мьютексами, обеспечивающими чтение и запись, заключается в операциях запроса на блокирование. Вместо одной операции блокирования здесь предусмотрено две: Функция Блокировка чтения-записи реализуется с помощью объектов типа pthread_rwlock_t. Этот же тип имеет атрибутный объект, который инкапсулирует атрибуты объекта блокировки. Функции установки и чтения атрибутов перечислены в табл. 5.5. Объект типа pthread_rwlock_t может быть закрытым (для разделения между потоками одного процесса) или разделяемым (для разде Таблица 5.4. Операции, используемые для блокировки ч Инициализация • int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); Запрос на блокировку • int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); • int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); • int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout); • int pthread_rwlock_timedwrlock( pthread_rwlock_t | *restrict rwlock, const struct timespec *restrict abs_timeout); Освобождение блокировки • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); Тестирование блокировки • int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); • int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); Разрушение • int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); Таблица 5.5. Функции доступа к атрибутному объекту типа pthread_rwlock_t • int pthread_rwlockattr_init (pthread_rwlockattr_t * attr); Инициализирует атрибутный объект блокировки чтения-записи,заданный параметром • int pthread_rwlockattr_destroy (pthread_rwlockattr_t * attr) Разрушает атрибутный объект б • int pthread_rwlockattr_setpshared (pthread_rwlockattr_t * attr, int pshared); int pthread_rwlockattr_getpshared (const pthread_rwlockattr_t * restrict attr, int *restrict pshared); Устанавливает или возвращает атрибут • PTHREAD_PROCESS_SHARED (разрешает б • PTHREAD_PROCESS_PRIVATE (блокировка чтения-записи разделяется между потоками одного процесса)
Использование блокировок чтения-записи для реализации стратегии доступа
Блокировки чтения-записи можно использовать для реализации стратегии доступа CREW (параллельное чтение и исключающая запись). Согласно этой стратегии возможность параллельно считывать данные может быть предоставлена сразу нескольким задачам, но только одна задача получит право доступа для записи. При выполнении монопольной записи в этом случае не будет дано разрешение на параллельное чтение данных. Использование блокировок чтения-записи для защиты критических разделов продемонстрировано в листинге 5.3. // Листинг 5.3. Пример использования потоками блокировок // чтения-записи //... pthread_t ThreadA, ThreadB, ThreadC, ThreadD ; pthread_rwlock_t RWLock; void *producerl(void *X) { pthread_rwlock_wrlock(&RWLock) ; // Критический раэдел. pthread_rwlock_unlock(&RWLock) ; return(0); } void *producer2 (void *X) { pthread_rwlock_wrlock(&RWLock) ; // Критический раздел. pthread_rwlock_unlock(&RWLock) ; } void *consumerl(void *X) { pthread_rwlock_rdlock(&RWLock); // Критический раздел. pthread_rwlock_unlock(&RWLock); return(0); } void *consumer2(void *X) { pthread_rwlock_rdlock(&RWLock); // Критический раздел. pthread_rwlock__unlock(&RWLock); return(0); } int main(void) { pthread_rwlock_init(&RWLock,NULL); // Устанавливаем атрибуты мьютекса. pthread_create(&ThreadA, NULL, producerl, NULL) pthread_create(&ThreadB, NULL, consumerl, NULL) pthread_create(&ThreadC,NULL,producer2,NULL) pthread_create(&ThreadD,NULL, consumer2,NULL) //.. . return(0); } В листинге 5.3 создаются четыре потока. Два потока, ThreadA и ThreadC, выполняют роль изготовителей, а остальные два (ThreadB и ThreadD) — потребителей. Все потоки имеют критический раздел, который защищается объектом блокировки чтения-записи RWLock. Потоки ThreadB и ThreadD могут входить в свои критические разделы параллельно или последовательно, но это исключено, если поток ThreadA или ThreadC пребывает в своем критическом разделе. Потоки ThreadA и ThreadC не могут входить в свои критические разделы параллельно. Частичная таблица решении для листинга 5.3 показана в табл. 5.6. Таблица 5.6. Час (выполняет запись) (выполняет чтение) (выполняет запись) (выполняет чтение) Нет Нет Нет Да Нет Нет Да Нет Нет Да Нет Нет Нет Да Нет Да Да Нет Нет Нет
Условные переменные
В листинге 4.6 поток-«потребитель» содержал цикл: 15 while(TextFiles.empty) 16 {} Поток-«потребитель» выполнял итерации цикла до тех пор, пока в очереди TextFiles были элементы. Этот цикл можно заменить условной пере Условная переменная имеет тип pthread_cond_t. Ниже перечислены типы операций, которые может она выполнять: • инициализация; • разрушение; • ожидание; • ожидание с ограничением по времени; • адресная сигнализация; • всеобщая сигнализация; Операции инициализации и разрушения выполняются условными переменными подобно аналогичным операциям других мьютексов. Функции класса pthread_cond_t, которые реализуют эти операции, перечислены в табл. 5.7. Ожидание int pthread_cond_wait(pthread_cond_t * restrict cond, pthread_mutex_t *restrict mutex); int pthread_cond_timedwait( pthread_cond_t * restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); Сигнализация int pthread_cond_signal(pthread_cond_t*cond); int pthread_cond_broadcast( pthread_cond_t *cond); Разрушение int pthread_cond_destroy(pthread_cond_t*cond); Инициализация int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); pthread_cond_t cond =PTHREAD_C OND_INITIALIZER; Условные переменные используются совместно с мьютексами. При попытке заблокировать мьютекс поток или процесс будет заблокирован до тех пор, пока мьютекс не освободится. После разблокирования поток или процесс получит мьютекс и продолжит свою работу. При использовании условной переменной ее необходимо связать с мьютексом. //. . . pthread_mutex_lock(&Mutex) ; pthread_cond_wait(&EventMutex, &Mutex); //. . . pthread_mutex_unlock(&Mutex) ; Итак, некоторая задача делает попытку заблокировать мьютекс. Если мьютекс уже заблокирован, то эта задача блокируется. После разблокирования задача освободит мьютекс Выполняя адресную сигнализацию, задача уведомляет другой поток или процесс о том, что произошло некоторое событие. Если задача ожидает сигнала для заданной условной переменной, эта задача будет разблокирована и получит мьютекс. Если сразу несколько задач ожидают сигнала для заданной условной переменной, то разблокирована будет только одна из них. Остальные задачи будут ожидать в очереди, и их разблокирование будет происходить в соответствии с используемой стратегией планирования. При выполнении операции всеобщей сигнализации уведомление получат все задачи, ожидающие сигнала для заданной условной переменной. При разблокировании нескольких задач они будут состязаться за право владения мьютексом в соответствии с используемой стратегией планирования. В отличие от операции ожидания, задача, выполняющая операцию сигнализации, не предъявляет прав на владение мьютексом, хотя это и следовало бы сделать. Условная переменная также имеет атрибутный объект, функции которого перечислены в табл. 5.8. Таблица 5.8. Функции доступа к атрибутному объекту для условной переменной типа pthread_cond_t • int pthread_condattr_init ( pthread_condattr_t * attr) Инициализирует атрибутный объект условной переменной, заданный параметром attr, значениями, действующими по умолчанию для всех атрибутов, определенных реализацией; • int pthread_condattr_destroy ( pthread_condattr_t * attr) ; Разрушает атрибутный объект условной переменной, заданный параметром attr. Этот объект можно инициализировать повторно, вы-звав функцию pthread_condattr_init • int pthread_condattr_setpshared ( pthread_condattr_t * attr,int pshared); • int pthread_condattr_getpshared ( const pthread_condattr_t * restrict attr, int *restrict pshared); Устанавливает или возвращает атрибут process-shared атрибутного объекта условной переменной, заданного параметром attr. Параметр pshared может содержать следующие значения: • PTHREAD_PROCESS_SHARED (разрешает блокировку чтения-записи, разделяемую любыми потоками, которые имеют доступ к памяти, выделенной для этой условной переменной, даже если потоки принадлежат различным процессам); • PTHREAD_PROCESS_PRIVATE (Условная Переменная разделяется между потоками одного процесса) • int pthread_condattr_setclock ( pthread_condattr_t * attr, clockid_t clock_id); • int pthread_condattr_getclock ( const pthread_condattr_t * restrict attr, clockid_t * restrict clock_id); Устанавливает или возвращает атрибут
Использование условных переменных для управления отношениями синхронизации
Условную переменную можно использовать для реализации отношений синхронизации, о которых упоминалось выше: старт-старт (CC), финиш-старт (ФС), старт-финиш (СФ) и финиш-финиш (ФФ). Эти отношения могут существовать между потоками одного или различных процессов. В листингах 5.4 и 5.5 представлены примеры реализации ФС- и ФФ-отношений синхронизации. В каждом примере определено два мьютекса. Один мьютекс используется для синхронизации доступа к общим данным, а другой — для синхронизации выполнения кода. // Листинг 5.4. ФС-отношения синхронизации между // двумя потоками //. . . float Number; pthread_t ThreadA,ThreadB; pthread_mutex_t Mutex, EventMutex; pthread_cond_t Event; void * worker1 (void *X) { for(int Count = l;Count < 100;Count++){ pthread_mutex_lock(&Mutex); Number++; pthread_mutex_unlock(&Mutex); cout << «worker1: число равно» << Number << endl; if(Number == 50){ pthread_cond_signal(&Event); } } cout << «Выполнение функиии worker1 завершено.» << endl; return(0); } void * worker2 (void *X) { pthread_mutex_lock(&EventMutex); pthread_cond_wait(&Event,&EventMutex); pthread_mutex_unlock(&EventMutex); for(int Count = 1;Count < 50;Count++){ pthread_mutex_lock(&Mutex); Number = Number + 20; pthread_mutex_unlock(&Mutex); cout << «worker2: число равно» << Number << endl; } cout « «Выполнение функции worker2 завершено.» « endl; return(0); }; int main (int argc, char *argv[]) { pthread_mutex_init(&Mutex,NULL); pthread_mutex_init(&EventMutex,NULL); pthread_cond_init(&Event, NULL); pthread_create(&ThreadA, NULL, workerl, NULL); pthread_create(&ThreadB, NULL, worker2 , NULL); //. . . return (0); } В листинге 5.4 показан пример реализации ФС-отношений синхронизации. Поток ThreadA не может завершиться до тех пор, пока не стартует поток ThreadB. Если значение переменной Number станет равным 50, поток ThreadA сигнализирует о этом потоку ThreadB. Теперь он может продолжать выполнение до самого конца Поток ThreadB не может начать выполнение до тех пор, пока не получит сигнал от потока ThreadA. Поток ThreadB использует объект EventMutex вместе с условной переменной Event. Объект Mutex используется для синхронизации доступа для записи значения разделяемой переменной Number. Для синхронизации различных событий и доступа к критическим разделам задача может использовать несколько мьютексов. Пример реализации ФФ-отношений синхронизации показан в листинге 5.5. // Листинг 5.5. ФФ-отношения синхронизации между // двумя потоками //... float Number; pthread_t ThreadA, ThreadB ; pthread_mutex_t Mutex, EventMutex; pthread_cond_t Event; void *workerl(void *X) { for(int Count = l;Count < 10;Count++){ pthread_mu tex_l ock (&Mutex); Number++; pthread_mutex_unlосk(&Mutex); cout « «workerl: число равно " << Number « endl; } pthread_mutex_lock(&EventMutex) ,- cout « «Функция workerl в состоянии ожидания. " « endl; pthread_cond_wait (&Event, &EventMutex) ; pthread_mutex_unlock(&EventMutex); return(0); } void *worker2 (void *X) { for(int Count = l;Count < 100;Count++){ pthread_mutex_lock(&Mutex) ; Number = Number * 2 ; pthread_mutex_unlock(&Mutex) ; cout « «worker2: число равно " « Number « endl; } pthread_cond_signal (&Event) ; cout « «Функция worker2 послала сигнал " « endl; return(0); } int main(int argc, char *argv[]) { pthread_mutex_init (&Mutex,NULL) ; pthread_mutex_init (&EventMutex,NULL) ; pthread_cond_init (&Event, NULL) ; pthread_create(&ThreadA, NULL,workerl, NULL); pthread_create (&ThreadB, NULL, worker2, NULL) ; //.. . return (0); } В листинге 5.5 поток ThreadA не может завершиться до тех пор, пока не завершится поток ThreadB. Поток ThreadA должен выполнить цикл 10 раз, а ThreadB — 100. Поток ThreadA завершит выполнение своих итераций раньше ThreadB, но будет ожидать до тех пор, пока поток ThreadB не просигналит о своем завершении. CC- и СФ-отношения синхронизации невозможно реализовать подобным образом. Эти методы используются для синхронизации пор
Объектно-ориентированный подход к синхронизации
Одно из преимуществ объектно-ориентированного программирования состоит в защите, которую обеспечивает инкапсуляция компонентов данных объекта. Инкапсуляция может обеспечить для пользователя объектов «стратегии доступа к объектам и принципы их применения» [ 24 ]. В примерах, представленных в этой главе, за применяемые стратегии доступа вся ответственность возлагалась на пользователя данных. С помощью объектов и инкапсуляции ответственность можно переложить с пользователя данных на сами данные. При таком подходе создаются данные, которые, в отличие от функций, являются безопасными для потоков. Для реализации такого подхода данные многопоточного приложения (по возможности) необходимо инкапсулировать с помощью С++-конструкций class или struct. Затем инкапсулируйте такие механизмы синхронизации, как семафоры, блокировки для обеспечения чтения-записи и мьютексы событий. Если данные или механизмы синхронизации представляют собой объекты, создайте для них интерфейсный класс. Наконец, объедините объект данных с объектами синхронизации посредством наследования или композиции, чтобы создать объекты данных, которые будут безопасны для потоков. Этот подход подробно рассматривается в главе 11.
Резюме
Для координации порядка выполнения процессов и потоков Для описания синхронизации данных используются некоторые типы алгоритмов модели PRAM. Стратегию доступа EREW (исключающее чтение и исключающая запись) можно реализовать с помощью мьютексного семафора. Мьютексный семафор защищает критический раздел, обеспечивал последовательный вход в него. Эта стратегия разрешает либо доступ для чтения, либо доступ для записи. Стандарт POSIX определяет мьютексный семафор типа
Объединение возможностей параллельного программирования и C++ средств на основе PVM
Система програм Система PVM в качестве средства связи между параллельно выполняющимися задачами поддерживает модель передачи сообщений. Приложение взаимодействует с PVM посредством библиотеки, которая состоит из API-интерфейсов, предназначенных для управления процессами, отправки и получения сообщений, сигнализации процессов и т.д. С++-программа взаимодействует с PVM-библиотекой точно так же, как с любыми другими библиотеками функций. С++-программе для получения доступа к функциям РVM-библиотеки не нужно создавать специальную форму или архитектуру,в то врем
Классические модели параллелизма, поддерживаемые системой PVM
Система PVM поддерживает модели MIMD (Multiple-Instruction, Multiple-Data— множество потоков команд, множество потоков данных) и SPMD (Single-Program, Multiple-Data — одна программа, множество потоков данных) параллелизма. В действительности SPMD — это вариант модели SIMD (Single-Instruction, Multiple-Data — один поток команд, множество потоков данных). Эти модели разбивают программы на потоки команд и данных. В модели MIMD программа состоит из нескольких параллельно выполняющихся потоков команд, причем каждому из них соответствует собственный локальный поток данных. По сути, каждый процессор здесь имеет собственную память. В PVM-среде модель MIMD считается моделью с распределенной памятью (в отличие от модели с общей памятью). В моделях с общей памятью все процессоры «видят» одни и те же ячейки памяти. В модели с распределенной памятью связь между хранимыми в ней значениями обеспечивается посредством механизма передачи сообщений. Однако модель SPMD подразумевает наличие одной программы (одного набора команд), которая параллельно выполняется на нескольких компьютерах, причем эти одинаковые на всех машинах программы обрабатывают различные потоки данных. PVM-среда поддерживает как MIMD-, так и SIMD-модели или их сочетание. Четыре классические модели параллелизма показаны на рис. 6.1. Обратите внимание на то, что модели SISD и MISD (см. рис.6.1) неприменимы к системе PVM. Модель SISD описывает однопроцессорную машину, а для модели MISD вооб
Библиотека PVM для языка С++
К функциональным возможностям PVM из С++-программы можно получить доступ с помо • Управление процессами. • Упаковка сооб • Распаковка сооб • Обмен задач сигналами. • Управление буферо • Функции обработки инфор • Групповые операции. Эти библиотечные функции легко интегрировать в С++ среду. Префикс pvm_ в имени каждой функции позволяет не забыть о ее принадлежности соответствующему пространству имен. Для использования PVM-функций необходимо включить в программу заголовочный файл pvm3 . h и скомпоновать ее с библиотекой libpvm. В программах 6.1 и 6.2 демонстрируется, как работает простая PVM-программа. Инструкции по компиляции и выполнению программы 6.1 приведены в разделе «Профиль програ // Программа 6.1 #include «pvm3.h» #include int main(int argc,char *argv[]) { int RetCode,MessageId; int PTid, Tid; char Message[100j; float Result[l]; PTid = pvm_mytid; RetCode = pvm_spawn(«program6-2»,NULL,0,''",l,&Tid); if(RetCode == 1){ MessageId = 1; strcpy(Message,«22»); pvm_initsend(PvmDataDefault); pvm_pkstr(Message); pvm_send(Tid,MessageId); pvm_recv(Tid,MessageId); pvm_upkfloat(Result,1,1); cout « Result[0] « endl; pvm_exit; return(0) ; } else{ cerr << «Задачу породить невозможно. " « endl; pvm_exit; return(1) ; } } Профиль программы 6.1 Имя программы |program6 -1.cc Описание |Использует функцию pvn_send{) для пересылки числа в другую PVM-задачу, которая выполняется параллельно с данной (программа6.2), и функцию pvm_recv для получения числа от этой задачи. Требуемая библиотека libpvm3 Требуемые заголовки Инструкции по компиляции и компоновке программ gcс++ -о program6-l -I $PVM_ROOT/include -L $PVM_ROOT/lib/ | SPVM_ARCH -1 pvm3 *Среда для тестирования Solaris8,PVM 3.4.3, SuSE Linux 7.1, gcc 2.95.2. Инструкции по выполнению ./program6-l Примеча В програм Обратите внимание на то, что обе программы 6.1 и 6.2 содержат обра // Программа 6.2 #include «pvm3.h» #include «stdlib.h» int main(int argc, char *argv[]) int MessageId, Ptid; char Message[100]; float Num,Result,-Ptid = pvm_parent; MessageId = 1; pvm_recv(Ptid,MessageId) ; pvm_upkstr(Message) ; Num = atof(Message); Result = Num / 7.0001r pvm_initsend(PvmDataDefault); pvm_pkfloat(&Result,1,1); pvm_send(Ptid,MessageId); pvm_exit; return(0); Профиль программы 6.2 Имя программы program6-2.cc Описание Эта программа принимает число от родительского процесса и делит его на 7. Затем она отправляет результат своему родительскому процессу. Требуемая библиотека libpvm3 . . Требуемые заголовки < pvm3.h> Инструкции по компиляции и компоновке программы Среда для тестирования |fiuJJE Onux 7.1 gnu С++ 2.95.2, Solaris 8 Workshop 6, PVM 3.4.3. У4нструкции по выполнению Эта программа порождается программой 6.1. |Примечания Необходимо запустить на выпол
Компиляция и компоновка C++/PVM-npoгpaмм
Версия 3.4.x PVM-среды представлена в виде единой библиотеки libpvm3 . а. Чтобы скомпилировать PVM-программу, необходимо включить в ее код заголовочный файл $ с++ -о mypvm_program -I $PVM_ROOT/include program.cc -L$PVM_ROOT/lib -lpvm3 Переменная среды $PVM_ROOT указывает на каталог, в котором инсталлирована библиотека PVM. При выполнении этой команды создается двоичный файл mypvm_program. Для выполнения программ 6.1 и 6.2 сначала необходимо инсталлировать PVM-среду. Выполнить PVM-программу можно одним из трех основных способов: запустить автономный выполняемый (двоичный) файл, использовать PVM-консоль или среду XPVM.
Выполнение PVM-программы в виде двоичного файла
Во-первых, необходимо запустить программу pvmd; во-вторых, на каждом компьютере, включенном в PVM-среду, корректно ско Здесь PVM_ARCH содержит имя архитектуры компьютера (см. табл. 6.1 и параграфы 1 и 2 из раздела6.2.5). Для выполняемых программ должны быть установлены соответствую pvmd hostfile & Здесь hostfile — это файл конфи Если эта программа порождает другие задачи, то они запустятся автоматически.
Запуск PVM-программ c помощью PVM-консоли
Для выполнения программ с помо Получив приглашение на ввод ко pvm> spawn -> MyPvmProgram
Запуск PVM-программ c помощью XPVM
Кроме PVM-консоли, можно использовать графический интерфейс XPVM для X Windows. На рис. 6.2 показано диалоговое окно сеанса работы с XPVM-интерфейсом. Библиотека PVM не требует, чтобы С++-программа придерживалась какой Практика показывает, что функции pvm_mytid и pvm_parent необходи Рис. 6.2. Диалоговое окно графического интерфейса XPVM Таблица 6.1. Семь категорий фу Используются для управления PVM-процесса Применяются для упаковки сооб Используются для получения сооб Применяются для си Используются для инициализации, очистки и размещения буферов, предназначенных для приема и отправки сообщений, которыми обмениваются PVM-процессы Применяются для получения информации о PVM-процессах и выполнения дру Используются для объединения процессов в группы и выполнения других групповых операций
Требования к PVM-программам
Если PVM-среда реализуется в виде сети компьютеров, то, прежде чем ваша С++-программа начнет взаимодействовать с ней, необходимо обработать следующие элементы. Параграф 1 Следует установить переменные среды PVM_ROOT и PVM_ARCH. Переменная среды PVM_ROOT должна указывать на каталог, в котором инсталлирована PVM-б иблиотека. $ PVM_ROOT=/usr/lib/pvm3 setenv PVM_ROOT /usr/lib/pvm3 $ export PVM_ROOT Переменная среды PVM_ARCH идентифицирует архитектуру компьютера. Каждый компьютер, включенный в среду PVM, должен быть идентифицирован архитектурой. Например, Ultrasparcs-компьютеры имеют обозначение SUN4SOL2, а Linux-компьютеры — обозначение LINUX. В табл. 6.2 перечислены самые распространенные архитектуры для PVM-среды. Эта таблица содержит имя и тип компьютера, соответствую $PVM_ARCH=LIMJX setenv PVM_ARCH LINUX $export PVM_ARCH Таблица 6.2. Самые распростра AFX8 Alliance LINUX 80386/486 PC (UNIX) ALPHA DEC Alpha MASPAR Maspar BAL Sequent Balance MIPS MIPS 4680 BFLY BBN ButterflyTC2000 NEXT NeXT BSD386 80386/486 PC (UNIX) PGON Intel ParagonIntel Paragon CM2 «Мыслящая машина» CM2 PMAX DECstation 3100,5100 CM5 «Мыслящая машина» CM5 RS6K IBM/RS6000 CNVX Convex С-серии RT IBM RT CNVXN Convex С-серии SGI Silicon Graphics IRIS CRAY C-90, YMP,T3D (доступный порт) SGI5 Silicon Graphics IRIS CRAY2 Cray-2 SGIMP SGI Multiprocessor CRAYSIMP CrayS-MP SUN3 Sun3 6.2. Библио DGAV Data General Aviion SUN4 Sun 4, SPARCstation E88K Encore 88000 SUN2SOL2 Sun 4, SPARCstation HP300 НР-9000 Model 300 SUNMP SPARC Multiprocessor HPPA НР-9000 PA-RISC SYMM Sequent Symme^ I860 Intel iPSC/860 TITN Stardent Titan IPSC2 Intel iPSC/2 386 Host U370 IBM 370 KSRI Kendall Square KSR-1 UVAX DEC LicroVAX Параграф 2 Выполняемые файлы любых программ, участвующих в среде PVM, должны быть размещены на всех компьютерах, включенных в среду PVM, или доступны всем компьютерам, включенным в среду PVM. При этом каждая программа должна быть скомпилирована для работы с учетом конкретной архитектуры. Это означает, что, если в среду PVM включены процессоры UltraSparcs, PowerPCs и Intel, то мы должны иметь версию программы, скомпилированную для каждой архитектуры. Эту версию программы следует разместить в известном для PVM месте. Таким местом часто служит каталог $HOME /pvm3/bin. Этот каталог может быть также задан в файле конфигурации PVM, который обычно имеет имя hostfile или .xpvm_hosts (если используется среда XPVM). Файл hostfile должен содержать такую запись: ep=/usr/local/pvm3/bin Эта запись означает, что любые пользовательские выполняемые файлы, необходимые для среды PVM, можно найти в каталоге /usr/local/pvm3 /bin. Параграф 3 Пользователь, запускаю Параграф 4 Создайте на каждо Параграф 5 Создайте файл $HOME /.xpvm_hosts и/или файл $HOME /pvm_hosts, в котором перечислите все подлежа Главное внимание необходимо уделить сетевому доступу пользователя, запускаю # Строки комментариев начинаются с символа "#" # (пустые строки игнорируются). # Строки, начинаю # включить компьютеры в среду PVM позднее. Если # имя компьютера не предваряется символом "&", # этот компьютер включается в среду PVM # автоматически. flavius marcus &cambius lo=romulus &karsius # Символ означает стандартные опции для # следую # dx=/export/home/fred/pvm3/lib/pvmd &octavius # Если компьютеры являются частью типичного # linux-кластера, то их имена можно использовать # для включения узлов кластера в среду PVM # вместе с другими узлами. _
Объединение динамической С++-библиотеки c библиотекой PVM
Поскольку доступ к PVM-средствам обеспечивается через коллекцию библиотечных функций, С++-программа использует PVM как любую другую библиотеку. Следует иметь в виду, что каждая PVM-програм
Методы использования PVM-задач
Работу, которую выполняет С++-программа, можно распределить между функциями, объектами или их сочетаниями. Действия, выполняемые программой, обычно делятся на такие логические категории: операции ввода-вывода, интерфейс пользователя, обработка базы данных, обработка сигналов и ошибок, числовые вычисления и т.д. Отделяя код интерфейса пользователя от кода обработки файлов, а также код процедур печати от кода числовых вычислений, мы не только распределяем работу програ Не самая удачная идея — попытаться директивно навязать параллелиз Соблюдение первичности логики и вторичности параллелизма имеет несколько последствий для С++-программ. Это означает, что мы могли бы порождать PVM-задачи из функции main или из функций, вызываемых из функции main (и даже из других функций). Мы могли бы порождать PVM-задачи из методов, прина
Реализация модели SPMD (SIMD) c помощью PVM-и С++-средств
Вариант 1 на рис. 6.4 представляет ситуацию, при которой функция main порождает от 1 до N задач, причем каждая задача выполняет один и тот же набор инструкций, но на различных наборах данных. Су // Листинг б.1. Вызов функции pvm_spawn из // функции main int main(int argc, char *argv[]) { int TaskId[10]; int TaskId2[5]; // 1-е порождение: pvm_spawn(«set_combination»,NULL,0,"",10,TaskId); // 2-е порождение: pvm_spawn(«set_combination», argv, 0,"",5,TaskId2); //. . . } В листинге 6.1 при первом порождении создается 10 задач. Каждал задача будет выполнять один и тот же набор инструкций, содержа При второ // Листинг 6.2. Использование нескольких вызовов // функции pvm_spawn из функции main int main(int argc, char *argv[]) { int Taskl; int Task2; int Task3; //.. . pvm_spawn(«set_combination», NULL,1,«hostl»,l,&Taskl); pvm_spawn(«sec_combination»,argv,1,«host2»,1, &Task2); pvm_spawn(«set_combination»,argv++,l,«host3»,l,&Task3); //. . . } Подход к созданию задач, продемонстрированный в листин Как и в дру // Листинг б.З. Создание сочетаний из заданных множеств int main(int argc,char *argv[]) { int RetCode,TaskId[4]; RetCode = pvm_spawn («pvm_generic_combination11, NULL, 0, "", 4,TaskId); if(RetCode == 4) { colorCombinations (TaskId[0] , 9) ; colorCombinations(TaskId[l] ,12) ; numericCombinations(TaskId[2],4); numericCombinations(TaskId[3],3); saveResult(TaskId[0]); saveResult(TaskId[l]); saveResult(TaskId[2]); saveResult(TaskId[3]); pvm_exit ; } else{ cerr « «Ошибка при порождении сыновнего процесса.» « endl; pvm_exit ; } return(0); } В листинге 6.3 обратите внимание на порождение четырех PVM-задач: pvm_spawn(«pvm_generic_combination» ,NULL, 0, н » ,4,TaskId) ; Каждая порожденнал задача должна выполнять програ // Листинг 6.4. Определение функции colorCombinations void colorCombinations(int TaskId,int Choices) { int MessageId =1; char *Buffer; int Size; int N; string Source(«blue purple green red yellow orange silver gray "); Source.append(«pink black white brown light_green aqua beige cyan "); Source.append(«olive azure magenta plum orchid violet maroon lavender»); Source. append (" \n**) ; Buffer = new char[(Source.size + 100)]; strcpy(Buffer,Source.c_str); N = pvm_initsend(PvmDataDefault); pvm_pkint(&Choices,1,1); pvm_send(TaskId,MessageId); N = pvm_initsend(PvmDataDefault); pvm_pkbyte(Buffer,strlen(Buffer),1); pvm_send(TaskId,MessageId); delete Buffer; } В листингеб.З от // Листинг 6.5. Использование PVM-задач для генерирования // сочетаний чисел void numericCombinations(int TaskId,int Choices) { int MessageId = 2; int N; double ImportantNumbers[7] = {3.00e+8,6.67e-ll,1.99e+30, 6.2. Библио 1.67e-27,6.023e+23,6.63e-34, 3.14159265359}; N = pvm_initsend(PvmDataDefault); pvm_pkint(&Choices,1,1) ; pvm_send(TaskId,MessageId) ; N = pvm_initsend(PvmDataDefault); pvm_pkdouble (ImportantNumbers, 5,1) ; pvm_send(TaskId,MessageId) ; } В функции numericCombinations из листинга 6.4 PVM-задача использует pvrt_pkbyte(Buffer,strlen(Buffer) ,1) ; pvm_send(TaskId,MessageId) ; А функция numericCombination( ) отправляет свои данные PVM-задача pvm_pkdouble (ImportantNumbers, 5,1) ; pvn_send(TaskId,MessageId) ; Функция colorCombinations в листинге6.4 создает строку названий цветов, азатем копирует ее в // Листинг 6.6. Использование тега MessageId для // распознания типов данных pvm_bufinfo (N, &NumBytes, &MessageId, &Ptid) ; if(MessageId == 1){ vector Buf = new char[NumBytes]; pvm_upkbyte(Buf, NumBytes,1); strstream Buffer; Buffer « Buf « ends,- while(Buffer.good) { Buffer » Color; if(!Buffer.eof){ Source.push_back(Color); } } generateCombinations } if(MessageId == 2){ vector } Здесь используется тег MessageId, позволяю Если тег MessageId содержит число 2, то Объявив, какого типа данные будет содержать вектор Source, остальную часть функции в програ
Реализация модели MPMD (MIMD) с помощью PVM-и С++-средств
В то время как 6.2. Библиотека PVM для языка С++ 231 // Листинг 6.7. Использование PVM для реализации // MPMD-модели вычисления int main(int argc, char *argv[]) { int Taskl[20]; int Task2[50]; int Task3[30]; //... pvm_spawn («pvm_generic_combination», NULL, 1, «hostl»,20,Taskl); pvm_spawn («generate_plans», argv, 0, "", 50, Task2) ; pvm_spawn(«agent_filters»,argv++,l, «host 3»,30,&Task3) ; //.. . } При выполнении кода, представленного в листинге 6.7, создается 100 задач. Первые 20 задач генерируют сочетания. Слелующие 50 по мере создания сочетаний генерируют планы на их основе. Последние 30 задач отфильтровывают самые удачные планы из набора планов, сгенерированного предыдущи При желании мы можем воспользоваться преиму Рис. 6.5. Неко § 6.1. Обозначение сочетаний Предположим, 6.3. Базовые меха Если у нас есть м
Базовые механизмы PVM
Среда PVM состоит из двух компонентов: PVM-демона (pvmd) и библиотеки pvmd. Один PVM-демон pvmd выполняется на каждом компьютере в виртуальной машине. Этот демон служит в качестве маршрутизатора сооб Библиотека pvmd состоит из функций, которые позволяют одной PVM-задаче взаимодействовать с другими. Эта библиотека также включает функции, которые позволяют PVM-задаче связываться со своим демоном pvmd. Базовал архитектура PVM-среды показана на рис. 6.6. РУМтреда состоит из нескольких PVM-задач. Каждал задача должна содержать один или несколько буферов отправки сооб • управление процессами; • упаковка сооб • распаковка сооб • управление буфером сооб Несмотря на су Рис. 6.6. Базовая архи
Функции управления процессами
Библиотека PVM содержит шесть часто используе Функция pvm_spawn используется для создания новых PVM-задач. При вызове этой функции pvm_spawn(«agent_filters'\argv++,l,«host 3»,30,&Task3); 6.3. Базовые меха Сикопсис # inc lude " pvm3 . h» int pvm_spawn(char *task, char **argv, int flag, char *location,int ntask,int *taskids); int pvmJcill(int taskid); int pvm_exit(void) ; intpvn_addhosts(char **hosts,int nhosts,int *status); int pvm_delhosts(char **hosts,int nhosts,int *status); int pvm_halt(void) ; Параметр task содержит имя программы, которую должна выполнить функция pvm_spawn . Поскольку про char *Hosts[ ] = {«porthos», «dartagnan»,«athos»}; pvm_addhosts («porthose», l,&Status) ; //.. . pvm_addhosts (Hosts, 3 Параметр Hosts обычно содержит имена компьютеров (одно или несколько), перечисленных в файле .rhosts или .xpvm_hosts. Пара При выполнении этой функции компьютер с именем dartagnan будет извлечен из среды PVM. Функции pvm_addhosts и pvm_delhosts можно вызывать во время выполнения приложения. Это позволяет программисту динамически изменять размеры среды PVM. Любая PVM-задача, выполняемал на компьютере, который удаляется из PVM-среды, будет аннулирована. Демоны, выполняю
Упаковка и отправка сообщений
Гейст Бигулин (Geist Beguelin) и его колле PVM-демоны и задачи могут формировать и отправлять произвольной длины сообщения, содержащие типизированные данные. Если содержащиеся в сообщениях данные имеют несовместимые форматы, то при передаче между компьютерами их можно преобразовать, используя стандарт XDR1. Сообщения помечаются во время отправки с помощью определенного пользователем целочисленного кода и мотуг быть отобраны для приема посредством адреса источника, или тега. Отправитель сообщения не ожидает от получателя подтверждения приема (квитирования), а продолжает работу сразу после отправки сообщения в сеть. Затем буфер сообщений может быть очищен или вновь использован по назначению. Сообщения буфери-зируются до тех пор, пока не будут приняты получателем. PVM-система надежно доставляет сообщения адресатам, если таковые существуют. При оправке сообщений от каждого отправителя каждому получателю их порядок сохраняется. Это означает, что если отправителем было послано несколько сообщений, они будут получены адресатом в том же порядке, в котором были отправлены. Библиотека PVM содержит семейство функций, используемых для упаковки данных различных типов в буфер оправки. В это семейство входят функции упаковки, предназначе Таблица 6.3. Фу int pvm_pkbyte(char *cp, int count, int std) ; int pvm_pkcplx(float *xp, int count, int std) ; int pvm_pkdcplx(double *zp, int count, int std) ; int pvm_pkdouble(double *dp, int count, int std) ; 6.3. Базовые механизмы PVM 237 int pvm_pkfloat(float *fp, int count, int std); int pvm_pkint(int *np, int count, int std) ; int pvm_pklong(long *np, int count, int std) ; int pvm_pkshort(short *np, int count, int std) ; int pvm_pkstr(char *cp) ; Все функции упаковки, перечисленные в табл. 6.3, используются для сохранения массиваданных в буфере отправки. Обратите вни Формат XDR (External Z>ata .Representation) — это стандарт, используемый для описания и шифрования данных. Слелует иметь в виду, что компьютеры, включенные всрелу PVM, могут быть совершенно разными, т.е. среда PVM, например, может состоять из Sun-, Macintosh-, Crays- и AMD-компьютеров. Эти компьютеры могут отличаться размерами машинных слов и по-разному сохранять различные типы данных. В некоторых случалх компьютеры могут различаться и битовой организацией. Стандарт XDR позволяет компьютерам обмениваться данными вне зависимости от типа их архитектуры. Формат Raw используется для отправки данных в собственно PvmDataDefault XDR PvmDataRaw Без специального кодирования PvmDataInPlace В буфер отправки копируются лишь указатели и раз Вот пример: int BufferId; BufferId = pvm_initsend(PvmDataRaw); //.. . Здесь константа PvmDataRaw, переданнал функции pvm_initsend в качестве параметра, означает, что данные упаковываются в буфер как есть, т.е. без специально В библиотеке PVM прелусмотрено несколько функций, имею Синопсис # include « pvm3 .h» int pvm_send(int taskid, int messageid); int pvm_psend(int taskid, int messageid, char *buffer,int len, int datatype); int pvm_mcast(int *taskid,int ntask,int messageid); В каждой из этих функций параметр taskid представл pvm_bufinfo (N, &NumBytes, &MessageId, &Ptid) ; //. . . switch(MessageId) { case 1 : // Некоторые действия, break; case 2 : // Другие действия, break //. . . } В данном случае функци За исключение Синопсис # inc lude " pvm3 . h» int pvm_recv(int taskid, int messageid) ; int pvm_nrecv(int taskid, int messageid) ; int pvm_precv(int taskid, int messageid, char *buffer, int size, int type, int sender, int messagetag, int messagelength); int pvm_trecv(int taskid,int messageid, struct timeval *timeout); int pvm_probe(int taskid , int messageid); Функция pvm_recv используется о //... float Value[10] ; pvm _recv (400002,2) ; pvn_unpkfloat(400002, Value,l) ; cout « Value.. Здесь фу Тогда как функции pvm_recv , pvm_nrecv и pvm_trecv принимают сооб PVM_STR PVM_BYTE PVM_SHORT PVM_INT PVM_FLOAT PVM_DOUBLE PVM_LONG PVM_USHORT PVM_CPLX PVM_DCPLX PVM_UINT PVM_ULONG Функция pvm_trecv позволяет программисту организовать процедуру получения сооб #include «pvm3.h» //. . . struct timeval TimeOut; TimeOut.tv_sec = 1000; int TaskId; int MessageId; TaskId = pvm_parent; MessageId = 2; pvro_trecv(TaskId,MessageId, &TimeOut) ; //... Здесь переменная TimeOut содержит член tv_sec, установленный равным ЮОО с. Структуру timeval можно использовать для установки временных значений в секундах и микросекундах. Структура timeval имеет следую struct timeval{ long tv_sec; // секунды long tv_usec; // микросекунды }; Этот пример означает, что функция pvm_trecv заблокирует вызываю Функция pvm_probe определяет, поступило ли сооб Синопсис #include «pvm3 .h» int pvm_getsbuf (void) ; int pvm_getrbuf (void) ; int pvm_setsbuf(int bufferid); int pvm_setrbuf(int bufferid); int pvm_mkbuf(int Code); int pvm_freebuf(int bufferid); В библиотеке PVM предусмотрено шесть полезных функций управления буферами, которые можно использовать для установки, идентификации и динамического создания буферов отправки и приема. Функция pvm_getsbuf используется для получения номера активного буфера отправки. Если теку PvmDataDefault XDR PvmDataRaw В зависи PvmDataInPlace Используются только указатели на данные и их размер При успешном выполнении функция pvm_mkbuf возвра
Доступ к стандартному входному потоку (stdin) и стандартному выходному потоку (stdout) со стороны PVM-задач
Среда PVM связывает воедино коллекцию ко
Получение доступа к стандартному выходному потоку (cout) из сыновней задачи
Поведение выходных данных, записанных в выходной поток stdout или по
Резюме
Библиотека PVM, отличаю
Обработка ошибок, исключительных ситуаций и надежность программного обеспечения
Одна из главных целей разработки и проектирования программного обеспечения— создать программу, которая бы отвечала требованиям пользователя и работала корректно и надежно. Пользователи требуют от ПО корректности и надежности, независимо от его конкретного назначения. Использование ненадежных программ в любой сфере — финансовой, промышленной, медицинской, научной или военной— может иметь разрушительные последствия. Зависимость людей и механизмов от ПО на всех уровнях нашего общества вынуждает его создателей сделать все возможное, чтобы их детище было надежным, робастным и отказоустойчивым. Эти требования налагают дополнительную ответственность на разработчиков и проектировщиков ПО, которые создают системы, содержащие параллелизм. Программы с параллелизмом или компоненты, которые выполняются в распределенных средах, содержат больше (по сравнению с ПО без параллелизма) программных уровней. Чем больше уровней, тем сложнее управлять таким ПО. Чем выше сложность системы, тем больше изъянов может остаться в ней невыявленными. А чем больше изъянов в ПО, тем выше вероятность того, что оно откажет, причем в самый неподходящий момент. Для программ, разбиваемых на параллельно выполняемые или распределенные задачи, характерны дополнительные сложности, которые проявляются в процессе поиска правильного решения, связанного с декомпозицией работ (work breakdown stmcture-WBS). Кроме того, здесь необходимо учитывать проблемы, которые являются неотъемлемой частью именно сетевых коммуникаций. Помимо проблем коммуникации и декомпозиции, не следует забывать о таких «прелестях» синхронизации, как «гонка» данных и взаимоблокировка. Параллельное программирование «по определению» практически всегда сложнее последовательного, а следовательно, обработка ошибок и исключительных ситуаций для параллельных программ требует больше усилий (и умственных, и физических, и временных), т.е. «больше» программирования. Интересно отметить, что разработка ПО развивается в направлении приложений, которые требуют параллельного и распределенного программирования. В проектировании современного ПО распространены Internet- и Intranet-модели. Нынче становятся нормой (а не исключением) многопроцессорные компьютеры общего назначения. Встроенные и промышленные вычислительные устройства становятся все более высокоорганизованными и мощными. Для серверного развертывания «де-факто» становится стандартом понятие кластера. Мы считаем, что нынешним разработчикам и проектировщикам ПО не остается ничего другого, как разрабатывать и проектировать надежные приложения для многопроцессорных и распределенных сред. И, безусловно, излишне повторять, что требования, предъявляемы к ПО такого рода, постоянно возрастают как по сложности, так и организации. Во многих примерах программ этой книги мы не приводим кода обработки ошибок и исключительных ситуаций, чтобы не отвлекать внимание читателя от основной идеи или концепции. Однако важно иметь в виду, что использованные здесь примеры имеют вводный характер. В действительности объем кода, посвященного обработке ошибок и исключительных ситуаций в программах, включающих параллелизм или рассчитанных на распределенную среду, довольно значителен. Обработка ошибок и исключительных ситуаций должна быть составной частью проекта ПО на каждом этапе его разработки. Мы — сторонники моделирования на основе раскрытия параллелизма в области проблемы и ее решения. И именно на этапе моделирования следует заниматься разработкой моделей подсистем обработки ошибок и исключительных ситуаций. В главе 10 показано, как можно использовать язык UML (Unified Modeling Language — унифицированный язык моделирования) для визуализации проектирования систем, требующих параллельных или распределенных методов программирования. Разработка подсистем обработки ошибок и исключительных ситуаций лишь выиграет от применения средств UML и самого процесса визуализации, который ничем другим заменить нельзя. Следовательно, в качестве исходной цели вам необходимо представить надежность разрабатываемого ПО с помощью таких инструментов, как UML, диаграммы событий, событийные выражения, диаграммы синхронизации и пр. В этой главе рассматриваются преимущества ряда методов проектирования, которые способствуют визуализации проекта подсистемы обработки ошибок и исключительных ситуаций. Кроме того, в качестве основы для разработки надежного и отказоустойчивого ПО используются встроенные средства языка С++, содержащие иерархию классов исключений.
Надежность программного обеспечения
Ошибка — это дефект в программе, который при некоторых условиях приводит к ее отказу. К отказу могут привести различные совокупности условий, причем эти условия могут повторяться. Следовательно, ошибка может быть источником не одного, а нескольких отказов. Ошибка (дефект) — это свойство программы, а не результат (свойство) ее выполнения или поведения. Именно этот смысл мы вкладываем в понятие термина «bug». Ошибка ПО — это следствие оплошности, или недоработки (error), программиста. Ошибки, которые допускает программист или разработчик ПО, могут возникнуть из-за неверной интерпретации требований к ПО или некачественного, некорректного или недостаточно полного перевода этих требований в код. Если программист совершает оплошности такого рода, он вносит в программу ошибки, или дефекты. При выполнении дефектного кода может произойти сбой программы. Ошибки ПО можно обнаружить только при выполнении кода. Очистить программу от ошибок, а следовательно, и не допустить возможность отказа, позволяет процесс тестирования и отладки ПО. Обратите внимание на то, что мы используем термины «дефект» и «ошибка» взаимозаменяемо. Термин «оплошность» мы относим к допускаемым программистом промахам, которые являются причиной дефектов ПО. Отказоустойчивость—это свойство, которое позволяет некоторой части ПО оставаться в исправном состоянии или восстанавливать работоспособность после программных сбоев, вызванных ошибками, внесенными в ПО в результате недоработки программистов. Одни отказы ПО являются результатом наличия дефектов в программах, другие же— результатом исключительных условий (необязательно созданными оплошностью программиста), которые могут создаться в оборудовании или используемых программных пролуктах. Например, сетевая карта, поврежденная в результате всплеска напряжения, может привести соответствующую часть ПО к сбою. Вирус может нарушить процесс передачи данных, в результате чего может отказать программа, которая зависит от этого процесса. Пользователь может нечаянно удалить критические компоненты из системы, что неминуемо приведет к ее отказу. Перечисленные выше неприятности вызываются не из-за дефектов в программе, а создаются в результате условий, которые мы называем исключительными сигуациями.
Отказы в программных и аппаратных компонентах
При проектировании надежного и отказоустойчивого ПО мы должны поставить цель создать такое ПО, которое бы продолжало функционировать даже после отказа некоторых ero компонентов (аппаратных или программных). Если наше ПО претен-лует на то, чтобы называться отказоустойчивым, оно должно обладать средствами, которые могли бы прелусматривать последствия аппаратных или программных ошибок. По крайней мере наши отказоустойчивые проекты должны обеспечивать не мгновенное прекращение работы системы, а постепенное сокращение ее возможностей. Если наше ПО является отказоустойчивым, то в случае отказа отдельного его компонента (компонентов) оно должно продолжать функционирование, но на более низком уровне. Ошибки, которые наше ПО должно обрабатывать, можно разделить на две категории: программные и аппаратные. На рис. 7.1 показана схема некоторых аппаратных компонентов, а также уровни ПО, которые могут включать ошибки. На рис. 7.1 мы отделили аппаратные компоненты от программных, поскольку методы обработки аппаратных сбоев часто отличаются от методов обработки программных ошибок. Здесь также выделены различные уровни ПО. Некоторые из них находятся вне «досягаемости» разработчика (т.е. он не может ими управлять напрямую) и требуют специального рассмотрения процесса обработки исключений и ошибок. На этапах проектирования, разработки и тестирования ПО обязательно следует принимать во внимание возможность аппаратных сбоев и наличия ошибок в различных «слоях» ПО. Для программ, которым присущ параллелизм или состоящих из распределенных компонентов, следует учитывать дополнительные обстоятельства, весьма «благоприятные» для возникновения аппаратных сбоев. Например, в распределенных программах используется взаимодействие аппаратных и программных средств. Ошибка, «закравшался» в компонент, отвечающий за это взаимодействие, может привести к отказу всей системы. Программы, разработанные для параллельной работы процессоров, могут сбоить, если ожидаемое количество процессоров окажется недос-гупным. Даже если средства связи и процессоры прекрасно отработали при загрузке системы, ее отказ возможен в любой момент после начала функционирования. Исключительная ситуация может возникнуть в любом из компонентов оборудования и на любом уровне ПО. Кроме того, каждый программный уровень может содержать дефекты, которые необходимо каким-то образом обрабатывать. На этапе проектирования ПО следует рассматривать возможные исключительные ситуации и ошибки в программах, присущие каждому уровню ПО в отдельности. Ведь варианты восстановления приложения после возникновения исключительных ситуаций и исправления ошибок, которые возможны на уровне 2, отличаются от вариантов, применимых к уровню 3. К сбоям, которые возможны на различных уровнях ПО и в аппаратных компонентах, следует добавить сбои, характеризующиеся архитектурной областью локализации, специфической для каждого приложения. Например, на рис. 7,2 показано, как по мере увеличения дистанции между задачами возрастает уровень сложности обработки ошибок и исключительных ситуаций. Чем больше в программных или аппаратных компонентах дистанция между параллельно выполняющимися задачами, тем более высокий уровень организации требуется для проектирования компонентов обработки исключительных ситуаций иошибок. Изучив рис. 7.1 и 7.2, можно понять: для того, чтобы спроектировать и разработать надежное ПО, необходимо прелусмотреть не только, какие возможны исключительные ситуации и ошибки, но и где они могут возникнуть.
Определение дефектов в зависимости от спецификаций ПО
Спецификация ПО — это своего рода «эталон», позволяющий определить, имеет ли данная часть ПО дефекты. Мы не можем оценить корректность программных компонентов без доступа к программным спецификациям. Спецификация ПО содержит описание и требования, из которых должно быть ясно, что должен делать данный программный компонент и чего он делать не должен. Общеизвестно, что довольно трудно написать полные, исчерпывающие и точные спецификации. Спецификации могут представлять собой формальные документы и требования, составленные конечными пользователями, аналитиками, специалистами по созданию пользовательского интерфейса, специалистами в предметной области и др. Спецификации могут также выглядеть как множество целей и не жестко определенных задач, устно излагаемых пользователями проектировщикам и разработчикам ПО. Любое отклонение компонента ПО от его спецификации является дефектом. Чем выше качество спецификации, тем проще выявить дефекты и понять, где программист сделал ошибки. Если спецификация проекта расплывчата, с плохо определенными элементами и нечетко описанными требованиями, то определение протраммных дефектов для такого проекта представляет собой движущуюся мишень. Если спецификации неоднозначны, то трудно сказать, что дефектно, а что нет. Точно так же невозможно утверждать, прав ли был разработчик. Туманно определенные спецификации являются причиной так же туманно определяемых ошибок. В таких условиях создание отказоустойчивого и надежного ПО попросту невозможно.
Обработка ошибок или обработка исключительных ситуаций?
В общем случае ошибки ПО (которые являются результатом оплошности или недоработки программиста) должны быть обнаружены и исправлены на этапах тестирования, перечисленных в табл. 7.1. Таблица 7 .1. Типы тестирования, используемые в процессе разработки ПО (unit testing) ПО тестируется поэлементно. Под элементом может подразумеваться отдельный про раммный модуль, коллекция модулей, функция, процедура, ал оритм, объект, про рамма или компонент (integration testing) Тестируется некоторая совокупность элементов. Элементы объединяются влогические группы, и каждая группатестирует-ся как единый блок (элемент). Эти группы могут подвергаться одинаковым проверкам. Если группа элементов проходит тест, ее присоединяют к тестируемой совокупности, которая в свою очередь должна быть протестирована с новым дополнением. Увеличение количества элементов, подлежащих тестированию, должно подчиняться формулам комбинаторики (regression testing) Программные модули должны повторно тестироваться, если в них были внесены изменения. Регрессивное тестирование дает гарантию, что изменение любого компонента не приведет к потере функциональности (stress testing) Тестирование, которое проводится для компонента или всей системы при предельных и «запредельных» значениях входных параметров. Использование траничных условий позволяет определить, что может произойти с компонентом или системой в нештатных ситуациях (o erational testing) Тестирование системы с полной нагрузкой. Для этого используется реальнал среда, создающая реальную нагрузку. Этот тип тестирования также применяется для определения производительности системы в совершенно незнакомой среде (s ecification testing) Компонент проверяетс при сравнении с исходными спецификаци ми. Именно спецификаци устанавливает, какие компоненты включены в систему и какие взаимоотношения должны быть между ними. Этот этап вл ется частью процесса верификации ПО (acce tance testing) Тестирование этого типа выполняется конечным пользователем модуля, компонента или системы для определения его (ее) производительности. Этот этап является частью процесса аттестации ПО Во время процесса тестирования и отладки программные дефекты должны быть обнаружены и ликвидированы. Однако исключительные ситуации (исключения) обрабатываются во время выполнения программы. Следует различать исключительные и нежелательные условия. Например, если мы спроектировали программу, которая будет добавлять в список числа, вводимые пользователем, а пользователь будет вводить и числа, и символы, которые не являются числами, то такая ситуация относится к нежелательной, а не к исключительной. Мы должны проектировать программы, которые были бы робастными, т.е. устойчивыми к ошибкам, прелусматривал проверку корректности входных данных. Ввод данных в программу должен быть организован таким образом, чтобы пользователь был вынужден вводить данные, которые требуются нашей программе для надлежащего выполнения. Если, например, спроектированный нами компонент программы сохраняет информацию на внешнем устройстве, и программа попадает в ситуацию отсутствия свободного пространства на этом устройстве, то такие условия работы программы также можно назвать нежелательными, а не исключительными, или экстраординарными. Исключительные ситуации мы связываем с необычными условиями, а не с нежелательными. Методы обработки исключительных ситуаций предназначены для непредвиденных обстоятельств. Ситуации же, которые являются нежелательными, но вполне возможными и потому предсказуемыми, должны обрабатываться с применением обычной программной логики, например: if <входные данные неприемлемы, то> <повторно запрашиваем входные данные> else <выполняем нужную операцию> end if Такая проверка условий — одна из основополагающих граней искусства программирования. Продемонстрированный стиль программирования позволяет не допустить возникновения многих проблем, но эта модель ситуации не «дотягивает» до определения исключительной. Существуют различия между дефектами и исключительными ситуациями, а также между исключительными ситуациями и нежелательными условиями. С дефектами справляются путем тестирования и отладки. Нежелательные условия обрабатываются в рамках обычной программной логики, а исключительные ситуации — методами обработки исключений. Различия между характеристиками обработки ошибок, исключений и нежелательныхусловий сведены в табл. 7.2. Таблица7.2. Различия между характеристиками обработки ошибок, исключений и нежелательных условий Логические ошибки обнаруживаются на этапе тестирования и отладки Описывает непредвиденные условия во время выполнения Описывает нежелательные условия, которые весьма вероятны во время выполнения Корректно работающие программы не содержат ошибок Корректно написанные программы могут попадать в исключительные ситуации Корректно написанные программы могут попадать в нежелательные ситуации Для предупреждения и исправления ошибок используется программная логика Для восстановления работоспособности программы после возникновения исключительных ситуаций используются методы обработки исключений Для исправления нежелательных условий используется программная логика Поддерживается нормальный ход выполнения программы Нормальный ход выполнения программы нарушается Делается попытка поддержать нормальный ход выполнения программы Наша цель — так построить компоненты обработки ошибок и обработки исключений, чтобы затем их можно было объединить с другими компонентами, составляющими параллельные или распределенные приложения. Эти компоненты должны обладать средствами идентификации проблем и уведомления о них, а также возможностями их корректировки или восстановления работоспособности приложения. Под восстановлением и корректировкой подразумеваются самые различные способы достижения поставленной цели: от предложения пользователю еще раз ввести данные (с подсказкой, например, их правильного формата) до перезагрузки подсистемы в рамках ПО. Действия по восстановлению и корректировке могут включать обработку файлов, возврат из базы данных, изменение сетевого маршрута, маскирование процессоров, повторную инициализацию устройств, а для некоторых систем даже замену элементов оборудования. Компоненты обработки ошибок и исключительных ситуаций могут быть выполнены в различных формах: от простых предписаний до интеллектуальных агентов, единственное назначение которых состоит в предвидении ситуаций сбоя и их предотвращении. Компонентам обработки ошибок и исключений в ответственных участках ПО уделяется значительное внимание. Архитектура упрощенного компонента обработки ошибок представлена на рис. 7.3. Компонент 1 на рис. 7.3— это простой компонент отображения (map), который содержит список номеров ошибок и их описания. Компонент 2 содержит объект, который преобразует номера ошибок в адреса переходов, функций или подсистем. По номеру ошибки компонент 2 определяет направление перехода. Компонент 3 преобразует номера ошибок в иерархическую структуру отчетов и логику отчетов. Иерархическая структура отчетов содержит данные о том, кого (или что) необходимо уведомить об ошибке. Логика отчетов определяет, что должно включать это уведомление. Компонент 4 содержит два объекта отображения. Первый преобразует номера ошибок в объекты, назначение которых — скорректировать некоторые ситуации сбоя (условия). Второй преобразует номера ошибок в объекты, которые возвращают систему в стабильное или хотя бы частично стабильное состояние. Упрощенный компонент обработки ошибок, показанный на рис. 7.3, можно применить к ПО любого размера и формы. Характер использования компонентов обработки ошибок и исключительных ситуаций определяется требуемой степенью надежности ПО.
Надежность ПО: простой план
Напомним, что мы различаем ошибочные и неудобные (нежелательные) условия. Неудобные или нежелательные условия должны обрабатываться обычной программной логикой. Ошибки (дефекты) требуют специального программирования. В книге Страуструпа • Вариант1. Завершить программу. • Вариант 2. Возвратить значение, обозначающее «ошибку». • Вариант 3. Возвратить значение, обозначающее нормальное завершение, и оставить программу в состоянии с необработанной ошибкой. • Вариант 4. Вызвать функцию, предназначенную для вызова в случае ошибки. Эти четыре альтернативы можно «примерить» к отношениям типа «изготовитель-потребитель». Очевидно, что завершать программу при каждом обнаружении ошибки попросту неприемлемо. Здесь мы согласны со Страуструпом. В таких случалх следует поступать более изобретательно. Что касается варианта 2, то примитивный возврат значения ошибки действительно может помочь в некоторых ситуациях, но далеко не во всех. Не каждое возвращаемое значение может интерпретироваться как успешное или неудачное. Например, если значение, возвращаемое некоторой функцией, имеет вещественный тип, и область определения функции включает как отрицательные, так и положительные значения, то какое тогда значение функции можно использовать для представления ошибки? Другими словами, это не всегда возможно. С нашей точки зрения, вариант 3 также неприемлем. Ведь если «изготовитель» возвращает значение, обозначающее нормальное завершение, «потребитель» продолжит работу, предположив, что его запрос был выполнен, а это может вызвать еще большие проблемы. Осталось рассмотреть вариант 4. Он требует более внимательного подхода при обсуждении обработки как ошибок, так и исключительных ситуаций.
План А: модель возобновления, план Б: модель завершения
При обнаружении ошибки или исключительной ситуации существует два основных плана для реализации варианта4. Первый план состоит в попытке скорректировать условия, которые вызвали сбой, а затем возобновить выполнение с точки, в которой была обнаружена ошибка или исключительнал ситуация. Этот подход называетс
Использование объектов отображения для обработки ошибок
Компонент отображения (map) можно использовать как составную часть стратегии обработки ошибок или обработки исключений. Назначение отображения — связать один элемент с другим. Например, отображение можно использовать для связи номеров ошибок с их описаниями: //.. . map ErrorTable[123] = «Деление на нуль»; ErrorTable[4556] = «Отсутствие тонального вызова»; //. . . Здесь число 123 связано с описанием «Деление на нуль». Тогда при выполнении инструкции cout « ErrorTable[123] « endl; в объект выходного потока cout будет записана строка «Деление на нуль». Помимо отображения встроенных типов данных, можно также отображать (т.е. находить соответствие) определенные пользователем объекты, содержащие данные встроенных типов. Вместо того, чтобы некоторое отображение просто возвращало описательное сообщение для каждого номера ошибки, можно позаботиться о том, чтобы оно возвращало объект с соответствующим номером ошибки. Этот объект может иметь методы, предназначенные для коррекции ошибок, составления отчетов об ошибках и их регистрации (записи ошибок в системный журнал). Например, предположим, что у нас есть следующий определенный пользователем объект: defect_response: class defect_response{ protected: //. . . int DefectNo; string Explanation; public: bool operator<(defect_response &X); virtual int doSomething(void); string explanation(void); //... }; Теперь мы можем внести в отображение объекты типа defect_response: //... map defect_response * Response; Response = new defect_response; ErrorTable[123] = Response; //... Этот код связывает объект отклика (на ошибку) с номером ошибки 123. Благодаря полиморфизму объект отображения может содержать указатели на любой объект типа defect_response или любой объект, который выведен из него. Предположим, что у нас есть следующий класс: class exception_response : public defect_response{ //.. . public: int doSomething(void) //... }; Этот класс exception_response является потомко //... map defect_response * Response; exception_response *Response2; Response = new defect_response; Respone2 = new exception_response; ErrorTable[123] = Response; // Хранит объект типа // defect_response. ErrorTable[456] = Response2; // Хранит объект типа // exception_response. //... Это определение означает, что объект типа ErrorTable может связывать с соответствующим но //... defect_response *ProblemSolver; ProblemSovler = ErrorTable[123]; ProblemSolver->doSomething; ProblemSovler = ErrorTable[456]; ProblemSovler->doSomething; //... Несмотря на то что Переменная ProblemSolver представляет собой указатель на объект defect_response, полиморфизм позволяет этой переменной указывать на объект типа exception_response или любой другой объект, выведенный из класса defect_response. Поскольку метод doSomething объявлен виртуальным в классе defect_response, компилятор может выполнить динамическое связывание. Это дает гарантию корректного вызова метода doSomething при выполнении приложения. Именно динамическое связывание позволяет каждому потомку класса defect_response определить собственный метод doSomething . Нам нужно, чтобы вызов метода doSomething зависел от того, ссылка на какой именно потомок класса defect_response используется при этом. Рассматриваемый метод позволяет связывать номера ошибок с объектами, имеющими отношение к обработке определенных сбойных ситуаций. С помощью этого метода можно значительно упростить код обработки ошибок. В листинге 7.1, например, показано, как значение, возвращаемое некоторой функцией, можно использовать для выбора соответствующего объекта обработки ошибок. // Листинг 7.1. Использование значений, возвращаемых // функцией, для определения корректного // объекта типа ErrorHandler void importantOperation(void) { //. . . Result = reliableOperation; if(Result != Success){ defect_response *Solver; Solver = ErrorTable[Result]; Solver->doSomething; } else{ // Продолжение обработки. } // . . . } В листинге 7.1 обратите внимание на то, что мы не используем последовательность if- или case-инструкций. Объект отображения позволяет получить непосредственный доступ к желаемому объекту обработки ошибок по индексу. Конкретный метод doSomething, вызываемый в листинге 7.1, зависит от значения переменной Result. Безусловно, данный пример демонстрирует упрощенную схему обработки ошибочных ситуаций. Так, например, в листинге 7.1 не показано, кто (или что) отвечает за управление динамически выделяемой памятью для объектов, хранимых в отображении ErrorTable. Кроме того, здесь не учтено, что функции reliableOperation и doSomething могут выполниться неудачно. Поэтому реальный код будет, конечно же, несколько сложнее, чем тот, что приведен в листингe 7.1. Но все же этот пример ясно показывает, как одним «ударом» обработать множество ситуаций сбоя. Мы можем пойти еще дальше. В листинге 7.1 предполагается, что все возможные ошибки будут охвачены объектами типа ErrorTable. Все ErrorTable-объекты представляют собой либо объекты типа defect_response, либо объекты, выведенные из класса defect_response. А что, если у нас будет несколько семейств классов обработки ошибок? В листинге 7.2 показано, как с помощью шаблонов сделать функцию importantOperation более общей. // Листинг 7.2. Использование шаблона в функции // importantOperation template T ErrorTable; //.. . U *Solver; //... Solver = ErrorTable[Result]; Solver->doSomething ; //... }; В листинге 7.2 тип ErrorTable не ограничен объекта
Механизмы обработки исключительных ситуаций в С++
В идеале во время тестирования и отладки должны быть ликвидированы все дефекты протраммы или по крайней мере максимально возможное их количество. Кроме того, следует обработать нежелательные и неудобные условия с использованием обычной программной логики. После устранения всех (или почти всех) дефектов и обработки нежелательных и неудобных условий все остальные «неприятности» попадают в разряд исключительных ситуаций. Обработка исключительных ситуаций в С++ по void importantOperation { / / executeImportCode // Возникает исключительная ситуация. impossible_condition ImpossibleCondition; throw ImpossibleCondition; //... } catch (impossible_condition &E) { // Выполнение действий, связанный с объектом E. //... } Функция importantOperation( ) пытается выполнить свою работу и сталкивается с необычными условиями, с которыми она не в состоянии справиться. В нашем примере она создает объект типа impossible_condition и использует ключевое слово throw для генерирования этого объекта. Блок кода, в котором используется ключевое слово catch, предназначен для перехвата объектов типа impossible_condition. Этот блок кода называется try{ //... importantOperation; //. . . } catch(impossible_condition &E) { // Выполнение действий, связанных с объектом E. // - . . Здесь при выполнении функции importantOperation возможно возникновение условий, с которыми она не в состоянии справиться. В этом случае функция сгенерирует исключение, в результате чего управление будет передано первому обработчику, который принимает объект исключений типа impossible_condition. Этот обработчик либо сам справится с этой исключительной ситуацией, либо сгенерирует исключение, с которым придется иметь дело другому обработчику исключений. Объекты, генерируемые при исключительных ситуациях, могут быть определены пользователем, причем они могут просто содержать коды ошибок или сообщения об ошибках, которые способны помочь обработчику исключений выполнить его работу. Если бы мы использовали объекты, подобные объектам типа exception_response из листингов 7.1 и 7.2, то обработчик исключений мог бы применить их для решения проблемы либо для восстановления работоспособного состояния программы. Для создания объектов исключений можно также использовать встроенные С++-классы исключений.
Классы исключений
Стандартная библиотека классов С++ содержит девять классов исключений, разделенных на две основные группы (группа динамических ошибок и группа логических ошибок), которые приведены в табл. 7.3. Группа Таблица 7.3. Классы ди амических и логических ошибок range_error domain_error underflow_error invalid_argument overflow_error length_error out_of_range
Классы runtime__error
На рис. 7.4 показана схема отношений между классами для семейства классов runtime_error. Это семейство выведено из класса exception. Из класса runtime_error выведено три класса: range_error, overflow_error Hunderflow_error, которые сооб Каждый класс обеспечивает определен // Листинг 7.3. Генерирование объекта класса exception и // объекта класса logic_error try{ exception X; throw(X) ; } catch(const exception &X) { cout « X.what << endl; } try{ logic_error Logic(«JIorn4ecKaH ошибка»); throw(Logic); } catch(const exception &X) { cout << X.what « endl; } Объекты базового класса exception обладают лишь конструкторами, деструкторами, средствами присваивания, копирования и простейшего вывода отчетной информации. При сбое они не способны его скорректировать. Здесь можно рассчитывать лишь на вывод сообщения об ошибке, возвращаемого методом what классов исключений. Это сообщение будет определяться строкой, переданной конструктору для объекта класса logic_error. В листинге7.3 переданная конструктору строка «Логическая ошибка» будет возвращена методом what в catch-блoкe и выведена в виде сообщения.
Классы logic_error
Семейство классов logic_error выведено из класса exception. И в самом деле, большинство функций классов этого семейства также унаследовано от класса exception. Класс exception содержит метод Подобно классам семейства
Выведение новых классов исключений
Классы исключений можно использовать как есть, т.е. просто для вывода сообщений с описанием происшедших ошибок. Но в качестве метода обработки исключений такой подход практически бесполезен. Просто знать о возникновении исключительной ситуации — не слишком большой шаг на пути повышения надежности ПО. Реальная польза иерархии классов исключений состоит в обеспечении ими архитектурной карты дорог для проектировщика и разработчика. Классы исключений предусматривают основные типы ошибок, которые разработчик может уточнить. Многие исключительные ситуации, которые возникают в среде выполнения, можно было бы отнести к категориям, «охватываемым» семействами классов logic_error или runtime_error. В качестве примера возьмем класс runtime_error и продемонстрируем, как можно «сузить» его специализацию. Класс runtime_error является потомком класса exception. Специализацию класса можно определить с помощью механизма наследования. Вот пример: class file_access_exception : public runtime_error{ protected: //... int ErrorNumber; string DetailedExplanation; string FileName; //... public: virtual int takeCorrectiveAction(void); string detailedExplanation(void); //... }; Здесь класс file_access_exception наслелует класс runtime_error и получает специализацию путем добавления нескольких членов данных и функций-членов. В частности, добавляется метод takeCorrectiveAction . Этот метод можно использовать в качестве вспомогательного средства, с помощью которого обработчик исключений мог бы выполнять работу по коррекции ситуации и восстановлению работоспособности программы. Объект класса file_access_exception «знает», как идентифицировать взаимоблокировку и как ее прекратить. Кроме того, он содержит специализирован try{ //... fileProcessingOperation; //.. . } catch(file_access_exception &E) { cerr « E.what << endl; cerr « E.detailedExplanation « endl; E.takeCorrectiveAction; // Обработчик выполняет дополнительные действия // по корректировке ситуации. //.. . } Этот метод позволяет создать объекты отображения ExceptionTable, подобные объектам отображения ErrorTable из
Защита классов исключений от исключительныхситуаций
Объекты исключений генерируются в случае, когда некоторый программный компонент сталкивается с аномалией программного или аппаратного характера. Однако следует отметить, что объекты исключений сами не должны генерировать исключений. Ведь если окажется, что обработка одной исключительной ситуации слишком сложна и потенциально может вызвать возникновение другой исключительной ситуации, то схему такой обработки необходимо пересмотреть, упростив ee везде, где только это возможно. Механизм обработки исключительных ситуаций неоправданно усложняется именно тогда, когда код обработчика может генерировать исключения. Именно поэтому большинство методов в классах исключений содержат пустые спецификации throw-инструкций. // Объявление класса исключения. class exception { public: exception throw {} exception(const exception&) throw {} exception& operator=(const exception&) throw {return *this;} virtual ~exception throw {} virtual const char* what const throw; }; Обратите внимание на отсутствие аргументов в объявлениях throw -методов. Пустые аргументы означают, что данный метод не может сгенерировать исключение [12]. Если он попытается это сделать, во время компиляции будет выдано сообщение об ошибке. Если базовый класс не может сгенерировать исключение, то соответствующий метод в любом производном классе также не сделает этого.
Диаграммы событий, логические выражения и логические схемы
Обработку исключительных ситуаций необходимо использовать в качестве «последней линии обороны», поскольку ее механизм в корне меняет естественную передачу управления в программе. Существуют схемы, которые пытаются замаскировать этот факт, но эти схемы обычно не характеризуются гибкостью, достаточной для программ, реализующих методы параллелизма или распределения. В подавляющем большинстве ситуаций, в которых есть соблазн использовать обработчики, перехватывающие абсолютно все исключения, программную логику можно сделать более ошибкоустойчивой с помощью ее усовершенствования или жесткой обработки ошибок. Для облегчения идентификации ко Мы используем диаграммы событий для построения схемы действия обработчика исключительных сигуаций. На рис. 7.6 схематично изображена система, состоящая из семи задач, помеченных буквами А, В, С, D, E, F и H. Обратите внимание на то, что каждая метка (обозначающал задачу) расположена над переключателем. Если переключатели закрыты, компонент функционирует, в противном случае — нет. Крайняя точка слева представляет начало, а крайняя точка справа — конец выполнения. Для успешного завершения программы необходимо найти путь через действующие компоненты. Попробуем продемонстрировать, как применить эгу диаграмму к нашему случаю обработки исключений. Предположим, что мы начинаем программу с выполнения задачи А. Чтобы успешно завершить программу, необходимо корректно решить обе задачи А и С. На языке диаграммы это означает, что переключатели А и С должны быть закрыты. На нашей диаграмме событий переключатели А и С находятся на одной ветви, что свидетельствует об их параллельном выполнении. Если произойдет отказ в любой из этих задач (А или С), будет сгенерировано исключение. Обработчик исключений мог бы снова начать выполнение задач А и С. Однако анализ нашей диаграммы событий показывает, что завершение всей программы будет успешным, если успешным будет выполнение либо ветви АС, либо ветви DE, либо ветви FBH. Поэтому мы проектируем наш обработчик исключений таким образом, чтобы он выполнял один из альтернативных наборов компонентов (например, DE или FBH). Наборы компонентов (AC, DE и FBH) связаны между собой отношением ИЛИ. Это значит, что к успешному завершению программы приведет успешное выполнение любого набора параллельно выполняемых компонентов. Таким образом, простая диаграмма событий (см. рис. 7.6) позволяет понять, как следует построить обработчик исключений. Выражение S =(AC + DE + FBH) часто называют логическим выражением, или булевым. Это выражение означает, что для пребывания системы в устойчивом состоянии (т.е. ее надежной работы) необходимо успешное выполнение одной из следующих групп задач: (А и С) или (D и E) или (F и В и H). По диаграмме событий нетрудно также понять, какие комбинации отказов компонентов могут привести к отказу системы. Например, если откажут только компоненты E и F, то система успешно отработает, если при этом «не подвелут» компоненты А и С. Но если бы дали сбой компоненты А, D и H, то систему в этом случае уже ничего бы не спасло от отказа. Диаграмма событий и логическое выражение — это очень полезные средства для описания параллельных зависимых и независимых компонентов, а также для построения схемы действия обработчика исключительных ситуаций. Например, используя диаграмму событий (см. рис. 7.6), мы можем наметить следующий подход к обработке исключений для нашего примера: try{ start(task А and В) } catch(mysterious_condition &E) { try{ if(!(А && В)){ start(F and В and H) } } catch(mysterious_condition &E){ start(D and E) } }; Этот вид стратегии призван улучшить надежность системы. Слелует также отметить, что параллельно выполняемые программные компоненты и альтернативные варианты для планирования безотказной работы системы можно отобразить с помощью традиционной логической схемы, показанной на рис. 7.7. Итак, на рис. 7.7 показано три И-схемы, объединяемые на основе ИЛИ-отношений для получения результата S (который означает успешное завершение работы системы). Диаграмма событий (см. рис. 7.6) и логическая схема (см. рис. 7.7) — это примеры простых методов, которые можно использовать для визуализации критических путей (ветвей) и критических компонентов в некоторой части ПО. После идентификации критических путей и компонентов разработчик должен прелусмотреть ответные действия, которые должна выполнить система в случае, если откажет любой из критических компонентов. Если при этом используется модель завершения, то обработчик исключений не делает попытку возобновить выполнение ПО с точки, в которой возникла исключительная ситуация. Вместо этого осуществляется выход из функции или процедуры, в которой произошло исключение, и предпринимаются действия по пе-револу системы в стабильное (насколько это возможно) состояние. Но если используется модель возобновления, то корректируются условия, создавшие аномалию, и программа возобновляется с точки, в которой возникла исключительнал ситуация. Важно отметить, что при реализации модели возобновления возможны определенные трудности. Например, предположим, что наш код содержит слелующую последовательность вложенных вызовов процедур: try{ А вызывает В В вызывает С С вызывает D D вызывает E E сталкивается с аномалией, с которой не может справиться } catch(exception Q) { } Если в процелуре E возникла аномалия и было сгенерировано исключение, то возможна проблема со стеком вызовов. Нужно также решить вопрос с разрушением объектов и проблему «подвешенных» значений, возвращаемых процедурами. Подумайте, что произойдет, если процедуры С и D являются рекурсивными? Даже если мы откорректируем условие, вызвавшее исключение в процедуре E, то как вернуть программу в состояние, в котором она пребывала непосредственно перед выбросом исключения? А ведь мы должны сохранить информацию в стеке, таблицы создания и разрушения объектов, таблицы прерываний и пр. Это потребует больших затрат и обеспечения сложного взаимодействия между вызывающими и вызываемыми сторонами. Bce вышесказанное обозначило лишь поверхностный слой трудностей. Из-за сложности реализации модели возобновления и благодаря тому факту, что разработка больших систем может обойтись без нее, для С++ была выбрана модель завершения. В книге [44] Страуструп дает полное обоснование того, почему комитет ANSI в конце концов выбрал для механизма обработки исключений модель завершения. Но если, несмотря на то что модель возобновления действительно сопряжена с большими трудностями, надежность и бесперебойность ПО являются критичными факторами, то лля реализации этой модели все же имеет смысл приложить соответствующие усилия. При этом стоит иметь в виду, что С++-средства обработки исключений можно использовать и для реализации модели возобновления.
Резюме
Создание надежного ПО — серьезное занятие. К вопросам обработки исключительных ситуаций и исправления ошибок следует подходить с особой ответственностью. Тщательное тестирование и отладка каждого компонента ПО должны быть основными средствами защиты от программных дефектов. Обработку исключений необходимо внести в систему или подсистему ПО после того, как оно прошло этап строжайшего тестирования. Механизм генерирования исключений не следует использовать в качестве общего правила для обработки ошибок, поскольку он нарушает обычный ход выполнения программы. К средствам генерирования исключений следует прибегать только после того, как будут исчерпаны все остальные меры. Программист, который планирует проектировать более полные и полезные (с его точки зрения) классы исключений, должен использовать стандартные классы обработки исключений в качестве архитектурных «дорожных карт». Стандартные классы, не специализированные с помощью наследования, могут лишь уведомлять об ошибках. Можно создать более полезные классы исключений, которые бы обладали корректирующими функциями и большей информативностью. В общем случае, как модель завершения, так и модель возобновления позволяют продолжать выполнение программы. Обе эти модели предлагают альтернативу простому прерыванию программы при обнаружении ошибки. Более полное рассмотрение темы обработки исключительных ситуаций можно найти в работе [44].
Распределенное объектно-ориентированное программирование
Распределенные объекты — это объекты, которые являются частью одного приложения, но размещены в различных Необходи • Необходимые ресурсы (например, базы данных, специализированные процессоры, модемы, принтеры и т.п.) расположены на рааличных компьютерах. Клиентские объекты (объекты, формирующие запрос на обслуживание) взаимодействуют с серверными объектами (объектами, реагирующими на запрос обслуживания) для получения доступа к этим ресурсам. • Для выполнения некоторой важной работы или решения насущной проблемы необходимо скооперировать объекты, различающиеся временем разработки, разработчиками и местоположением. • Агенты, реализованные как объекты, отличающиеся узкой специализацией, требуют собственных адресных пространств, поскольку они запускаются как отдельные процессы. • Объекты используются в качестве базовых модулей, которые реализованы как отдельные программы, каждая из которых имеет собственное адресное пространство. • Объекты реализованы в SPMD- или МРМП-архитектуре, рассчитанной на использование параллельного программирования, причем эти объекты расположены в различных процессах и на различных компьютерах. В объектно-ориентированном приложении выполняемая программой работа делится между несколькими объектами. Эти объекты представляют собой модели определенной реальной личности, реального места, предмета или идеи. Выполнение объектно-ориентированной программы вынуждает ее объекты взаимодействовать между собой в соответствии с правилами, заложенными в этой модели. В распределенном объектно-ориентированном приложении некоторые взаимодействующие объекты будут создаваться различными программами, которые, возможно, выполняются на различных компьютерах. В главе 3 упоминалось о том, что каждая выполняемал программа включает один или несколько процессов. Каждый процесс обладает собственными ресурсами. Например, любой процесс имеет собственную память, дескрипторы файлов, стековое пространство, идентификатор и т.п. Задачи, выполняемые в одном процессе, не имеют прямого доступа к ресурсам, принадлежащим другому процессу. Если задачам, выполняемым в одном процессе, необходима информация, хранимая в памяти другого процесса, то эти два процесса должны явно обменяться информацией с помощью файлов, каналов, общей памяти, переменных среды или сокетов. Объекты, которые принадлежат различным процессам и нуждаются во взаимодействии между собой, также должны использовать один из перечисленных выше способов явного обмена информацией. Как правило, С++-разработчик при разработке распределенного объектно-ориентированного приложения сталкивается с необходимостью решения следующих проблем. • Деко • Обеспечение связи между объектами, принадлежащими различным процесса • Синхронизация взаимодействия между локальны • Обработка ошибок и исключений в распределенной среде.
Декомпозиция задачи и инкапсуляция ее решения
Проектирование объектно-ориентированного программного обеспечения — это процесс перевода требований к ПО в проект, в котором с помощью объектов моделируется каждый аспект разрабатываемой системы и выполняемой ею работы. Центральное место в этом проекте отводится структуре и иерархии коллекций объектов, а также их взаимоотношениям и взаимодействиям. Для поддержки понятия модели ПО в С++ используется ключевое слово class. Существует два базовых типа моделей. Первый тип модели — масштабированное представление некоторого процесса, концепции или идеи. Этот тип модели используется для анализа или экспериментирования. Например, класс применяется для разработки модели молекулы, т.е. с помощью концепции С++-класса можно смоделировать гипотетическую структуру некоторого химического процесса, происходящего в молекулах. Программным путем можно затем изучить поведение молекулы при внедрении новых групп атомов. Второй тип модели ПО — воспроизведение некоторой реальной задачи, процесса или идеи. Цель этой модели — заставить некоторую часть системы ПО или приложения функционировать подобно ее «прототипу». В этом случае ПО занимает место некоторого компонента или некоторого физического предмета в неавтоматизированной системе. Например, мы можем использовать концепцию класса для моделирования калькулятора. При корректном моделировании всех его характеристик и поведения можно создать экземпляр этого класса и использовать в качестве настоящего калькулятора. Программный калькулятор здесь будет играть роль реального калькулятора. Таким образом, смоделированный нами класс может служить в качестве виртуального лублера некоторого реального лица, места, предмета или идеи. Главное в программной модели — ухватить суть реального предмета. Во всех моделях, показанных на рис. 8.1, предполагается, что объекты могут быть на одном и том же или на разных компьютерах (главное — они принадлежат разным процессам). Уже сам факт принадлежности рааличным процессам делает объекты распределенными [13]. Все модели представляют рааличные подходы к распределению работы приложения между объектами.
Взаимодействие между распределенными объектами
Если объекты относятся к одному и тому же процессу, то в качестве средств межобъектно Для реализации связи между распределенными объектами разработан ряд протоколов. Двумя наиболее важными из них являются
Синхронизация взаимодействия локальных и удаленных объектов
Для синхронизации доступа к данным и ресурсам со стороны нескольких объектов, принадлежащих различным процессам, но расположенных на одном компьютере, можно использовать мьютексы и семафоры, поскольку каждый процесс, хотя и отделенный от других, все же получает доступ к системной памяти компьютера. Эту системную память функционально можно рассматривать как разновидность памяти, разделяемой между процессами. Но если процессы распределены между различными компьютерами, то следует помнить, что разные компьютеры не имеют никакой общей памяти, и поэтому схемы синхронизации в этом случае должны быть реализованы по-другому. Синхронизация доступа (в зависимости от используемой WBM-модели) может потребовать интенсивного взаимодействия между распределенными объектами. Поэтому мы расширим традиционные методы синхронизации с помощью коммуникационных возможностей спецификации CORBA.
Обработка ошибок и исключений в распределенной среде
Возможно, одной из самых сложных областей обработки исключительных ситуаций или ошибок в распределенной среде считается область частичных отказов. В распределенной системе могут отказать один или несколько компонентов, в то время как другие компоненты будут функционировать в «предположении», что в системе все в полном порядке. Если такая ситуация (например, отказ одной функции) возникает в локальном приложении, т.е. когда все компоненты принадлежат одному и тому же процессу, об этом нетрудно уведомить все приложение в целом. Но для распределенных приложений все обстоит иначе. На одном компьютере может отказать сетевая карта, а объекты, выполняемые на других компьютерах, могут вообще не «узнать» о том, что где-то в системе произошел отказ. Что случится, если один из объектов попытается связаться с другим объектом и вдруг окажется, что сетевые связи с ним оборвались? Если при использовании модели равноправных узлов (в которой мы формируем различные группы объектов по принципу решения различных аспектов проблемы) одна из групп откажет, то как об этом отказе «узнают» другие группы? Более того, какое поведение мы должны «навязать» системе в такой сигуации? Должен ли отказ одного компонента приводить к отказу всей системы? Если даст сбой один клиент, то должны ли мы прекратить работу сервера? А если откажет сервер, то нужно ли останавливать клиент? А что, если сервер или клиенты продемонстрируют лишь частичные отказы? Поэтому в распределенной системе, помимо «гонок» данных и взаимоблокировок, мы должны также найти способы справляться с частичными отказами компонентов. И снова-таки подчеркиваем, важно найти распределенный подход к С++-механизму обработки исключительных сигуаций. Для начала нас удовлетво-рятвозможности, предоставляемые спецификацией CORBA.
Доступ к объектам из других адресных пространств
Объекты, разделяющие одну область действия (видимости), могут взаимодействовать, получал доступ друг к другу по именам, псевдонимам или с помощью указателей. Объект доступен только в случае, если «видимо» его имя или указатель на него. Видимость имен объектов определяется областью действия. С++ рааличает четыре основ-ныхуровня областей действия: • блока; • функции; • файла; • класса. Вспомните, что блок в С++ определяется фигурными скобками {}, поэтому присваивание значения Y переменной X в листинге 8.1 недопустимо, так как переменная Y видима только внутри блока. Функции main неизвестно имя переменной Y за пределами блока, конец которого обозначен закрывающейся фигурной скобкой. // Листинг 8.1. Простой пример области действия блока int main(int argc, char argv[]) { int X; int Z; { int Y; Z = Y; // Вполне правомочное присваивание. //.. . } X = Y ; // Неверно, поскольку имя Y уже не определено. } Однако имя Y видимо для любого другого кода из того же блока, в котором определена переменная Y. Имя, объявляемое внутри функции или ее объявления, получает область видимости этой функции. В листинге 8.1 переменные X и Z видимы только для функции main , и к ним нельзя получить доступ из других функций. Понятие области видимости файла относится к исходным файлам. Поскольку С++-программа может состоять из нескольких файлов, мы можем создавать объекты, которые видимы в одном файле и невидимы в другом. Имена, обладающие областью видимости файла, видимы, начиная с местоположения их объявления и заканчивая концом исходного файла. Имена с областью видимости файла не должны объявляться ни в одной из функций. Обычно их называют глобальными переменными. Имена, которые характеризуются областью видимости объекта, видимы для любой функции-члена, объявленной как часть этого объекта. Мы используем область видимости объекта в качестве первого уровня доступа к членам объекта. Закрытый, защищенный и открытый интерфейсы объекта определяют второй уровень. И хотя само имя объекта может быть видимым, закрытые и защищенные его члены тем не менее имеют ограниченный доступ. Область действия просто сообщает нам, видимо ли имя объекта. В нераспределенной программе область действия ассоциируется с единым адресным пространством. Два объекта в одном и том же адресном пространстве могут получать доступ друг к другу по имени или указателю и взаимодействовать, просто вызывал методы друг друга. // Листинг 8.2. Использование объектов, которые вызывают // методы других объектов из того же // адресного пространства //.. . some_object А; another_object В; dynamic_object *C; C = new dynamic_object; //... B.doSomething(A.doSomething ); A.doSomething(B.doSomething ); C->doMore (A.doSomething ) ; //... В листинге 8.2 объекты А и В находятся в одной области видимости, т.е. объект В видим для объекта А, а объект А видим для объекта В. Объект А может вызывать функции-члены объекта В, и наоборот. А что можно сказать об областях види
. IOR-доступ к удаленным объектам
Объектнал ссылка специального типа IOR (Interoperable Object Reference) — это стандартный формат объектной ссылки для распределенных объектов. Каждый CORBA-объект имеет IOR-ссылку. IOR-ссылка — это дескриптор, который уникально идентифицирует объект. В то время как обычный указатель содержит простой машинный адрес для объекта, IOR-ссылка может содержать номер порта, имя хоста (имя компьютера в сети), объектный ключ и пр. В С++ для доступа к динамически создаваемым объектам используется указатель. Указатель содержит информацию о том, где в памяти компьютера расположен объект. При разыменовании указателя на объект используется полученный адрес для доступа к членам этого объекта. Однако процесс разыменования указателя на объект (с целью получения доступа к нему) требует больших усилий, когда этот объект находится в другом адресном пространстве и, возможно, на другом компьютере. Указатель в этом случае должен содержать достаточно информации, чтобы сообщить точное местоположение объекта. Если объект расположен в другой сети, указатель должен содержать (прямо или косвенно) сетевой адрес, сетевой протокол, имя хоста, адрес порта, объектный ключ и физический адрес. Стандартнал IOR-ссылка действует как разновидность распределенного указателя на удаленный объект. Набор компонентов, содержащихся в IOR-ссылке под протоколом IIOP, показан на рис. 8.2. Пон Логические компоненты IOR-ссылки: Хост Порт Объектный ключ Другие компоненты Идентифицирует Internet-хост Содержит но ер порта TCP/IP, в которо целевой объект при-ни ает запросы Значение,которое однозначно преобразуется в конкретный объект Дополнительнал инфор ация, которую ожно использовать при обращениях, напри ер для безопасности Рис. 8.2. Набор компонентов, содержащихся в IOR-ссылке подпротоколом IIOP
Брокеры объектных запросов (ORB)
ORB-брокер действует от имени программы. Он посылает сообщения удаленному объекту и возвращает сообщения от него. Поведение ORB-брокера можно сравнить с посредником между локальными и удаленными объектами. ORB-брокер решает все вопросы, связанные с маршрутизацией запроса от программы к удаленному объекту и с маршрутизацией ответа программе, принятого от удаленного объекта. Такое посредничество делает коммуникации между системами практически прозрачными. ORB-брокер избавляет программиста от необходимости программирования сокетов между процессами, выполняющимися на различных компьютерах. И точно так же он устраняет необходимость в программировании каналов и очередей с FIFO-дисциплиной между процессами, выполняющимися на одном компьютере. Он берет на себя немалый объем сетевого программирования, без которого не обойтись при создании распределенных программ. Более того, он стирает различия между операционными системами, языками программирования и аппаратными средствами. При программировании локальных объектов программисту больше не нужно беспокоиться о том, на каком языке реализованы удаленные объекты, на какой платформе они выполняются и к какой сети они «приписаны»: Internet или локальной intranet. ORB-брокер использует IOR-ссылки, чтобы упростить взаимодействие между компьютерами, сетями и объектами. Обратите внимание на то, что IOR-ссылка (см. рис. 8.2) содержит информацию, которая может быть использована для TCP/IP-соединений. Мы представили лишь частичное описание IOR-компонентов, поскольку IOR-дескриптор должен быть «черным ящиком» для разработчика. ORB-брокер использует IOR-ссылки, чтобы найти объект назначения. Обнаружив объект, ORB-брокер активизирует его и передает аргументы, необходимые для вызова этого объекта. ORB-брокер ожидает завершения обслуживания запроса и возвращает вызывающему объекгу ожидаемую информацию или исключение, если вызов метода оказался неудачным. Упрощенная последовательность действий, выполняемых ORB-брокером от имени локального объекта, показана на рис. 8.3. Действия, перечисленные на рис. 8.3, представляютупро УПРОЩЕННАЯ ПОСЛЕДОВАТЕЛЬНОСТЬ ДЕЙСТВИЙ ORB-БРОКЕРА ПРИ ВЫЗОВЕ МЕТОДА УДАЛЕННОГО ОБЪЕКТА _ 1. Найти удаленный объект. _ 2. Активизировать модуль, содержа 3. Передать аргументы удаленному объекту. _ 4. Ожидать ответа после вызова метода удаленного объекта. _ 5. Вернугьлокальномуобъекту информацию или исключение, если вызовудаленного метода оказался неуспешным. _ Рис. 8.3. Упрощенная последовательность действий, выполняемых ORB-брокером от имени локального объекта // Программа 8.1 1 using namespace std; 2 #include «adding_machine_impl.h» 3 #include 4 #include 5 #include 7 8 int main(int argc, char *argv[]) 9 { 10 CORBA::ORB_var Orb = CORBA::ORB_init(argc, argv, «mico-local-orb»); 11 CORBA::BOA_var Boa = Orb->BOA_init(argc,argv,«mico-local-boa»); 12 ifstream In(«adding_machine.objid»); 13 string Ref; 14 if('In.eof){ 15 In » Ref; 16 } 17 In.close; 18 CORBA::Object_var Obj = Orb->string_to_object(Ref.data); 19 adding_machine_var Machine =adding_machine::_narrow(Obj); 20 Machine->add(700); 21 Machine->subtract(250); 22 cout << «Результат равен " « Machine->result« endl; 23 return(0); 24 } 25 26 При выполнении строки 10 ORB-брокер инициализируетс Machine->add(700) ; Machine->subtract(250) ; cout « «Результат равен " « Machine->result « endl; И хотя вызовы этих методов сделаны в нашей локальной области види // Программа 8.2 1 #include 2 #include 3 #include «adding_machine_impl.h» 4 5 6 7 8 int main(int argc, char *argv[]) 9 { 10 CORBA::ORB_var Orb = CORBA: :ORB_init(argc,argv,«mico-local-orb»); 11 CORBA::BOA_var Boa = Orb->BOA_init(argc,argv,«mico-local-boa») ; 12 adding_machine_impl *AddingMachine =new adding_machine_impl; 13 CORBA::String_var Ref = Orb->object_to_string(AddingMachine); 14 ofstream Out(«adding_machine.objid»); 15 Out « Ref « endl; 16 Out.close ; 17 Boa->impl_is_ready (CORBA: : ImplementationDef : :_nil ) ; 18 Orb->run; 19 CORBA: :release(AddingMachine) ; 20 return(0); 21 } 22 23 Обратите внимание на то, что программа-«изготовитель» также должна инициализировать объект Orb (в строке 10). Это — одно из важных требований, предъявляемых к CORBA-ориентированным программам, поскольку каждая программа реализует взаимодействие с удаленными объектами с помощью ORB-брoкepa. Именно поэтому инициализация ORB-объекта— первое действие, которое должна выполнить CORBA-программа. В строке 12 объявляется реальный объект
Язык описания интерфейсов (IDL):более «пристальный» взгляд на CORBA-объекты
Язык описания интерфейсов (IDL) — стандартный язык объектно-ориентированного проектирования, который используется для разработки классов, предназначенных для распределенного программирования. Он применяется для отображения интерфейса класса и отношений между классами, а также для определения прототипов функций-членов, типов параметров и типов значений, возвращаемых функциями. Одно из основных назначений языка IDL — отделить интерфейс класса от его реализации. Но дл Таблица8 .1. Ключевые слова IDL abstract enum native struct any factory Object supports attribute FALSE octet typedef boolean fixed oneway unsigned case float out union char in raises ValueBase const inout readonly valuetype cell interface sequence void double long short wchar exception module string Ключевые слова, перечисленные в табл. 8.1, представл • типы, определенные пользователем; • последовательности, определенные пользователе • типы массивов; • рекурсивные типы; • семантику исключений; • модули (по аналогии с пространствами имен); • единичное и множественное наследование; • поразрядные арифметические операторы. Приведем IDL-определение для класса adding_machine из листинга 8.2: interface adding_machine{ void add(in unsigned long X); void subtract(in unsigned long X); long result; } Это определение начинается с ключевого слова CORBA interface. Обратите внимание на то, что данное объявление интерфейса класса adding_machine не включает ни одной переменной, которая бы могла хранить результат выполнения операций сложения и вычитания. Методы add и subtract принимают в качестве параметра одно значение типа unsigned long. Объявление типа параметра сопровождается ключевым словом CORBA in, который говорит о том, что данный параметр является входным (mput). Это объявление класса хранится в отдельном исходном файле adding_machine.idl. Исходные файлы, содержащие ГОЬюпределения, должны иметь . idl-расширения. Прежде чем такой файл можно будет использовать, его необходимо преобразовать к С++чЈюрмату. Это преобразование можно выполнить с помощью препроцессора или отдельной программы. Все CORBA-реализации включают IDL-компиляторы. Существуют IDL-компиляторы лля языков С, Smalltalk, С++, Java и др. IDL-компилятор преобразует ГОЬюпределения в код соответствующего языка. В данном случае IDL-компилятор преобразует объявление интерфейса в легитимный C++-код. В зависимости от конкретной CORBA-реализации IDL-компилятор вызывается с использованием синтаксиса, который будет подобен слелующему: idl adding_machine.idl При выполнении этой команды создается файл, содержащий С++-код. Поскольку наше IDL-определение хранится в файле adding_machine. idl, MICO IDL-компилятор создаст файл adding_machine. h, который будет содержать несколько каркасных C++-классов и CORBA-типов данных. Базовые IDL-типы данных приведены в табл. 8.2. Таблица 8.2. Базовые IDL long 1 > _2»-2' 5 - 1 > 16 бит 0-2 v - 1 > 32 бит long -2 31 - 2 31 -1 >= 32 бит short -2 15 - 2 15 -1 >= 16 бит unsigned long 0 - 2 32 -1 >= 32 бит unsigned short 0-2 16 -1 >= 16 бит float IEEE с обычной точностью >= 32 бит double IEEE с двойной точностью >= 64 бит char ISO атинский-1 >=8 бит string ISO атинский-1, за иск ючением ASCII NULL Переменный boolean TRUE ИЛИ FALSE Не определен octet 0-255 >=8 бит any Произвольный тип, идентифицируемый динамически Переменный Даже после того как IDL-компилятор создаст из определения интерфейса С++-код, реализация методов интерфейсного класса остается все еще неопределенной. IDL-компилятор создает несколько С++-конструкций, которые предназначены для использования в качестве базовых классов. В листинге 8.3 показано два класса, сгенерированных MICO IDL-компилятором на основе файла // Листинг 8.3. Два класса, сгенерированные // MICO IDL-компилятором из файла // adding_machine.idl class adding_machine : virtual public CORBA::Object{ public: virtual ~adding_machine; #ifdef HAVE_TYPEDEF_OVERLOAD typedef adding_machine_ptr _ptr_type; typedef adding_machine_var _var_type; #endif static adding_machine_ptr _narrow(CORBA::Object_ptr obj ); static adding_machine_ptr _narrow(CORBA::AbstractBase_ptr obj ); static adding_machine_ptr _duplicate(adding_machine_ptr _obj ){ CORBA::Object::_duplicate (_obj); return _obj; } static adding_machine_ptr _nil{ return 0; } virtual void *_narrow_helper( const char *repoid ); static vector static bool _narrow_helper2( CORBA::Object_ptr obj ); virtual void add( CORBA::ULong X ) = 0; virtual void subtract( CORBA::ULong X ) = 0; virtual CORBA::Long result = 0; protected: adding_machine{}; private: adding_machine( const adding_machine& ); void operator=( const adding_machine& ); }; class adding_machine_stub : virtual public adding_machine{ public: virtual ~adding_machine_stub; void add( CORBA::ULong X ); void subtract( CORBA::ULong X ); CORBA::Long result; private: void operator=( const adding_machine_stub& ); }; Файл adding_machine.idl — это входные данные для компилятора, а файл adding_machine.h вместе с каркасны // Листинг 8.4. Класс реализации структурных классов, // созданных IDL-компилятором class adding_machine_impl : virtual public adding_machine_skel { private: CORBA::Long Result; public: adding_machine_impl (void){ Result = 0; }; void add(CORBA::ULong X){ Result = Result + X; }; void subtract(CORBA::ULong X){ Result = Result - X; }; CORBA::Long result(void){ return(Result); }; Один из каркасных файлов, созданных IDL-ко 1. Проектирование интерфейсов классов, отношений и иерархии с использование 2. Использование IDL-ко 3. Использование наследования для создания пото Мы рассмотрим этот процесс более детально ниже в этой главе. Но сначала познакомимся с базовой структурой программы потребителя.
Анатомия базовой CORBA-программы потребителя
Одной из самых распространенных моделей для применения распределенного программирования является модель «изготовитель-потребитель». В этой модели одна программа играет роль «изготовителя», а другая — «потребителя». Изготовитель создает некоторые данные или предлагает ряд услуг, которыми пользуется потребитель (например, наша программа могла бы по требованию генерировать уникальные номерные знаки). Предположим, потребитель — это программа, которая создает запросы на новые номерные знаки, а изготовитель — это программа, которая их генерирует. Обычно потребитель и изготовитель размещаются в различных адресных пространствах. Компоненты такой программы и действия, которые должно содержать большинство CORBA-программ потребителей, представлены на рис. 8.4. Для взаимодействия с объектами, выполняемыми на других компьютерах или расположенными в других адресных пространствах, каждая программа— участница взаимодействия должна объявить ORB-объект. После этого программа-потребитель может получить доступ к его функциям-членам. Как показано на рис. 8.4, ORB-объект инициализируется путем следующего вызова: При выполнении этой инструкции ORB-oбъект инициализируется. Для ORB-объектов используется тип CORBA: :ORB_var. В CORBA-реализациях объекты, тип которых помечается суффиксом _var, берут на себя заботу об освобождении базовой ссылки (в отличие от объектов, тип которых помечается суффиксом _ptr). Аргументы командной строки передаются конструктору ORB-объекта вместе с идентификатором orb_id. В данном случае идентификатором orb_id служит строка «mico-local-orb». Строка, передаваемал функции инициализации ORB_init , зависит от конкретной CORBA-реализации. Полученный объект называют обслуживающим ( После инициализации ORB-объекта и объектного адаптера разработчику CORBA-приложения необходимо позаботиться об IOR-ссылке для удаленного объекта (объектов). Как показано на рис. 8.4, IOR-ссылка считывается из файла adding_machine.ior . IOR-ссылка была записана в этот файл в строковой форме. ORB-объект используется для преобразования IOR-ссылки из строки снова в объектную форму с помощью метода string__to_object . Как показано на рис. 8.4, это реализуется с помощью следующего вызова: CORBA::Object_var Obj = Orb->string_to_object(Ior.c_str); Здесь функция lor. c_str возвра adding_machine_var Machine = adding_machine::_narrow(Obj); При выполнении этой инструкции создается ссылка на объект типа adding_machine. Программа-потребитель Machine->add(500); Machine->subtract(125) ; При выполнении этих инструкций вызываются
Анатомия базовой CORBA-программы изготовителя
Изготовитель отвечает за обеспечение программ-потребителей данными, функциями или другими услугами. Изготовитель вместе с потребителем и составляют распределенное приложение. Каждал CORBA-программа изготовителя проектируется в расчете на существование программ-потребителей, которые булут нуждаться в предоставляемых ею услугах. Следовательно, каждая программа-изготовитель должна создавать обслуживающие объекты и IOR-ссылки, посредством которых к этим объектам можно получить доступ. На рис. 8.5 представлена простая программа-изготовитель, используемая «в содружестве» с программой-потребителем, отображенной на рис. 8.4. На рис. 8.5 также перечислены основные компоненты, которые должна содержать любая CORBA-программа изготовителя. Обратите внимание на то, что части А обеих программ по сути одинаковы. Как потребителю, так и изготовителю требуется ORB-объект для связи друг с другом. Этот ORB-объект используется для получения ссылки на объектный адаптер. На рис. 8.5 приведен следующий вызов: CORBA::BOA_var Boa = Orb->BOA_init(argc,argv,«mico-local-boa»); Итак, вызов этой функции используется для получения ссылки на объектный адаптер, который служит посредником между ORB-брокером и объектом, реализующим запрашиваемые методы. Слелует иметь в виду, что CORBA-объекты должны начинаться только как объявления интерфейсов. На некотором этапе процесса разработки производный класс обеспечит реализацию CORBA-интерфейса. Объектный адаптер действует как посредник между интерфейсом, с которым связан ORB-брокер. и реальными методами, реализованными производным классом. Объектные адаптеры используются для доступа к обслуживающим объектам и объектам реализации. Изготовитель (см. рис. 8.5) создает объект реализации в части В, используя следующий вызов: При выполнении этой инструкции создается объект, который обеспечит реализацию услут, потенциально запрашиваемых клиентскими объектами (или потребителями). Обратите также внимание на то, что в части С (см. рис. 8.5) программа-изготовитель использует объект ORB для преобразования IOR-ссылки в строку и записывает ее в файл
Базовый npoeкт CORBA-приложения
Итак, из программ, представленных на рис. 8.4 и 8.5, видно, что д После получения IOR-ссылки и приведения ее к соответствующему типу вызов удаленного метода в программе клиента (потребителя) подобен вызову обычного метода в любой С++-программе. В CORBA-примерах этой книги предполагается использование протокола IIOP (Internet Inter ORB Protocol). Поэтому ORB-брокеры (см. рис. 8.6) связываются с помощью протокола TCP/IP. IOR-ссылка должна содержать информацию о местоположении удаленного объекта, достаточную для реализации TCP/IP-связи. В качестве объектного адаптера обычно используется переносимый объектный адаптер. Но для некоторых программ (более старых и простых) можно использовать базовый объектный адаптер. Различие между этими двумя адаптерами мы рассмотрим ниже в этой главе. Каждое CORBA-приложение имеет один или несколько обслуживающих объектов, которые реализуют интерфейс, разработанный в IDL-классе. Простейшие программы потребителя и изготовителя, показанные на рис. 8.4 и 8.5, могут выполняться на одном компьютере в различных процессах или на различных компьютерах. Если эти программы выполняются на одном компьютере, файл adding_machine. ior должен быть доступен из обеих программ. Если они выполняются на различных компьютерах, этот файл должен быть послан клиентскому компьютеру по FTP-протоколу, электронной почте, HTTP-протоколу и т.д. Детали компиляции и выполнения этих программ описаны в разделах «Профиль программы 8.1» и «Профиль программы 8.2».
IDL-компилятор
IDL-компилятор представляет собой инструмент, предназначенный для перевода IDL-определений класса в С++-код. Этот код состоит из коллекции «каркасных» определений классов, перечислимых типов и шаблонных классов. Для CORBA-программ, приведенных в этой книге, в качестве IDL-компилятора используется MICO IDL-компилятор. В табл. 8.3 перечислены опции командной строки, которые чаще всего применяются при вызове этого IDL-компилятора. Таблица 8.3. Самые распространенные опции командной строки, применяемые при вызове IDL-компилятора • --boa Генерирует «каркасные» конструкции, которые используют базовый объектный адаптер (basic object adapter — BOA). Эта опция испо • --no-boa Отключает генерирование кода «каркасных» конструкций для BOA • --poa Генерирует «каркасные» конструкции, которые испо • --no-poa Отк • • —version Выводит версию спецификации MICO • -D • -I Ключи -boa и -poa (см. табл. 8.3) позволяют определить, на какой тип адаптера будут ориентированы создаваемые «каркасные» конструкции. Например, при выполнении слелующей команды idl -poa -no-boa adding_machine.idl будет получен файл При вводе команды idl -h будет сгенерирован полный список ключей IDL-компилятора. Если в поставке MICO надлежащим образом инсталлированы man-страницы, то ввод команды man idl обеспечит полное описание доступных IDL-ключей. Проектирование IDL-классов — первый шаг в CORBA-программировании. На слелующем этапе необходимо определить способ хранения и считывания IOR-ссылок на удаленные объекты.
Получение IOR-ссылки для удаленных объектов
ORB-класс содержит две функции-члена (string_to_object и object_to_. string ), которые можно использовать для преобразования IOR-объектов из строк в объекты типа Object_ptrs и обратно. Функция-член string_to_object принимает параметр типа const char * и преобразует его в объект типа Object_ptr. Функция-член Object_to__string принимает параметр типа Object_ptr и преобразует его в указатель типа char *. Эти методы являются составной частью интерфейса ORB-клаеса. Метод Object_to_string используется для получения объектной IOR-ссылки строковой формы. IOR-ссылку, представленную в виде строки, можно передать программам клиентов (потребителей) различными способами, например: • Электронная почта • Разделяемые файловые системы (NFS-оборудование) • FTP-протокол • Встраивание в HTML-документы • Java-аплеты/сервлеты • Аргументы командной строки • Разделяемая память • Традиционные средства межпроцесной связи (IPC), т.е. каналы, FIFO-очереди и пр. • Переменные среды CGI-команды приема и отправки данных Затем программа приема данных получает строковый вариант IOR-ссылки и использует функцию-член string_to_object ORB-объектадля преобразования IOR-ссылки в CORBA-объект ptr. Этот CORBA-объект ptr затем «сужается» (т.е. приводится к соответствующему типу) и используется для инициализации локального объекта. В программах 8.1 и 8.2 для передачи IOR-ссылки между программой-потребителем и программой-изготовителем используются строковые формы объектов и файл. Строковую форму IOR-ссылок можно использовать для обеспечения очень гибкой связи с удаленными объектами, которые могут размещаться практически в любом месте Internet, intranet или extranet. Существует реализация MIWCO (Wireless Mico) — открытая реализация спецификации wCORBA [16], стандарт беспроводной спецификации CORBA, который можно использовать для улучшения мобиль-ности объектов. Эта беспроводнал спецификация позволяет реализовать связь посредством MIOR-ссылки (Mobile IOR). Она поддерживается для TCP-, UDP- и WAP WDP-механизмов передачи (Wireless Application Protocol Wireless Datagram Protocol). Мультиагентные и распределенные агентные системы также могут воспользоваться преимуществами стандартов IOR-ссылок. IOR- и MIOR-ссылки являются частью «строительных блоков» слелующего поколения объектно-ориентированных Web-служб. Важно отметить, что хотя строковые IOR-ссылки обеспечивают гибкость и переносимость, они не могут идеально подходить ко всем ситуациям и конфигурациям. Передача файла, который содержит IOR-ссылку, — не слишком приемлемое требование для многих систем. Трудно с точки зрения практичности требовать от приложений клиента и сервера разделять одну и ту же файловую систему или сеть. А с точки зрения безопасности строковый вариант IOR-ссылки вообще исключается как достойный рассмотрения. Если приложение типа «клиент-сервер» велико по объему и достаточно разнотипно, то требование разделения строковой формы IOR
Служба имен
Стандарт службы имен обеспечивает механизм преобразовани Служба имен действует как разновидность телефонного справочника, в котором по имени и Обратите внимание на то, что пос Для обхода именного графа в процессе решения распределенной задачи применяются известные алгоритмы обхода графов. При этом различные пути обхода именного графа могут представлять различные решения задачи. Служба имен обеспечивает автора запроса доступом к именным контекстахм и именным графам. К именным контекстам доступ осуществляется через именные графы, а к связям — через именные контексты. Связывание обеспечивает прямое соответствие имени и объектной ссылки. Рассмотрим программу 8.3, в которой представлен простой вариант «изготовителя», создающего связывание по имени и соотносящего это связывание с некоторым именным контекстом. // Программа 8.3 1 #include 2 #include 3 #include «permutation_impl.h» 4 #define MICO_CONF_IMR 5 #include 6 #include 7 #include 8 #include 9 #include 11 12 int main(int argc, char *argv[]) 13 { 14 CORBA::ORB_var Orb = CORBA: :ORB_init(argc,argv,«mico-local-orb»); 15 CORBA::Object_var PoaObj =Orb->resolve__initial_references(«RootPOA»); 16 PortableServer::POA_var Poa =PortableServer::POA::_narrow(PoaObj); 17 PortableServer::POAManager_var Mgr =Poa->the_POAManager; 18 inversion Server; 19 PortableServer: :ObjectId_var Oid =Poa->activate_object(&Server); 20 Mgr->activate ; 21 permutation_ptr ObjectReference = Server.__this; 22 CORBA::Object_var NameService =Orb->resolve_initial_references («NameService»); 23 CosNaming: :NamingContext_var NamingContext =CosNaming::NamingContext::_narrow (NameService); 24 CosNaming: :Name name; 25 name.length (1) ; 26 name[0].id = CORBA::string_dup («Inflection»); 27 name[0].kind = CORBA::string_dup (""); 28 NamingContext->bind (name, ObjectReference); 29 Orb->run; 30 Poa->destroy(TRUE,TRUE); 31 return(0) ; 32 } 33 34 § 8.1. Семантические сети Семантическал сеть (semantic network) — это одна из са Овалы в семантической сети называются
Использование службы имен и создание именных контекстов
При выполнении строки 22 серверная про CORBA::Object_var NameService =Orb->resolve_initial_references(«NameService»); Помимо получения объектных ссылок на хранилище реализаций (Implementation Repositoiy) и хранилище интерфейсов (Interface Repositoiy), метод ORB-объекта resolve_initial_references используется CosNaming::NamingContext_var NamingContext = CosNaming::NamingContext::_narrow(NameService); При таком подходе мы получаем начальный именной контекст, который играет роль контекста, действующего по умолчанию. Обнаружив службу имен и создав начальный именной контекст, серверная программа может добавлять в контекст пары (связывания по имени) «имя/объектнал ссылка». Имена могут представлять собой объекты доменов или другие контексты. Чтобы добавить в контекст пару «имя/объектная ссылка», необходимо сначала создать имя. Имена реализуются в стандарте CORBA посредством структуры NameComponent. struct NameComponent { //.. . Istring_var id; Istring_var kind; } В CORBA-реализации MICO структура NameComponent объявляется в файле CosNaming. h. Структура NameComponent содержит два атрибута: id и kind. Первый атрибут используется для хранения текста имени, а второй представляет собой идентификатор, который можно использовать для классификации объекта, например так. //... CosNaming::Name ObjectName; ObjectName.length(1); ObjectName.id = Corba::string_dup (" train») ; ObjectName.kind=Corba::string_dup(«land_transportation»); NamingContext->bind(ObjectName,ObjectReference) ; //... Здесь объяв Детали инсталляции и функционирования службы и // Листинг 8.5. Сценарий внесения записи в хранилище // реализаций и запуска службы имен // micod -ORBIIOPAddr inet:hostname:portnumber —forward & imr create NameService poa 'which nsd* IDL:omg.org/CosNaming/ NamingContext:1.0#NameService \ -ORBImplRepoAddr inet:hostname:portnumber \ -ORBNamingAddr inet:hostname:hostname:portnumberportnumber imr create permutation persistent "'pwd'/permutation_server \ -ORBImplRepoAddr inet:hostname:portnumber \ -ORBNamingAddr inet:hostname:portnumber» IDL:permutation:1.0 \ -ORBImplRepoAddr inet:hostname:portnumber \ -ORBNamingAddr inet:hos tname:portnumber imr activate permutation -ORBImplRepoAddr inet:hostname:portnumber \ -ORBNamingAddr inet:hostname:portnumber Этот сценарий можно использовать в сочетании с кодом сервера, приведенным в программе 8.3. Приведенный здесь сценарий реально позволяет автоматически запустить программу-сервер permutation_server. Обратите вни
Служба имен «потребитель-клиент»
Программа 8.3 связывает имя объекта с именным контекстом. Программа 8.4 содержит текст программы-потребителя, которая использует службу имен для доступа к связкам «объект-ссылка», которые были созданы в программе 8.3. Программа 8.3 генерирует перестановки любой строки символов, которую она получает. Для перестановки изменяется местоположение символов в строке. Например, эти строки Objcte JbOetc tbOjec Ojbect JObetc Ojbcet JtObec представ // Программа 8.4 1 int main(int argc, char *argv[]) 2 { 3 4 try{ 5 CORBA::ORB_var Orb = CORBA::ORB_init(argc,argv,«mico-local-orb»); 6 object_reference Remote(«NameService»,Orb); 7 Remote.objectName(«Inflection»); 8 permutation_var Client =permutation::_narrow(Remote.objectReference); 9 char Value[1000]; strcpy(Value,«Common Object Request Broker»); 11 Client->original(Value); 12 int N; 12 for(N = 0;N < 15;N++) 14 { 15 cout « «Значение функции nextPermutation "<< Client- >nextPermutation « endl; 16 ) 17 } 18 catch(CosNaming::NamingContext::NotFound_catch &exc) { 19 cerr << " Исключение: объект не обнаружен.» « endl; 20 } 21 catch(CosNaming::NamingContext::InvalidName_catch &exc) { 22 cerr << «Исключение: некорректное имя.» « endl; 23 } 24 25 return(0); 26 } Для доступа к соответствую 1. Получить ссылку на службу имен. 2. С помощью службы имен получить ссылку на соответствующий именной контекст. 3. С помощью именного контекста получить ссылку иа соответствующий объект. Действие 1 реализуетс //.. . CORBA::Object_var NameService; NameService = Orb->resolve_initial_references («NameService»); //... Функция resolve_initial_references возвратит объектную ссылку на службу имен. В действии 2 эта ссылка используетс CosNaming: :NamingContext_var NameContext; NameContext = CosNaming::NamingContext::_narrow (NameService); В действии 3 значение объектной ссылки NameService «сужается», т.е. приводится к соответствую Name .length (1); Name[Q].id = CORBA::string_dup («Inflection»); Name[C].kind = CORBA::string_dup (""); try { ObjectReference = NameContext->resolve (Name); } Метод resolve возвращает объектную ссылку, связанную с заданным именем объекта. В данном случае задано имя «Inflection». Обратите внимание на то, что такое же имя связывается с именным контекстом в программе8.3 (строка28). Если программа-потребитель имеет объектную ссылку, она может ее «сузить», а затем с ее помощью получить доступ к удаленному объекту. Процесс получения объектной ссылки на удаленный объект вполне тривиален, и поэтому имеет смысл его упростить путем инкапсуляции соответствующих компонентов в классе. class object_reference{ //.. . protected: CORBA::Object_var NameService; CosNaming::NamingContext_var NameContext; CosNaming::Name Name; CORBA::Object_var ObjectReference; public: object_reference(char *Service,CORBA::ORB_var Orb); CORBA::Object_var objectReference(void); void objectName(char *FileName,CORBA::ORB_var Orb); void objectName(char *OName); //. . . } Про В про Remote.obj ectReference; После этого программа-потребитель получает доступ к удаленному объекту. Класс object_reference обеспечивает выполнение некоторых необходимых действий и тем самым упрощает написание программы-потребителя. Ко object_reference::object_reference(char *Service, CORBA::ORB_var Orb) { NameService = Orb->resolve_initial_references (Service); NameContext = CosNaming::NamingContext::_narrow ( NameService); } Этот ко void object_reference::objectName(char *OName) { Name.length (1); Name[0].id = CORBA::string_dup (OName); Name[0].kind = CORBA::string_dup (""); try { ObjectReference = NameContext->resolve (Name); } catch(...) { cerr « " Problem resolving Name " « endl; throw; } } После вызова метода objectName программа-потребитель получает доступ кссылке на удаленный объект. Теперь остается лишь вызвать метод objectReference (это реализуется в строкев программы8.4). В методе objectName основную часть работы выполняет функция resolve . Программы 8.3 и 8.4 образуют простое распределенное приложение «клиент-сервер», которое для доступа к объектным ссылкам вместо строковой формы IOR-ссылок использует службу имен. В сетях intranet или Internet можно использовать оба подхода. Эти же варианты применяются в качестве опорных структурных компонентов в контексте новой модели Web-служб.
Подробнее об объектных адаптерах
Помимо службы имен и объекта именованного контекста, сервер в программе 8.3 также использует переносимый объектный адаптер. Вспомните, что адаптер (см. рис. 8.6) действует как своего рода посредник между ORB-брокером и обслуживающим объектом, который в действительности выполняет работу CORBA-объекта. Мы можем сравнить этот обслуживающий объект с «наемным» писателем, который пишет книту от имени «подуставшей» знаменитости. С этой знаменитостью наперебой общаются журналисты, литературные агенты и юристы. Знаменитость удостаивается всех почестей, но реальную работу делает за него другой человек. CORBA-объект «публикует» интерфейс с внешним миром и играет роль «знаменитости» в CORBA-программе. Программа-клиент (или потребитель) взаимодействует с интерфейсом, который обеспечивает CORBA-объект, но реальную работу выполняет обслуживающий объект, играя роль «наемного» писателя. Обслуживающий объект имеет собственный протокол, который может отличаться от используемого CORBA-объектом. CORBA-объект может предоставить С++-интерфейс для связи склиентом. Обслуживающий объект может быть реализован на Java, Smalltalk, Fortran и других языках программирования. Объектный адаптер обеспечивает интерфейс с обслуживающим объектом. Он адаптирует этот интерфейс, чтобы реализация обслуживающего объекта была прозрачна для ORB-брокера и программы-клиента. CORBA-реализация должна нормально поддерживать два типа объектных адаптеров: Basic Object Adapter (BOA) и Portable Object Adapter (POA). Сначала стандарт CORBA был ориентирован на использование ВОА-адаптера, но он не был достаточно гибким. Поэтому и был разработан РОА-адаптер, который нашел более широкое применение. ВОА-адаптер обладает минимальным набором средств, но его вполне можно использовать для активизации объектных реализаций на базе информации, которая содержится в хранилище реализаций (табл. 8.4). Таблица 8.4. Некоторые элементы, содержащиеся в хранилище реализац лиотека permethod ВОА-адаптер, чтобы приступить к выполнению объекта изготовителя (сервера), использует такие записи из хранилища реализаций, как режим активизации и путь. И хотя в ряде более простых примеров, приведенных в этой главе, используется ВОА-адаптер, мы рекоменлуем для серьезной CORBA-разработки применять РОА-адаптер. РОА-адаптер поддерживает: • прозрачную активизацию объекта; • транзитные объекты; • не • перманентные (постоянные) объекты за пределами сервера. Возможно, наиболее важной функцией РОА-адаптера является взаимодействие собслуживающими объектами. CORBA-спецификация определяет обслуживающий объект следующим образом. Обслуживающий объект — объект языка программирования, который реализует запросы к одному или нескольким объектам. Обслуживающие объекты в общем случае существуют в контексте процесса сервера. Запросы на получение объектных ссылок обслуживаются ORB-брокером, действующим в качестве связующего звена, и трансформируются в вызовы конкретных обслуживающих объектов. Во время своего жизненного цикла объект может быть связан с несколькими обслуживающими объектами. Каждый обслуживающий объект должен иметь по крайней мере один POA-адаптер. Но возможны и другие конфигурации (рис. 8.10). РОА-адаптерами управляют специальные объекты управления, или POA-менеджеры. CORBA-спецификация определяет РОА-менеджер таким образом: Сервер в программе 8.3 служит простым примером использования объектов POA- адаптеров и РОА-менеджеров. Более подробное рассмотрение POA выходит за рамки нашей книги. За деталями обращайтесь к работе [20]. MICO-поставка также содержит ряд примеров использования мощных средств POA.
Хранилища реализаций и интерфейсов
ORB-брокер для определения местоположения объектов, когда строковые IOR-ссылки недоступны, использует хранилище реализаций. Хранилища реализаций представляют собой удобное место для хранения информации, связанной со спецификой среды (например, данные о системе безопасности, детали отладки и т.д.). Хранилище реализаций должно содержать информацию, достаточную для того, чтобы ORB-брокep мог отыскать путь объекта и выполняемый файл. Утилита imr в поставке MICO используется для управления хранилищем реализаций. Она позволяет отображать записи хранилища реализаций, вносить в него новые и удалять ненужные, например: imr create permutation persistent "'pwd' /permutation_server \ -ORBImplRepoAddr inet:hostname:portnumber \ -ORBNamingAddr inet:hostname:portnumber» IDL:permutation:1.0 \ При выполнении этой ко
Простые pacnpeделенные Web-службы, использующие CORBA-спецификацию
Адреса для хранилищ реализаций и служб имен можно встроить в код HTML и использовать как часть CGI-обращения к Web-серверу. Этот метод с помощью CORBA-спецификации можно применить для реализации простых распределенных Web-служб. В листинге 8.6 представлен простой HTML-код. При щелчке на гиперссылке будет выполнен CORBA-клиент. Этот CORBA-клиент может затем получить доступ к серверу, используя адрес хранилища реализаций и службы имен, который был передан в CGI-команде HTML-кода. // Листинг 8.6. HTML-документ со встроенным обращением к // CORBA-программе (клиенту)
Здесь клиент ссылается на программу, которая должна получить доступ к CORBA-изготовителю (серверу). У клиента есть имя объекта, с которым ему необходимо связаться, а для дальнейших действий он использует службу имен. Этот метод не требует загрузки кода на компьютер пользователя. Совсем наоборот, код клиента, выполняясь на Web-сервере, должен получить доступ к CORBA-ориентированной программе-серверу независимо от ее местоположения (или в intranet-сети, подключенной к Web-серверу, или где-нибудь в другом месте Internet). Программа-клиент должна ответить HTML-браузеру, используя соответствующий CGI-протокол. Простая конфигурация Web-служб с CORBA-компонентами показана на рис. 8.11.
Помимо протокола http, для запуска CORBA-ориентированных клиентов и серверов можно использовать сетевой теледоступ telnet. Протоколы http и telnet можно использовать для поддержки глобального распределения CORBA-компонентов. При проектировании распределенных компонентов, ориентированных на функционирование в сети Internet или intranet, важно не забывать о системе безопасности (соответствующем ПО и данных). И хотя реализации и требования, предъявляемые к безопасности, выходят за рамки этой книги, мы подчеркиваем их важную роль в любом проекте распределенной системы. Для хранения информации, имеющей отношение к безопасности, можно использовать хранилище реализаций. Любую CORBA-реализацию можно использовать в сочетании с протоколом защищенных сокетов (Secure Socket Layer — SSL) и специальной оболочкой SSH (Secure Shell).
Маклерская служба
Помимо строковых IOR-ссылок и службы имен, CORBA-спецификация включает более прогрессивный и динамический метод получения объектных ссылок, име-куемый
И точно так же, как при связывании нескольких именных контекстов формируются именные графы, при связывании нескольких маклеров формируются маклерские графы. Именные и маклерские графы — это мощные методы представления знаний и функциональных возможностей. Именные и маклерские графы обеспечивают функционирование глобальных Web- и telnet-служб. Обход именных и маклерских графов может включать участки, которые потенциально имеют «ответвления» в какую-нибудь локальную сеть, intranet, extranet или Internet. Подобно именным контекстам маклеры обычно представляют определенные типы объектов. Например, мы могли бы позаботиться о том, чтобы маклеры одного типа имели доступ к объектам кредитных карточек, а маклеры другого — к объектам сжатия и шифрования. Можно создать маклеры, которые бы занимались объектами погоды и географии. А еще мы могли бы «научить» маклеры интересоваться финансовой деятельностью и страхованием. Объединив все эти маклеры, получим маклерский граф. Если один маклер будет работать от имени других, мы получим то, что можно назвать федерацией маклеров. Когда клиент описывает одному маклеру услуги, в которых он нуждается, а затем маклер общается со своими коллегами, чтобы найти эти услуги, то клиент и этот маклер включаются в федерацию маклеров. Это — самая мощная и гибкая форма «запроса, который не важно, где и кем будет удовлетворен». Когда федерация маклеров возвратит объектную ссылку, может оказаться, что она «родом бог-знает-откуда» и может быть реализована обслуживающим объектом (объектами), операционная система и язык программирования которого совершенно чужд программе клиента. Федерация маклеров обеспечивает доступ к очень большим и разнообразным коллекциям услуг. Следует иметь в виду, что CORBA-стандарт включает беспроводную спецификацию wCORBA, используемую для разработки мобильных агентных и мультиагентных систем. На рис. 8.12 показана базовая архитектура CORBA-ориентированного приложения типа «клиент-сервер», которое делает запросы к маклерам.
Програ
Таблица 8.5. Термины, связанные с темой программирования маклеров
• Экспортер Рекламирует услугу с помощью маклера. Экспортер может быть провайдером услуг или анонсировать услугу от имени кого-то другого
• Импортер Использует маклер для поиска услуг, соответствующих некоторому критерию. Импортер может быть потенциальным клиентом услуг или импортировать услугу от имени кого-то другого
• Предложение Содержит описание анонсируемой услуги. Описание состоит из имени-услуги и типа услуги, объектной ссылки и свойств объекта
Парадигма «клиент-сервер»
Термины «клиент» и «сервер» часто применяются к различным видам программных приложений. Парадигма «клиент-сервер» состоит в разделении работы на две части, представляемые процессами или потоками. Одна часть, клиент, создает запросы на получение данных либо действий. Другая часть, сервер, выполняет эти запросы. Роли запрашивающей и отвечающей стороны в большинстве случаев определяются логикой самих приложений. Термины «клиент-сервер» используются на уровне операционной системы для описания отношений типа «изготовитель-потребитель», которые могут существовать между процессами. Например, если для взаимодействия двух процессов используется FIFO-очередь, один из процессов «играет» роль сервера, а другой — роль клиента. Иногда клиент может «исполнить» роль сервера, если сам будет получать запросы. Аналогично сервер будет выступать в роли клиента, если ему потребуется обращаться с запросами к другим программам. Конфигурация «клиент-сервер» — основнал архитектура распределенного программирования. При этом тип сервера обычно характеризует все приложение. Некоторые наиболее популярные типы программных серверов перечислены в табл. 8.6.
Таблица 8.6. Основные типы программных серверов
•
•
•
•
•
«Классная доска» и мультиагентные системы — это две основные архитектуры, используемые в данной книге для поддержки параллельного и распределенного программирования. Особое внимание мы уделяем логическим серверам (см. табл. 8.6). Логический сервер — это специальный тип сервера приложений, который используется для решения задач, требующих интенсивных символьных и, возможно, параллельных вычислений. Процесс формирования некоторого вывода и делукции часто тяжелым бременем ложится на процессор и может значительно выиграть от использования параллельно работающих процессоров. Обычно чем больше процессоров доступно логическим серверам, тем лучше. Мультиагентные архитектуры и архитектуры «классной доски», рассматриваемые в главах 12 и 13, опираются на понятие распределенных логических серверов, которые могут совместными усилиями решать проблемы в сетевой среде, intranet или Internet. Несмотря на то что агентный подход и стратегия «классной доски» формируют архитектуру с уклоном в сторону равноправных узлов, они являются клиентами логических серверов. Распределенные объекты используются для реализации всех компонентов системы, а CORBA-спецификация позволяет упростить сетевое программирование.
Резюме
Распре
Реализация моделей SPMD и MPMD с помощью шаблонов и MPI-программирования
Понятие параметризованного программирования поддерживается шаблонами. Основная идея параметризованного программирования — обеспечить максимально благоприятные условия для многократного использования ПО путем реализации его проектов в максимально возможной общей форме. Шаблоны функций поддерживают обобщенные абстракции процедур, а шаблоны классов — обобщенные абстракции данных. Обычно компьютерные программы уже представляют собой обобщенные решения некоторых конкретных проблем. Программа, которая суммирует два числа, обычно рассчитана на сложение любыхдвух чисел. Но если программа выполняет только операцию сложения, ее можно обобщить, «научив» выполнять идругие операции над двумя любыми числами. Если мы хотим получить самую общую программу, можем ли мы остановиться лишь на выполнении различных операций над двумя числами? А что если эти числа будут иметь различные типы, т.е. комплексные и вещественные? Можно заложить в разработку программы выполнение различных операций не только над любыми двумя числами, но и над значениями различных типов или классов чисел (например, значениями типа int, float, double или комплексными). Кроме того, мы хотели бы, чтобы наша программа выполняла любую бинарную операцию на любой паре чисел — главное, чтобы эта операция была легальна для этих двух чисел. Если мы реализуем такую программу, ее возможности в плане многократного использования будут просто грандиозными. Эту возможность С++-программисту предоставляют шаблоны функций и классов. Такого вида обобщения можно добиться с помощью параметризованного программирования.
Парадигма параметризованного программирования, полдерживаемал средствами С++, в сочетании с объектноориентированной парадигмой, также поддерживаемой средствами С++, обеспечивают уникальный подход к MPI-программированию. Как упоминалось в главе 1, MPI (Message Passing Interface — интерфейс передачи сооб
Декомпозиция работ для MPI-интерфейса
Одним из преимуществ использования MPI-интерфейса перед традиционными UNIX/Linux-процессами и сокетами является способность MPI-среды запускать одновременно несколько выполняемых файлов. MPI-реализация может запустить несколько выполняемых файлов, установить между ними базовые отношения и идентифицировать каждый выполняемый файл. В этой книге мы используем MPICH-реализацию MPI-интерфейса [17]1. При выполнении команды $ mpirun -np 16 /tmp/mpi_example1 будет запущено 16 процессов. Каждый процесс будет выполнять программу с именем mpi_example1. Все процессы могут использовать разные доступные процессоры. Кроме того, каждый процесс может выполняться на отдельном компьютере, если MPI работает в среде кластерного типа. Процессы при этом будут выполняться параллельно. Команда mpirun представляет собой основной сценарий, который отвечает за запуск MPI-заданий на необходимом количестве процессоров. Этот сценарий изолирует пользователя от подробностей запуска параллельных процессов на различных компьютерах. Здесь будет запущено 16 копий программы mpi_examplel. Несмотря на то что стандарт MPI-2 определяет функции порождения, которые можно использовать для динамического добавления программ к выполняемому MPI-приложению, этот метод не популярен. В общем случае необходимое количество процессов создается при запуске MPI-приложения. Следовательно, во время старта этот код тиражируется N раз. Описаннал схема легко поддерживает модель параллелизма SPMD (SIMD), поскольку одна и та же программа запускается одновременно на нескольких процессорах. Данные, с которыми каждой программе нужно работать, определяются после запуска программ. Этот метод старта одной и той же программы на нескольких процессорах можно развить, если нужно реализовать модель MPMD. Вся работа MPI-программы делится между несколькими процессами, запускаемыми на старте программы. Информация о распределении «обязанностей» (т.е. кто что делает и какие процессы работают с какими данными) содержится в самой выполняемой программе. Компьютеры, задействованные в этой работе, перечис
Дифференциация задач по рангу
Во время старта процессов, включенных в MPI-приложение, МРI-среда назначает каждому процессу ранг и группу коммуникации. Ранг хранится как int-значение и служит в качестве идентификатора процесса для каждой MPI-задачи. Группа коммуникации определяет, какие процессы можно включить во взаимодействие типа «точка-точка». Сначала все MPI-процессы относят к группе, действующей по умолчанию. Заменить членов группы коммуникации можно, запустив приложения. После старта каждого процесса необходимо определить его ранг с помощью функции MPI_Comm_rank . Функция MPI_Comm_rank возвращает ранг вызывающего процесса. В первом аргументе, передаваемом функции, вызывающий процесс определяет, с каким коммуникатором он связывается, а его ранг возвращается во втором аргументе. Пример использования функции MPI_Comm_rank показан в листинге 9.1.
// Листинг 9.1. Использование функции MPI_Comm_rank //.. .
int Tag = 33;
int WorldSize;
int TaskRank;
MPI_Status Status;
MPI_Init (&argc, &argv) ;
MPI_Comm_rank(MPI_COMM_WORLD, &TaskRank) ; MPI_Comm_size(MPI_COMM_WORLD, &WorldSize) ; //.. .
Коммуникатору MPI_COMM_WORLD по умолчанию при запуске назначаются все MPI-задачи. MPI-задачи группируются по коммуникаторам, которые определяют группу коммуникации. В листинге 9.1 ранг возвращается в переменной TaskRank. Каждый процесс должен иметь уникальный ранг. После определения ранга задаче передаются соответствующие данные либо определяется код, который ей надлежит выполнить. Рассмотрим следующие варианты.
if(TaskRank == 1){ if(TaskRank == 1){
// Некоторые действия. // Используем одни данные.
} }
if (TaskRank == 2){ if(TaskRank == 2){
// Другие действия. // Используем другие данные.
} }
В первом варианте ранг используется для разграничения между процессами выполняемой работы, а во втором — для разграничения данных, которые они должны обрабатывать. Несмотря на то что каждый выполняемый MPI-файл стартует с одним и тем же кодом, модель MPMD (MIMD) можно реализовать с помощью рангов и соответствующего ветвления программы. Аналогично после определения ранга данным процесса можно назначить некоторый тип, тем самым определив конкрет-ные данные, с которыми должен работать конкретный процесс. Ранг также используется при передаче сообщений. MPI-задачи идентифицируют одна другую при обмене сообщениями по рангам и ко
MPI_Send(Buffer,Count, MPI_LONG, TaskRank, Tag,Comm) ;
будет отправлено Count значений типа long MPI-процессу с рангом, равным значению TaskRank. Параметр Buffer представляет собой указатель на данные, посылаемые процессу TaskRank. Параметр Count характеризует количество элементов в буфере Buffer, а не его раз
MPI_Recv(Buffer, Count, MPI_INT, TaskRank, Tag, Comm, &Status);
будет получено Count значений типа int от процесса с рангом TaskRank. Инициатор вызова будет заблокирован до тех пор, пока не получит сообщение от процесса с рангом TaskRank и соответствующим значением тега (Tag). MPI-интерфейс для параметров ранга и тега поддерживает групповые символы. Такими групповыми символами являются значения MPI_ANY_TAG и MPI_ANY_SOURCE. При использовании этих значений вызывающий процесс примет следующее полученное им сообщение независимо от его источника и тега. Параметр Status имеет тип MPI_Status. Информацию об операции приема можно получить из объекта Status. Параметр статуса содержит три поля: MPI_SOURCE, MPI_TAG и MPI_ERROR. Следовательно, объект Status можно использовать для определения тега и источника процесса-отправителя. При известном количестве процессов-участников можно точно определить отправителей сообщений и их получателей. Обычно для этого используется конкретное приложение. Распределение работы также зависит от приложения. Перед началом работы каждый процесс сразу же определяет, сколько других процессов включено в приложение. Это реализуется следующим вызовом: MPI_Comm_size(MFI_COMM_WORLD, &WorldSize) ;
С помощью этой функции определяется размер группы процессов, связанных с конкретным коммуникатором. В данном используется стандартный коммуникатор (MPI_COMM_WORLD). Количество процессов-участников возвращается в параметре WorldSize. Этот параметр имеет тип int. Если каждому процессу известно значение WorldSize, значит, он знает, сколько процессов связано его коммуникатором.
Группирование задач по коммуникаторам
Процессы связываются не только с ранга
Благодаря использованию рангов и коммуникаторов MPI-задачи легко идентифицировать и различать. Ранг и коммуникатор позволяют структурировать программу как SPMD- или MPMD-модель либо как некоторую их комбинацию. Для упрощения кода MPI-программы мы используем ранг и коммуникатор в сочетании с параметризованным программированием и объектно-ориентированными методами. Шаблоны можно использовать не только при
|Таблица 9.1. Функции, используемыедля работы с коммуникаторами
int
MPI_Intercomm_create
(MPI_Comm LocalComm,int LocalLeader, MPI_Comm PeerComm, int remote_leader, int MessageTag, MPI_Comm *CommOut);
Создает
inter
-коммуникатор из двух
intra
коммуникаторов
int
MPI_Intercomm_merge
(MPI_Comm Comm, int High,
MPI_Comm *CommOut);
Соз
ает
intra
-коммуникатор из
inter- коммуникатора
int
MPI_Cartdim_get
(MPI_Comm Comm,int *NDims);
Возвращает декартову топологическую информацию, связанную с коммуникатором
int
MPI_Cart_create
(MPI_Comm CommOld, int NDims, int *Dims, int *Periods, int Reorder, MPI_Comm *CommCart)
Создает новый коммуникатор, к которому присоединяется топологическая информация
int
MPI_Cart_sub
(MPI_Comm Comm, int *RemainDims, MPI_Comm *CommNew);
Делит коммуникатор на подгруппы, которые образуют декартовы подсистемы более низкой размерности
int
MPI_Cart_shift
(MPI_Comm Comm, int Direction, int Display,int *Source,int *Destination);
Считывает смещенные ранги источника и приемника при заданном направлении и величине смещения
int
MPI_Cart_map
(MPI_Comm CommOld, int NDims, int *Dims, int *Periods, int *Newrank);
Преобразует процесс в декартову топологическую информацию
int
MPI_Cart_get
(MPI_Comm Comm, int MaxDims, int *Dims, int *Periods, int *Coords);
Возвращает декартову топологическую информацию, связанную с коммуникатором
int
MPI_Cart_coords
(MPI_Comm Comm, int Rank, int MaxDims, int *Coords);
Вычисляет координаты процесса в декартовой топологии при заданном ранге в группе
int
MPI_Comm_create
(MPI_Comm Comm, MPI_Group Group, MPI_Comm *CommOut) ;
Создает новый коммуникатор
int
MPI_Comm_rank
(MPI_Comm Comm, int *Rank ) ;
Вычисляет и возвращает ранг вызывающего процесса в коммуникаторе
int
MPI_Cart_rank
(MPI_Comm Comm, int *Coords, int *Rank );
Вычисляет и возвращает ранг процесса в коммуникаторе при заданном декартовом местоположении
int
MPI_Comm_compare
(MPI_Comm Comm1, MPI_Comm Comm2, int *Result);
Сравнивает два коммуникатора Comm1 и Comm2
int
MPI_Comm_dup
( MPI_Comm CommIn, MPI_Comm *CommOut) ;
Дублирует уже существующий коммуникатор со всей его кашированной информацией
int
MPI_Comm_free
( MPI_Comm *Comm) ;
Отмечает объект коммуникатора как освобожденный
int
MPI_Comm_group
( MPI_Comm Comm, MPI_Group *Group);
Получает доступ к группе, связанной с заданным коммуникатором
int
MPI_Comm_size
( MPI_Comm Comm, int *Size);
Вычисляет и возвращает размер группы, связанной с заданным коммуникатором
int
MPI_Comm_split
(MPI_Comm Comm, int Color,int Key,MPI_Comm *CommOut) ;
Создает новые коммуникаторы на основе цветов и ключей
int
MPI_Comm_test_inter
( MPI_Comm Comm, int *Flag);
Определяет, является ли коммуникатор inter-коммуникатором
int
MPI_Comm_remote_group
( MPI_Comm Comm, MPI_Group *Group);
Получает доступ к удаленной группе, связанной с заданным inter-коммуникатором
int
MPI_Comm_remote_size
( MPI_Comm Comm, int *Size);
Вычисляет и возвращает размер удаленной
группы, связанной с заданным inter-
коммуникатором
Анатомия MPI-задачи
На рис.9.1 представлена каркасная MPI-программа. Задачи, выполняемые этой программой, просто сообщают свои ранги MPI-задаче с нулевым рангом. Каждая MPI-программа должна иметь по крайней мере функции MPI_Init и MPI_Finalize. Функция MPI_Init инициализирует MPI-среду для вызывающей задачи, а функция MPI_Finalize освобождает ресурсы этой MPI-задачи. Каждая MPI-задача должна вызвать функцию MPI_Finalize до своего завершения. Обратите вни
Использование шаблонных функций для представления MPI-задач
Шаблоны функции позволяют обоб
template
return( X * Y);
}
Для такой шаблонной функции, как эта, используются необходимые пара
//. . .
multiplies
multiplies
Здесь параметр T за
Реализация шаблонов и модельБРМО (типы данных)
Пара
//Листинг 9.2. Использование шаблонных функций для // определения «фронта работ» МР1-задач
int main(int argc, char *argv[]) {
//.. .
int Tag = 2; int WorldSize; int TaskRank; MPI_Status Status; MPI_Init(&argc,&argv) ,-
MPI_Comm_rank (MPI_COMM_WORLD, &TaskRank) ; MPI_Comm_size (MPI_COMM_WORLD, &WorldSize) ; //.. .
switch(TaskRank) {
case 1: multiplies
case 2: multiplies
break; //case n:
//.. .
}
}
Поскольку не существует двух задач с одинаковым ранго
// Листинг 9.3. Использование контейнерных шаблонов в // качестве аргументов шаблонных функций
template
//. . -
locate(Key) //. . .
}
// . . .
MPI_Comm_rank(MPI_COMM_WORLD, &TaskRank); // . . .
switch(TaskRank) {
case 1: {
graph
search
}
break; case 2: {
graph
}
break;
//. . .
В листин
Использование полиморфизмадля реализации MPMD-модели
Полиморфиз
Самолеты, вертолеты, автомобили и подводные лодки — все это потомки класса vehicle (транспортные средства). Объект класса vehicle может заводить мотор, перемещаться вперед, поворачивать вправо, поворачивать влево, останавливаться и пр. В листинге 9.4 демонстрируется, как функция travel использует объект класca vehicle для совершения компьютеризованного путешестви
// Листинг 9.4.
//Функция travel, которая в качестве параметра использует объект класса vehicle
void travel(vehicle *Transport) {
Transport->startEngine; Transport->moveForward ; Transport->turnLeft;
//.. .
Transport-> stop;
}
int main(int argc, char *argv[J) {
//.. . car *Car;
Transportation = new Vechicle; travel(Car); //.. .
}
Функция travel принимает указатель на объект класса vehicle и вызывает методы объекта класса vehicle. Обратите внимание на то, что функция main в листинге 9.4 объявляет объект типа саг, а не vehicle, а также на то, что функции travel вместо объекта типа vehicle передается объект типа car. Это возможно благодаря тому, что в С++ указатель на класс может ссылаться на объект этого типа или на любой объект, который является потомком этого типа. Поскольку класс саг является производным от класса vehicle, то указатель на тип vehicle может ссылаться на объект типа саг. Функция travel написана без учета того, какими конкретно типами vehicle- объектов она будет манипулировать. Для функции travel вполне достаточно, чтобы ее vehicle -объекты могли запускать мотор, двигаться вперед, поворачивать влево, вправо и т.д. Если vehicle -объект способен выполнять эти действия, то функция travel сможет справиться со своей работой. Обратите внимание на то, что на рис. 9.2 методы класса vehicle объявлены как виртуальные (virtual). Объявление методов виртуальными в базовом классе является необходимым условием динамического полиморфизма. В каждом из классов car, helicopter, submarine и airplane будут определены следующие функции.
startEngine; moveForward; turnLeft; turnRight; stop; //.. .
При этом объявление каждой функции будет соответствовать типу транспортного средства. Несмотря на то что транспортное средство каждого типа способно двигаться вперед, метод, в котором обеспечивается движение автомобиля, отличается от метода перемещения подводной лодки. Управление поворотом вправо у самолета отличается от управления таким же поворотом у автомобиля. Следовательно, транспорт-ное средство каждого типа должно реализовать необходимые операции для получения законченного описания «своего» класса. Поскольку эти операции объявляются как виртуальные в базовом классе, они и являются кандидатами для реализации полиморфизма. Если vehicle -указатель, переданный функции travel , в действительности ссылается на объект типа car, то методами, вызываемыми в этой функции (startEngine , moveForward и пр.), реально окажутся те, которые определены в классе car. Если vehicle -указатель, переданный функции travel , вдействительности ссылается на объект класса airplane, то методами, вызываемыми в этой функции, реально окажутся те, которые определены в классе airplane. Это и есть тот случай, когда
Этот тип полиморфиз
В этом случае при обра
$ mpirun -np 16 /trap/search_n_rescue
// Листинг 9.5. Реализация MPI-задачами простого
// поиска и имитации спасения поврежденных
// объектов
template
set
{
//.. .
Transport->startEngine; Transport->moveForward(XDegrees); Transport->turnLeft(YDegrees); //.. .
if (Location.find(Transport->location == Object){ // . .. rescue
}
//.. .
}
int main(int argc, char *argv[])
326 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов..
{
//...
int Tag = 2; int WorldSize; int TaskRank; MPI_Status Status; MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD, &TaskRank); MPI_Comm_size(MPI_COMM_WORLD, &WorldSize); //. . .
switch(TaskRank) {
case 1: {
//. . .
car * Car;
set
travel
}
break;
case 2:
{
//.. .
helicopter *BlueThunder; set
NationalAirSpace,
AirSpace);
//.. .
}
//case n: //. . .
}
}
Программа search_n_rescue будет запущена в 16 процессах, причем все процессы потенциально могут выполняться на различных процессорах, а все процессоры — находиться на различных компьютерах. Несмотря на то что все процессы выполняют один и тот же код, их действия могут радикально различаться (как и данные, с которыми они работают). Шаблоны и полиморфизм позволяют отличать одну MPI-задалу от другой (а значит, и данные, которые они будут использовать). Обратите внимание на то, что в листи
Основные два типа полиморфизма, которые мы здесь демонстрируем, — это
Таблица 9.2. Различные типы полиморфизма, которые можно использовать для упрощения МРI-задач
Динамический
Наследование и виртуальные методы
Вся информация, необходимая для определения того, какие виртуальные методы будет вызывать функция, неизвестна до выполнения программы
Параметрический
Шаблоны
Механизм, в котором один и тот же код используется для различных типов, которые передаются как параметры
Введение MPMD-модели c помощью функций -объектов
Функции-объекты используются в стандартных алгоритмах для реализации горизонтального полиморфизма. Полиморфизм, реализованный с помощью передачи параметра vehicle *Transport в листинге 9.5, является вертикальным, поскольку для функционирования необходимо, чтобы классы были связаны наследованием. При горизонтальном полиморфизме классы связаны не наследованием, а интерфейсом. Все функции-объекты определяют операторную функцию operator . Функции-объекты позволяют разрабатывать MPI-задачи с использованием некоторой общей формы.
// Функция-объект class some_class{ //.. .
operator; //
};
template
//
Т Result; Result = X //. . .
}
Шаблонная функция mpiTask будет работать с любым типом T, который имеет соответствующим образом определенную функцию operator .
//. . .
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &TaskRank); MPI_Comm_size(MPI_COMM_WORLD, &WorldSize); //. . .
if(TaskRank == 0){ //. . .
user_defined_type M; mpiTask(M); //.. .
}
if(TaskRank == N){ //.. .
some_other_userdefined_type N; mpiTask (N) ;
}
//----
Этот горизонтальный полиморфизм не имеет отношения к наследованию или виртуальным функциям. Поэтому, если наша MPI-задача получит свой ранг, а затем объявит тип объекта, в котором определена функция operator , то при вызове функции mpiTask ее поведение будет продиктовано содержимым метода operator . Тогда, несмотря на идентичность всех процессов, запу
Как упростить взаимодействие между MPI-задачами
Помимо упрощения и сокращения размеров кода МРТзадачи с помощью полиморфизма и шаблонов, мы можем также упростить взаимодействие между MPI-задачами, воспользовавшись преимуществами перегрузки операторов. Функции MPI_Send и MPI_Recv имеют следующий формат:
MPI_Send(Buffer, Count, MPI_LONG, TaskRank, Tag, Comm);
MPI_Recv(Buffer,Count,MPI_INT, TaskRank, Tag, Comm, &Status);
При вызове этих функций необходимо, чтобы пользователь указал тип применяемых здесь данных и буфер, предназначенный для хранения посылаемых или принимаемых данных. Спецификация типа посылаемых или принимаемых данных может иметь довольно громоздкий вид и чревата последующими ошибками при передаче неверного типа. В табл. 9.3 приведены прототипы MPI-функций отправки и приема данных и их краткое описание.
Таблица 9.3 Прототипы MPI-функций отправки и приема данных
#include «mpi.h»
int MPI_Send (void *Buffer,int Count, MPI_Datatype Туре, int Destination, int MessageTag, MPI_Comm Comm) ; Выполняет базовую отправку данных
int MPI_Send_init (void *Buffer,int Count, MPI_Datatype Type, int Destination, int MessageTag, MPI_Comm Comm, MPI_Request *Request); Инициализирует дескриптор для стандартной отправки данных
int MPI_Ssend (void *Buffer,int Count, MPI_Datatype Type, int Destination, int MessageTag, MPI_Comm Comm); Выполняет базовую отправку данных с синхронизацией
int MPI_Ssend_init (void *Buffer,int Count, MPI_Datatype Type, int Destination, int MessageTag, MPI_Comm Comm, MPI_Request *Request); Инициализирует дескриптор для стандартной отправки данных с синхронизацией
int MPI_Rsend (void *Buffer,int Count, MPI_Datatype Type, int Destination, int MessageTag, MPI_Comm Comm) ; Выполняет базовую отправкуданных с сигналом готовности
int MPI_Rsend_init (void *Buffer,int Count, MPI_Datatype Type, int Destination, int MessageTag, MPI_Comm Comm, MPI_Request *Request); Инициализирует дескриптор для стандартной отправки данных с сигналом готовности
int MPI_Isend (void *Buffer,int Count, MPI_Datatype Type, int Destination, int MessageTag, MPI_Comm Comm, MPI_Request *Request ); Запускает отправку без блокировки
int MPI_Issend (void *Buffer,int Count, MPI_Datatype Туре, int Destination, int MessageTag, MPI_Comm Comm, MPI_Request *Request); Запускает синхронную отправку без блокировки
int MPI_Irsend (void *Buffer,int Count, MPI_Datatype Туре, int Destination, int MessageTag, MPI_Comm Comm, MPI_Request *Request); Запускает неблокирующую отправкуданных с сигналом готовности
int MPI_Recv (void *Buffer,int Count, MPI__Datatype Type, int source, int MessageTag, MPI_Comm Comm, MPI_Status *Status); Выполняет базовый прием данных
int MPI_Recv_init (void *Buffer,int Count, MPI_Datatype Type, int source, int MessageTag, MPI_Comm Comm, MPI_Request *Request); Инициализирует дескриптор для приема данных
int MPI_Irecv (void *Buffer,int Count, MPI_Datatype Type, int source, int MessageTag, MPI_Comm Comm, MPI_Request *Request); Запускает прием данных без блокировки
int MPI_Sendrecv (void *sendBuffer, int SendCount, MPI_Datatype SendType, int Destination, int SendTag, void *recvBuffer, int RecvCount, MPI_Datatype RecvYype, int Source, int RecvTag, MPI_Comm Comm, MPI_Status *Status); Отправляет и принимает сообщение
int MPI_Sendrecv_replace (void *Buffer,int Count, MPI_Datatype Туре, int Destination, int SendTag,int Source,int RecvTag, MPI_Comm Comm, MPI_Status *Status); Отправляет и принимает сообщение с использованием единого буфера
Наша цель — обеспечить отправку и получение MPI-данных с помо
//...
int X; float Y;
user_defined_type Z;
cout « X << Y « Z;
//...
Здесь разработчик не должен указывать типы данных при вставке их в объект cout. Для вывода этих данных трех типов достаточно определить оператор "<<". Анало
//...
int X; float Y;
user_defined_type Z;
cin >> X >> Y >> Z;
//...
В инструкции ввода данных их типы не задаются. Перегрузка операторов позволяет разработчику использовать этот метод для MPI-задач. Поток cout реализуется из класса ostream, а поток cin — из класса istream. В этих классах определены операторы "<<" и ">>" для встроенных С++-типов данных. Например, класс ostream содержит ряд перегруженных операторных функций "<<".
//.. .
ostream& operator<<(char с);
ostream& operator<<(unsigned char с);
ostream& operator<<(signed char с);
ostream& operator<<(const char *s);
ostream& operator<<(const unsigned char *s);
ostream& operator<<(const signed char *s);
ostream& operator<<(const void *p);
ostream& operator<<(int n);
ostream& operator<<(unsigned int n);
ostream& operator<<(long n);
ostream& operator<<(unsigned long n);
//.. .
С помощью этих определений пользователь классов ostream и istream применяет объекты cout и cin, не указывал типы передаваемых данных. Этот метод перегрузки можно использовать для упрощения МРI- взаимодействия. Мы рассмотрели идею PVM-потока в главе 6. Здесь мы применяем тот же подход к созданию MPI-потока, используя структуру классов istream и ostream в качестве руководства для разработки класса mpi_stream. Потоковые классы состоят из компонентов состояния, буфера и преобразования. Компонент состояния представлен классом ios; компонент буфера — классами streambuf, stringbuf или filebuf. Компонент преобразования обслуживается классами istream, ostream, istringstream, ostringstream, ifstream и ofstream. Компонент состояния отвечает за инкапсуляцию состояния потока. Класс ios включает формат потока, информацию о состоянии (работоспособное или состояние отказа), факт достижения конца файла (eof). Компонент буфера используется для хранения считываемых или записываемых данных. Классы преобразования предназначены для перевода данных встроенных типов в потоки байтов и обратно. UML-диаграмма семейства классов iostream показана на рис. 9.3.
Перегрузка операторов «<<» и «>>» для организации взаимодействия между MPI-задачами
Взаимоотношения и функциональность классов, показанных на рис. 9.3, можно использовать как своего рода образец для проектирования класса mpi_streams. И хотя проектирование потоковых MPI-классов требует больше предварительной работы по сравнению с непосредственны
// Листинг 9.6. Фрагмент объявления
// класса mpi_stream
class mpios{ protected:
int Rank;
int Tag;
MPI_Comm Comm;
MPI_Status Status;
int BufferCount;
//.- . public:
int tag(void);
//...
}
class mpi_stream public mpios{ protected:
mpi_buffer Buffer;
//.. .
public: //.. .
mpi_stream(void) ;
mpi_stream(int R,int T,MPI_Comm С);
void rank(int R);
void tag(int T);
void comm(MPI_Comm С);
mpi_stream &operator<<(int X);
mpi_stream &operator<<(float X);
mpi_stream &operator<<(string X);
mpi_stream &operator<<(vector
mpi_stream &operator<<(vector
mpi_stream &operator<<(vector
mpi_stream &operator<<(vector
mpi_stream &operator>>(int &X);
mpi_stream &operator>>(float &X);
mpi_stream &operator>>(string &X);
mpi_stream &operator>>(vector
mpi_stream &operator>>(vector
mpi_stream &operator>>(vector
mpi_stream &operator>>(vector
//. . .
};
Для того чтобы сократить описание, мы объединили классы impi _stream и ompi _stream в единый класс mpi _stream. И точно так же, как классы istream и ostream перегружают операторы "<<" и ">>", мы обеспечим их перегрузку в классе mpi_stream. В листинге 9.7 показано, как можно определить эти перегруженные операторы:
// Листинг 9.7. Определение операторов и **»*
//. . .
mpi_stream &operator<<(string X) {
MPI_Send(const_cast
MPI__CHAR, Rank, Tag, Comm) ; return(*this);
}
// Упрощенное управление буфером, mpi_stream &operator<<(vector
long *Buffer;
Buffer = new long[X.size]; copy(X.begin,X.end,Buffer);
MPI_Send(Buffer,X.size,MPI_LONG,Rank,Tag,Comm); delete Buffer; return(*this);
}
// Упрощенное управление буфером, mpi_stream &operator>>(string &X) {
char Buffer[10000];
MPI_Recv(Buffer,10000,MPI_CHAR,Rank,Tag,Comm, &Status); MPI_Get_count(&Status,MPI_CHAR,&BufferCount); X.append(Buffer); return(*this);
}
Назначение класса mpios в листинге 9.7 такое же, как у класса ios в семействе классов iostream, а именно: поддерживать состояние класса mpi_stream. Все типы данных, которые должны использоваться в ваших MPI-приложениях, должны иметь операторы "<<" и ">>", перегруженные с учетом каждого типа данных. Здесь мы продемонстрируем несколько простых перегруженных операторов. В каждом случае мы представляем упро
//. . .
int X; float Y;
vector
mpi_s tream S tream (Rank, Tag, MPI_WORLD_COMM) ;
Stream « X << Z; Stream << Y;
//...
Stream >> Z;
Такой подход позволяет программисту, поддерживал потоковое пре
Резюме
Реализация SPMD- и MPMD-моделей параллелизма во многом выигрывает от использования шаблонов и механизма полиморфизма. Несмотря на то что MPT интерфейс включает средства динамического С++-связывания, в нем не используются преимущества методов объектно-ориентированного программирования. Это создает определенные трудности для разработчиков, использующих стандарт MPI. Для упрощения MPMD-программирования можно успешно использовать такие свойства объ-ектноориентированного программирования, как наследование и полиморфизм. Параметризованное программирование, которое поддерживается с помощью C++-шаблонов, позволяет упростить SPMD-программирование MPI-задач. Разделение работы программы между объектами — это естественный способ реализовать параллелизм в приложении. Для того чтобы облегчить взаимодействие между группами объектов, характеризующимися различной степенью ответственности за выполняемую работу, семейства объектов в MPI-приложении можно связать с коммуникаторами. Для поддержки потокового представления используется перегрузка операторов. Применение методов объектно-ориентированного и параметризованного программирования в рамках одного и того же MPI-приложения является воплощением муль-типарадигматического подхода, который упрощает код и во многих случалх уменьшает его объем. Тем самым упрощается отладка программ, их тестирование и поддержка. МРТзадачи, реализованные с помощью шаблонных функций, характеризуются более высокой надежностью при использовании различных типов данных, чем отдельно определенные функции с последующим обязательным выполнением операции приведения типа.
Визуализация проектов параллельных и распределенных систем
Модель системы представляет собой своего рода информационное тело, «собранное» с целью изучения системы и лучшего ее понимания разработчиками и специалистами, которые должны ее поддерживать. При моделировании системы должны быть идентифицированы отдельные ее части, атрибуты, атакже действия, выполняемые системой. Моделирование — важный инструмент впроцессе проектирования любой системы, поэтому очень важно добиться того, чтобы разработчики до конца понимали систему, которую разрабатывают. Моделирование помогает выявить заложенный в систему параллелизм и понять, как именно следует реализовать ее распределение.
Унифицированный язык моделирования (Uniflted Modeling Language — UML) содержит графические средства, используемые для проектирования, визуализации, моделирования и документирования артефактов системы программного обеспечения. Язык UML представляет собой фактический стандарт для моделирования объект-нсюриентированных систем. Этот язык использует символы и условные знаки для обозначения артефактов системы ПО, отображаемых с различных точек зрения и при различной фокусировке. Язык UML вобрал в себя методы объектно-ориентирован-ного анализа и проектирования, предложенные Гради Бучем (GradyBooch), Джеймсом Рамбау Qames Rumbaugh) и Айваром Джекобсоном (Ivar Jacobson) в 1980-х и 1990-х годах. Он был принят рабочей группой по развитию стандартов объектного программирования (Object Management Group — OMG), международной организацией, состоящей из разработчиков ПО и производителей информационных систем и насчитывающей более 800 членов. Принятие UML дало разработчикам ПО не просто единый язык, а инструмент для анализа объектов, их описания, визуализации и документирования.
В этой главе мы покажем, как можно визуализировать и смоделировать параллельную и распределенную систему с помощью UML. Помимо помощи в разработке системы, моделирование позволяет идентифицировать области параллелизма (где именно?), понять необходимость применения синхронизации и взаимодействия подсистем (когда именно?), а также продумать степень распределения объектов (как именно?). Мы рассматриваем диаграммные методы визуализации и моделирования параллельных систем со структурной и поведенческой точек зрения. Однако следует отметить, что классы, объекты и системы, используемые в этой главе как примеры, служат целям демонстрации и необязательно отражают реальные классы, объекты или структуры, используемые в действительно существующей системе.
Визуализация структур
При рассмотрении системы со структурной точки зрения акцент ставится на ее статических частях, т.е. нас интересует, как построены элементы системы. В этом случае изучаются атрибуты, свойства и операции, выполняемые системой, а также ее организация, устройство (состав компонентов) и взаимоотношение элементов в системе. В этом разделе рассматриваются диаграммные методы, используемые для моделирования:
• классов, объектов, шаблонов, процессов и потоков;
• организации объектов, работающих «в одной команде».
Изображаемые при моделировании системы элементы могут быть концептуальными или физическими.
Классы и объекты
Класс — это
Язык UML содержит средства для графического представления класса. Для простейшего изображения класса достаточно начертить прямоугольник и написать на нем имя класса. При использовании только одного имени говорят, что это простое гшя. С помощью диаграммы класса можно также отобразить атрибуты и услуги. предоставляемые пользователю этого класса (или операции, выполняемые этим классом). Чтобы включить в диаграмму атрибуты и операции, прямоугольник отображается с тремя горизонтальными отделениями. В верхнем отделении записывается простое имя класса, в среднем — атрибуты, а в нижнем — операции. Разделы атрибутов и операций можно пометить словами «атрибуты» и «операции» соответственно. Имя класса должно быть указано в любом случае, а раздел атрибутов или операций — по необходимости. Это значит, что если нужно указать один из разделов (атрибутов или операций), то другой отображается пустым. Различные способы представления класса показаны на рис. 10.1.
На рис. 10.1 представ
Ино
С помощью диаграммы класса можно отобразить объект, или экземпляр класса. Как и при использовании класса, простейшее представление объекта состоит в изображении прямоугольника, который содержит подчеркнутое имя объекта. Тем самым указывается именованный экземпляр класса. Именованный экземпляр класса можно сопровождать именем класса или обойтись без него.
mySchedule именованный экземпляр
mySchedule: student_schedule именованный экземпляр с именем класса
Поскольку реальное имя объекта может быть известно только для программы, которая его объявляет, то в системной документации, возможно, имеет смысл указывать анонимные экземпляры классов. Анонимный объект класса можно представить следующим образом.
:student_schedule
Такой тип обозначения
Количество экземпляров, которое может иметь класс, называется
На рис. 10.2 множественность класса student_schedule указана как диапазон 1..7 , а это означает, что наименьшее количество расписаний в нашей системе равно 1, а наибольшее — 7. Приведем еще несколько примеров обозначения множественности класса.
1 Один экземпляр
1..n От одного до заданного числа n.
1.. * От одно
0..1 От нуля до единицы
0 * От нуля до бесконечности
* Бесконечное количество экземпляров
Безусловно, бесконечное количество экземпляров будет ограничено объемом внутренней или внешней памяти.
Отображение информации об атрибутах и операциях класса
Диаграмма класса может содержать более подробную информацию об атрибутах иоперациях класса. В разделе атрибутов можно указать тип данных и/или значение по умолчанию (если оно предусмотрено) для класса и значения атрибутов для объектов. Например, типы данных, содержащиеся в разделе атрибутов класса student_schedule, могут иметь следующий вид.
StudentNumber : string;
Term : string
StudentSchedule : map
ScheduleIterator : map
Для oбъeктa mySchedule эти атрибуты могут принимать такие значения.
StudentNumber : string = «102933»
Term:string = «Spring»
Методы могут быть отображены с параметрами и с указанием типов возвращаемых ими значений.
studentSchedule(&X : map
StudentNumber : string
Фу
На диаграмме класса можно также отобразить свойства атрибутов и операций (методов). Свойства атрибутов помогают описать характер использования того или иного атрибута, что дает возможность судить о том, можно ли его изменять или нет. Так, для описания атрибутов используются три свойства: changeable, addOnly и frozen. Краткое описание этих свойств приведено в табл. 10.1. Для определения методов используются четыре свойства: isQuery, sequential, guarded и concurrent. Они также описаны в табл.10.1. Свойства sequential, guarded и concurrent имеют отношение к параллельности выполнения методов. Свойство sequential описывает операцию, ответственность за синхронизацию которой лежит на инициаторе ее вызова. Такие операции не гарантируют целостности объекта. Свойство guarded описывает параллельно выполняемую операцию с уже встроенной синхронизацией. При этом guarded -операции означают, что в каждый момент времени возможен только один ее вызов. Свойство concurrent описывает операцию, которая позволяет ее одновременное использование. Операции, описываемые с помощью свойств guarded и concurrent, гарантируют целостность объекта. Гарантия целостности объекта применима к операциям, которые изменяют состояние объекта.
Таблица 10.1. Свойс
{changeable} На значения этого типа атрибута никакие ограничения не налагаются
{addOnly} Для атрибутов, y которых значение множественности >1, можно добавлять дополнительные значения. Созданное значение невозможно удалить или изменить
{frozen} После инициализации объекта значение атрибута изменить нельзя
{isQuery} При выполнении метода этого типа состояние объекта остается неизменным. Этот метод возвращает значения
{sequential} Пользователи этого метода для обеспечения гарантии последовательного доступа к нему должны использовать синхронизацию. При множественном параллельном доступе к этому метолу целостность объекта подвергается опасности
{guarded} Синхронизированный последовательный доступ к этому методу встроен в объект; целостность объекта гарантируется
{concurrent} К этому метолу разрешен множественный параллельный доступ: целостность объекта при этом гарантируется
Свойства guarded и concurrent можно использовать для отражения модели PRAM (Parallel Random-Access Machine — параллельнал машина с произвольным доступом). Если метод считывает и/или записывает данные в память, доступную для другого метода, который также считывает и/или записывает данные в гу же память, этот метод может быть описан как PRAM-алгоритм. При этом можно использовать соответствующие свойства, например, такие.
CR (Concurrent Read — параллельное чтение)
concurrent
CW (Concurrent Write — параллельная запись)
concurrent
CRCW (Concurrent Read Concurrent Write — параллельное чтение, параллельная запись)
concurrent
EW (Exclusive Write — монопольнал запись)
guarded
ER (Exclusive Read — монопольное чтение)
guarded
EREW (Exclusive Read Exclusive Write — монопольное чтение, монопольная запись)
guarded
Описание класса student_schedule можно сделать еще более подробным, указав с помощью свойств, как использовать его (класса) атрибуты и операции.
StudentNumber : string {frozen}
Term : string {changeable}
StudentSchedule : map
scheduleDayOfWeek(&X : vector
studentNumber : string {isQuery, concurrent}
Атрибут StudentNumber представляет собой константу типа string. После присвоения значение константы изменить нельзя. Если объект student_schedule используется для того же студента, но для различных периодов времени, то атрибуты Term и StudentSchedule должны быть модифицируемыми. Метод scheduleDayOfWeek принимает вектор курсов (vector
На диаграмме класса можно отобразить е
Симво
public
(+) Об
ий доступ
protected
(#) Доступ имеет сам к
асс и его потомки
private
(-) Доступ имеет то
ько сам к
асс
Организация атрибутов и операций
От того, как будут организованы атрибуты и операции в соответствующих отделениях диаграммы класса, зависит степень успешности использования этого класса. Атрибуты и операции можно упорядочить по алфавиту, уровню доступа или категориям. Как оказалось, алфавитный порядок вряд ли поможет узнать, как могут называться те или иные атрибуты или операции (если документация находится в руках пользователя системы), или какие из них еще не определены (если документация используется в процессе разработки). Упорядочение по уровню доступа зарекомендовало себя гораздо лучше. В этом случае пользователь четко видит, какие атрибуты и операции являются, например, общедоступными (public) или закрытыми (private). Знание перечня защищенных (protected) членов поможет расширить возможности класса или специализировать ero, используя механизм наследования. Такое упорядочение просто реализовать с помощью символов видимости (+, - и #) или C++-спецификаторов доступа (public, private и protected,).
Существует несколько способов разбиения атрибутов и операций по кате
• конструктор по умолчанию;
• деструктор;
• конструктор копии;
• операции присваивания;
• операции сопоставления на равенство;
• операции ввода-вывода;
• операции хеширования;
• операции запросов.
Этот список можно использовать в качестве основного перечня категорий для классификации операций, определяемых в классе. В этот перечень можно внести категории, которые позволяют указать дополнительные характеристики для атрибутов и операций.
• static
• const
• virtual
• pure virtual
• friend
При выборе категорий следует исходить из того, какал из них лучше всего описывает услуги, предоставляемые классом. Имя категории справа и слева заключается вдвойные угловые скобки («. . .»). На рис. 10.3 показано два возможных способа организации атрибутов и операций для класса student_schedule, использующих: символы видимости и спецификаторы доступа (рис. 10.3, а) и категории минимально-гостандартного интерфейса (рис. 10.3, б).
Шаблонные классы
Шаблонный класс представляет собой механизм, который позволяет в качестве параметра в определении класса использовать тип. Шаблон определяет действия, которые выполняются над переданным ему типом. В С++ параметризованный класс создается с помощью ключевого слова template.
template
Параметр Туре представляет любой тип, передаваемый шаблону. Это может быть встроенный С++-тип или определенный пользователем класс. При объявлении параметра Туре шаблон связывается с эле
Контейнер map использует для ключа тип string, а для значения — тип vector. Контейнер vector содержит объекты определенно
map
map
vector
vector > Вектор отображений, которые устанавливают соответствие между числом и строкой
Шаблонные классы также представляются как прямоугольники. Параметризованный тип представляется как прямоугольник (меньшего размера), начертанный штриховой линией и расположенный в правом верхнем углу прямоугольника класса. Шаблонный класс может быть
Этот вариант называется
Отношения между классами и объектами
Язык UML определяет три типа отношений между классами:
• зависимости;
• обобщения;
• ассоциации.
Зависимость, обобщение и ассоциацию можно рассматривать как различные классификации отношений, поскольку существует множество типов зависимостей, обобщений и ассоциаций, которые можно определить. Каждал классификация отношений имеет собственный символ представления. Таким символом является отрезок прямой (начертанный сплошной или пунктирной линией) между элементами, который может увенчиваться стрелкой некоторого типа. Для более детального определения отношений отрезки прямых могут дополняться стереотипами и специальными обозначениями («украшениями»).
<
размещен рядом со стрелкой, которая отображает зависимость используемых объектов. Под «украшениями» понимаются текстовые или графические элементы, добавляемые к базовой интерпретации элемента и используемые для документирования сведений о спецификации элемента. Например, ассоциация отображается в виде отрезка сплошной линии между элементами.
Зависимость обозначается пунктирной направленной линией (со стрелкой), которая указывает на зависимую конструкцию. Отношение зависимости следует применять в случае, когда одна конструкция использует другую. Обобщение обозначается сплошной направленной линией со стрелкой, указывающей на родительский класс (суперкласс). Отношение обобщения следует применять в случае, когда одна конструкция выведена из другой. Ассоциация обозначается сплошной линией, которая соединяет одинаковые или различные конструкции. Отношение ассоциации следует применять в случае, когда одна конструкция структурно связана с другой. Некоторые стереотипы и ограничивающие условия, которые применяются к зависимостям, приведены в табл. 10.2. Эти стереотипы используются для отображения зависимостей между классами, интерактивными объектами, состояниями и пакетами. Стереотипы и ограничивающие условия, которые могут применяться к обобщениям и ассоциациям, приведены в табл. 10.3 и 10.4. Если стереотипы используют графические «украшения», они показаны в таблицах.
Таблица 10.2. Стереотипы, применяемые к зависимостям
<< bind >> (<< связать>>)
источник реализует шаблонный приемник, используя peальные параметры
<
видимость источника распространяется на содержимое приемника
<
источник является экземпляром приемника;используется для определения отношений между классами и объектами
<< instantiate>>(<< создать экземпляр>>)
источник создает экземпляры приемника;используется для определения отношений между классами и объектами
<< refine>> (<< уточнить >>)
источник представляет более высокий уровень детализации, чем приемник; используетсядля определения отношений между производным и базовым классами
<< use >>
источник зависит от открытого (public) интерфейса приемника
(<< использовать>>)
<< become>>(<< стать>>)
объект-приемник совпадает с объектом-источником, но в более поздний период жизненного цикла объекта; приемник может иметь другие значения, состояния и пр.
<
объект-источник вызывает метод приемника
(<< вызвать>>)
<< сору >>(<< копировать>>)
объект-приемник является точной и независимой копией объекта-источника
<
исходному пакету предоставляется право ссылаться на элементы приемного пакета
<
данный прецедент приемника расширяет поведение источника
<
данный прецедент источника может включать прецедент приемника
Ассоциации имеют еще один уровень детализации, который может быть применен к стереотипам, перечисленным в табл. 10.4:
• Имя Ассоциация может и
• Роль Роль обозначает функцию, которую выполняет класс, представленный на одном конце линии ассоциации, относительно класса, представленного на другом конце этой линии.
• Множественность Обозначение множественности может использоваться для указания количества объектов, которые могут быть связаны с помощью данной ассоциации. Множественность можно отображать на обоих концах линии ассоциации.
• Передвижение Передвижение по ассоциации может быть однонаправленным, если объект 1 связан с объектом 2, но объект 2 не связан с объектом 1.
Таблица 10.3. Стереотипы и огра
• Стереотип << implementation >> (« реализация ») потомок наслелует реализацию родителя, но не делает открытыми (public) его интерфейсы и не поддерживает их
• Ограничение { complete } ({полнота}) Обусловливает, что все потомки в обобщении получили имена, и никаких дополнительных потомков больше не было выведено
• Ограничение { incomplete }({неполнота}) не все потомки в обобщении получили имена, и дополнительные потомки могут быть выведены
• Ограничение { disjoint } ({несовместимость}) объекты родителя не могут иметь больше одного потомка, используемого в качестве типа
• Ограничение { overlapping }({перекрытие}) объекты родителя могут иметь больше одного потомка, используемого в качестве типа
Таблица 10.4. Стереотипы и ограничивающие условия, которые могут применяться к ассоциациям
• navigation (передвижение) Описывает однонаправленную (нереверсивную) ассоциацию, при которой объект 1 связан с объектом 2, но объект 2 не связан с объектом 1
• aggregation (агрегирование) Описывает связь «целое-часть», при которой «часть» во время своего существования связана не только с одним «целым»
• composition (композиция) Описывает связь «целое-часть», при которой «часть» во время своего существования может быть связана только с одним «целым»
• Ограничение { implicit } ({неявное}) Обусловливает, что отношение является концептуальным
• Ограничение { ordered } ({упорядоченность}) Обусловливает, что объекты на одном конце ассоциации упорядочены
• Свойство { changeable } ({модифицируемость}) Описывает, что может быть добавлено, удалено и изменено между двумя объектами
• Свойство { addOnly } ({расширяемость}) Описывает новые связи, которые могут быть добав
• Свойство { frozen } ({жесткость}) Описывает связь, которая после добавления к объекту на противоположном конце ассоциации не может быть изменена или удалена
Интерфейсные классы
Интерфейсный класс используется для модификации интерфейса другого класса или множества классов. Такая модификация упрощает использование класса, делает его более функциональным, безопасным или семантически корректным. Примерами интерфейсных классов могут служить
// Листинг 10.1. Использование класса stack в качестве
// интерфейсного класса
template < class Container > class stack {
//...
public:
typedef Container::value_type value_type;
typedef Container::size_type size_type; protected:
Container с;
public:
bool empty(void) const {return c.empty;}
size_type size(void) const {return c.size;}
value_type& top(void) {return c.back; }
const value_type& top const {return c.back; }
void push(const value_type& x) {c.push.back(x); }
void pop(void) {c.pop.back; }
};
Класс stack объявляется путе
stack < vector< T > > Stack;
В данном случае типом Container является класс vector, но в качестве класса реализации для интерфейсного класса stack (вместо класса vector) можно использо-ватьлюбой контейнер, который определяет следующие методы:
empty size back push.back pop.back
Класс stack поддерживает се
Существует несколько способов отображения интерфейса. Один из них — круг, рядом с которым (чаще — под ним) записывается имя интерфейсного класса. Этот способ показан на рис. 10.5,
Для отображения отношений
Организация интерактивных объектов
Как видите, классы и интерфейсы можно использовать в качестве строительных блоков (т.е. базовых элементов) при создании более сложных классов и интерфейсов. В распределенной или параллельной системе возможно существование больших исложных структур, сотрудничающих с другими структурами, что создает объединение классов и интерфейсов, работающих вместе над достижением общих целей системы. В языке UML такое поведение называется
Сотрудничество отображается в виде эллипса (начертанного пунктирной линией), содержащего название вариа
Отображение параллельного поведения
При отражении поведенческой характеристики системы акцент ставится на ее динамических аспектах. С этой точки зрения нас интересует, как ведут себя элементы системы при взаимодействии с другими элементами той же системы. Именно во взаимодействии одних элементов с другими и проявляются особенности параллелизма. Диаграммы, используемые в этом разделе, позволяют смоделировать:
• поведение объекта в течение его периода существования;
• поведение объектов, которые совместно работают ради достижения конкретной цели;
• поток управления с акцентом на определенном действии или последовательности действий;
• синхронизацию действий элементов и взаимодействие между ними.
В этом разделе также описаны диаграммы, используемые для моделирования распределенных объектов.
Сотрудничество объектов
Сотрудничество объектов заключается в привлечении друг друга к работе с целью выполнения некоторой конкретной задачи. Они не вступают в постоянные отношения. Одни и те же объекты могут привлекаться разными объектами для выполнения различных задач. Сотрудничество объектов можно представить в виде диаграммы сотрудничества. Диаграммы сотрудничества имеют структурную и интерактивную части. Структурную часть мы рассмотрели выше. Интерактивнал часть отображается в виде графа, вершинами которого являются объекты — участники рассматриваемого сотрудничества. Связи между объектами представляются ребрами. Ребра могут сопровождаться сообщениями, передаваемыми между объектами, вызовами методов и индикаторами стереотипов, которые позволяют подробнее отобразить характер связи.
Связь между объектами имеет тип ассоциации. С двумя связанными объектами мотут выполняться действия. В результате действия может измениться состояние одного или двух объектов. Приведем примеры различных типов действий, связанных с объектами.
• create Объект может быть создан
• destroy Объект может быть разрушен
• call Операция, определенная в одном объекте, может быть вызвана другим объектом или им самим
• return Объекту возвращается значение
• send Объекту может быть послан сигнал
При вызове и выполнении любо
Эти действия могут иметь место, если принимающий объект видим для вызывающего. Для объяснения причины видимости объекта можно использовать следующие стереотипы.
• association Объект видим по причине существования ассоциации (самый общий случай)
• parameter Объект видим, поскольку он является параметром для вызывающего объекта
• local Объект видим, поскольку он имеет локальную область видимости для вызывающего объекта
• global Объект видим, поскольку он имеет глобальную область видимости для вызывающего объекта
• self Объект вызывает собственный метод
Помимо перечисленных, возможно применение и других стереотипов.
При вызове некоторого метода возможен вызов других методов иными объектами. Последовательность выполнения операций можно отобразить с помо
Как показано на рис. 10.7, объект MainObject выполняет две операции в слелующей последовательности:
1: << create >>
2: Value := performAction(ObjectF)
При выполнении операции 1 объект MainObject создает объект Obj ectA. Объект ObjectA локален по отношению к объекту MainObject (поскольку имеет место включение объектов). Это инициирует первую последовательность операций во вложенном потоке управлени
Объект ObjectA вызывает собственный метод. Выполнение объектом собственно
1.1.1 : initializeB
1.1.2: initializeC
В этой последовательности два других объекта (которые локальны по отношению кобъекту ObjectA) инициализируются посредством вызова соответствую
является началом еще одной вложенной последовательности действий. Объекту ObjectA передается объект ObjectD. Объект ObjectA вызывает операцию, определенную в объекте ObjectD:
2.1: doAction
Объект ObjectA имеет право вызвать эту операцию, поскольку объект ObjectD является параметро
Процессы и потоки
При использовании языка UML дл
Язык UML позволяет представить активный объект или класс таким же способом, как статический объект, за исключением того, что периметр прямоугольника, обозначающего этот объект или класс, обводится более жирной линией. В этом случае можно также использовать следующие два стереотипа:
process
thread
Индикаторы этих стереотипов позволяют отобразить рааличие между двумя типами активных объектов. На рис. 10.8 показана PVM-задача в виде активного класса и активного объекта. Диаграмма сотрудничества может состоять из активных объектов.
Отображение нескольких потоков выполнения и взаимодействия между ними
В параллельной и распределенной системе возможно существование нескольких потоков выполнения, которые относятся к одному или нескольким процессам. Эти процессы и потоки могут выполняться в одной компьютерной системе с несколькими процессорами либо распределяться между несколькими различными компьютерами. Для представления каждого потока выполнения используется активный объект или класс При создании активного объекта инициируется независимый поток выполнения. При разрушении активного объекта этот поток прекращает свое существование. Моделирование потоков в системе позволяет успешно осуществить управление, синхронизацию и взаимодействие между ними.
В диаграмме сотрудничества для идентификации потоков используются числа и стрелки со сплошной заливкой наконечника. В диаграмме сотрудничества, которая состоит из активных объектов параллельной системы, имя активного объекта представляется порядковыми числами операций, выполняемых активным объектом. Активный обьект может вызвать метод, определенный в другом объекте, и приостановить выполнение до тех пор, пока этот метод не завершится. Стрелки используются не только для отображения направления хода выполнения потока, но и природы его поведения. Стрелки со сплошной заливкой наконечника используются для представления синхронного вызова, а стрелка с однореберным наконечником — для представления асинхронного вызова. Поскольку один и тот же метод может быть вызван сразу несколькими активными объектами, то для описания синхронизации этого метода можно использовать такие его свойства:
• sequential
• guarded
• concurrent
На рис. 10 9 представлена диаграмма сотрудничс ства нескольких активных объектов, которые «совместными усилиями» создают расписание студента. Объект
MajorAgent Создает список имеющихся основных курсов
MinorAgent Создает список имеющихся непрофилирующих курсов
FilterAgent Фильтрует список курсов и генерирует список возможных курсов
ScheduleAgent Генерирует несколько вариантов расписаний на основе списка возможных курсов
Объект schedule_of_courses содержит все и
Объекты blackboard и schedule_of_courses доступны при параллельном к ним обращении со стороны нескольких агентов. В данном варианте сотрудничества оба эти объекта видимы для всех агентов. А
MajorAgentl: currentDegreePlan
MajorAgent2 : coursesTaken
MajorAgent3 : scheduleOfCourses
MajorAgent4 : suggestionsForMajor
MinorAgentl:currentDegreePlan
MinorAgent2:coursesTaken
MinorAgent3:scheduleOfCourses
MinorAgent4:suggestionsForMinor
Как видите, к имени активного объекта, который вызывает эти методы, присоединяется порядковый номер. Оба объекта параллельно вызывают методы объектов blackboard и schedule_of_courses. Все эти методы параллельно синхронизированы и защищены от одновременного вызова. Методы masterList и possibleCourses имеют свойство guarded. Одни объекты могут модифицировать содержимое курсов, а другие— считывать его. Поэтому методы masterList и possibleCourses защищены разрешением только последовательного к ним доступа (EREW).
Последовательность передачи сообщений между объектами
В то время как в диаграмме сотрудничества основное внимание уделяется структурной организации и взаимодействию объектов, совместно выполняющих некоторую за-далу или реализующих прецедент (вариант использования системы), в диаграмме последовательностей акцент ставится на временном упорядочении вызовов методов или процелур, составляющих данную задалу или прецедент. В диаграмме последовательностей имя каждого объекта или консгрукции отображается в собственном прямоугольнике. Все прямоугольники размещаются в верхней части диаграммы, вдоль ее оси X. В диаграмму следует включать только основных исполнителей прецедента и наиболее важные функции, в противном случае диаграмма будет перенасыщена деталями и утратит свою полезность. Объекты упорядочиваются слева направо, начинал с объекта или про-целуры, которая является инициатором действия для большинства второстепенных объектов или процедур. Вызовы функций отображаются вдоль оси Y сверху вниз в порядке возрастания значения времени. Под каждым прямоугольником наносятся вертикальные линии, представляющие «жизненные пути» (линии жизни) объектов. Стрелки со сплошной заливкой наконечника, направленные от линии жизни одного объекта клинии жизни другого, обозначают вызовы функций или методов (причем такая стрелка всегда направлена от инициатора вызова). Стрелки с «реберными» наконечниками имеют обратное направление (т.е. к инициатору вызова), обозначая возврат из функции или метода. Каждый вызов функции помечается ее именем. Помимо имени, при необходимости отображается информация об аргументах и условиях вызова, например:
[list != empty]
getResults
Функция или метод не выполнится, если заданное условие не будет истинны
На рис. 10.10 показана диагра
Деятельность объектов
Язык UML можно использовать для моделирования видов деятельности объектов — участников конкретной операции или прецедента. В этом случае строится
Действие и деятельность и
Диаграмма (видов) деятельности представляет собой граф, узлы которого обозначают действия или виды деятельности, а ребра — безусловные переходы. Безусловность перехода состоит в том, что для того, чтобы он произошел, не требуется никакого события. Переход происходит сразу же по завершении предыдущего действия или вида деятельности. Эта диаграмма содержит ветви решений, символы начала, останова и синхронизации, которые объединяют несколько действий (или видов деятельности) или обеспечивают их разветвление. Состояния действий и видов деятельности представляются аналогичным образом. Для представления состояния действия или деятельности в языке UML используется стандартный символ блок-схемы, который обычно служит для отображения точек входа и выхода. Этот символ применяется независимо от типа действия или деятельности. Мы предпочитаем использовать стандартные символы блоксхемы, которые позволяют отличить действия ввода-вывода (параллелограмм) от действий обработки или преобразования (прямоугольник). Описание действия или вида деятельности, т.е. имя функции, выражения, прецедента или программы, отображается в соответствующем элементе графа. Состояние деятельности может дополнительно включать отображение действий входа и/или выхода.
По завершении одного действия происходит немедленный переход к началу следующего. Переход обозначается стрелкой с двухреберным наконечником, направленной от одного состояния к другому (следующему). Переход, который указывает на состояние, называется
Диаграммы деятельности подобно блок-схемам имеют символ решения. Символ решения имеет форму ромба с одним входящим переходом и двумя (или более) выходящими переходами. Выходящие переходы сопровождаются условиями, которые определяют дальнейшее направление передачи управления. Это условие представляет собой обычное булево выражение. Выходящие переходы должны охватывать все возможные варианты ветвления. На рис. 10.11 показан символ решения, используемый при определении необходимости построения источника знаний.
Ино
При создании объекта MajorAgent вызывается его конструктор, который (см. рис. 10.12) инициирует три параллельных потока выполнения. После завершения этих трех действий потоки соединяются в единый поток, назначение которо
Эту диаграмму можно разбить на три отдельных раздела, именуемых «плавательными дорожками». В каждой такой дорожке происходят действия или виды деятельности конкретного объекта, компонента или прецедента. «Плавательные дорожки» разделены на диаграмме вертикальными линиями. Одно действие (или вид деятельности) может происходить только в одной дорожке. Линии переходов и линии синхронизации могут пересекать одну или несколько дорожек. Действия или виды деятельности, обозначенные в одной и той же или различных дорожках, но находящиеся при этом на одном уровне, являются параллельными. Диаграмма деятельности с «плавательными дорожками» показана на рис. 10.13.
Назначение этой диаграммы деятельности — смоделировать последовательность действий объекта blackboard, который
Конечные автоматы
С помощью конечных автоматов отображается поведение единой логической конструкции, определяющей последовательность ее преобразований в качестве ответов на внутренние и внешние события в течение ее линии жизни. Такой единой логической конструкцией может быть система, прецедент или объект. Конечные автоматы используются для моделирования поведения одного элемента. Элемент может реагировать на такие события, как процедуры, функции, операции и сигналы. Элемент мо-жет также отвечать на факт истечения времени. Когда происходит подобное событие, элемент реагирует на него определенным видом деятельности или путем выполнения некоторого действия, которое приводит к изменению состояния этого элемента или созданию некоторого артефакта. Выполняемое в этом случае действие должно зависеть от текущего состояния элемента. Под состоянием понимается ситуация, которая создается в результате выполнения элементом некоторого действия или его ответа на некоторое событие в течение его линии жизни.
Конечный автомат можно представить в виде таблицы или ориентированного графа, именуемого
Диаграммы состояний используются для моделирования динамических аспектов объекта, прецедента или системы. Диаграммы последовательностей, видов деятельности, сотрудничества и (добавленнал) диаграмма состояний используются для моделирования поведения системы (или объекта) в период ее (его) активности. Структур-нал часть диаграммы сотрудничества и диаграмма классов позволяют смоделировать структурную организацию объекта или системы. Диаграммы состояний прекрасно подходят для описания поведения объекта вне зависимости от конкретного прецедента. Их следует использовать не для описания поведения нескольких взаимодействующих или сотрудничающих объектов, а для описания поведения объекта, системы или прецедента, который претерпевает ряд преобразований, причем именно в случае, когда одно преобразование может быть вызвано несколькими событиями. Речь идет о таких логических конструкциях, которые весьма активно реагируют на внутренние или внешние события.
10.2. Отображение параллельного поведения 367
В диаграмме состояний узлы представляют состояния, а ребра— переходы. Состояния обозначаются прямоугольниками с закругленными углами, внутри которых записываются названия состояний. Переходы изображаются линиями с двухребер-ными стрелками, связывающими исходное и целевое состояния, причем острие стрелки должно указывать на целевое состояние. Существуют также начальное и конечное состояния. Начальное состояние представляет собой начало работы конечного автомата. Оно обозначается черной точкой с ребром перехода к первому состоянию автомата. Конечное состояние, означающее, что система, прецедент или объект достигли конца своей линии жизни, отображается черной точкой, встроенной в окружность.
Состояние имеет несколько частей (они перечислены в табл. 10.5). Состояние можно представить простым отображением его названия в центре соответствующей вершины диаграммы состояний (прямоугольника с закруглёнными углами). Если внутри этого прямоугольника необходимо отобразить также некоторые действия, то для названия состояния должен быть выделен отдельный раздел в верхней части прямоугольника. Действия перечисляются под этим разделом и должны иметь сле-лующий формат отображения:
метка [Условие] / действие или деятельность
Расс
Здесь do — это метка, которая используется для обозначения выполнения указанного действия до тех пор, пока объект находится в данном состоянии. Имя validate(data) — это имя вызываемой функции, а data — имя аргумента, с которым она вызывается. Если действие состоит в обращении к функции или метолу, то аргументы желательно указывать.
Таблица 10.5. Состав
Условие — это условное выражение, которое приводится к значению ЛОЖЬ или ИСТИНА. Если условие дает значение ИСТИНА, выполняется действие или осуществляется деятельность, напри
Действие выхода (exit) send(data) защищено выражением data valid, которое при вычислении может дать ложное или истинное значение. Если при выходе из данного состояния это выражение даст значение ИСТИНА., то будет вызвана функция send(data). Использование выражения защиты необязательно.
Переходы из одного состояния (объекта, системы или прецедента) в другое происходят при наступлении событий. Существует два вида переходов, которые мотут осуществляться без изменения состояния (объекта, системы или прецедента) — это внутренние и самопереходы.
Переход между разными состояниями означает, что между ними существует некоторое отношение. В то время, как объект находится в одном (исходном) состоянии, может произойти некоторое событие или могут создаться определенные условия, которые заставят этот объект перейти в другое (целевое) состояние. Таким образом, переход объекта из состояния в состояние инициируется событием. Один переход может иметь несколько параллельно существующих исходных состояний. В этом случае они соединяются перед осуществлением перехода. Один переход также может иметь несколько параллельно существующих целевых состояний, и тогда имеет место разветвление. Составные части перехода перечислены в табл. 10.6. Переход изобра-кается линией, направленной от исходного состояния к целевому. Имя инициатора события отображается рядом с переходом. Подобно действиям и видам деятельности, события также могут быть защищены. Переход может быть безусловным, а это значит, что для его осуществления не требуется никакого специального события. При выходе из исходного состояния объект немедленно переходит в целевое состояние.
Таблица 10.6. Составные части перехода
Параллельные подсостояния
Подсостояние позволяет еще больше упростить описание модели поведения системы с параллелизмом
Состояние
Распределенные объекты
Распределенные объекты — это объекты, выполняющиеся на различных процессорах, принадлежащих различным компьютерам.
Диаграмма развертывания состоит из узлов и объектов или компонентов, которые размещаются в этих узлах.
Существует два способа смоделировать местоположение компонентов или объектов в UML-диаграмме развертывания: посредством вложения или использования тегированного значения.
Согласно первому способу компоненты, которые располагаются в узле, перечисляются внутри символьного обозначения узла. Второй способ предлагает отображать местоположение компонентов в символе компонента. Узлы являются частью диаграммы развертывания. В качестве символа узла используется куб. Куб может иметь два отдельных раздела: один будет содержать индикатор стереотипа, описывающий тип узла, а второй — список компонентов, относящихся к этому узлу (первый способ). При использовании символа компонента (второй способ) тегу location (местоположение) присваивается имя уала, в котором размещается данный компонент. Тег
Тег location может быть частью любой диаграммы, в которой местоположение компонентов является существенным фактором (например, в диаграммах сотрудничества, объектов или видов деятельности). На рис. 10.17 отображены два способа обозначения местоположения компонентов в распределенной системе. В части
Визуализация всей системы
Система состоит из множества элементов, включал подсистемы, которые сотрудничают между собой с целью выполнения конкретных задач. Сотрудничество — это агрегирование конструкций, соединяемых в процессе регулярного взаимодействия.
Рассмотренные в этой главе диаграммные методы позволяют разработчику взглянуть на систему с различных точек зрения, с различных уровней, как извне, так и изнутри. В этом разделе мы обсудим моделирование системы в целом. Это означает, что на самом высоком уровне моделирования следует отображать только основные компоненты или функциональные элементы. Диаграммные методы, предлагаемые для рассмотрения в этом разделе, используются для моделирования развертывания системы и ее архитектуры. И хотя этот раздел — последний в этой главе, моделирование и документирование системы в целом должно быть первым этапом ее проектирования и разработки.
Визуализация развертывания систем
Развертывание системы — последний этап в ее разработке. При развертывании системы имеет смысл смоделировать реальные физические компоненты исполняемой версии системы. Диаграмма развертывания отображает конфигурацию элементов оборудования и программных компонентов. Программные компоненты представляют собой такие реальные выполняемые модули, как активные объекты (процессы), библиотеки, базы данных и пр. Диаграмма развертывания состоит из узлов и компонентов. Компоненты - это экземпляры физической реализации логических элементов. Например, класс— это логический элемент, который может быть реализован в виде одного или нескольких компонентов. Класс можно разделить на процессы или потоки, и каждый процесс или поток в диаграмме развертывания может быть компонентом. Компоненты класса могут выполняться на различных узлах одного компьютера (потоки/процессы) или различных компьютерах (процессы).
Узел обозначается в виде куба. Узлы соединяются связями. Компоненты и узлы также могут соединяться связями. Как упоминалось выше, узел может содержать список компонентов, либо компонент может быть отображен отдельно от узла, но при этом необходимо показать связь между ними. Компонент можно представить в виде прямоугольника с указанием тегов в его левой части. Имя компонента указывается внутри его символьного обозначения.
Для отображения более крупных частей системы компоненты можно сгруппировать в пакеты или подсистемы. Пример диаграммы развертывания показан на рис. 10.18. Здесь пользователи подключаются к системе через intranet. Узлы являются частью кластера компьютеров. Они группируются в пакет. Пользователи подключаются к кластеру как к единому элементу. В каждом узле перечисляются программные компоненты, которые на немустановлены. Взаимодействие межлуузлами обеспечивается посредством сетевого узла.
Архитектура системы
Моделирование и документирование архитектуры системы — это ее описание па самом высоком уровне. Гради Буч, Джеймс Рамбау и Айвар Джекобсон определяю, архитектуру как
набор важных решений по организации системы программного обеспечения, выбор структурных элементов и их интерфейсов, посредством которых составляется система, вместе с их поведением, определенным на периоды их сотрудничества, объединение этих структурных и поведенческих элементов в более крупные подсистемы и архитектурный стиль, который направляет эту организацию — эти элементы и их интерфейсы, их варианты взаимодействия и их композицию.
Моделирование и документирование архитектуры системы должно охватывать ее логические и физические элементы, а также структуру и поведение системы на самом высоком уровне.
Архитектура системы — это ее описание с различных точек зрения, но с акцентом на структуре и организации системы. Ниже представлены различные точки зрения.
Очевидно, что эти «поля зрения» (представления о системе) частично перекрываются и взаимодействуют между собой. Например, в описании назначения системы могут упоминаться прецеденты, а при описании ее реализации процессы часто представляют в качестве компонентов. Программные компоненты используются как в части реализации, так и части развертывания системы. При описании архитектуры системы очень полезно строить диаграммы, которые отражают каждый из перечисленных выше ее «портретов».
Систему можно разложить иа подсистемы и модули. Подсистемы и модули могут быть подвергнуты дальнейшей декомпозиции и разложены на компоненты, узлы, классы, объекты и интерфейсы. В языке UML подсистемы и модули, используемые на архитектурном уровне документации, называются пакетами. Пакет можно использовать для организации элементов в группу, которая описывает общую цель этих элементов. Пакет представляется в виде прямоугольника со вкладкой (ярлыком), расположенной над его верхним левым углом. Символ пакета должен содержать его название. Пакеты в системе могут связывать отношения, построенные на основе композиции, агрегирования, зависимости и наследования. Для того чтобы отличать один тип пакета от другого, можно использовать индикаторы стереотипов. На рис. 10.19 показаны пакеты, входящие в систему составления расписаний. Для системного пакета используется индикатор <
Одни пакеты могут содержать другие пакеты. В этом случае имя пакета указывается во вкладке. На рис. 10.19 также показано содержимое каждой подсистемы.
Резюме
Модель системы представляет собой своего рода информационное тело, «собранное» с целью изучения системы. При моделировании любой системы не обойтись без документирования ее различных аспектов. Поскольку в создании системы обычно занято множество людей, очень важно, чтобы все они пользовались одним языком. Таким языко
Диа
Диаграммы развертывания используются для моделирования системы с точки зрени
Проектирование компонентов для поддержки параллелизма
При реализации параллелизма в программном обеспечении необходимо следовать одному важном)- правилу: параллелизм нужно обнаружить, а не внести извне. Иногда цель увеличения быстродействия программы не является достаточно оп-равданной для насаждения параллелизма в логику программы, которая по своей природе является последовательной. Параллелизм в проекте должен быть естественным следствием требований системы. Если параллельность определена в технических требованиях ксистеме, то следует с самого начала рассматривать варианты архитектуры и алгоритмы, которые поддерживают параллелизм. В противном случае необходимость паралле-лизма «всплывет» в уже существующей системе, которая изначально была нацелена лишь на выполнение последовательных действий. Такал участь часто постигает системы, которые первоначально разрабатывались как однопользовательские, а затем постепенно вырастали во многопользовательские, или системы, которые с функциональной точки зрения слишком далеко отошли от исходных спецификаций. В таких системах намерение внести в систему параллелизм можно сравнить с попыткой «махать руками после драки», и в этом случае для поддержки параллельности остается лишь делать архитектурные «пристройки». В этой книге мы описываем методы реализации естественного параллелизма. Другими словами, если мы знаем, что нам нужно обеспечить параллелизм, нас интересует, как это сделать, используя средства С++?
Мы представляем архитектурный подход к управлению параллелизмом в программе, используя преимущества С++-поддержки объектно-ориентированного программирования и универсальности. В частности, С++-средства поддержки наследования, полиморфизма и шаблонов успешно применяются для реализации архитектурных решений и программных компонентов, которые поддерживают параллельность. Методы объектно-ориентированного программирования обеспечивают поддержку десяти типов классов, перечисленных в табл. 11.1.
Таблица 11.1. Типы объектно-ориентированных классов
Безусловно, эти типы классов особенно полезны для проектов, в которых предполагается реализовать параллельность. Дело в том, что они позволяют внедрить принцип компоновки из стандартных блоков. Мы обычно начинаем с примитивных компонентов, используя их для построения классов синхронизации. Классы синхронизации позволят нам создавать контейнерные и каркасные классы, рассчитанные на безопасное внедрение параллелизма. Каркасные классы представляют собой строительные блоки, предназначенные для таких параллельных архитектур более высокого уровня, как мультиагентные системы и «доски объявлений». На каждом уровне сложность параллельного и распределенного программирования уменьшается благодаря использованию различных типов классов, перечисленных в табл. 11.1.
Итак, начнем с интерфейсного класса. Интерфейсный (или адаптерный) класс испоользуется для модификации или усовершенствования интерфейса другого класса или множества классов. Интерфейсный класс может также выступать в качестве оболочки, созданной вокруг одной или нескольких функций, которые не являются членами конкретного класса Такая роль интерфейсного класса позволяет обеспечить обьектно-ориентированный интерфейс с программным обеспечением, которое необязательно является объектно-ориентированным. Более того, интерфейсные классы позволяют упростить интерфейсы таких библиотек функций, как POSIX threads, PVM и MPI. Мы можем «обернуть» необъектно-ориентированную функцию в объектно-ориеитированный интерфейс; либо мы можем «обернуть» в интерфейсный класс некоторые данные, инкапсулировать их и предоставить им таким образом объектно-ориентированный интерфейс. Помимо упрощения сложности некоторых библиотек функций, интерфейсные классы используются для обеспечения разработчиков ПО согласующимся интерфейсом API (Application Programmer Interface). Например, С++-программисты, которые привыкли работать с iostream-классами, получат возможность выполнять операции ввода-вывода, оперируя категориями обьектно-ориентированпых потоков данных. Кривая обучения существенно минимизируется, если новые методы ввода-вывода описать в виде привычного iostream-представлеиия. Например, мы можем представить библиотеку средств передачи сообщений MPI как коллекцию потоков.
mpi_stream Stream1;
mpi_stream Stream2;
Streaml << Messagel << Message2 << Message3;
Stream2 >> Message4;
//. . .
Нри таком подходе программист может целиком сосредоточиться на логике программы и не ломать голову над соблюдением требований к синтаксису библиотеки MPI.
Как воспользоваться преимуществами интерфейсных классов
Зачастую полезно использовать инкапсуляцию, чтобы скрыть детали библиотек функций и обеспечить создание самодостаточных компонентов, которые годятся для многократного использования. Возьмем для примера мьютекс, который мы рассматривали в главе 7. Вспомним, что мьютекс— это переменная специального типа, ис-пользуемая для синхронизации. Мьютексы позволяют получать безопасный доступ к критическом) разделу данных или кода программы. Существует шесть основных функций, предназначенных для работы с переменной типа pthread_mutex_t (POSIX Threads Mutex).
Все эти функции принимают в качестве параметра указатель на переменную типа pthread_mutex_t. Для инкапсуляции доступа к переменной типа pthread_mutex_t и упрощения вызовов функций, которые обращаются к мьютексным переменным, можно использовать интерфейсный класс. Рассмотрим листинг 11.1, в котором объявляется класс mutex.
// Листинг 11.1. Объявление класса mutex
class mutex{ protected:
pthread_mutex_t *Mutex;
pthread_mutexattr_t *Attr; public:
mutex(void)
int lock(void);
int unlock(void);
int trylock(void);
int timedlock(void);
};
Объявив класс mutex, используем его для определения мьютексных пере
Функции-члены класса mutex определяются путем заключения в оболочку вызовов соответствующих Pthread-функций, например, так.
// Листинг 11.2. Функции-члены класса mutex
mutex::mutex(void) {
try{
int Value;
Value = pthread_mutexattr_int(Attr); //. . .
Value = pthread_mutex_init(Mutex,Attr); //. . .
\
}
int mutex::lock(void) {
int RetValue;
RetValue = pthread_mutex_lock(Mutex); //. . .
return(ReturnValue);
}
Благодаря инкапсуляции мы также защищаем переменные типа pthread_mutex_t * и pthread_mutexattr_t *. Другими словами, при вызове методов lock, unlock, trylock и других нам не нужно беспокоиться о том, к каким мьютексным переменным или переменным атрибутов будут применены эти функции. Возможность скрывать информацию (посредством инкапсуляции) позволяет программисту писать вполне безопасный код. С помощью свободно распространяемых версий Рthread-функций этим функциям можно передать любую переменную типа pthread_mutex_t. Однако при передаче одной из этих функций неверно заданного мьютекса может возникнуть взаимоблокировка или отсрочка бесконечной длины. Инкапсуляция переменных типа pthread_mutex_t и pthread_mutexattr_t в к
Теперь мы можем использовать такой встроенный интерфейсный класс, как mutex, в любых других пользовательских классах, предназначенных для безопасной обработки потоков выполнения. Предположим, мы хотели бы создать очередь с многопоточной поддержкой и многопоточный класс pvm_stream. Очередь будем использовать для хранения поступающих событий для множества потоков, образованных в программе. На некоторые потоки возложена ответственность за отправку сообщений различным PVM-задачам. PVM-задачи и потоки выполняются параллельно. Несколько потоков выполнения разделяют единственный PVM-класс и единственную очередь событий. Отношения между потоками, PVM-задачами, очередью событий и классом pvm_stream показаны на рис. 11.1.
Очередь, показанная на рис. 11.1, представляет собой критический раздел, поскольку она совместно используется несколькими выполняемыми потоками. Класс pvm_stream — это также критический раздел и по той же причине. Если эти критические разделы не синхронизировать и не защитить, то данные в очереди и классе pvm_stream могут разрушиться. Тот факт, что несколько потоков могут одновременно обновлять либо очередь, либо код класса pvm_stream, открывает среду для «гонок». Чтобы не допустить этого, мы должны обеспечить нашу очередь и к
Обратите внимание на то, что класс x_queue содержит к
// Листинг 11.3. Объявление класса x_queue
template
protected:
queue
mutex Mutex;
//...
public:
bool enqueue(T Object);
T dequeue(void);
//...
};
Метод enqueue используется для добавления элементов в очередь, а метод dequeue — для удаления их из очереди. Каждый из этих методов рассчитан на использование oбъeктaMutex. Определение этих методов приведено в листинге 11.4.
// Листинг 11.4. Определение методов enqueue и dequeue
tempIate
{
Mutex.lock; EventQ.push(Object); Mutex.unlock;
}
Leinplr.te
{
T Object; //. . .
Mutex.lock;
Object = EventQ.front
EventQ.pop;
Mutex.unlock ;
//. . .
return(Object);
}
Теперь очередь может функционировать (принимать новые элементы и избавляться от ненужных) в многопоточной среде. ПотокВ (см. рис.11.1) добавляет элементы в очередь, а потокА удаляет их оттуда. Класс
Класс pvm_stream (см. рис. 11 1) также является критическим разделом, поскольку оба потока выполнения (А и В) имеют доступ к потоку данных. Опасность возникновения «гонок» данных здесь вполне реальна, поскольку потокА и поток В могут получить доступ к потоку данных одновременно. Следовательно, мы используем класс mutex в нашем классе pvm_stream для обеспечения необходимой синхронизации.
// Листинг 11.5. Объявление класса pvm_stream
class pvm_stream{
protected:
mutex Mutex;
int TaskId;
int MessageId;
// . - -
public:
pvm_stream & operator <<(string X);
pvm_stream & operator «(int X);
pvm_stream &operator <<(float X);
pvm_stream &operator>>(string X);
//.. .
};
Как и в классе x_queue, объект Mutex используется применительно к функциям, которые могут изменить состояние объекта класса pvm_stream. Например, мы могли определить один из операторов "«" следующим образом
// Листинг 11.6. Определение оператора << для
// класса pvm_stream
pvm_stream &pvm_stream::operator<<(string X) {
//...
pvm_pkbyte(const_cast
Mutex.lock;
pvm_send(TaskId,MessageId);
Mutex.unlock;
//.. .
return(*this);
}
Класс pvm_stream использует объекты Mutex для синхронизации доступа к его критическому разделу точно так же, как это было сделано в классе x_queue. Важно отметить, что в обоих случалх инкапсулируются pthread_mutex-функции . Программист не должен беспокоиться о правильном синтаксисе их вызова. Здесь также используется более простой интерфейс для вызова функций lock и unlock . Более того, здесь нельзя перепутать, какую pthread_mutex_t*-nepeмeннyю нужно использовать с pthread_mutex-функциями. Наконец, программист может объявить несколько экземпляров класса mutex, не обращалсь снова и снова к функциям библиотеки Pthread. Раз мы сделали ссылку на Pthread-функции в определениях методов клlacca mutex, то теперь нам достаточно вызывать только эти методы.
Подробнее об объектно-ориентированном взаимном исключении и интерфейсных классах
Чтобы справиться со сложностью написания и поддержки программ с параллелизмом, попробуем упростить API-интерфейс с соответствующими библиотеками. В некоторых системах, возможно, имеет смысл создать библиотеки Pthreads, MPI, атакже стандартные функции использования семафоров и разделяемой памяти как часть единого решения. Все эти библиотеки и функции имеют собственные протоколы и синтаксис. Но у них есть много общего. Поэтому мы можем использовать интерфейсные классы, наследование и полиморфизм для создания упрощенного и непротиворечивого интерфейса, с которым непосредственно будет работать программист. Мы можем также скрыть от наших приложений детали реализации конкретной библиотеки. Если приложение опирается только на методы, используемые в наших интерфейсных классах, то оно будет защищено от изменений, вносимых в реализацию функций, обновлений библиотек и прочих «подводных» реструктуризации. В конце концов, работа над интерфейсом (интерфейсными классами) с компонентами параллелизма и библиотеками функций позволит существенно понизить уровень сложности параллельного программирования. Итак, рассмотрим подробнее, какие методы разработки интерфейсных классов можно реализовать для поддержки параллелизма.
«Полуширокие» интерфейсы
Базовый POSIX-семафор используется для синхронизации доступа к критическому разделу нескольких процессов, а базовый POSIX -поток— для синхронизации доступа к критическому разделу нескольких потоков. В обоих случалх используются переменные синхронизации и ряд функций, работающих с этими переменными. Библиотеки MPI и PVM содержат примитивы передачи сообщений и обладают средствами порождения задач. Но интерфейсы этих библиотек различны. Нетрудно предположить, что работа прикладного программиста была бы эффективней, если бы он сосредоточил свое внимание на логике и структуре программы. Однако там, где семантика программы теряет свою ясность из-за необходимости использовать библиотеки, в которых попадаются аналогичные функции, а сами библиотеки отличаются синтаксисом и протоколами, у программиста возникают немалые трудности. Отсюда вытекает потребность универсализации интерфейса, который бы подходил для работы с разными библиотеками.
Существует по крайней мере два подхода к разработке общего интерфейса для семейства, или коллекции классов. Объектно-ориентированный подход начинается с общего и переходит к частностям посредством наследования. Другими словами, возьмем минимальный набор характеристик и атрибутов, которыми должен обладать каждый член рассматриваемого сехмейства классов, а затем посредством наследования будем конкретизировать характеристики для каждого класса. При таком подходе по мере «спуска» по иерархии классов интерфейс становится все более «узким». Второй подход часто используется в коллекциях шаблонов. Шаблонные методы начинаются c конкретного и переходят к более общему посредством «широких» интерфейсов. «Широкий» интерфейс включает обобщение всех характеристик и атрибутов (см. книгу Страуструпа «
Безотносительно к реализации деталей, операции блокировки, разблокировки и «пробной» блокировки являются характеристиками переменных синхронизации. Поэтому мы создадим базовый класс, который будет служить «трафаретом» для целого семейства классов. Объявление класса synchronization_variable представленовлистинге 11.7.
// Листинг 11.7. Объявление класса synchronization_variable
class synchronization_variable{
protected:
runtime_error Exception;
//.. .
public:
int virtual lock(void) = 0;
int virtual unlock(void) = 0;
int virtual trylock(void) = 0;
//.. .
} ;
Обратите внимание на то, что методы синхронизации класса synchronization_variable объявлены виртуальными и инициализированы значением 0. Это означает, что они являются чисто виртуальными методами, что делает класс
// Листинг 11.8. Объявление класса мьютекс, который
// наследует класс synchronization_variable
class mutex : public synchronization_variable {
protected:
pthread_mutex_t *Mutex;
pthread_mutexattr_t *MutexAttr;
//.. .
public:
int lock(void) ;
int unlock(void);
int trylock(void);
//. . .
};
Класс mutex должен обеспечить реализации для всех чисто виртуальных функций. Если эти функции определены, значит, политика, предложеннал абстрактным классом, выдержана. Класс mutex теперь не является абстрактным, поэтому из него и из его потомков можно создавать объекты. Каждый из методов класса mutex заключает в оболочку соответствующую Pthread-функцию. Например, код
int mutex::trylock(void) {
//.. .
return(pthread_mutex_trylock(Mutex); //. . .
}
обеспечивает интерфейс для функции pthread_mutex_trylock. Интерфейсные варианты функций lock, unlock и trylock упрощают вызовы функций библиотеки Pthread. Наша цель — использовать инкапсуляцию и наследование для определения всего семейства мьютексных классов. Процесс наследования — это процесс специализации. Производный класс включает дополнительные атрибуты или характеристики, которые отличают его от предков. Каждый атрибут или характеристика, добавленная в производный класс, конкретизирует его. Теперь мы, используя наследование, можем спроектировать специализацию класса mutex путем введения понятия мьютексного класса, способного обеспечить чтение и запись. Наш обобщенный класс mutex предназначен для защиты доступа к критическому разделу. Если один поток заблокировал мьютекс, он получает доступ к критическому разделу, защищаемому этим мьютексом. Иногда такая мера предосторожности оказывается излишне суровой. Возможны ситуации, когда вполне можно разрешить доступ нескольких потоков к одним и тем же данным, если ни один из этих потоков не модифицирует данные. Другими словами, в некоторых случаях мы можем ослабить блокировку критического раздела и «намертво» запирать его только для действий, которые стремятся модифицировать данные, разрешал при этом доступ для действий, которые предполагают лишь считывание или копирование данных. Такой вид блокировки называется
Архитектура «классной доски» служит прекрасным примером структуры, которая может использовать преимущества «мьютексов считывания» и мьютексов более общего назначения. Под «классной доской» понимается область памяти, разделяемал параллельно выполняемыми задачами. «Классная доска» используется для хранения решений некоторой проблемы, которую совместными усилиями решает целая группа задач. По мере приближения задач к решению проблемы каждая из них записывает результаты на «классную доску» и просматривает ее содержимое с целью поиска результатов, сгенерированных другими задачами, которые могут оказаться полезными для нее. Структура «классной доски» является критическим разделом. В действительности мы хотим, чтобы одновременно только одна задача могла обновлять содержимое «классной доски». Однако ее одновременное считывание мы можем позволить любому количеству задач. Кроме того, если несколько задач уже считывает содержимое «классной доски», нам нужно, чтобы оно не начало обновляться до тех пор, пока все эти задачи не завершат чтение. «Мьютекс считывания» как раз подходит для такой ситуации, поскольку он может управлять доступом к «классной доске», разрешал его только считывающим задачам и запрещал его для записывающих задач. Но если решение проблемы будет найдено, содержимое «классной доски» необходимо обновить. В процессе обновления нам нужно, чтобы ни одна считывающал задача не получила доступ к критическому разделу. Мы хотим заблокировать доступ для чтения до тех пор, пока не завершит обновление записывающал задача. Следовательно, нам нужно создать «мьютекс записи». В любой момент времени удерживать этот «мьютекс записи» может только одна задача. Поэтому мы делаем различие между мьютексом, который блокируется для считывания, но не для записи, и мьютексом, который блокируется для записи, но не для считывания. С использованием мьютекса считывания у нас может быть несколько параллельных считывающих задач, а с использованием мьютекса записи — только одна записывающал задача. Описаннал схема является частью модели CREW (Concurrent Read Exclusive Write — параллельное чтение, монопольнал запись) параллельного программирования.
Для разработки спецификации нашего мьютексного класса нам нужно наделить его способностью выполнять блокировки считывания и блокировки записи. В библиотеке Pthreads предусмотрены мьютексные переменные блокировки чтения-записи и атрибутов:
Эти переменные используются совместно с 11ю pthread_rwlock-функциями. Мы используем наш интерфейсный класс rw_mutex для инкапсуляции переменных pthread_rwlock_t и pthread_rwlockattr_t, а также для заключения в оболочку Pthread-функций мьютексной организации чтения-записи.
Синопсис
#include
int pthread_rwlock_init(pthread_rwlock_t *,const pthread_rwlockattr_t *);
int pthread_rwlock_destroy(pthread_rwlock_t *) ;
int pthread_rwlock_rdlock(pthread_rwlock_t *);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *);
int pthread_rwlock_wrlock(pthread_rwlock_t *);
int pthread_rwlock_trywrlock(pthread_rwlock_t *);
int pthread_rwlock_unlock(pthread_rwlock_t *);
int pthread_rwlockattr_init(pthread_rwlockattr_t *);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *,int *);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *, int) ;
// Листинг 11.9. Объявление класса rw_mutex, который
// содержит объекты типа pthread_rwlock_ t
// и pthread_rwlockattr_t
class rw_mutex : public mutex{
protected:
struct pthread_rwlock_t *RwLock;
struct pthread_rwlockattr_t *RwLockAttr;
public:
//.. .
int read_lock(void);
int write_lock(void);
int try_readlock(void);
int try_writelock(void);
//.. .
};
Класс
Пока мы создаем «узкий» интерфейс. На данном этапе мы заинтересованы в обеспечении минимального набора атрибутов и характеристик, необходимых для обобщения нашего класса mutex с использованием мьютексных типов и функций из библиотеки Pthread. Но после создания «узкого» интерфейса для класса mutex мы воспользуемся им как основой для создания «полуширокого» интерфейса. «Узкий» интерфейс обычно применяется в отношении классов, которые связаны наследованием. «Широкие» интерфейсы, как правило, применяют к классам, которые связаны функциями, а не наследованием. Нам нужен интерфейсный класс для упрощения работы с классами или функциями, которые принадлежат различным библиотекам, но выполняют подобные действия. Интерфейсный класс должен обеспечить программиста удобными рабочими инструментами. Для этого мы берем все библиотеки или классы с подобными функциями, отбираем все общие функции и переменные и после некоторого обобщения помещаем их в большой класс, который содержит все требуемые функции и атрибуты. Так определяется класс с «широким» интерфейсом. Но если включить в него (например, в класс rw_mutex) только интересующие нас функции и данные, мы получим «полуширокий» интерфейс. Его преимущества перед «широким» интерфейсом заключаются в том, что он позволяет нам получать доступ к объектам, которые связаны лишь функционально, и ограничивает множество методов, которыми может пользоваться программист, теми, которые содержатся в интерфейсном классе с узким «силуэтом». Это может быть очень важно при интеграции таких больших библиотек функций, как MPI и PVM с POSIX-возможностями параллелизма. Объединение MPI-, PVM- и POSIX-средств дает сотни функций с аналогичными целями. Затратив время на упрощение этой функциональности в интерфейсных классах, вы позволите программисту понизить уровень сложности, связанный с параллельным и распределенным программированием. Кроме того, эти интерфейсные классы становятся компонентами, которые можно многократно использовать в различных приложениях.
Чтобы понять, как подойти к созданию «полуширокого» интерфейса, построим интерфейсный класс для POSIX-семафора. И хотя семафор не является частью библиотеки Pthread, он находит аналогичные применения в многопоточной среде. Его можно использовать в среде, которая включает параллельно выполняемые процессы и потоки. Поэтому в некоторых случалх требуется объект синхронизации более общего характера, чем наш класс mutex.
Определение класса semaphore показано в листинге 11.10.
// Листинг 11.10. Объявление класса semaphore
class semaphore : public synchronization_variable( protected:
sem_t * Semaphore; public://.. .
int lock(void);
int unlock(void);
int trylock(void);
//. . .
};
Синопсис
int sem_init(sem_t *, int, unsigned int) ;
int sem_destroy(sem_t *);
sem_t *sem_open(const char *, int, ...);
int sem_close(sem_t *);
int sem_unlink(const char *);
int sem_wait(sem_t *);
int sem_trywait(sem_t *);
int sem_post(sem_t *);
int sem_getvalue(sem__t *, int *);
Обратите внимание на то, что класс semaphore имеет такой же интерфейс, как и наш класс mutex. Чем же они различаются? Хотя интерфейсы классов mutex и semaphore одинаковы, реализация функций lock , unlock , trylock и тому подобных представляет собой вызовы семафорных функций библиотеки POSIX .
// Листинг 11.11. Определение методов lock, unlock и
// trylock для класса semaphore
int semaphore::lock(void) (
//.. .
return(sem_wait(Semaphore));
}
int semaphore::unlock(void) {
//. . .
return(sem_post(Semaphore));
}
Итак, теперь функции lock , unlock , trylock и тому подобные заключают в оболочку семафорные функции библиотеки POSIX, а не функции библиотеки Pthread. Важно отметить, что семафор и мьютекс — не одно и то же. Но их можно использовать в аналогичных ситуациях. Зачастую с точки зрения инструкций, которые реализуют параллелизм, механизмы функций lock и unlock имеют одно и то же назначение. Некоторые основные различия между мьютексом и семафором указаны в табл. 11.2.
Таблица 11.2. Ос
•
• Мьютексы и переменные условий разделяются между потоками
• Мьютекс деблокируется теми же потоками, которые его заблокировали
• Мьютекс либо блокируется, либо деблокируется
•
• Семафоры обычно разделяются между процессами, но их разделение возможно и между потоками
• - Освобождать семафор должен необязательно тот процесс или поток, который его удерживал
• Семафоры управляются количеством ссылок. Стандарт POSIX включает именованные семафоры
Несмотря на важность различий в семантике (см. табл. 11.2), часто их оказывается недостаточно для оправдания применения к семафорам и мьютексам совершенно различных интерфейсов. Поэтому мы оставляем «полуширокий» интерфейс для функций lock, unlock и trylock с одним предостережением: программист должен знать различия между мьютексом и семафором. Это можно сравнить с ситуацией, которая возникает с такими «широкими» интерфейса
Поддержка потокового представления
Помимо использования интерфейсных классов для упрощения программирования и создания новых «широких» интерфейсов библиотек средств параллелизма и передачи сообщений, имеет смысл также расширить существующие интерфейсы. Например, объектно-ориентированное представление потоков данных можно расширить за счет использования каналов, FIFO-очередей и таких библиотек передачи сообщений, как PVM и MPI. Эти компоненты используются ради достижения межпроцессного взаимодействия (Inter-Process Communication — IPC), межпотокового взаимодействия (Inter-Thread Communication — ITC), а в некоторых случалх и взаимодействия между объектами (Object-to-Object Communicaton — OTOC). Если взаимодействие имеет место между параллельно выполняемыми потоками или процессами, то канал связи может представлять собой критический раздел. Другими словами, если несколько процессов (потоков) попытаются одновременно обновить один и тот же канал, FIFO-очередь или буфер сообщений, непременно возникнет «гонка» данных. Если мы собираемся расширить объектно-ориентированный интерфейс потоков данных за счет включения компонентов из библиотеки PVM или MPI, нам нужно быть уверенными в том, что доступ к этим потокам данных будет безопасен с точки зрения параллелизма. Именно здесь могут пригодиться наши компоненты синхронизации, спроектированные в виде интерфейсных классов. Рассмотрим простой класс pvm_stream.
// Листинг 11.12. Объявление класса pvm_stream, который
// наследует класс mios
class pvm_stream : public mios{
protected:
int TaskId;
int MessageId;
mutex Mutex;
//...
public:
void taskId(int Tid);
void messageId(int Mid);
pvm_stream(int Coding=PvmDataDefault);
void reset(int Coding = PvmDataDefault);
pvm_stream &operator<<(string &Data);
pvm_stream &operator>>(string &Data);
pvm_stream &operator>>(int &Data);
pvm_stream &operator<<(int &Data);
//. . .
};
Этот класс обработки потоков данных предназначен для инкапсуляции состояния активного буфера в PVM-задаче. Операторы вставки "<<" и извлечения ">>" можно использовать для отправки и приема сообщений между PVM-процессами. Здесь мы рассмотрим использование этих операторов только для обработки строк и значений типа int. Интерфейс этого класса далек от совершенства. Поскольку этот класс предназначен для обработки данных любого типа, мы должны расширить определения операторов "<<" и ">>". А так как мы планируем использовать класс pvm_stream в многопоточной программе, мы должны быть уверены в том, что объект класса pvm_stream безопасен для потоков. Поэтому мы включаем в качестве члена нашего класса pvm_stream класс mutex. Поскольку сообщение может быть направлено для конкретной PVM-задачи, класс pvm_stream инкапсулирует для нее активный буфер. Наша цель — использовать классы ostream и istream в качестве «путеводителя» по функциям, которые должен иметь класс pvm_stream. Вспомним, что классы ostream и istream являются классами трансляции. Они переводят типы данных в обобщенные потоки байтов при выводе и обобщенные потоки байтов в конкретные типы данных при вводе. Используя классы istream и ostream, программисту не нужно погружаться в детали вставки в поток или выделения из потока данных того или иного типа. Мы хотим, чтобы и поведение класса pvm_stream было аналогичным. Библиотека PVM располагает различными функциями для каждого типа данных, которые необходимо упаковать в буфер отправки или распаковать из буфера приема. Например, функции pvm_pkdouble pvm_pkint pvm_pkfloat используются для упаковки double-, int- и float-значений соответственно. Аналогичные функции существуют и для других типов данных, определенных в С++. Мы бы хотели поддерживать наше потоковое представление, т.е. чтобы ввод и вывод данных можно было представить как обобщенный поток байтов, который перемещается в программу или из нее. Следовательно, мы должны определить операторы вставки (<<) и извлечения (>>) для каждого типа данных, который мы собираемся использовать при обмене сообщениями между PVM-задачами. Мы также моделируем состояние потока данных в соответствии с классами istream и ostream, которые содержат компонент ios, предназначенный для хранения состояния этого потока. Поток данных может находиться в состоянии ошибки либо в одном из различных состояний, которые выражаются восьмеричным, десятичным или шестнадцатеричным числом. Поток также может пребывать в нормальном, заблокированном или состоянии конца файла. Класс pvm_stream должен не только содержать компонент, который поддерживает состояние потока данных, но и методы, которые устанавливают заданное или исходное состояние PVM-задачи, а также считывают его. Наш класс pvm_stream для этих целей содержит компонент mios. Этот компонент поддерживает состояние потока данных и активного буфера отправки и приема информации. На рис. 11.4 представлены две диаграммы классов: одна отображает отношения между основными классами библиотеки iostream, а вторая — отношения между классом pvm_stream и ero компонентами.
Обратите внимание на то, что классы istream и ostream наследуют класс
Перегрузка операторов "«" и "»" для PVM-потоков данных
Итак, рассмотрим определение операторов "«" и ">>" для класса pvm__stream. Оператор вставки (<<) используется для заключения в оболочку функций pvm_send и pvm_pk. Вот как выглядит определение этого операторного метода.
// Листинг 11.13. Определение оператора "<<" для класса
// pvm_stream class
pvm_stream &pvm_stream::operator<<(int Data) {
//...
reset;
pvm_pkint(&Data,1,1); pvm_send(TaskId,MessageId); //.. .
return(*this);
}
Подобное определение существует для каждого типа данных, которые будут обрабатываться с использованием класса pvm_stream. Метод reset унаследован от класса mios. Этот метод используется для инициализации буфера отправки
int Value = 2004;
pvm_stream MyStream;
//...
MyStream << Value;
//.. .
Оператор извлечения данных (>>) используется подобным образом, но для получения сообщений от PVM-задач. В действительности оператор ">>" заключает в оболочку функции pvm_recv и pvmupk . Определение этого операторного
// Листинг 11.14. Определение оператора для класса
// pvm_stream
pvm_stream &pvm_stream::operator>>(int &Data) {
int BufId;
//. . .
BufId = pvm_recv(TaskId,MessageId);
StreamState = pvm_upkint(&Data,l,l); //.. .
return(*this);
}
Этот тип определения позволяет получать сообщения от PVM-задач с помощью оператора извлечения данных.
int Value;
pvm_stream MyStream;
MyStream >> Value;
Поскольку каждый из рассмотренных операторных методов возвращает ссылку на тип pvm_stream , операторы вставки и извлечения можно соединить в цепочку.
Mystream << Valuel << Value2;
Mystream >> Value3 >> Value4;
Используя этот простой синтаксис, программист изолирован от более громоздкого синтаксиса функций pvm_send, pvm_pk, pvm_upk и pvm_recv . При этом он работает с более знакомыми для него объектно-ориентированными потоками данных. В данном случае поток данных представляет буфер сообщений, а элементы, которые помещаются в него или извлекаются оттуда, представляют сообщения, которыми обмениваются между собой PVM-процессы. Вспомните, что каждый PVM-процесс имеет отдельное адресное пространство. Поэтому операторы "<<" и ">>" не только маскируют вызовы функций pvm_send и pvm_recv, они также маскируют заложенную в них организацию связи. Поскольку класс pvm_stream можно использовать в много-поточной среде, операторы вставки и извлечения данных должны обеспечивать безопасность потоков выполнения.
Класс pvm_stream (см. рис. 11.4) содержит класс mutex. Класс mutex можно использовать для защиты критических разделов, которые имеются в классе pvm_stream. Класс pvm_stream инкапсулирует доступ к буферу отправки и буферу приема данных. Взаимодействие потоков выполнения и класса pvm_stream с буферами pvm_send и pvm_receive показано на рис. 11.5.
Критическими разделами являются не только буферы отправки и приема данных. Класс mios, используемый для хранения состояния класса pvm_stream, также является критическим разделом. Для защиты этого компонента можно использовать класс mutex.
При обращении к операторам вставки и извлечения данных можно использовать объект Mutex.
// Листинг 11.15.
//Определение операторов «<<» и «>>» для класса pvm_stream
pvm_stream &pvm_stream::operator<<(int Data) {
//.. .
Mutex.lock; reset;
pvm_pkint(&Data,1,1); pvm_send(TaskId,MessageId); Mutex.unlock; //.. .
return(*this);
}
pvm_stream &pvm_stream::operator>>(int &Data) {
int BufId; //. . .
Mutex.lock;
BufId = pvm_recv(TaskId,MessageId);
StreamState = pvm_upkint(&Data,1,1);
Mutex.unlock;
//. . .
return(*this);
}
Этот вид защиты позволяет сделать класс pvm_stream безопасным. Здесь мы не представили код обработки исключений или другой код, который бы позволил предотвратить бесконечные отсрочки или взаимную блокировку. Основнал идея в данном случае — сделать акцент на компонентах и вариантах архитектуры, которые пригодны для поддержки параллелизма. Интерфейсный класс mutex и класс pvm_stream можно использовать многократно, и оба они поддерживают параллельное программирование. Предполагается, что объекты класса pvm_stream должны использоваться PVM-задачами при отправке и приеме сообщений. Но это не является жестким требованием. Для того чтобы пользователь мог применить концепцию класса pvm_stream к своим классам, для них необходимо определить операторы вставки (<<) и извлечения (>>).
Пользовательские классы, создаваемые для обработки PVM-потоков данных
Чтобы понять, как определенный пользователем класс можно использовать совместно с классом pvm_stream, попробуем усовершенствовать возможности PVM-палитры, представленной в главе 6. Класс палитры представляет простую коллекцию цветов. Для удобства будем сохранять цвета в векторе строк (vector
Начне
// Листинг 11.16. Объявление класса spectral_palette
class spectral_palette : public pvm_object{
protected:
//. . .
vector
public:
spectral_palette(void);
//...
friend pvm_stream &operator>>(pvm_stream &In,spectral_palette &Obj);
friend pvm_stream &operator<<(pvm_stream &Out,spectral_palette &Obj);
//. . .
Обратите внимание на то, что класс spectral_palette в листинге 11.16 наследует класс pvm_object. Класс pvm_object тем самым обеспечивает своего наследника доступом к идентификатору задачи и идентификатору сообщения. Вспомните, что идентификаторы задачи и сообщения используются во многих PVM-функциях. С помощью определения операторов вставки (<<) и извлечения (>>) объекты класса spectral_palette можно пересылать между параллельно выполняемыми PVM-задачами. Метод, используемый для класса spectral_palette, очень прост, и его можно так же успешно применить к любому пользовательскому классу. Поскольку класс pvm_stream должен иметь эти операторы для встроенных типов данных и контейнеров, которые содержат значения встроенных типов данных, в пользовательском классе необходимо определить только операторы "<<" и ">>" для перевода их представления в любой встроенный тип данных или стандартный контейнер. Вот как, например, определяется оператор "<<" для класса spectral_palette в листинге 11.17.
// Листинг 11.17. Определение оператора для
// класса spectral_palette
pvm_stream &operator<<(pvm_stream &Out, spectral_palette &Obj)
{
int N;
string Source;
for(N = 0;N < Obj.Colors.size;N++) {
Source.append(Obj.Colors[N]);
if( N Source.append(" "); } } Out.reset; Out.taskId(Obj.TaskId); Out.messageId(Obj.MessageId); Out << Source; return(Out); } Рассмотрим подробнее определение этой операции вставки в листинге 11.17. Поскольку класс pvm_stream работает только со встроенными типами данных, цель пользовательского оператора "<<" — перевести пользовательский объект в последовательность значений встроенных типов данных. Этот перевод является одной из основных обязанностей классов, «отвечающих» за потоковое представление данных. В данном случае объект класса spectral_palette должен быть переведен в строку «цветов», разделенных пробелами. Список цветовых значений сохраняется в строке Source. Рассматриваемый процесс перевода позволяет применить к объекту этого класса оператор "<<", который был определен для строкового типа данных. Имея определения этих операторов, API-интерфейс программиста становится более удобным, чем при использовании ори // Листинг 11.18. Использование объектов классов // pvm_stream и spectral_palette pvm_stream TaskStream; spectral_palette MyColors; //. . . TaskStream.taskId(20001); TaskStream.messageId(l); //.. . TaskStream « MyColors; //.. . Здесь объект MyColors пересылается в соответствующую PVM-задачу. На рис. 11.6 показаны компоненты, используемые для поддержки объектов TaskStream и MyColors. Каждый компонент на рис. 11.6 можно детализировать и оптимизировать в отдельности. Каждый представленный здесь уровень обеспечивает дополнительный слой изоляции от сложности этих компонентов. В идеале на самом высоком уровне программист должен заниматься только деталями, связанными с данной предметной областью. Такой высокий уровень абстракции позволяет программисту самым естественным образом представлять параллелизм, который вытекает из требований предметной области, не углубляясь при этом в синтаксис и сложные последовательности вызовов функций. Компоненты, представленные на рис. 11.6, следует рассматривать лишь как малую толику библиотеки классов, которую можно использовать для PVM-программ и многопоточных PVM-программ. Те же методы можно применять для взаимодействия между параллельно выполняемыми задачами, которые не являются частью PVM-среды. Ведь существует множество приложений, которые требуют реализации параллельности, но не нуждаются во всей полноте функционирования механизма PVM-cреды. Для таких приложений вполне достаточно использования функций ехес, fork или pvm_spawn . Примерами таких приложений могут служить программы, которые требуют создания нескольких параллельно выполняемых процессов, и приложения типа «клиент-сервер». Для таких нePVM - или неМРI-приложений также может потребоваться организация межпроцессного взаимодействия. Для параллельно выполняемых процессов, создаваемых посредством fork-exec- последовательности вызовов или функций pvm_spawn, имело бы смысл поддерживать потоковое представление данных. Понятие объектно-ориентированного потока данных можно также расширить с помощью каналов и FIFO-очередей.
Объектно-ориентированные каналы и FIFO-очереди как базовые элементы низкого уровня
Приступая к разработке объектноориентированных каналов, начнем с рассмотрения базовых характеристик и поведения каналов в целом. Канал представляет собой средство взаимодействия между несколькими процессами. Для того чтобы процессы могли взаимодействовать, необходимо обеспечить между ними передачу информации определенного вида. Эта информация может представлять данные или команды, предназначенные для выполнения. Обычно такая информация преобразуется в последовательность данных и помещается в канал, а затем считывается процессом с другого конца канала. При считывании из канала данные снова преобразуются, чтобы обрести смысл для считывающего процесса. В любом случае при передаче от одного процесса другому эти данные должны где-то храниться. Мы называем область хранения информации буфером данных. Для размещения данных в этом буфере и извлечения их оттуда необходимо выполнять соответствующие операции. Но прежде чем говорить о выполнении таких операций, необходимо позаботиться о существовании самого буфера данных. Объектно-ориентированный канал должен обладать средствами, которые поддерживают операции создания и инициализации буфера данных. После завершения взаимодействия между процессами буфер данных, используемый для хранения информации, становится ненужным. Это означает, что наш объектно-ориентированный канал должен «уметь» удалять буфер данных после его использования. Из этого «введения в каналы» вырисовываются по крайней мере пять основных компонентов, которыми должен обладать объектно-ориентированный канал: • буфер; • операция вставки данных в буфер; • операция извлечения данных из буфера; • операция создания/инициализации буфера; • операция ликвидации буфера. Помимо этих пяти базовых компонентов, канал должен иметь два конца. Один конец предназначен для вставки данных, а другой — для их извлечения. К этим двум концам могут получать доступ различные процессы. Чтобы наше описание канала было полным, мы должны включить в него порт ввода и порт вывода, к которым могут подключаться различные процессы. В результате мы получаем уже семь базовых компонентов, составляющих описание нашего объектно-ориентированного канала: • порт ввода; • порт вывода; • буфер; • операция вставки данных в буфер; • операция извлечения данных из буфера; • операция соз • операция ликви Эти компоненты образуют минимальный набор характеристик, составляющих описание канала. Уточнив базовые компоненты, можно поразмыслить о том, как при разработке объектно-ориентированного канала лучше всего использовать существующие системные API-интерфейсы или структуры данных. В разработке каналов попробуем для начала применить те же методы (инкапсуляцию и перегрузку операторов), которые мы использовали при разработке класса pvm_stream. Обратите внимание на то, что пять из семи выше перечисленных базовых компонентов являются общими лля многих основных структур данных и типов контейнеров, которые обычно используются для операций ввода-вывода. В большинстве случаев UNDC/Linux-средства работы с файлами поддерживают: • буферы; • операции вставки данных в буфер; • операции извлечения данных из буфера; • операции создания буфера; • операции удаления буфера. Для инкапсуляции функций, предоставляемых системными UNIX/Linux-службами, мы используем понятие интерфейсных С++-классов и создаем объектно-ориентированные версии сервисных функций ввода-вывода. Если в случае с классом pvm_stream для библиотеки PVM нам приходилось начинать «с нуля», то здесь мы можем воспользоваться преимуществами существующей стандартной библиотеки С++ и библиотеки классов iostreams. Вспомните, что библиотека классов iostreams поддерживает объектно-ориентированную модель потоков ввода и вывода. Более того, эта объектно-ориентированнал библиотека оснащена поддержкой буферизации данных и всех операций, связанных с использованием буфера. На рис. 11.7 показана простая диаграмма класса basic_iostream. Основные компоненты класса basic_iostream можно описать тремя видами классов: компонент буфера, компонент преобразования и компонент состояния [23]. Компонент буфера используется в качестве области промежуточного хранения байтов информации. Компонент преобразования отвечает за перевод анонимных последовательностей байтов в значения и структуры данных соответствующих типов, а также за перевод структур данных и отдельных значений в анонимные последовательности байтов. Компонент преобразования отвечает за обеспечение программиста потоковым представлением байтов, в котором все операции ввода-вывода независимо от источника и приемника обрабатываются как поток байтов. Компонент состояния инкапсулирует состояние объектно-ориентированного потока и позволяет определить, какой тип форматирования применим к байтам данных, которые содержатся в компоненте буфера. Компонент состояния также содержит информацию отом, в каком режиме был открыт поток: дозаписи, создания, монопольного чтения, монопольной записи, а также о том, будут ли числа интерпретироваться как шестна-дцатеричные, восьмеричные или двоичные. Компонент состояния также можно использовать для определения состояния ошибки операций ввода-вывода, выполняемых над компонентом буфера. Опросив этот компонент, программист может определить, в каком состоянии находится буфер, условно говоря, в хорошем или плохом. Эти три компонента представляют собой объекты, которые можно использовать совместно (для формирования полнофункционального объектноориентированного потока) или в отдельности (в качестве вспомогательных объектов в других задачах). Пять из семи базовых компонентов нашего потока уже реализованы в библиотеке классов iostreams. Поэтому нам остается лишь дополнить их компонентами портов ввода и вывода. Для этого мы можем рассмотреть системные средства поддержки потоков. В среде UNIX/Linux создать канал можно с помощью вызовов системных функций (листинг 11.19). // Листинг 11.19. Использование системного вызова для // создания канала int main(int argc, char *argv[]) { //.. . int Fd[2]; pipe(Fd); //.. . } Функция pipe предназначена для создания структуры данных канала, которую можно использовать для взаимодействия между родительским и сыновним процессами. При успешном обращении к функции pipe она возвращает два дескриптора файла. (Дескрипторы файлов представляют собой целые значения, которые используются для идентификации успешно открытых файлов.) В этом случае дескрипторы сохраняются в массиве Fd. Элемент Fd[0] используется при открытии файла для чтения, а элемент Fd[1] — при открытии файла для записи. После создания эти два дескриптора файлов можно использовать при вызове функций read и write. Функция write обеспечивает вставку данных в канал посредством дескриптора Fd[1], а функция read — извлечение данных из канала посредством дескриптора Fd[0]. Поскольку функция pipe возвращает дескрипторы файлов, доступ к каналу можно получить с помощью системных средств работы с файлами. Для определения максимально возможного количества доступных дескрипторов файлов, открытых одним процессом, можно использовать системную функцию sysconf(_SC_OPEN_MAX), адля определения размера канала — функцию pathconf(_PC_PIPE_BUF). Эти два файловых дескриптора представляют наши логические порты ввода и вывода соответственно. Мы также используем их для связи с библиотекой классов iostreams. В частности, они обеспечивают связь с классом буфера. Ко Таблица 11.3. Три типа буферных классов basic_streambuf Описывает поведение различных потоковых буферов с целью управления входными и выходными последовательностями символов basic_stringbuf Связывает входные и выходные последовательности с последовательностью произвольных символов, которая может быть использо-ванадля инициализации или доступна в качестве строкового объекта basic_filebuf Связывает входные и выходные последовательности символов с файлом Рассмотрим подробнее класс basic_filebuf. Тогда как класс basic_streambuf используется в качестве объектно-ориентированного буфера в операциях ввода-вывода с применением стандартного потока, а класс basic_stringbuf — в качестве объектно-ориентированного буфера для памяти, класс basic_filebuf применяется в качестве объектно-ориентированного буфера для файлов. Рассмотрев интерфейс для класса basic_filebuf и интерфейс для классов преобразования (basic_ifstream, basic_ofstream и basic_fstream), можно найти способ связать дескрипторы файлов, возвращаемые системной функцией pipe , с объектами класса basic_iostream. На рис. 11.8 показаны диаграммы классов для семейства fstream-классов. Обратите вни Синопсис #include // UNIX-системы ifstream(int fd) fstream(int fd) ofstream(int fd) // gnu C++ void attach(int fd) ;
Связь каналов c iostream-объектами с помощью дескрипторов файлов
Существует три iostream-класса (ifstream, ofstream и fstream), которые мы можем использовать для подключения к каналу. Объект класса ifstream используется для ввода данных, объект класса ofstream — для их вывода, а объект класса fstream можно применять и в том и в другом случае. Несмотря на то что непосредственная поддержка дескрипторов файлов и потоков ввода-вывода не является частью стандарта ISO, в большинстве UNIX- и Linux-сред поддерживается С++-ориентированный iostream-доступ к дескрипторам файлов. В библиотеке GNU С++ iostreams предусмотрена поддержка дескриптора файла в одном из конструкторов классов ifstream, ofstream и fstream и в методе attach( ) , определенном в классах ifstream и ofstream. UNIX-компилятор языка С++ ко //... int Fd[2]; Pipe(Fd); ifstream IPipe(Fd[0]) ; ofstream OPipe(Fd[1]) ; будут созданы объектно-ориентированные каналы. Объект IPipe будет играть роль входного потока, а объект OPipe— выходного. После создания эти потоки можно применять для связи между параллельно выполняемыми процессами с использованием потоково // Листинг 11.20. Создание канала и использование // функции attach int Fd[2]; ofstream OPipe; //.. . pipe(Fd); //.. . OPipe.attach(Fd[1]); //.. . OPipe << Value << endl; Такой способ использования объектно-ориентированных каналов предполагает существование сыновнего процесса, который может считывать из них информацию. В программе 11.1 для создания двух процессов используется fork-инструкция. Родительский процесс отправляет значение сыновнему процессу с помощью iostreams-ориентированного канала. // Программа 11.1 1 #include 2 #include 3 #include 4 #include 5 #include 7 8 9 10 int main(int argc, char *argv[]) 11 { 12 13 int Fd[2]; 14 int Pid; 15 float Value; 16 int Status; 17 if(pipe(Fd) != 0) { 18 cerr « «Ошибка при создании канала " « endl; 19 exit(l); 20 } 21 Pid = fork; 22 if(Pid == 0){ 23 ifstream IPipe(Fd[0]); 24 IPipe » Value; 25 cout « «От процесса-родителя получено значение» << Value << endl; 26 IPipe.close; 27 } 28 else{ 29 ofstream OPipe(Fd[l]); 30 OPipe « M_PI « endl; 31 wait(&Status); 32 OPipe.close; 33 34 } 35 36 } Вспомните, что значение 0, возвращаемое функцией fork, принадлежит сыновнему процессу. В программе 11.1 канал создается при выполнении инструкции, расположенной на строке 17. А при выполнении инструкции, расположенной на строке 29, родительский процесс открывает канал для записи. Файловый дескриптор Fd[1] означает «записывающий» конец канала. К этому концу канала (благо f (Профиль программы 11.1 Имя программы program11-1.cc Оп Программа 11.1 демонстрирует использование объектно-ориентированного потока c использованием анонимных системных каналов. Для создания двух процессов, |которые взаимодействуют между собой с помощью операторов вставки («) и из-!влечения (»), программа использует функцию fork. Требуемые заголовки Инс C++ -о program11-1 program11-1.cc Среда для Solaris 8, SuSE Linux 7.1. Инструкции по выполнению ./program11-1 Компилятор gnu С++ также под // Листинг 11.21. Подключение файловых дескрипторов к // объекту класса ofstream int main (int argc, char *argv[]) { int Fd[2]; ofstream Out; pipe(Fd); Out.attach(Fd[l]); // - . . // Межпроцессное взаимодействие. //. . . Out.close; } При вызове функции Out.attach(Fd[1] ) объект класса ofstream связывается с файловым дескриптором канала. Теперь Любая информация, которая будет помещена в объект Out, в действительности запишется в канал. Использование операторов извлечения и вставки для выполнения автоматического преобразования формата является основным достоинством использования семейства fstream -классов в сочетании с канальной связью. Возможность применять пользовательские средства извлечения и вставки избавляет программиста от определенных трудностей, которые могут иметь место при программировании каналов связи. Поэтому вместо явного перечисления размеров данных, записываемых в канал и читаемых из него, при управлении доступом для чтения-записи мы используем только количество передаваемых через канал элементов, что существенно упрощает весь процесс. К тому же такое «снижение себестоимости» немного упрощает параллельное программирование. Рекоменлуемый нами метод состоит в использовании архитектуры, в основе которой лежит принцип «разделяй и властвуй». Главное — правильно расставить компоненты «по своим местам» — и программирование станет более простым. Например, поскольку канал связывается с объектами классов ofstream и ifstream, мы можем использовать информацию, хранимую компо Для чтения данных из канала и записи данных в канал можно также испо
Доступ к анонимным каналам c использованием итератора ostream_iterator
Канал можно также испо Таблица»11.4. Операции, доступныедля классов ostream_iterator и istream_iterator istream_iterator ostream_iterator ++r инкремент (префиксная форма) r++ инкремент (постфиксная форма) Обычно эти итераторы используются вместе с iostreams-классами и стандартными алгоритмами. Итератор ostream_iterator предназначен только для последовательно выполняемой записи. После доступа к некоторому элементу программист не может вернуться к нему опять, не повторив всю итерацию сначала. При использовании этих итераторов канал обрабатывается как последовательный контейнер. Это означает, что при связывании канала с iostreams-объектами посредством итератора ostream_iterator и файловых дескрипторов мы можем применить стандартный алгоритм обработки данных для ввода их из канала и вывода их в канал. Причина того, что эти итераторы можно использовать вместе с каналами, состоит в связи, которая существует между итераторами и iostreams-классами. На рис. 11.10 представлена диаграмма, отображающая отношения между итераторами ввода-вывода и iostreams-классами. На рис. 11.10 также показано, как эти классы взаимодействуют с объектно-ориентированным каналом. Рассмотрим подробнее, как итератор ostream_iterator используется с объектом класса ostream. Если инкрементируется указатель, мы ожидаем, что он будет указывать на следующую область памяти. Если же инкрементируется итератор ostream_iterator, он переме Тогда X является объектом типа ostream_iterator. При выполнении операции инкремента X++; итератор X перейдет к слелую *X = Y; значение Y будет отображено на стандартном устройстве вывода. Дело в том, что оператор присваивания "=" перегружен дл ostream_iterator будет создан объект X с использованием аргумента cout. Второй аргумент в конструкторе является разделителем, который автоматически будет размещаться после каждого int -значения, вставляемого в поток данных. Объявление итератора ostream_iterator выглядит следующим образом (листинг 11.22). // Листинг 11.22. Объявление класса ostream_iterator template protected: ostream* _M_stream; const char* _M_string; public: typedef output_iterator_tag iterator_category; typedef void value_type; typedef void difference_type; typedef void pointer; typedef void reference; ostream_iterator(ostream& _s) : _M_stream(&_s),_M_string(0) {} ostream_iterator(ostream& _s, const char* _с): _M_s tream (&_s) , _M_string (_с) { } ostream_iterator<_Tp>& operator=(const _Tp& _value) { *_M_stream << _value; if (_M_string){ *_M_stream << _M_string; return *this; } ostream_iterator<_Tp>& operator* { return *this; } ostream_iterator<_Tp>& operator++ { return *this; } ostream_iterator<_Tp>& operator++(int) { return *this; } }; Конструктор класса ostream_iterator принимает ссылку на объект класса ostream. Класс ostream_iterator находится с классом ostream в отношении агрегирования. Назначение класса istream_iterator прямо противоположно классу ostream_iterator. Он используется с объектами класса istream (а не с объектами класса ostream). Если объекты классов istream_iterator и ostream_iterator связаны с iostream-объектами, которые в свою очередь связаны с файловыми дескрипторами канала, то при каждом инкрементировании итератора типа istream_iterator из канала будут считываться данные, а при каждом инкрементировании итератора типа ostream_iterator в канал будут записываться данные. Чтобы продемонстрировать, как эти компоненты работают вместе, рассмотрим две программы (11.2 и 11.2.1), в которых используются анонимные каналы связи. Про-грамма11.2 представляет родительский процесс, а программа11.2.1— сыновний. В»родительской» части для создания сыновнего процесса используются системные функции fork и execl . При том, что файловые дескрипторы наследуются сыновним процессом, их значения незамедлительно становятся достоянием программы 11.2.1 благодаря вызовуфункции execl . // Программа 11.2 10 int main(int argc, char *argv[]) 11 { 12 13 int Size,Pid,Status,Fdl[2],Fd2[2]; 14 pipe(Fdl); pipe(Fd2); 15 strstream Buffer; 16 char Value[50]; 17 float Data; 18 vector 19 Buffer « Fdl[0] « ends; 20 Buffer » Value; 21 setenv(«Fdin»,Value,l); 22 Buffer.clear; 23 Buffer « Fd2[l] « ends; 24 Buffer » Value; 25 setenv(«Fdout»,Value,l); 26 Pid = fork; 27 if(Pid != 0){ 28 ofstream OPipe; 29 OPipe.attach(Fdl[l] ) ,- 30 ostream_iterator 31 OPipe « X.size « endl; 32 copy(X.begin,X.end,OPtr); 33 OPipe « flush; 34 ifstream IPipe; 35 IPipe.attach(Fd2[0]); 36 IPipe » Size; 37 for(int N = 0; N < Size;N++) 38 { 39 IPi ре » Data; 40 Y.push_back(Data); 41 } 42 wait(&Status); 43 ostream_iterator 44 copy(Y.begin,Y.end,OPtr2); 45 OPipe.close; 46 IPipe.close; 47 } 48 else{ 49 execl("./programll-2b»,«programll-2b»,NULL); 50 } 51 52 return(0); 53 } В строках 21 и 25 системнал функция setenv используется для передачи значений файловых дескрипторов сыновнему процессу. Это возможно благодаря тому, что сыновний процесс наслелует среду родительского процесса. Мы можем устанавливать переменные среды в программе с помощью вызова функции setenv . В данном случае мы устанавливаем их следующим образом. Fdin=filedesc; Fdout=filedesc; Сыновний процесс затем использует системный вызов getenv( ) для считывания значений переменных Fdin и Fdout. Значение переменной Fdin будет представлять «считывающий конец» канала для сыновнего процесса, а значение переменной Fdout — «записывающий». Использование системных функций setenv и getenv обеспечивает просгую форму межпроцессного взаимодействия (interprocess communication — IPC) между родительским и сыновним процессами. Каналы создаются при выполнении инструкций, приведенных в строке 14. Родительский процесс присоединяется к одному концу канала для операции записи с помощью метода attach (строка29). После присоединения любые данные, помещенные в объект OPipe типа ofstream, будут записаны в канал. Итератор типа ostream_iterator подключается к объекгу OPipe при выполнении следующей инструкции (строка 30): ostream_iterator Теперь итератор OPtr ссылается на объект OPipe. После каждой порции помещаемых в канал данных будет вставляться разделитель "\n». С помощью итератора OPtr мы можем поместить в канал любое количество float -значений. При этом мы можем связать с каналом несколько итераторов различных типов. Но в этом случае необходимо, чтобы на «считывающем» конце канала данные извлекались с использованием ите раторов соответствующих типов. При выполнении слелующей инструкции из программы 11.2 в канал сначала помещается количество элементов, подлежащих передаче: OPipe « X.size « endl; Сами элементы отправляются с использованием одного из стандартных С++-алгоритмов: copy(X.begin ,X.end ,OPtr) ; Алгоритм copy копирует содержимое одного контейнера в контейнер, связанный с итератором приемника. Здесь итератором приемника является объект OPtr. Объект OPtr связан с объектом OPipe, поэтому при выполнении алгоритма copy («уместившегося» в одной строке кода) в канал переписывается все содержимое контейнера. Этот пример демонстрирует возможность использования стандартных алгоритмов для организации взаимодействия между различными частями сред параллельного или распределенного программирования. В данном случае алгоритм copy пересылает информацию от одного процесса другому (из одного адресного пространства в другое). Эти процессы выполняются параллельно, и алгоритм copy значительно упрощает взаимодействие между ними. Мы подчеркиваем важность этого подхода, поскольку, если есть хоть какал-то возможность упростить логику параллельной или распределенной программы, ею нужно непременно воспользоваться. Ведь межпроцессное взаимодействие — это один из самых сложных разделов параллельного или распределенного программирования. С++-алгоритмы, библиотека классов iostreamS и итератор типа ostream_iterator как раз и позволяют понизить уровень сложности разработки таких программ. Использование манипулятора flush (в строке 33) гарантирует прохождение данных по каналу. В программе 11.2.1 сыновний процесс сначала получает количество объектов, принимаемых от канала (в строке 36), а затем для считывания самих объектов использует объект IPipe класса istream. // Программа 11.2.1 11 class multiplier{ 12 float X; 13 public: 14 multiplier(float Value) { X = Value;} 15 float &operator(float Y) { X = (X * Y);return(X);} 16 }; 17 18 19 int main(int argc,char *argv[]) 20 { 21 char Value[50] ; 22 int Fd[2] ; 23 float Data; 24 vector 25 int NumElements; 26 multiplier N(12.2); 27 strcpy(Value,getenv(«Fdin»)); 28 Fd[0] = atoi(Value); 29 strcpy(Value,getenv(«Fdout»)); 30 Fd[l] = atoi(Value); 31 ifstream IPipe; 32 ofstream OPipe; 33 IPipe.attach(Fd[0]) ; 34 OPipe.attach(Fd[l]) ; 35 ostream_iterator 36 IPipe » NumElements; 37 for(int N = 0;N < NumElements;N++) 38 { 39 IPipe » Data; 40 X.push_back(Data); 41 } 42 OPipe « X.size « endl; 43 transform(X.begin,X.end,OPtr,N); 44 OPipe « flush; 45 return(0); 46 47 } Сыновний процесс считывает элементы данных из канала, помещает их в вектор, азатем выполняет математические преобразования над каждым элементом вектора, после чего отправляет их назад родительскому процессу. Математические преобразования (строка43) выполняются с использованием стандартного С++-алгоритма transform и пользовательского класса multiplier. Алгоритм transform применяет к каждому элементу контейнера операцию, а затем результат этой операции помещает в контейнер-приемник. В данном случае контейнером-приемником служит объект Optr, который связан с объектом OPipe. Заголовки, которые необходимо включить в программу 11.2.1, приведены в разделе «Профиль программы 11.2.1». Профиль программы 11.2.1 Имя программы program11-2b.cc Описа Требуемые заголовки Инструкции no компиляции и компоновке программ с++ -o»programll-2b programll-2b.ee Инструкции по выполнению [Эта программа запускается программой 11.2. Несмотря на то что классы библиотеки iostream, итераторы типа istream_iterator и ostream_iterator упрощают программирование канала, они не изменяют его поведение. По-прежнему остаются в силе вопросы блокирования и проблемы, связанные с корректным порядком открытия и закрытия каналов, рассмотренные в главе 5. Но использование основных механизмов тех же методов объектно-ориентированного программирования все же позволяет понизить уровень сложности параллельного и распределенного программирования.
FIFO-очереди (именованные каналы),
Методы, которые мы использовали для реализации объектно-ориентированных анонимных каналов, обладают двумя недостатками. Во-первых, любым процессам, которые взаимодействуют с другими процессами, нужен доступ к файловым дескрипторам, возвращаемым при вызове системной функции pipe . Поэтому существует проблема получения этих файловых дескрипторов для всех процессов-участников. Эта проблема легко решается, если процессы связаны отношение Чтобы связать анонимные каналы с объектами классов ifstream и ofstream, мы использовали нестандартное связывание с файловым дескриптором. Нестандартность ситуации вытекает из того, что «брак» между файловыми дескрипторами и iostreams-объектами пока не «освящен» стандартом ISO С++. Поэтому безопаснее использовать FIFO-структуры. К FIFO-файлу специального типа можно получить доступ с помощью имени в файловой системе, в которой «официально» поддерживается связывание с объектами С++-классов ifstream и ofstream. Поэтому точно так же, как мы упрощали межпроцессное взаимодействие (IPC) с помощью iostream-классов и анонимного канала, мы упрощаем доступ к FIFO-структуре. FIFO-структура, основные функции которой совпадают с функциями анонимного канала, позволяет распространить возможности взаимодействия на классы, не связанные никакими родственными отношениями. Однако каждая программа — участник взаимодействия должна при этом «знать» имена FIFO-структур. Это требование, казалось бы, напоминает ограничение, с котороым мы встречались при использовании файловых дескрипторов. Однако FIFO — это все же «шаг вперед». Во-первых, при открытии анонимного канала только система определяет, какие файловые дескрипторы доступны в данный момент. Это означает, что программист не в состоянии полностью контролировать ситуацию. Во-вторых, существует ограничение на количество файловых дескрипторов, котороми располагает система. В-третьих, поскольку FIFO-структурам имена присваиваются пользователем, то количество таких имен не ограничивается. Файловые дескрипторы должны принадлежать файлам, открытым ранее (и причем успешно), а FIFO-имена — это всего лишь имена. FIFO-имя определяется пользователем, а файловые дескрипторы— системой. Имена файлов связываются с объектами классов ifstream, fstream и ofstream с помощью либо конструктора класса либо метода open. В программе 11.3.1 для связывания объектов классов ofstream и ifstream с FIFO-структурой используется конструктор. // Программа 11.3.1 14 using namespace std; 15 16 const int FMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; 17 18 int main(int argc, char *argv[]) 19 { 20 21 int Pid,Status,Size; 22 double Value; 25 mkfifo("/tmp/channel.l»,FMode) ; 26 mkfifo (" / tmp/channel. 2», FMode) ; 28 vector 29 vector 30 ofstream OPipe("/tmp/channel.l»,ios::app); 31 ifstream IPipe("/tmp/channel.2»); 32 OPipe << X.size « endl; 33 ostream_iterator 34 copy(X.begin,X.end,Optr); 35 OPipe « flush; 36 IPipe » Size; 37 for (int N = 0;N < Size; N++) 38 { 39 IPipe » Value; 40 Y.push_back(Value); 41 } 42 43 IPipe.close; 44 OPipe.close; 45 unlink("/tmp/channel.1»); 46 unlink("/tmp/channel.2»); 47 cout « accumulate(Y.begin,Y.end,-13.0) « endl; 48 49 return(0); 50 } В программе 11.3.1 используется две FIFO-структуры. Вспомните, что FIFO-структуры являются однонаправленными компонентами. Поэтому, если процессы должны обмениваться данными, то необходимо использовать по крайней мере две FIFO-структуры. В программе 11.3.1 они называются channel.1 и channel.2. Обратите внимание на установку флагов полномочий для FIFO-структур (строка 16). Эти полномочия означают, что владелец FIFO-структуры имеет право доступа для чтения и записи, а все остальные — право доступа только для чтения. При выполнении строки 30 FIFO-структура channel.1 будет открыта только для вывода данных. Тот же результат можно было бы получить следующим образом Используемые здесь параметры алгоритма open означают, что FIFO-структура будет открыта в режиме дозаписи. В программе 11.3.1 алгоритм copy используется для вставки объектов в объект OPipe типа fstream и косвенно в FIFO-структуру. Мы могли бы также использовать здесь объект типа fstream:fstreamOPipe("/tmp/channel.l», ios::out | ios::app); В этом случае взаимодействие процессов было бы ограничено выводом данных только в режиме дозаписи. Если бы мы не использовали флаг ios: :app , попытка объекта типа ofstream создать FIFO-сгруктуру (см. строку 30) была бы неудачной. К сожалению, такой вариант работать не будет. Создание FIFO-структур находится в компетенции функции mkfifo. В строках 45 и 46 программы 11.3.1 FIFO-структуры удаляются из файловой систе Профиль программы 11.3.1 Имя программы program11-3a.cc Описание Для пересылки контейнерного объекта через FIFO-структуру используются объекты ТИпа ostream_iterator и ofstream. Для извлечения информации из FIFO-структуры применяется объект типа ifstream. Требуемые заголовки Инструкции по компиляции и компоновке программ с++ -о program11-3a program113a.сс Среда для тестирования SuSE Linux 7.1, gcc 2.95.2, Solaris 8, Sun Workshop 6. Инструкции по выполнению ./program11-3a & program11-3b Примечания Сначала запускается программа 11.3.1. Программа11.3.2 содержит инструкцию sleep, которая восполняет собой отсутствие реальной синхронизации. Программа 11.3.2 считывает данные из FIFO-структуры channel. 1 и записывает информацию в FIFO-структуру channel. 2. // Программа 11.3.2. Считывание данных из FIFO-структуры // channel.l и запись информации в // FIFO-структурУ channel.2 10 using namespace std; 11 12 class multiplier{ 13 double X,- 14 public: 15 multiplier(double Value) { X = Value;} 16 double &operator(double Y) { X = (X * Y);return(X);} 17 }; 18 19 20 int main(int argc,char *argv[]) 21 { 22 23 double Size; 24 double Data; 25 vector 26 multiplier R(1.5); 27 sleep(15); 28 fstream IPipe("/tmp/channel.1»); 29 ofstream OPipe("/tmp/channel.2»,ios::app); 30 if(IPipe.is_open){ 31 IPipe » Size; 32 } 33 else{ 34 exit(l); 35 } 36 cout « «Количество элементов " << Size << endl; 37 for(int N = 0;N < Size;N++) 38 { 39 IPipe » Data; 40 X.push_back(Data); 41 } 42 OPipe « X.size « endl; 43 ostream_iterator 44 transform(X.begin,X.end,Optr,R); 45 OPipe << flush; 46 OPipe.close; 47 IPipe.close; 48 return(0); 49 50 } Обратите внимание на то, что в программе 11.3.1 FIFO-стуктура channel.l открывается для вывода данных, а в программе 11.3.2 та же FIFO-структура channel. 1 — для ввода данных. Слелует иметь в виду, что FIFO-структуры действуют как однонаправленные механизмы связи, поэтому не пытайтесь пересылать данные в обоих направлениях! Достоинство использования iostreams -классов в сочетании с FIFO-структурами состоит в том, что мы можем использовать iostreams -методы применительно к FIFO-структурам. Например, в строкеЗО мы используем метод is_open класса basic_filebuf, который позволяет определить, открыта ли FIFO-структура. Если она не открыта, то программа 11.3.2 завершается. Детали реализации программы 11.3.2 приведены в разделе «Профиль программы 11.3.2». Профиль программы 11.3.2 Имя программы programll-3b.ee Описание Программа считывает объекты из FIFO-структуры с помощью объекта типа ifstream. Для пересылки данных через FIFO-структуру здесь используется итератор типа ostream_iterator и стандартный алгоритм transform. Требуемые заголовки > Инструкции по компиляции и компоновке программ с++ -о programll-3b programll-3b.сс ; Среда для тестирования SuSE Linux 7.1, GCC 2.95.2, Solaris 8, Sun Workshop 6.0. Инструкции по выполнению program11.3a & program11-3b Примечания Cначала запускается программа11.3.1. Программа11.3.2 содержит инструкцию Sleep, которая восполняет собой отсутствие реальной синхронизации.
Интерфейсные FIFO-классы
Упростить межпроцессное взаимодействие (IPC) можно не только с помощью iostreams-классов или классов istream_iterator и ostream_iterator, но и посредством инкапсуляции FIFO-механизма в FIFO-классе (листинг 11.23). // Листинг 11.23. Объявление FIFO-класса class fifo{ mutex Mutex; //.. . protected: string Name; public: fifo &operator<<(fifo &In, int X); fifo &operator<<(fifo &In, char X); fifo &operator>>(fifo &Out, float X); //.. . }; В этом случае мы можем легко создавать объекты класса fifo с помощью конструктора, а также передавать их как параметры и принимать в качестве значений, возвращаемых функциями. Мы можем использовать их в сочетании с классами стандартных контейнеров. Применение такой конструкции значительно сокращает объем кода, необходимого для функционирования FIFO-механизма. Более того, «классовый» подход создает условия для обеспечения типовой безопасности и вообще позволяет программисту работать на более высоком уровне.
Каркасные классы
Под • компоненты проверки достоверности; • компоненты выделения лексем; • компоненты грамматического разбора; • компоненты синтаксического анализа; • компоненты лексического анализа. Эти части ПО можно объединить, чтобы сформировать уже знакомую нам программную конструкцию (листинг11.24). // Листинг 11.24. Объявление класса language_processor // и определение метода process_input class language_processor { //... protected: virtual bool getString(void) = 0; virtual bool validateString(void) = 0; virtual bool parseString(void) = 0; //... public: bool process_input(void); }; bool language_processor::process_input(void) { getString; validateString; parseString; //.. . compareTokens; //.. . } Во-первых, класс language_processor является абстрактным и базовым, поскольку он содержит чисто виртуальные функции: virtual bool getString(void) = 0; virtual bool validateString(void) = 0; virtual bool parseString(void) = 0; Это означает, что класс language_processor не предназначен для непосредственного использования. Он служит в качестве проекта для производных классов. Особенно стоит остановиться на методе process_input . Этот метод представляет собой план работы, которую предстои т обобщит ь классу language_processor. Во многих отношениях именно это и отличает каркасные классы от классов других типов. Каркасный класс описывает не только обобщенную структуру и характер отношений между компонентами, но содержит и заранее определенные последовательности выполняемых действий. Однако в таком своеобразном описании поведения не указываются детали его реализации. В данном случае модель поведения задается набором чисто виртуальных функций. Каркасный класс не определяет, как именно эти действия должны быть выполнены, — важно то, что они должны быть выполнены, причем в определенном порядке. А производный класс должен обеспечить реализацию всех чисто виртуальных функций. При этом ответственность за корректность выполняемых действий целиком возлагается на производный класс. Каркасные классы по определению — договорные классы. Для достижения успешного результата требуется надлежащее выполнение обеих частей договора. Каркасный класс намечает четкий план, а производный реализует этот план в виде конкретного определения чисто виртуальных функций. Последовательность действий, «намеченная» методом process_input , соблюдается в таких приложениях. • Компиляторы • Интерпретаторы ко • Обработчики естественных языков • Програ • Упаковка-распаковка • Протоколы пересылки файлов • Графические интерфейсы пользователей • Контроллеры устройств Корректная разработка каркасного класса language_processsor (при надлежащем его тестировании и отладке) позволяет ускорить разработку широкого диапазона приложений. Понятие каркасного класса также полезно использовать при разработке приложений, к которым предъявляются требования параллелизма. Так, использование агент-ных каркасов и каркасов «классной доски» фиксирует базовую структуру параллелизма и схемы работы в этих структурах. Майкл Вулдридж в своей книге [51] предлагает следующий обобщенный цикл управления агентами. Алгоритм: цикл управления агентами В = ВО while true do get next percept p В = brf(B,p) I = deliberate(B) П= plan(B,I) execute(П) end while_ Эта модель поведения реализуется широким диапазоном рациональных агентов. Если вы разрабатываете программу, в которой используются рациональные агенты, то скорее всего эта последовательность действий будет реализована в вашей программе. На фиксации последовательностей действий такого типа и «специализируются» каркасные классы. Для цикла управления агентами функции brf , deliberate и plan должны быть объявлены чисто виртуальными функциями. Цикл управления агентами определяет, в каком порядке и как должны вызываться эти функции, а также сам факт того, что они должны быть вызваны. Однако конкретное содержание функции определит производный класс. При надлежащем определении цикла управления агентами будет решен целый класс проблем. Ведь системы, состоящие из множества параллельно выполняющихся агентов, постепенно становятся стандартом для реализации приложений параллельного программирования. Такие системы часто называют Каркасные классы обеспечивают своих потомков не только планом действий (что весьма полезно для параллельных или распределенных систем), но и такими компонентами синхронизации, как объектно-ориентированные мьютексы, семафоры и потоки сообщений. Структура «классной доски» — полезное средство для взаимодействия множества агентов— представляет собой критический раздел, поскольку сразу несколько агентов должны иметь возможность одновременно считывать из нее информацию и записывать ее туда. Следовательно, каркасный класс должен обеспечить базовую структуру для отношений между агентами, компонентами синхронизации и»классной доской». Например, листинг 11.25 содержит два метода, которые каркасный класс мог бы использовать для доступа к «классной доске». // Листинг 11.25. Определение методов recordMessge и // getMessage для класса agent_framework int agent_framework::recordMessage(void) { Mutex.lock; BlackBoardStream << Agent[N].message; Mutex.unlock; } int agent_framework::getMessage(void) { } Mutex.lock; BlackBoardStream » Values; Agent[N].perceive(Values); Mutex.unlock; Здесь каркасный класс должен защищать доступ к «классной доске» с помощью объектов синхронизации. Поэтому, когда агенты считывают сообщения с «классной доски» или записывают их туда, синхронизация уже будет обеспечена каркасным классом. Программисту не нужно беспокоиться о синхронизации доступа к «классной доске». Базовая структура агентно-ориентированного каркасного класса agent_framework показана на рис. 11.11. Обратите внимание на то, что каркасный класс инкапсулирует объектно-ориентированные мьютексы и переменные условий. Агентно-ориентированный каркасный класс (см. рис. 11.11) для организации взаимодействия процессов в MPI- либо PVM-ориентированной системе должен использовать MPI- либо PVM-потоки сообщений. Вспомните, что эти потоки сообщений были разработаны как интерфейсные классы, что позволяет программисту для доступа к PVM- или MPI-классу использовать iostreams-представление. Если MPI- или PVM-классы не используются, агенты могут взаимодействовать через сокеты, каналы или даже общую память. В любом случае мы рекомендуем реализовать примитивы синхронизации с помощью интерфейсных классов, которые упрощают их использование. Структура «классной доски», показанная на рис. 11.11, является объектно-ориентированной и использует преимущества универсальности, обеспечиваемой шаблонными классами, что также упрощает реализацию параллелизма. Агенты, выполняемые параллельно, представляют эффектив-кую модель параллельного и распределенного программирования.
Резюме
Проблемы параллельного программирования, представленные в главе 2, можно эффективно решить, используя «строительные блоки», рассмотренные в этой главе. Роль интерфейсного класса в упрощении использования библиотек функций трудно преувеличить. Интерфейсный класс вносит логичность API-интерфейса путем заключения в оболочку вызовов функций из таких библиотек, как MPI или PVM. Интерфейсные классы обеспечивают типовую безопасность и возможность многократного использования кода, а также позволяют программисту работать в привычной «системе координат», как с PVM- или MPI-потоками данных. Межпроцессное взаимодействие (IPC) упрощается путем связывания канала или потоков сообщений сюБЧгеатБобъектами и перегрузки операторов вставки («) и извлечения (») для пользовательских классов. Класс ostream_iterator доказывает свою полезность в «оптовой» пересылке контейнерных объектов и их содержимого между процессами. Итераторы типа ostream_iterator и istream_iterator также обеспечивают свя-зующее звено между стандартными алгоритмами и IPC-компонентами и методами. Поскольку модель передачи сообщений используется во многих параллельных и распределенных приложениях, то любой метод, который упрощает передачу различных типов данных между процессами, упрощает программирование приложения в целом. К таким способам упрощения относится использование iostreams-классов и итераторов типа ostream_iterator и istream_iterator. Каркасный класс представлен здесь как базовый строительный блок параллельных приложений. Мы рассматриваем классы, подобные классам мьютексов, переменных условий и потоков, как компоненты низкого уровня, которые должны быть скрыты от программиста в каркасном классе (где это возможно!). При создании средне- и крупномасштабных приложений, которые тре-буют реализации параллелизма, программист не должен «застревать» на этих низкоуровневых компонентах. В идеале для удовлетворения требований параллельной обработки каркасный класс должен быть строительным блоком базового уровня и обеспечивать нас «готовыми» схемами равноправных элементов и взаимодействия типа «клиент-сервер». Мы можем использовать различные типы каркасных классов: для обработки чисел, баз данных или применения агентов, технологии «классной доски», GUI и т.д. Метод, который мы предлагаем для реализации параллелизма, состоит в построении приложений на базе коллекции каркасных классов, которые уже оснащены надлежащими компонентами синхронизации, связанными соответствующими отношениями. В главах 12 и 13 мы подробнее остановимся на каркасных классах, которые поддерживают параллелизм. Мы также рассмотрим использование стандартных С++-алгоритмов, контейнеров и объектов функций для управления процессом создания множества задач или потоков в приложениях, требующих параллелизма.
Реализация агентно-ориентированных архитектур
Если бы последовательное (процедурное) программирование позволяло находить решения в любых ситуациях, то не было бы необходимости для развития технологий параллельного и распределенного программирования. Во многих случалх методы последовательного программирования просто не отвечают требованиям и опыту современных пользователей компьютеров. В процессе поиска разработчиками новых подходов к решению все возрастающих проблем и создаются альтернативные модели программного обеспечения. Программисты находят более эффективные способы организации ПО. Структурное программирование было шагом вперед по сравнению с процедурным (изобиловавшим безусловными переходами), объектно-ориентированное программирование сменило структурное. Во многих отношениях агенты и агентно-ориентированное программирование можно рассматривать как очередную (более высокую) ступень развития программирования. Агенты представляют иной (более сложный) метод организации и представления распределенных/параллельных программ.
Что такое агенты
Когда объектное программирование впервые залвило о себе, сама трактовка понятия объекта вызвала большие споры. Подобные разногласия вызывает и трактовка понятия агента. Одни определяют агенты как автономные постоянно выполняющиеся про-траммы, которые действуют от имени пользователя. Однако это определение можно применить и к UNIX-демонам или даже некоторым драйверам устройств. Другие дополняют это определение тем, что агент должен обладать специальными знаниями пользователя, должен выполняться в среде, «населенной» другими агентами, и обязан действовать только в рамках заданной среды. Эти требования должны исключать другие программы, которые можно было бы до некоторой степени считать агентами. Например, многие агенты электронной почты действуют автономно и могут работать по многих средах. Кроме того, в различных кругах программистов для описания агентов появились такие термины, как софтбот, т.е. Существует определение, согласно которому агент определяется Несмотря на то что это определение имеет более структурированную форму, оно также нуждается в дальнейшем уточнении, поскольку под это определение попадают многие серверы (объектно-ориентированные и нет). Это определение в таком виде включило бы слишком много типов программ и программных конструкций. И хотя мы опираемся на FIPA-спецификацию, это базовое определение требует дальнейшей проработки.
Агенты: исходное определение
Одной из причин, по которой слово 1. Это определенный тип объекта (т.е. не все объекты являются агентами). 2. Его реализация использует понятие класса (для агентов весьма существенны инкапсуляция, наследование и полиморфизм). 3. Он содержит набор поведенческих вариантов и атрибутов, которые должны включатьубеждения, желания, намерения идействия. В рамках нашего изложения материала агенты по определению являются рациональными программными компонентами. Прежде чем мы перейдем к дельнейшему определению агентов, рассмотрим типы агентов, которые реализуются чаще всего.
Типы агентов
Существует несколько категорий агентов. Несмотря на то что не все агенты можно отнести к одной из них, с их помощью все же можно описать большинство агентов, которые уже нашли практическое применение. В табл. 12.1 перечислено пять основных категорий агентов. Очевидно, существуют и агенты смешанного типа, которые можно отнести к нескольким категориям одновременно, поскольку для распределения агентов по категориям нет никаких жестких правил. Эти категории представлены для удобства и используются в качестве отправной точки в попытке классифицировать агенты, которые, возможно, вам придется разрабатывать или использовать в своей работе. В табл. 12.1 не указаны компоненты, которые должны иметь агенты. Здесь определены лишь виды деятельности, которые характерны для агентов той или иной категории. При этом следует понимать, что эти категории не являются исключительной сферой агентов. Подобным образом по категориям можно разделить и другие классы ПО (например, экспертные и объектно-ориентированные системы). В нескольких случалх единственным отличием может оказаться сам факт того, что мы говорим об агентах, а не об объектах или экспертных системах. Табл
В чем состоит разница между объектами и агентами
Агент прежде всего должен отвечать условиям объектной ориентации. Это означает, что агенты и объекты имеют больше общего, чем многие специалисты хотели бы это признать. Именно функциональнал и конструктивная составляющие объектов сближают их с агентами. Объекты по определению самодостаточны и проявляют определенную автономность. Если степень автономности пересекает определенный порог, и объекту предоставляются такие когнитивные (познавательные) структуры данных, как те, что характерны для модели BDI, то такой объект является агентом. Автономный рациональный объект является агенто • • члена Слелует иметь в виду, что в объектно-ориентированном программировании подпрограммы, определенные для класса, называются set struct statement{ //. . . float ArrivalTime; float DepartureTime; string Destination; //.. . }; Здесь инструкции связаны с составлением расписания для некоторого вида общественного, транспорта. Коллекция этих инструкций хранится в С++-множестве set class agent{ //.. . set //. . . }; В классе agent для обработки множества Beliefs, чтобы сфор
Понятие об агентно-ориентированном программировании
Агентно-ориентированное программирование — это процесс назначения работы, порученной программе, одному или нескольким агентам. В декомпозиции работ (Work Breakdown Structure — WBS) в этом случае участвуют только агенты. Если всю работу, которую должна выполнить программа, можно назначить одному или нескольким агентам, мы имеем дело с чистой агентно-ориентированной программой, в которой весь необходимый объем проектирования и разработки требует только агентно-ориентированного программирования. Во многих ситуациях нарялу с агентами в приложении будут задействованы и другие виды объектов и систем, которые не являются агентно-ориентированными, и, следовательно, такое программирование нельзя назвать агентно-ориентированным. Подобное сотрудничество часто имеет место, когда агенты участвуют в работе серверов баз данных, серверов приложений и других типов объектно-ориентированных систем. При создании систем ПО — либо полностью агентно-ориентированных, либо только частично — создаются рациональные объектно-ориентированные программные компоненты. § 12:1 Дедукция, индукция и абдукция Дедукция, индукция и абдукция — это процессы, используе Все фигуры с тре Даннал фигура и Эта фигура — треугольник. <— Вывод получен по делукции. Правила генерирования вывода — это руководящие принципы и ограничения, которые определяют, как механизм рассуждений может переходить от одного утверждения кдругому. Правила генерирования вывода определяют, когда утверждения логически эквивалентны, и условия, при которых одно утверждение может быть преобразовано в другое. Основные правила генерирования вывода приведены в конце этого раздела. Процесс индукции позволяет механизму рассуждений делать вывод на основании множестваутверждений, являющихся фактами, например: Вчера шел дождь. Позавчера шел дождь. Дождь шел всю прошлую неделю. Завтра будет идти дождь. <— Вывод получен по индукции. Тогда как следствия, полученные в процессе дедукции объявляются непременно истинными (если правила генерирования вывода были применены корректно), то заключения, к которым приходят в процессе индукции, имеют лишь некоторую вероятность быть истинными. Насколько близко эта вероятность приближается к 100%, зависит от характера и контекста утверждений, а также данных, на которые они опираются. Процесс абдукции позволяет механизму рассуждений сделать наиболее правдоподобный вывод на основе набора утверждений или данных, например, так. Предметы одежды обвиняемого были обнаружены на месте преступления. Межлу обвиняемым и покойником недавно произошел бурный конфликт. ДНК обвиняемого была обнаружена на месте преступления. Обвиняемый виновен в свершении преступления. <— Вывод получен по абдукции. Делукция, индукция и абдукция — это три основных процесса логического мышления. Их роль в логике можно сравнить с ролью вычислений и арифметики в математике. Способность корректно переходить от посылок (утверждений, данных и фактов) кзаключениям является процессом, который мы называем Основные правила генерирования вывода
Роль агентов в распределенном программировании
Возникновение распределенных программ было вызвано практической необходимостью. Нетрудно представить, что существует некоторый ресурс, который нужен программе, но этот ресурс размещен на другом компьютере или в сети. Под такими ресурсами часто понимают базы данных, Web-серверы, серверы электронной почты, серверы приложений, принтеры и крупные запоминающие устройства. Подобными ресурсами обычно управляет часть ПО, именуемал сервером. Другал часть ПО, которой необходимо получить доступ к ресурсам, называется клиентом. Тот факт, что ресурсы и клиент расположены на рааличных компьютерах, приводит к необходимости использования распределенных архитектур. В большинстве случаев не имеет смысла объединять эти программы в одну большую и выполнять ее на одном компьютере и в едином адресном пространстве. Более того, существует множество программ, разработанных в различное время, разными разработчиками и для разных целей, но которые могут успешно использовать преимущества друг друга. Приложение, которое использовало эти программы, эволюционировало определенным образом и в итоге «заслужило звание» распределенного приложения. Поскольку эти программы отделены друг от друга, каждая из них должна иметь собственное адресное пространство и «свои» ресурсы. Когда эти программы используются для совместного решения задачи, они образуют распределенное приложение. Оказывается, что архитектура распределенной программы обнаружила высокую степень гибкости, что позволило применить ее к крупномасштабным приложениям. Во многих приложениях необходимость в распределенной архитектуре обнаруживается довольно поздно, «когда поезд уже ушел». Но если заранее идентифицировать такую необходимость, можно с успехом использовать соответствующие методы проектирования программного обеспечения. Если вы уже точно знаете, что вам нужно разрабатывать распределенное приложение, то следующий вопрос должен прозвучать так: «как именно оно должно быть распределено?». От ответа на этот вопрос будет зависеть, какую модель следует использовать в этом случае. Несмотря на существование множества различных моделей (равноправных узлов и типа «клиент/сервер»), в этой книге мы остановимся только надвух: мультиагентной архитектуре и архитектуре «классной доски». Оба эти вида архитектуры 1. Идентификация декомпозиции ПО распределенного решения. 2. Реализация эффективного и рационального взаимодействия между распределенными компонентами. 3. Обработка исключительных ситуаций, ошибок и частичных отказов. Несмотря на то что для реализации п. 2 в понятии класса агента нет ничего такого, что было бы свойственно только агентам, смысл п. 1 и 3 почти подразумевается в самой сути агента. Рациональность каждого агента определяет его назначение, а следовательно, и роль, которую он будет играть в решении ПО. Поскольку агенты самодостаточны и автономны, то хорошо продуманный класс агента должен включать необходимые меры по обеспечению их отказоустойчивости.
Агенты и параллельное программирование
При размещении агентов в среде с несколькими процессорами или параллельно выполняющимися потоками вы получаете такие же преимущества, как и при распределенном программировании, но с той лишь разницей, что сотрудничество между агентами программировать в этом случае гораздо проще. Для передачи сообщений между агентами, которые коллективно решают задачи некоторого вида, также можно использовать PVM- и MPI-среды. И снова-таки, рациональность агентов облегчает понимание, как следует провести декомпозицию работ для параллелизма. В параллельном программировании, как правило, встречаются такие проблемы. 1. Эффективное и рациональное разделение работы между несколькими компонентами. 2. Координация параллельно выполняющихся программных компонентов. 3. Разработка соответствующего взаимодействия (когда это необходимо) между компонентами. 4. Обработка исключительных ситуаций, ошибок и частичных отказов (если агенты функционируют на отдельных компьютерах). Мультиагентные параллельные архитектуры часто характеризуются как слабосвязанные, т.е. им присущ минимум взаимодействия и взаимозависимости. Каждый агент знает свою цель и обладает методами для ее достижения. В то время как п. 3 не подвластен классу агента, п. 1, 2 и 4 можно легко управлять с помощью классов агентов. Например, при использовании агентов влияние п. 2 уменьшается, поскольку каждый агент рационален, имеет цель, а также способы и средства ее достижения. Поэтому вся ответственность смещается с алгоритма координации и управления на действия каждого агента. Влияние п. 4 также уменьшается, поскольку агенты самодостаточны, рациональны и автономны, а кроме того, хорошо продуманный класс агента должен включать необходимые меры по обеспечению отказоустойчивости агентов. Поскольку состояние агента инкапсулировано, ответственность за защиту критических разделов в объекте агента целиком воалагается на класс агента. Агент должен приводить в исполнение собственные стратегии доступа к данным. Возможные стратегии доступа, из которых могут выбирать агенты, перечислены в табл. 12.2. Таблица 12.2. Стратегии доступа EREW Монопольное чтение, монопольная запись (Exclusive Read Exclusive Write) CREW Параллельное чтение, монопольная запись (Concurrent Read Exclusive Write) ERCW Монопольное чтение, параллельная запись (Exclusive Read Concurrent Write) CRCW Параллельное чтение, параллельная запись (Concurrent Read Concurrent Write) Класс каждого агента должен определить, какал именно стратегия доступа приемлема в мультиагентной среде. В ряде случаев реализуются не просто отдельные стратегии доступа, перечисленные в табл. 12.2, а их комбинации. Это позволяетупростить параллельное программирование, поскольку разработчик может работать на более высоком уровне и не беспокоиться о построении мьютексов, семафоров и пр. Мультиагентные решения позволяют разработчику не погружаться в детали координации вызова каждой функции и организации доступа к данным. Каждый агент имеет цель. Каждый агент рационален, а следовательно, обладает определенной логикой для достижения своей цели. Процесс программирования в этом случае больше напоминает делегирование задач, а не координацию задач, которая характерна для традиционного параллельного программирования. Поскольку агентно-ориентированное программирование — это объектно-ориентированное программирование специального вида, применительно к агентам используется более декларативный вид параллельного программирования по сравнению с традиционным процедурно-ориентированным программированием, которое часто реализуется такими языками, как Fortran или С. Разработчик лишь определяет,
Базовые компоненты агентов
Агент объявляется с использованием ключевого слова class. Компоненты агента должны состоять из С++-членов данных и функций-членов. Логическая структура класса агента показана на рис. 12.1. Класс агента (см. рис. 12.1) определяет типичные методы инициализации, чтения и записи, которые должен иметь практически любой объект. В «джентльменский набор» входят конструкторы, деструкторы, операторы присваивания, обработчики исключений и т.д. Атрибуты этого класса включают переменные состояния, определяющие объект. Если же ограничиться перечнем этих атрибутов и методов, мы получим только традиционный объект. Рациональный компонент создают когнитивные структуры данных и методы рассуждений (логического вывода). А ведь именно рациональный компонент трансформирует «обычный» объект в агент.
Когнитивные структуры данных
Под Тогда как для традиционных структур данных вполне обычными являются, например, алгоритмы сортировки и поиска, то для когнитивных структур данных более приемлемы методы рассуждений. Абстрактные типы данных, используемые вместе с когнитивными структурами данных, часто включают следующие: вопросы события факты вре предположения заблуждения убеждения цель утверждени заключения Безусловно, с когнитивными структурами данных можно сочетать и другие типы данных, но приведенные выше являются характеристиками программ, которые используют такие рациональные программные компоненты, как агенты. Эти абстрактные типы обычно реализуются как типы данных, объявленные с помощью ключевых слов struct или class. Напри struct question{ class justification{ //... //... string RequiredInformation; time EventTime; target_object QuestionDomain; bool Observed; string Tense; bool Present; string Mood; //... //... }; }; Шаблонные и контейнерные С++-классы можно использовать для организации таких когнитивных структур данных, как знания, например, так. class preliminary_knowledge{ //.. . map map set };
Методы рассуждений
Под Таблица 12.3. Основные способы реализации методов рассуждений Эти методы достаточно понятны и широко доступны во многих библиотеках, оболочках и языках программирования. Эти методы являются «строительными блоками» для базовых методов рассуждений. Чтобы понять, как происходит процесс рассуждения, используем одно из правил генерирования вывода, а именно молус поненс (правило отделения), и построим простой метод рассуждения. Возьмем следующее утверждение. Если существует автобусный маршрут из Детройта в Нью-Йорк, то Джон поедет в отпуск. Если мы выясним, что автобусный маршрут из Детройта в Нью-Йорк действительно существует, то будем знать, что Джон поедет в отпуск. Правило молус поненс имеет следующий формат. P Q P Q Здесь: P = Если су Мы могли бы спроектировать простой агент обеспечения решения, который позволит нам узнать, поедет Джон в отлуск или нет. Этому агенту нужно узнать все возможное об автобусных маршрутах. Предположим, у нас есть список автобусных маршрутов: Толедо-Кливленд Детройт-Чикаго Янгстаун-Нью-Йорк Кливленд-Колумбус Цинциннати-Детройт Детройт-Толедо Колумбус-Нью-Йорк Цинциннати-Янгстаун Каждый из этих маршрутов представляет обязательство, взятое на себя компанией ABC Bus Company. Если наш агент получит доступ к расписанию автобусных маршрутов этой компании, то приведенный выше список маршрутов можно будет использовать для представления некоторой части убеждений нашего агента. Возникает вопрос: как перейти от списка маршрутов к убеждениям? Для начала попробуем разработать простую структуру утверждений. struct existing_trip{ //. . . string From; time Departure; string То; time Arrival; //.. . }; Затем попытаемся использовать контейнерный класс для представления убеждений нашего агента в отношении автобусных маршрутов. set Если определенный автобусный маршрут содержится в множестве BusTripKnowledge, то наш агент убежден в том, что в указанное время автобус непременно отправится по этому маршруту из пункта отправления в пункт назначения. Итак, мы можем зафиксировать любой маршрут в соответствии с заданной структурой. //... existing__trip Trip; Trip. From. append (" Toledo " ) ; Trip.To.append( «Cleveland») ; Trip.Departure(«4:3 О») ; Trip.Arrival(«5:45») ; BusTripKnowledge. insert(Trip) ; //... Если мы поместим каждый маршрут в множество BusTripKnowledge, то убеждения нашего агента об автобусных перевозках компании ABC Bus Company будут полностью описаны. Обратите внимание на то, что прямого маршрута из Детройта в Нью-Йорк не существует. Но Джон может добраться в Нью-Йорк из Детройта более сложным путем, осуществив следующие переезды автобусом: из Детройта в Толедо; из Толедо в Кливленд; из Кливленда в Кол^мбус; из Колумбуса Нью-Йорк. Поэтому, несмотря на то, что компания ABC Bus Company не предоставляет прямого маршрута (из пункта А в пункт Б), она позволяет совершить переезд с большим количеством промежуточных остановок. Задача состоит в следую Наш простой агент будет использовать этот DFS-метод для выяснения, существует ли маршрут из Детройта в Нью-Йорк. Выяснив этот факт, агент может обновить свои убеждения насчет Джона. Теперь агент убежден, что Джон поедет в отпуск. Предположим, мы внесли дополнительное прелусловие относительно отпуска Джона. Если Джон обслужит 15 или больше новых клиентов, его доходы превысят (>) 150000. Если доходы Джона превысят 150000 и существует маршрут из Детройта в Нью-Йорк, то Джон отправится в отпуск. Теперь агент должен выяснить, превышают ли доходы Джона лумму 150000 и существует ли маршрут из Детройта в Нью-Йорк. Чтобы выяснить положение дел насчет доходов Джона, агент должен сначала узнать, обслужил ли Джон хотя бы 15 новых клиентов. Предположим, мы уверяем программного агента в том, что Джон обслужил 23 новых клиента. Затем агент должен убедиться в том, что его доходы превышают 150000. На основе содержимо А -> В (В и С) -> D А С D Здесь: А=ЕслиДжон обслужит не менее 15 новых клиентов, В = Доходы>150000, С = Су В этом примере агент убеждается, что эле При сочетании метода прямого построения цепочки и метода DFS создается процесс, в соответствии с которым одно предположение может быть подтверждено на основе уже принятых предыдущих. Это очень важный момент, поскольку наш агент при достижении цели должен знать, что в действительности следует считать корректным. Такой подход также влияет на отношение к вопросам параллельного программирования. Тот факт, что агент рационален и действует в соответствии с правилами построения рассуждений, позволяет разработчику сосредоточиться на корректном моделировании задачи, выполняемой агентом, а не на стремлении явно управлять параллелизмом в программе. Минимальные требования параллелизма, выражаемые тремя «китами» — декомпозицией, взаимодействием и синхронизацией (decomposition, communication, synchronization — DCS), — по большей части относятся к архитектуре агента. Каждый агент для своего поведения имеет логическое обоснование. Это обоснование должно опираться на хорошо определенные и хорошо понимаемые правила ведения рассуждений. Декомпозиция зачастую выражается в простом назначении агенту одного или нескольких основных указаний (директив). Декомпозиция работ в этом случае должна иметь естественный характер и в конце концов выразиться в параллельных или распределенных программах, которые нетрудно поддерживать и развивать. Взаимодействие агентов проще представить, чем взаимодействие анонимных • посредством чего должно происходить взаимодействие; • кому нужно взаимодействовать; • когда должно происходить взаимодействие; • какой формат должно иметь взаимодействие. Ответы на эти вопросы должны быть изначально заложены в проект агентов. Теперь осталось лишь определиться с физической реализацией взаимодействия агентов. Для этого можно воспользоваться библиотеками, которые поддерживают параллелизм. Наконец, что касается проблем синхронизации, то с ними можно легко справиться, поскольку именно логическое обоснование агента сообщает ему, когда он может и должен выполнять действия. Следовательно, сложные вопросы синхронизации сводятся к простым вопросам сотрудничества. Благодаря этому упрощается и задача разработчика в целом. Теперь рассмотрим базовую структуру агента и возможности его реализации в С++.
Реализация агентов в С++
Рассмотрим упрощенный вариант предыдущего примера агента и продемонстрируем, как его можно реализовать в С++. Цель этого агента — составлять график отпусков и выполнять подготовку к поездкам владельца компании ABC Auto Repair Company. В компании работают десятки служащих, и поэтому у хозяина нет времени заботиться о проведении своего очередного отпуска. Кроме того, если хозяин не получит определенного объема прибыли, об отпуске не может быть и речи. Поэтому владельцу компании хотелось бы, чтобы агент распланировал его отпуска равномерно по всему голу при условии процветания фирмы. По мнению владельца компании, главное, чтобы агент работал автоматически, т.е. после инсталляции на компьютере о нем можно было не беспокоиться. Когда агент определит, что подошло время для отпуска, он должен предъявить план проведения отпуска, забронировать места в отеле и проездные билеты, а затем по электронной почте представить хозяину маршрут. Владелец должен побеспокоиться только о формировании задания для агента. Он должен указать, куда желает отправиться и какой объем прибыли необходимо получить, чтобы запланированная поездка состоялась. Теперь рассмотрим, как можно спроектировать такой агент. Вспомним, что рациональный компонент (см. рис. 12.1) класса агента состоит из когнитивных структур данных и методов рассуждений (стратегий логического вывода). Когнитивные структуры данных (CDS) позволяют хранить убеждения, предположения, знания, заблуждения, факты и пр. Для доступа к этим когнитивным структурам данных в процессе решения проблемы и выполнения задач класс агента использует стратегии логического вывода. Для реализации CDS-структур данных и методов построения рассуждений можно использовать ряд контейнерных классов и алгоритмов, которые содержатся в стандартной библиотеке С++.
Типы данных предположений и структуры убеждений
Этот агент обладает убеждениями о показателях авторемонтной мастерской. Убеждения составляют информацию о том, сколько клиентов обслуживается в час, какова загрузка ремонтных секций в день и общий объем продаж (запчастей и услуг) за некоторый период времени. Кроме того, агент знает, что владелец фирмы любит путешествовать только автобусами. Поэтому агент хранит информацию об автобусных маршрутах, которые могут для отпускника оказаться привлекательными. В программе, насыщенной математическими вычислениями, используются в основном целочисленные значения и числа с плавающей точкой. В графических программах участвуют пиксели, линии, цвета, геометрические фигуры и пр. В агентно-ориентированной программе основными типами данных являются предположения, правила, утверждения, литералы и строки. Для построения типов данных, свойственных агентно-ориентированному программированию, мы будем опираться на объектно-ориентированную поддержку, прелусмотренную в С++. Итак, рассмотрим объявление класса предположения (листинг 12.1). // Листинг 12.1. Объявление класса предположения template //... protected: list bool TruthValue; public-virtual bool operator(void) = О; bool operator&&(proposition &X); bool operator||(proposition &X); bool operator||(bool X); bool operator&&(bool X); operator void*; bool operator!(void); bool possible(proposition &X); bool necessary(proposition &X); void universe(list //.. . }; Класс proposition (см. листинг 12.1) представляет собой упрощенную версию (с сокращённым набором функциональных возможностей). Назначение этого класса— сделать использование типа данных proposition таким же простым и естественным, как использование любого другого С++-типа данных. Обратите внимание на слелующее объявление в классе proposition: virtual bool operator(void) = 0; Таблица 12.4. Преобразование операторов влогические && ^ || v ! ~ possible necessary Это объявление чисто виртуального метода. Если в классе объявлен чисто виртуальный метод, это означает, что данный класс — абстрактный, и из него нельзя создавать объекты, поскольку в нем отсутствует определение этого метода. Метод лишь объявлен, но не определен. Абстрактные классы используются для определения стратегий и являются своего рода проектами производных классов. Производный класс должен определить все виртуальные функции, которые он наслелует от абстрактного класса. В данном случае класс proposition используется для определения минимального набора возможностей, которыми может обладать класс-потомок. Необходимо также отметить еще одну важную особенность класса proposition (см. листинг 12.1): это шаблонный класс. Он содержит такой член данных: list Этот член данных предполагается использовать для хранения значения предметной области, к которой относится предположение. В логике область рассуждения содержит все легальные сущности, которые могут рассматриваться при обсуждении. Здесь мы используем контейнер list. Поскольку в общем случае темы обсуждения могут быть самыми разными, мы используем контейнерный класс. Список UniverseOfDiscourse мы объявляем защищенным (protected), а не закрытым (private), чтобы к нему могли получить доступ все потомки класса proposition. Классу proposition также «знакомы» такие понятия модальной логики, как логическая необходимость и вероятность, которые весьма полезны в агентно-ориентированном программировании. Модальнал логика позволяет агенгу различать такие определения, как «вероятно, ИСТИНА» и «несомненно, ИСТИНА». Основные операторы, используемые для выражения логической необходимости и вероятности, перечислены в табл. 12.4. Мы определяем эти методы только в описательных целях; их реализация выходит за рамки рассмотрения в этой книге. Но они являются частью классов предположений, которые мы успешно применяем на практике. Чтобы сделать класс proposition «годным к употреблению», выведем из него новый класс и назовем его trip_announcement. Класс trip_announcement представляет собой утверждение о существовании автобусного маршрута из некоторого исходного пункта (отправления) в пункт назначения. Например, предположим, что существует автобусный маршрут из Детройта в Толедо. Эта информация позволяет сформулировать высказывание, которое может быть либо истинным, либо ложным. Если бы нас интересовало, когда это высказывание истинно или ложно, мы бы воспользовались понятиями временной логики. Временняя логика— это логика времени. Агенты также применяют обоснования, зависящие от времени. Но в данном случае все предположения относятся к текущему времени. Это утверждение декларирует, что в данное вре // Листинг 12.2. Объявление класса trip_announcement class trip_announcement : publiс proposition //.. . protected: string Origin; string Destination; stack bool operator(void); bool operator==(const trip_announcement &X) const; void origin(string X); string origin(void); void destination(string X); string destination(void); bool directTrip(void); bool validTrip(list string TempOrigin); stack friend bool operator||(bool X,trip_announcement &Y); friend bool operator&&(bool X,trip_announcement &Y); //. . . }; Обратите вни class trip_announcement : public proposition {... } ; обеспечивает класс proposition требуе Эти члены данных используются для указания пунктов отправления и назначения автобусного маршрута. Если автобусный маршрут требует пересадки с одного автобуса надругой и несколько остановок в пути, то член данных Candidates будет содержать полный путь следования. Следовательно, объект класса trip_armouncement представляет собой утверждение об автобусном маршруте и пути следования. В классе trip_armouncement также определены некоторые дополнительные операторы. Эти операторы позволяют уравнять класс trip_announcement «в правах» со встроенны // Листинг 12.3. Объявление класса performance_statement class performance__statement : public proposition //. . . int Bays; float Sales; float PerHour; public: bool operator (void); bool operator==(const performance__statement &X) const; void bays(int X); int bays(void); float sales(void); void sales(float X); float perHour(void); void perHour(float X); friend bool operator||(bool X,performance_statement &Y); friend bool operator&&(bool X,performance_statement &Y); //. . . }; Обратите вни class performance_statement : public proposition Благодаря это Bays Sales PerHour Такие высказывания, как «По секции 1 объе
Класс агента
Классы, представленные на рис. 12.2, образуют фундаментдля когнитивных структур данных агента, которые делают агента рациональным. Именно рационализм класса агента отличает его от других типов объектно-ориентированных классов. Рассмотрим объявление класса агента, приведенное в листинге 12.4. // Листинг 12.4. Объявление класса agent class agent{ //.. . private: performance_statement Manager1; performance_statement Manager2; performance_statement Manager3; trip_announcement Trip1; trip_announcement Trip2; trip_announcement Trip3; list list public: agent(void); bool determineVacationAppropriate(void); bool scheduleVacation(void); void updateBeliefs(void); void setGoals(void); void displayTravelPlan(void); //.. . } . Как и классы предположений, класс агента представляет собой упрощенную версию. Полный листинг объявления класса, который можно было бы использовать на практике, занял бы три или четыре страницы. Но для описательных целей, которые мы преслелуем в этой книге, приведенного вполне достаточно. Итак, класс agent содержит два контейнера-списка. li s t list Контейнеры типа list — это стандартные С++-списки. Каждый список используется для хранения коллекции текущих убеждений агента. «Мировоззрение» нашего простого агента ограничено знаниями об автобусных маршрутах и характеристиках успешности его владельца. Содержимое этих двух контейнеров представляет полные знания агента и набор его убеждений. Если в этих списках есть утверждения, в которые агент больше не верит, их следует удалить. Если в процессе рассуждений агент обнаруживает новые утверждения, они добавляются в список уже существующих убеждений. Агент имеет постоянный доступ к информации об автобусных маршрутах и эффективности ведения бизнеса его владельца и при необходимости может обновлять свои убеждения. Помимо убеждений, агент имеет цели, которые иногда представляются как желания в модели убеждений, желаний и намерений (Beliefs, Desires, Intentions — BDI). Цели поддерживают основные директивы, выдаваемые агенту клиентом. В нашем случае цели сохраняются в высказываниях, приведенных ниже. performance_statement Manager1; performance_statement Manager2; performance_statement Manager3; trip_announcement Trip1; trip_announcement Trip2; trip_announcement Trip3; С Намерения или планы должны быть обработаны аналогичным образом. Если агент может выполнить директивы, он распланирует поездку и по электронной почте подробно сообщит об этом своему владельцу. Агент приступает к своим обязанностям в момент создания объекта. Фрагмент конструктора агента представлен в листинге 12.5 // Листинг 12.5. Конструктор класса agent agent: :agent(void) { setGoals; updateBeliefs ; if(determineVacationAppropriate){ displayTravelPlan; scheduleVacation; cout « «Сообщение о возможности отпуска.» « endl; } else { cout « «В данное время отпуск нецелесообразен.» « endl; }
Цикл активизации агента
Многие определения агентов включают требования непрерывности и автономности. Идея состоит в том, что агент должен непрерывно выполнять поставленные перед ним задачи без вмешательства оператора. Агент обладает способностью взаимодействовать со своей средой и (до некоторой степени) контролировать ее благодаря наличию цепи обратной связи. Непрерывность и автономность часто реализуются в виде событийного цикла, при выполнении которого агент постоянно получает сообщения и информацию о событиях. Эти сообщения и события агент использует для обновления своей внутренней модели мира, намерений и предпринимаемых действий. Однако непрерывность и автономность — понятия относительные. Одни агенты должны активизироваться каждую микросекунду, в то время как другие — лишь один раз в год. А в случае программного обеспечения полетов в дальний космос агент может иметь цикл даже больше одного года. Поэтому мы не будем акцентировать внимание на физических событийных циклах и постоянно активных очередях сообщений. Такая организация может подходить для одних агентов, но оказаться непригодной для других. Мы пришли к выводу, что лучше всего здесь применить понятие логического цикла. Логический цикл может (или не может) быть реализован как событийный. Логический цикл может длиться от одной наносекунды до некоторого количества лет. Общий вид простого логического цикла активизации агента показан на рис. 12.3. Область рассуждения (см. рис. 12.3) представляет все, с чем наш агент может легитимно взаимодействовать. Эта область может состоять из файлов, информации от портов или устройств сбора данных. Получаемая информация должна быть представлена в виде предположений или утверждений (высказываний). Обратите внимание на существование цепи обратной связи от выходных данных агента к входным. Наш агент (см. листинг 12.4) активизируется только несколько раз в год. Следовательно, нет смысла помещать его в постоянно выполняющийся событийный цикл. Наш агент должен периодически активизироваться в течение года для выполнения своих задач. В листинге 12.5 представлен конструктор агента. При активизации агент устанавливает цели, обновляет убеждения, а затем определяет уместность отпуска. Если отпуск возможен, агент предпринимает некоторые действия и по электронной почте уведомляет об этом владельца фирмы. Если же отпуск в данное время нецелесообразен, владелец получает от агента сообщение другого содержания. 12.4.2.2. Стратегии логического вывода агента Этот агент обладает способностями рассуждать, реализованными частично классом proposition и частично //Листинг 12.6. Фрагменты определений класса // proposition и его потомков template proposition &X) { return((*this) &&X); template proposition &X) { return((*this) || X); template return((void*)(TruthValue)); bool trip__announcement::operator(void) { list return(true); } I = UniverseOfDiscourse.begin; if(validTrip(I,Origin)){ return(true); } return(false); } Операторы "||" и "&&", используемые в классах предположений, позволяют определить, истинно данное предположение или ложно. В каждом из этих определений операторов в конечном счете вызывается метод operator , определенный в классе-потомке. Обратите внимание на определение оператора "||" (см. листинг 12.6). Этот оператор определен следующим образом. template (proposition &X) { return((*this) || X); } Это определение позволяет использовать следующий код. trip_announcement А; performance_statement В; if (А || В) { // Какие-нибудь действия. } При вычис bool trip_announcement::operator(void) { list if(directTrip){ return(true); } I = UniverseOfDiscourse.begin; if(validTrip(I, Origin)){ return(true); } return(false); } При выполнении этого кода станет ясно, существует ли маршрут из заданного исходного пункта в некоторый пункт назнаначения. Например, предположим, что нас интересует переезд из Детройта в Колумбус, при этом область рассуждений содержит следующие данные: Детройт - Толедо Толедо - Колу Тогда объект класса trip_announcement «доложит» о то Детройт - Колу Объект класса trip_announcement действительно проверит, существует ли прямой маршрут из Детройта в Колумбус. Если он существует, объект возвратит значение ИСТИНА. В противном случае он попытается найти обходной путь. Подобное поведение реализуется так. if(directTrip){ return(true); } I = UniverseOfDiscourse.begin; if(validTrip(I,Origin)){ return(true); } «Самоопределением» истинности объект обязан оператору operator класса trip_anouncement. Метод directTrip довольно прост, и его работа заключается в последовательном просмотре области рассуждений на предмет существования следующего утверждения: Детройт - Колу Метод validTrip , чтобы узнать, существует ли обходной путь, использует технологию поиска вглубь (Depth First Search— DFS). Определения методов validTrip и directTrip приведены в листинге 12.7. // Листинг 12.7. Определения методов validTrip и // directTrip bool trip_announcement::validTrip(list { if(I == UniverseOfDiscourse.end){ if(Candidates.empty){ TruthValue = false; return(false); } else{ trip_announcement Temp; Temp = Candidates.top; I = find(UniverseOfDiscourse.begin, UniverseOfDiscourse.end,Temp); UniverseOfDiscourse.erase(I); Candidates.pop; I = UniverseOfDiscourse.begin; if(I != UniverseOfDiscourse.end){ TempOrigin = Origin; } else { TruthValue = false; return(false); } } > if((*I).origin == TempOrigin && (*I).destination == Destination){ Candidates.push(*I); TruthValue = true; return(true); } if((*I).origin == TempOrigin){ TempOrigin = (*I).destination; Candidates.push(*I); } I++; return(validTrip(I,TempOrigin)); bool trip_announcement: :directTrip(void) { list UniverseOfDiscourse.end, *this); if(I == UniverseOfDiscourse.end){ TruthValue = false; return(false); } TruthValue = true; return(true); В обоих методах validTrip и directTrip используется алгоритм find из стандартной библиотеки С++. UniverseOfDiscourse — это контейнер, который содержит убеждения агента и подготовленные для него утверждения. Вспомните, что одним из первых действий, предпринимаемых агентом, является вызов метода updateBeliefs, который заполняет контейнер UniverseOfDiscourse. Определение метода updateBeliefs приведено в листинге 12.8. // Листинг 12.8. Обновление убеждений void agent::updateBeliefs(void) { performance_statement TempP; TempP.sales(203.0); TempP.perHour(10 0.0); TempP.bays(4); PerformanceBeliefs.push_back(TempP); trip_announcement Temp; Temp.origin(«Detroit»); Temp.destination(«LA»); TripBeliefs.push_back(Temp); Temp.origin(«LA»); Temp.destination(«NJ»); TripBeliefs.push_back(Temp); Temp.origin(«NJ»); Temp.destination(«Windsor»); TripBeliefs.push_back(Temp); } На практике убеждения обычно поступают из среды выполнения агента ( т.е. из файлов, от датчиков, портов, устройств сбора данных и пр.). В листинге 12.8 инфор // Листинг 12.9. Метод установки целей агента void agent::setGoals(void) { Managerl.perHour(15.0); Managerl.bays(8); Managerl.sales(123.2 3); Manager2.perHour(2 5.3 4); Manager2.bays(4); Manager2.sales(12.33); Manager3.perHour(3 4.3 4); Manager3.sales(100000.12); Manager3.bays(10); Trip1.origin(«Detroit»); Tripl.destination(«Chicago»); Trip2.origin(«Detroit»); Trip2.destination(«NY»); Trip3.origin(«Detroit»); Trip3.destination(«Windsor»); } Эти директивы сообщают агенту о том, что его владелец хотел бы отправиться в отпуск из Детройта в Чикаго, из Детройта в Нью-Йорк или из Детройта в Виндзор. Помимо маршрутов, также устанавливаются финансовые цели. Чтобы отпуск состоялся, необходимо достижение одной или нескольких таких целей. После установки целей агент обновляет свои убеждения, и его следующая задача будет определена в зависимости от целей и убеждений при условии возможности планирования отпуска. И тогда вызывается второй компонент методов рассуждений агента: determineVacationAppropriate Этот метод передает контейнер UniverseOfDiscourse каждому из объектов предположен Это выражение можно озвучить так: если хотя бы одно из утверждений каждой группы истинно, то элемент W примет значение ИСТИНА. Для наше // Листинг 12.10. Второй метод рассуждений bool agent::determineVacationAppropriate(void) { bool TruthValue; Managerl.universe(PerformanceBeliefs); Manager2.universe(PerformanceBeliefs); Manager3.universe(PerformanceBeliefs); Tripl.universe(TripBeliefs); Trip2.universe(TripBeliefs); Trip3.universe(TripBeliefs); TruthValue = ((Managerl || Manager2 || Manager3) && (Tripl || Trip2 || Trip3)); return(TruthValue); } Обратите внимание на то, что списки TripBeliefs и PerformanceBeliefs являются аргументами метода universe объектов Trip и Manager. Именно здесь объекты предположений получают информацию из предметной области (UniverseOfDiscourse). Прежде чем объект класса proposition вызовет оператор operator, его контейнер UniverseOfDiscourse должен заполниться имеющимися у агента данными. В листинге 12.10 при вычислении выражения ((Managerl || Manager2 || Manager3) && (Tripl || Trip2 || Trip3)); оценивается шесть предположений (посредством выполнения оператора "||"). Оператор " | |" для каждого предположения выполняет оператор operator , который для определения истинности предположения использует список UniverseOfDiscourse. Слелует иметь в виду, что классы trip_announcement Hperformance_statement наследуют довольно много функций класса proposition. В листингах 12.6 и 12.7 было показано, как определяется оператор operator для класса trip_announcement, а в листинге12.11 приведено определение оператора operator для класса performance_statement. // Листинг 12.11. Класс performance_statement bool performance_statement::operator(void) { bool Satisfactory = false; list I = UniverseOfDiscourse.begin; while(I != UniverseOfDiscourse.end && !Satisfactory) { if(((*I).bays >= Bays) || ((*I).sales >= Sales) || ((*I).perHour >=PerHour)){ Satisfactory = true; } I++; } return(Satisfactory); } Оператор operator для каждого класса proposition играет «свою» роль в способности класса агента делать логические выводы. В листинге 12.6 показано, как вызывается оператор operator при каждом вычислении оператора " || " или "&&" для класса proposition или для одного из его потомков. Именно такое сочетание методов operator , определенных в proposition-классах, и методов класса agent образует стратегии логического вывода для класса agent. В дополнение к операторам "||" и "&&", определенным в классе proposition, классы trip_announcement и performance_statement содержат свои определения. friend bool operator||(bool X,trip_announcement &Y); friend bool operator&&(bool X,trip_announcement &Y); Эти friend -объявления позволяют использовать предположения в более длинных выражениях. Сделаем следующие объявления. //. . . trip_announcement А, В, С; bool X; X = А || В || С; //.. . При этом объекты А и В будут объединены с помощью операции ИЛИ, а результат этой операции будет иметь тип bool. Затем мы попробуем с помощью той же операции ИЛИ получить значение типа bool и объект типа trip_announcement: bool || trip_announcement Без приведенных выше friend -объявлений такая операция была бы недопустимой. Определение этих функций-«друзей» показано влистинге 12.12. // Листинг 12.12. Перегрузка операторов "||" и "&&" bool operator||(bool X,trip_announcement &Y) { return(X | | YO) ; } bool operator&&(bool X,trip_announcement &Y) { return(X && Y); } Обратите внимание на то, что в определении этих функций-«друзей» (благодаря ссылке на элемент Y ) также используется вызов функции operator . Эти функции определяются и в классе performance_statement. Наша задача — сделать использование proposition-классов таким же простым, как использование встроенных типов данных. В классе proposition также определен другой оператор, который позволяет использовать предположение естественным образом. Рассмотрим следующий код. //.. . trip_announcement А; if(A) { //... Некоторые действия. } //.. . Как в этом случае компилятор тестирует объект А? При выполнении инструкции if компилятор стремится найти в скобках значение целочисленного типа данных или типа bool. Но тип объекта А совсем другой. Мы хотим, чтобы ко template return((void*)(TruthValue)); } Это определение позволяет предположение любого типа, представленное «в единственном числе», протестировать как значение истинности. Например, когда наш класс agent собирается отправить по электронной почте владельцу фирмы сообщение, содержащее путь следования, агенту нужно определить, какой маршрут отвечает заданным требованиям. В листинге 12.13 представлен еще один фрагмент из методов обработки автобусных маршрутов. // Листинг 12.13. Метод displayTravelPlan void agent::displayTravelPlan(void) { stack if(Tripl){ Route = Tripl.candidates; } if(Trip2){ Route = Trip2.candidates; } if(Trip3){ Route = Trip3.candidates; } while(!Route.empty) { cout << Route.top.origin << " TO "« Route.top.destination « endl; Route.pop; } Обратите внимание на то, что объекты Tripl, Trip2 и Trip3 тестируются так, как будто они имеют тип bool. Метод candidates просто возвращает путь следования, соответствующий заданному маршруту. Таким образом, разработка стратегий логического вывода и когнитивных структур данных становится проще благодаря использованию перегрузки операторов и С++-шаблонов. Именно стратегии логического вывода и когнитивные структуры данных делают объект рациональным. C++-программист для разработки агентов использует конструкцию класса, а для реализации когнитивных сгруктур данных (CDS) — контейнерные объекты в сочетании со встроенными алгоритмами. Класс, который содержит CDS-структуры, становится рациональным, а рациональный класс — агентом.
Простая автономность
Поскольку наш простой класс агента не требует выполнения традиционного «цикла активизации», нам нужны другие средства, которые бы периодически активизировали агент без вмешательства человека. Возможны ситуации, когда агент нужно запускать на выполнение лишь иногда или только при определенных условиях. Среды UNIX/Linux оснащены утилитой crontab, которая представляет собой пользовательский интерфейс «хрон-систе Каждый эле минуты 0-59 часы 0-23 день 1-31 месяц 1-12 день недели 1-7 (1 — понедельник, 7 — воскресенье) команда может быть любой UNIX/Linux-командой, а также именем файла, который содержит агенты Созданный в таком формате текстовый файл передается «хрон-системе» с помощью слелующей команды: $crontab Например, предположи 15 8 * * * agentl 0 21 * * 6 agent2 * * 1 12 * agent3 После выпол агент agentl будет активизироваться каждый день в 8:15, агент agent2 — каждое воскресенье в 21:00, а агент agent3— каждый раз при наступлении первого декабря. Хрон-файлы можно при необходимости добавлять или удалять. Хрон-файлы могут содержать ссылки на другие хрон-задания, позволяя таким образом агенту «самому» перепланировать свою работу. Так, для обеспечения чрезвычайно гибкой, динамичной и надежной процедуры активизации агентов можно использовать сценарии оболочки в сочетании с утилитой crontab. Чтобы получить полное описание утилиты crontab, обратитесь к оперативным страницам руководства Средства crontab и at представляют собой простейший способ автоматизации или регулярного запуска агентов, который не требует постоянного выполнения циклов активизации. Эти утилиты надежны и гибки. Однако для реализации автоматической активизации агента также можно использовать хранилище, или репозиторий, реализаций и брокер объектных запросов (object request brokers — ORB), который мы рассматривали в главе 8. Стандартные CORBA-реализации также предоставляют средства организации событийных циклов. 12.5. Мультиагентные системы Мультиагентные системы— это системы, в которых задействовано несколько агентов, обладающих способностью в процессе решения некоторой задачи взаимодействовать, сотрудничать, «договариваться» или соперничать. У С++-разработчика программного обеспечения есть несколько вариантов для реализации мультиагентных систем. Агенты можно реализовать в отдельных потоках выполнения с помощью API-интерфейса POSIX thread. В этом случае одна программа разбивается на несколько потоков, каждый из которых содержит один или несколько агентов. Следовательно, агенты одного потока будут разделять одно и то же адресное пространство. Это позволяет агентам легко взаимодействовать путем использования глобальных переменных и простой передачи параметров. Если компьютер, на котором выполняется программа, содержит несколько процессоров, то агенты могут выполняться параллельно. В этом случае каждый агент должен быть оснащен объектами синхронизации (см. главы 5 и 11) и компонентами обработки исключительных ситуаций (см. главу7). Мультиагентные системы, реализованные посредством многопоточности, представляют самое простое решение, но тем не менее ограничивающее агентов рамками одного компьютера. Более гибкий подход к созданию мультиагентных систем предоставляет CORBA-реализация. Стандарт CORBA (помимо ядра спецификации CORBA) содержит спецификацию мультиагентного средства (multi-agent facility— MAF). MICO-реализацию, которую мы используем в CORBA-примерах этой книги, можно применять для реализации агентов, которые способны взаимодействовать через сети Internet, intranet и локальные сети. С++-привязка CORBA-стандарта имеет полную поддержку объектно-ориентированного представления и, следовательно, поддержку агентно-ориентированного программирования. В главе 13 мы рассмотрим, как можно использовать библиотеки PVM и MPI для поддержки агентов в контексте параллельного и распределенного программирования.
12.6. Резюме
Реализация технологии «классной доски» с использованием PVM-средств, потоков и компонентов
Одна из основных целей в параллельном программировании — разбить всю работу, предусмотренную для выполнения программой, на множество задач, которые могут при необходимости выполняться с определенной степенью параллелизма. Эта цель труднодостижима. Довольно сложно так провести декомпозицию работ (Work Breakdown Structure— WBS), чтобы создать соответствующий фундамент для параллелизма и обеспечить корректные и эффективные результаты работы. Для достижения этой цели мы используем методы моделирования и специальные архитектурные решения. На практике на этапе моделирования самой задачи и ее решения стараются выявить естественный параллелизм. Не следует в решение вносить параллелизм искусственно. Если задача и ее решение смоделированы надлежащим образом, то необходимый параллелизм обнаружится сам собой. Архитектура «классной доски» облегчает такой процесс моделирования. В частности, модель «классной доски» позволяет организовать и концептуализировать параллельность и взаимодействие компонентов в системе, которая требует применения параллельного или распределенного программирования.
Модель «классной доски»
Модель «классной доски» — это технология совместного решения задач. «Классная доска» используется для регистрации и координации действий, а также организации взаимодействия между двумя или больше программными решателями задач. Таким образом, в модели «классной доски» существует два основных типа компонентов: «классная доска» и решатели задач. Решатель задач — это про Модель «классной доски» не определяет никакой конкретной структуры ни для самой «классной доски», ни для источников знаний. Как правило, структура «классной доски» зависит от конкретной задачи.1 [22] Реализация источников знаний также зависит от специфики решаемой задачи. «Классная доска» — это концептуальная модель, описывающая отношения без представления структуры самой «классной доски» и источников знаний. Модель «классной доски» не диктует количество используемых источников знаний или их назначение. «Классная доска» может быть единственным глобальным или распределенным объектом, компоненты которого расположены на нескольких компьютерах. Системы «классной доски» могут состоять из нескольких «классных досок», и каждая из них «занимается» решением определенной части исходной задачи. Это делает модель «классной доски» чрезвычайно гибкой. Модель «классной доски» поддерживает параллельное и распределенное программирование. Во-первых, источники знаний, работая над решением части общей задачи, могут выполняться одновременно. Во-вторых, источники знаний могут быть реализованы в различных потоках или отдельных процессах одного или нескольких компьютеров. «Классная доска» может быть разделена на несколько отдельных частей, позволяющих параллельный доступ со стороны нескольких источников знаний. «Классная доска» легко поддерживает такие архитектурные варианты, как CREW (concurrent read, exclusive write — параллельное чтение и монопольнал запись), EREW (exclusive read, exclusive write — монопольное чтение и монопольная запись) и MIMD (multiple-instruction, multiple-data — множество потоков данных и множество потоков команд). Мы реализуем «классную доску» как глобальный объект или коллекцию объектов, а источники знаний — как отдельные потоки. Поскольку потоки разделяют одно и то же адресное пространство, к «классной доске», реализованной как глобальный объект или семейство объектов, будут получать доступ все потоковые источники знаний. Если источники знаний реализовать как отдельные процессы, выполняющиеся на одном или нескольких компьютерах, то «классную доску» имеет смысл реализовать как CORBA-объект или как коллекцию CORBA-объектов. Вспомните, что CORBA-объекты можно использовать для поддержки как параллельной, так и распределенной модели вычислений. Здесь мы используем технологию CORBA для поддержки «классной доски» как разновидность распределенной памяти, совместно используемой задачами, выполняющимися в различных адресных пространствах. Эти задачи могут быть PVM-типа (Рагаllеl Virtual Machine — параллельная виртуальная машина), задачами, порождаемыми традиционными fork-exec-вызовами функций, или задачами, порождаемыми библиотечными функциями posix_spawn . Две конфигурации памяти для реализации технологии «классной доски» показаны на рис. 13.1. В обоих случаях (см. рис. 13.1) все источники знаний имеют доступ к «классной доске». Источники знаний, размещенные в различных адресных пространствах, должны иметь сетевую связь с «классной доской», реализованной как один или несколько CORBA-объектов. Если источники зна
Методы структурирования «классной доски»
Методов структурирования «классной доски» не существует. Однако большинство реализаций этой технологии имеют определенные характеристики и атрибуты. Исходное содержимое «классной доски» обычно включает часть пространства решения задачи. Пространство решений должно содержать все частные и полное решения задачи. Например, предположим, что у нас есть механизм поиска изображений автомобилей в Internet. Этот механизм поиска может обрабатывать растровое или векторное изображение, чтобы определить, содержит ли оно изображение автомобиля, и если содержит, то отвечает ли оно параметрам поиска. Допустим, этот механизм поиска разработан с использованием модели «классной доски». Каждый источник знаний имеет свою специфику: один — специалист в области идентификации изображений покрышек, другой — идентифицирует зеркала задней обзорности, третий — эксперт по дверным ручкам для автомобилей и т.д. Каждая деталь автомобиля представляет малую часть пространства решений. Одни части пространства решений содержат полное изображение автомобиля с различных точек зрения (т.е. сверху, снизу, под углом 45° и т.д.), а другие — только отдельные детали автомобилей, например, фронтальную и заднюю части, крышу или багажник. На «классной доске» размещается растровое или векторное изображение, и отдельные источники знаний пытаются идентифицировать детали изображения, которые могут быть частями автомобиля. Если некоторая часть пространства решений совпадает с какой-нибудь частью изображения, эта часть изображения будет записана в другую часть «классной доски» как частное решение. Один источник знаний может поместить на «классную доску» дверную ручку идентифицируемого автомобиля, другой — дверцу. Если эти две части информации оказались на «классной доске», то какой-нибудь третий источник знаний может использовать эту информацию как вспомогательную при идентификации передней части автомобиля в исследуемом изображении. После того как будет идентифицирована передняя часть, она также размещается на «классной доске». Каждый из этих различных способов идентификации изображения автомобиля представляет часть пространства решений. Пространство решений иногда организуется иерархически. В нашем примере с автомобилем на вершине иерархии могут находиться полные изображения автомобиля, следующий уровень может состоять из различных видов передних и задних частей, еще один уровень может содержать двери, багажники, капоты, ветровые стекла и колеса. Каждый уровень описывает в этом случае меньшее, возможно, менее характерное изображение некоторой части автомобиля. Источники знаний могут работать одновременно на нескольких уровнях иерархии. Пространство решений также можно организовать в виде графа, в котором каждый узел представляет некоторую часть решения, а каждое ребро — отношения между двумя частными решениями. Пространство решений может быть представлено в виде одной или нескольких матриц, а каждый элемент матрицы будет содержать в этом случае полное или частное решение. Представление пространства решений— это важный компонент архитектуры «классной доски». Именно характер задачи часто определяет, как должно быть распределено пространство решений. Помимо компонента пространства решений, «классная доска» обычно имеет один или несколько компонентов (эвристических) правил. Компонент правил используется для определения того, какие источники знаний стоит использовать и какие решения принимать или отвергать. Компонент правил можно также применить для перевода частных решений с одного уровня иерархии пространства решений на другой. Компонент правил позволяет назначать приоритеты источникам знаний. Некоторые источники знаний могут «зайти в тупик». «Классная доска» может «снять отметку» с одной группы источников знаний в пользу другой, а также использовать компонент правил, чтобы предложить источникам знаний более потенциально подходящие гипотезы на основе уже сгенерированных частных гипотез. Помимо пространства решений и компонента правил, «классная доска» часто содержит начальные значения, значения ограничений и вспомогательные цели. В некоторых случаях «классная доска» может содержать одну или несколько очередей событий, используемых для приема входных данных либо из пространства задачи, либо от источников знаний. Логическая схема базовой архитектуры «классной доски» показана на рис. 13.2. «Классная доска» (см. рис. 13.2) имеет ряд сегментов, а каждый сегмент — различные реализации. Это говорит о том, что «классная доска» — это нечто большее, чем просто область глобальной памяти или традиционные базы данных. Хотя на рис. 13.2 показаны только основные компоненты, которые имеют многие «классные доски», этот вид архитектуры не ограничивается таким составом. К числу дополнительных компонентов потенциально можно отнести модели контекстов задачи и модели предметной области, которые могут оказаться полезными для решателей задач при навигации по пространству решений. С++-поддержка объектно-ориентированного проектирования и программирования прекрасно сочетается с требованиями гибкости, которые обычно предъявляются к модели «классной доски». Большинство архитектур «классной доски» может быть смоделировано с использованием С++-классов. Вспомните, что классы можно использовать для моделирования человека, местности, предмета или идеи, а»классные доски» используются для решения задач, в которых часто участвуют люди, местности, предметы или идеи. Поэтому весьма уместно применять С++-классы для моделирования объектов, которые содержит «классная доска». В своих реализациях модели «классной доски» мы используем преимущества контейнерных С++-классов и стандартных алгоритмов. Помимо встроенных классов, мы создаем интерфейсные классы для мьютексов и других переменных синхронизации, используемых в реализации «классной доски». Поскольку к «классной доске» могут получить доступ сразу несколько источников знаний одновре
Анатомия источника знаний
Источники знаний представляются как объекты, процедуры, множества правил, логические утверждения, а в некоторых случалх и целые программы. Источники знаний включают часть условий и часть действий. Если «классная доска» содержит информацию, которая удовлетворяет части условий некоторого источника знаний, то его часть действий активизируется. Инглемор (Englemore) и Морган (Morgan) в своей работе [14] четко описывают обязанности источника знаний. Каждый источник знаний отвечает за знание условий, при которых он может внести свой вклад в решение. Каждый источник знаний имеет предусловия, т.е. условия, которые должны быть записаны на «классной доске» и существовать до того, как будет активизировано тело источника знаний. Источник знаний можно рассматривать как большое правило. Главное, чем отличается правило от источника знаний, состоит в степени детализации знаний. Часть условий этого большого правила называется предусловием источника знаний, а часть действий — его телом. Здесь Инглемор и Морган не определяют ни единой детали части условий или части действий источника знаний. Они представляют собой логические конструкции. Часть условий может иметь форму простого значения булевого флага на «классной доске» или сложной последовательности событий, поступающих в очередь событий в пределах определенного периода времени. Аналогично часть действий источника знаний может быть выражена простой инструкцией, выполняющей операцию присваивания переменной некоторого выражения, или механизмом прямого построения цепочки в экспертной системе. Это описание широты диапазона еще раз подчеркивает гибкость модели «классной доски». Для наших целей вполне достаточно конструкции С++-класса и понятия объекта. Каждый источник знаний должен быть объектом. Часть действий источника знаний должна быть реализована в виде методов объекта, а часть условий — в виде его членов данных. Если объект находится в определенном состоянии, то его часть действий должна быть активизирована. Проще говоря, мы реализуем источники знаний в виде потоков или процессов. Следовательно, для каждого потока и для каждого процесса должен существовать только один источник знаний. Применяя к «классной доске» PVM-механизм, источник знаний будет эквивалентом PVM-задачи. Логическая схема источника знаний показана на рис. 13.3. Часть «Условия» каждого источника знаний обновляется «из закромов» «классной доски», а часть «Действия» источников знаний обновляет ее содержимое. Обратите внимание на то (см. рис. 13.3), что между пространством процесса и источником знаний (или между пространством потока и источником знаний) существует взаимно однозначное отношение. Важным атрибутом источника знаний является его автономность. Каждый источник знаний является специалистом в своей области и почти не зависит от других решателей задач. Это составляет одно из требуемых качеств для параллельной программы. В идеале задачи в параллельной программе могут выполняться одновременно, почти не нуждаясь во взаимодействии с другими задачами. Такое поведение в точности описывает схему модели «классной доски». Источники знаний действуют независимо, и любое взаимодействие осуществляется посредством «классной доски». Поэтому источник знаний (с его точки зрения) действует в одиночку, получал дополнительную информацию от «классной доски» и записывал на «классную доску» свои изыскания. О деятельности других источников знаний и их стратегиях поведения ему ничего не известно. В модели «классной доски» задача делится на ряд автономных или полуавтономных решателей задач. В этом и состоит преимущество модели «классной доски» перед другими моделями. В самой гибкой конфигурации источники знаний должны быть интеллектуальными агентами. Агент должен быть совершенно самодостаточным и способным действовать самостоятельно при минимальной потребности к взаимодействию с «классной доской». Именно интеллектуальный агент представляет самую грандиозную перспективу для реализации крупномасштабного параллелизма.
Стратегии управления для «классной доски»
В реализации модели «классной доски» прелусмотрено несколько уровней управления, обеспечивающих возможность параллельного функционирования источников знаний. На самом нижнем уровне их схемы синхронизации должны защищать целостность «классной доски». «Классная доска» является критическим разделом, поскольку она представляет собой совместно используемый модифицируемый ресурс. В параллельной среде доступ со стороны источников знаний для чтения и записи должен быть скоординирован и синхронизирован. Координация и синхронизация может включать блокировку файлов, семафоры, мьютексы и т.д. Этот уровень управления не включается непосредственно в решение, над которым работают источники знаний. Его можно назвать вспомогательным уровнем управления, и он не должен зависеть от специфики задачи, решаемой с помощью «классной доски». В нашем архитектурном подходе этот уровень управления реализуется интерфейсными классами (например, классами мьютекса и семафора, использованными в главе 11). Вспомните, что действия, инкапсулированные в этих классах, не зависят от приложения, в котором они используются. Для параллельных реализаций «классной доски» на этом уровне выбирается один (или больше) из четырех типов параллельного доступа, которыми должны обладать алгоритмы или эвристические правила источников знаний для физической реализации «классной доски». Другими словами, пользователи «классной доски» могут использовать EREW-, CREW-, ERCW- или CRCW-доступ. Именно характер доступа определяет, как будут использованы примитивы синхронизации. Описание упомянутых здесь типов доступа приведено в табл. 13.1. Таблица 13.1. Четыре типа параллельного доступа, используемых в модели «классной доски» EREW Exclusive Read Exclusive Write (монопольное чтение и монопольная запись) CREW Concurrent Read Exclusive Write (параллельное чтение и монопольная запись) ERCW Exclusive Read Concurrent Write (монопольное чтение и параллельная запись) CRCW Concurrent Read Concurrent Write (параллельное чтение и параллельная запись) При разделении «классной доски» на части будет определено, какие из типов параллельности (см. табл. 13.1) подходят больше всего. Самый гибкий тип (CRCW) доступа может быть достигнут в зависимости от структуры «классной доски». Например, если используется 16 источников знаний, и каждый из них получает доступ к собственному сегменту «классной доски», то такие источники знаний могут параллельно считывать данные с «классной доски» и записывать их туда, не испытывал проблем «гонки» данных. Следующий уровень управления включает выбор источников знаний. При этом определяется, какие из них следует включить в поиск решения и какие аспекты задачи им поручить. На этом уровне управления принимается решение перенести центр (фокус) внимания на ту или иную область задачи, что и определяет выбор соответствующих источников знаний. При решении задач любого типа всегда ставятся сле-лующие вопросы: «с чего начать?» и «что нужно для этого знать?». Уровень центра внимания отвечает за начальные условия задачи, а также определяет, какие источники знаний необходимо использовать и в какой момент они должны «вступить в игру». «Классной доске» должно быть известно, какими источниками знаний она может располагать, и обычно источники знаний принимают сообщения или параметры, которые предписывают, как им действовать или в какой области пространства решений следует начинать поиск. Для параллельных реализаций этот уровень управления определяет базовую модель параллелизма (распределение решателей задачи). Обычно для «классной доски» используется модель MPMD (Multiple Programs Multiple Data — множество программ и множество потоков данных), известнал также как MIMD (multiple-instruction, multiple-data — множество потоков команд и множество потоков данных), поскольку каждый источник знаний (решатель задачи) имеет собственную область специализации. Однако сама природа задачи иногда может дать право на использование такой популярной модели, как SPMD (Single Program Multiple Data —одна програ На слелующем уровне управления определяется, что делать с решением или частными решениями, записанными на «классной доске». Этот уровень управления должен оценить, могут ли источники знаний остановить работу, и является ли сгенерированное решение приемлемым, неприемлемым, частично приемлемым и т.д. Именно этим уровнем управления завершается видимость «классной доски» и всех частных или предварительных решений. Именно здесь осуществляется руководство общими стратегиями решения задач коллективными усилиями. В соответствии со структурой «классной доски» и источников знаний модель «классной доски» предполагает существование компонента управления, но не определяет, как он должен быть структурирован. Иногда компонент управления является частью «классной доски», а иногда он реализуется источниками знаний. В некоторых случаях компонент управления реализуется модулями, которые являются внешними по отношению к «классной доске». Компонент управления также может быть реализован любым сочетанием предыдущих вариантов. Источники знаний совместно ищут решение задачи. Следует отметить, что некоторые задачи имеют несколько решений. Одни из них могут находиться глубже в пространстве поиска, чем другие; поиск одних решений может быть более затратным по сравнению с поиском других, а некоторые решения могут быть недостаточно хорошо продуманы. Компонент управления не только руководит коллективными стратегиями поиска, выполняемого источниками знаний, но и контролирует частные или предварительные решения, чтобы убедиться, что источники знаний не реализуют какую-нибудь непрактичную стратегию поиска. Компонент управления выявляет бесконечные циклы, тупики или рекурсивные регрессии. Более того, компонент управления включается в выбор наилучших или наиболее подходящих источников знаний для данной задачи. По мере продвижения источников знаний к искомому решению компонент управления может разгрузить одни источники знаний за счет других. Стратегия управления должна быть тесно связана с со стратегиями поиска, которыми руководствуются источники знаний. Важно помнить, что все источники знаний могут использовать различные стратегии поиска и методы решения задачи. И хотя они работают с общей «классной доской», источники знаний по своей сути автономны и самодостаточны. Следовательно, этот уровень управления имеет двустороннее взаимодействие с источниками знаний. Возможные конфигурации управления и их уровни в архитектуре «классной доски» показаны на рис. 13.4. Обратите внимание на то, что в первой из представленных конфигураций (см. рис. 13.4) механизм управления содержится в самой «классной доске», а не в отдельном модуле и не в источниках знаний. В этой конфигурации блок управления проектируется как часть класса «классной доски». Поскольку на уровнях 2 и 3 необходимо двустороннее взаимодействие, имеет смысл, чтобы «классная доска» порождала процессы или потоки, которые будут содержать источники знаний. Если «классная доска» порождает процессы или потоки, ей нетрудно получить доступ к идентификационному номеру любого потока или процесса. Это позволяет «классной доске» легко передавать сообщения источникам знаний и осуществлять управление процессами и потоками. Если «классной доске» по некоторой причине нужно прекратить деятельность конкретного источника знаний, то доступ к идентификатору потока или процесса делает эту задачу очень простой. Обратите внимание на то, что в одном из представленных на рис. 13.4 вариантов блок управления яв
Реализация модели «классной доски» с помощью CORBA-объектов
Вспомните, что CORBA-объект (см. главу 8) является независимым от платформы распределенным объектом. К CORBA -объектам могут получать доступ процессы, выполняющиеся на одном или на разных компьютерах, подключенных к сети. Это делает CORBA-объекты кандидатами для использования в PVM-средах, когда программа делится на ряд процессов, которые могут (или не могут) выполняться на одном и том же компьютере. Обычно PVM-среда используется для передачи сообщений при вторичной роли общей памяти (если она вообще существует). Введение понятия разделяемого и доступного по сети объекта существенно усиливает вычислительные мощности PVM-среды. Следует иметь в виду, что с помощью CORBA-объектов можно смоделировать все, что позволяют смоделировать не распределенные объекты. Это означает, что PVM-задачи, которые имеют совместный доступ к CORBA-объектам, могут получать доступ к контейнерным объектам, объектам оболочки, шаблонов, доменов и другим видам вспомогательных объектов. В данном случае мы хотели бы, чтобы PVM-задачи имели доступ к объектам «классной доски». Поэтому модель передачи сообщений мы дополняем совместным доступом к сложным объектам. Помимо PVM-задач, получающих доступ к распределенным CORBA-объектам, к ним также могут обращаться задачи, порожденные функциями posix_spawn или fork-exec. Эти задачи выполняются в отдельных адресных пространствах одного и того же компьютера, но могут, тем не менее, связываться с CORBA-объектами, которые расположены либо на том же, либо на другом компьютере. Поэтому, несмотря на то что все задачи, созданные с помощью функций posix_spawn или fork-exec, должны размещаться на одном компьютере, CORBA-объекты могут располагаться на любом компьютере.
Пример использования CORBA-объекта «классной доски»
Чтобы продемонстрировать наше представление о CORBA-ориентированной «классной доске», рассмотрим ее реализацию, предложенную разработчиками из компании Ctest Laboratories. И хотя полное описание этого варианта выходит за рамки нашей книги, мы все же остановимся на самых важных аспектах «классной доски» и источников знаний, имеющих отношение к нашему архитектурному подхолу к параллельному программированию. «Классная доска» реализует услуги программно-ориентированного консультанта по составлению расписания учебных курсов. «Классная доска» решает задачи планирования учебных курсов для студента типичного колледжа. Студенты часто сталкиваются с проблемой «неудобного» расписания занятий. Во время регистрации курсов всегда существует конкуренция за места в аудиториях. В какой-то момент важные для студента курсы попросту «закрываются». Ведь не зря существует печально известное правило, соответствующее дисциплине обслуживания очереди: «первым пришел — первым обслужен». Поэтому во время регистрации, когда десятки тысяч студентов пытаются записаться на ограниченное количество курсов, важным фактором выступает своевременность. Студент желает пройти курсы, которые дают право на получение диплома. В идеале эти курсы должны быть разнесены во времени. Кроме того, студент хотел бы поддерживать определенную учебную нагрузку и иметь свободное время для домашних и факультативных занятий. Проблема состоит в том, что, когда студент готов взять выбранный им курс, прием на него может уже оказаться закрытым, и вместо него ему предлагаются другие курсы, которые его интересуют в меньшей степени. Курсы-заменители увеличивают стоимость и продолжительность обучения студента в колледже, что с точки зрения студента является негативным фактором. Но если курсы-заменители отвечают «посторонним» интересам студента (имеются в виду хобби или перспективные цели), то такие курсы-заменители могут оказаться допустимыми. Кроме того, существует ряд факультативных кусов, которые могут также давать право для «выхода на диплом». Студент хотел бы получить оптимальный набор курсов, который бы позволил ему в запланированные сроки (или досрочно) претендовать на диплом, оставаясь при этом в рамках намеченного бюджета с максимальной гибкостью участвуя в учебном процессе. Для решения этой задачи студент использует работающую в реальном масштабе времени программу составления расписания учебных курсов, основанную на технологии «классной доски». Важно отметить, что «классная доска» имеет доступ реального времени к академической характеристике студента и текущим курсам (с открытым или закрытым приемом) в любой момент периода регистрации. Кроме того, «классная доска» имеет доступ к дипломному плану студента, академическим требованиям для реализации этого плана, расписанию «готовности» студента посещать занятия, данным о его целях и предпочтениях и т.д. Все эти элементы моделируются с помощью С++- и CORBA-классов и образуют компоненты «классной доски». Для упрощения нашего примера мы рассмотрим только следующие четыре источника знаний: • консультант по общеобразовательным курсам; • консультант по основным курсам; • консультант по факультативным курсам; • консультант по непрофилирующим курсам. Итак, рассмотрим фрагмент CORBA-интерфейса «классной доски». // Листинг 13.1. CORBA-объявления, необходимые для нашего // класса «классной доски» typedef sequence interface black_board{ //. . . void suggestionsForMajor(in courses Major); void suggestionsForMinor(in courses Minor); void suggestionsForGeneral(in courses General); void suggestionsForElectives(in courses Electives); courses currentDegreePlan; courses suggestedSchedule; //. . . }; Главная цель интерфейса black_board — обеспечить доступ для чтения и записи со стороны источников знаний. В данном случае при разделении «классной доски» необходимо предусмотреть сегменты для каждого источника знаний. [23] Это позволяет источникам знаний получать доступ к «классной доске» посредством CRCW-стратегии. Другими словами, несколько типов источников знаний могут получить доступ к «классной доске» одновременно, но источники знаний одинакового типа должны быть ограничены применением CREW-стратегии. Любой метод или функция-член, с помощью которого источники знаний будут получать доступ к»классной доске», должен быть определен в интерфейсном классе black_board. Класс courses объявляется с использованием типа CORBA, и поэтому его можно применять в качестве параметра и значений, возвращаемых методами при взаимодействии между источниками знаний и «классной доской». Поэтому эти объявления класса black_board courses Minor; courses Major; будут использованы для представления информации, которая либо записывается на «классную доску», либо считывается с нее. Тип courses — это синоним для CORBA-типа sequence allocbuf freebuf get_buffer length operator[] release replace maximum Источники знаний будут взаимодействовать с «классной доской» с помощью этих методов. Объявление sequence
Реализация интерфейсного класса black_board
Обратите внимание на то, что в интерфейсном классе (см. листинг 13.1) нет объявлений переменных. Вспомните, что интерфейсный класс в CORBA-реализации ограничивается только объявлением методов. В интерфейсном классе не существует компонентов, предназначенных для хранения информации. CORBA-классы должны тесно контактировать с С++-реализациями до конца работы приложения. Реальные реализации методов и необходимых переменных вносятся в производный класс (выведенный из этого интерфейсного класса). Производный класс, выведенный из интерфейсного класса black_board, представлен в листинге 13.2. // Листинг 13.2. Фрагмент класса реализации для // интерфейсного класса black_board #include «black_board.h» #include class blackboard : virtual public POA_black_board{ protected: //. . . set set set set courses Schedule; courses DegreePlan; public: blackboard(void); ~blackboard(void); void suggestionsForMajor(const courses &X); void suggestionsForMinor(const courses &X); void suggestionsForGeneral(const courses &X); void suggestionsForElectives(const courses &X); courses *currentDegreePlan(void); courses *suggestedSchedule(void); //. . . } ; Этот класс реализации используется для предоставления реальных определений методов, объявленных в интерфейсном классе. Помимо реализации методов, производный класс может содержать компоненты данных, поскольку они не объявлены в качестве интерфейса. Обратите внимание на то, что класс реализации black_board, представленный в листинге 13.2, наследует непосредственно не интерфейсный класс black_board, а класс POA_black_board, который является одним из тех классов, которые создает IDL-компилятор от имени интерфейсного класса black_board. Объявление класса POA_black_board приведено в листинге 13.3. // Листинг 13.3. Фрагмент объявления класса POA_black_board, // созданного idl-компилятором для // интерфейсного класса black_board class POA_black_board : virtual public PortableServer::StaticImplementation { public: virtual -POA_black_board ; black_board_ptr _this ; bool dispatch (CORBA::StaticServerRequest_ptr); virtual void invoke (CORBA::StaticServerRequest_ptr); virtual CORBA::Boolean _is_a (const char *); virtual CORBA::InterfaceDef_ptr _get_interface ; virtual CORBA::RepositoryId _primary_interface (const PortableServer::ObjectId &, PortableServer::POA_ptr); virtual void * _narrow_helper (const char *); static POA_black_board * _narrow ( PortableServer::Servant); virtual CORBA::Object_ptr _make_stub (PortableServer:: POA_ptr, CORBA::Object_ptr); //.. . virtual void suggestionsForMajor (const courses& Major) = 0; virtual void suggestionsForMinor (const courses& Minor) = 0; virtual void suggestionsForGeneral ( const courses& General) = 0; virtual void suggestionsForElectives ( const courses& Electives) = 0; virtual courses* currentDegreePlan = 0; virtual courses* suggestedSchedule = 0; //. . . protected: POA_black_board {}; private: POA_black_board (const POA_black_board &); void operator= (const POA_black_board &); }; Обратите внимание на то, что класс в листинге 13.3 является абстрактны virtual courses* suggestedSchedule = 0; Это означает, что данный класс нельзя использовать напря
Порождение источников знаний в конструкторе «классной доски»
«Классная доска» реализуется как распределенный объект, использующий CORBA-протокол. В данном случае одной из основных целей «классной доски» является порождение источников знаний. Это важный момент, поскольку «классная доска» должна иметь доступ к идентификационным номерам задач. Начальное состояние «классной доски» (оно устанавливается в конструкторе) включает информацию о студенте, его академической характеристике, текущем семестре, требованиях для получения диплома и т.д. С помощью «классной доски», исходя из начального состояния, определяется, какие источники знаний следует запустить в работу. Иначе говоря, оценив начальную задачу и исходное состояние системы, «классная доска» составляет список запускаемых на выполнение источников знаний. Каждый источник знаний имеет соответствующий двоичный файл, а для хранения имен этих файлов «классная доска» использует контейнер Solvers. Позже, при функционировании конструктора, с по
Порождение источников знаний с помощью PVM-задач
Конструктор «классной доски» содержит следующий вызов алгоритма, for_each(Solve.begin,Solve.end, Task); Алгоритм for_each применяет операторный метод объекта функции (созданного для класса задачи) к каждому элементу контейнера Solve. Этот метод используется для порождения источников знаний в соответствии с моделью MIMD, при реализации которой все источники знаний имеют различную специализацию и работают с различными наборами данных. Объявление этого класса задач приведено в листинге 13.4. // Листинг 13.4. Объявление класса задачи class task{ int Tid[4]; int N; //. . . public: //. . . task(void) { N = 0; } void operator(string X); }; void task::operator(string X) { int cc; pvm_mytid; cc = pvm_spawn(const_cast N++; } blackboard::blackboard(void) { task Task; vector //.. . // Determine which KS to invoke //. . . Solve.push_back(KS1); Solve.push_back(KS2); Solve.push_back(KS3); Solve.push_back(KS4); for_each(Solve.begin, Solve.end, Task); } Этот класс task инкапсулирует порожденный процесс. Он содержит идентификационный но Особый интерес для нашей «классной доски» представляют операции pvm_barrier и pvm_joingroup, поскольку существуют ситуации, в которых «классная доска» не запускает новые источники знаний до тех пор, пока определенная группа источников знаний не завершит свою работу. Для блокирования вызывающего процесса до нужного момента (до окончания обработки данных соответствующими источниками знаний) можно использовать операцию pvm_barrier . Например, «классная доска» в качестве консультанта по выбору курсов обучения не будет активизировать источник знаний, отвечающий за составление расписания, до тех пор, пока не представят свои предложения источники знаний, которые специализируются на основных, общеобразовательных, второстепенных и факультативных курсах. Поэтому «классная доска» будет использовать операцию pvm_barrier для ожидания завершения работы этой группы PVM-задач. На рис. 13.5 представлена UML-диаграмма видов деятельности, которая позволяет понять, как синхронизируются источники знаний и «классная доска». Барьер синхронизации здесь реализуется с помощью операций pvm_barrier и pvm_joingroup . Реализация операторной функции для объекта задачи приве Таблица 13.2. Групповые PVM-операции int pvm_joingroup (char *groupname); Вносит вызывающий процесс в группу groupname, а затем возвращает int-значение, которое представляет собой номер процесса в этой группе int pvm_lvgroup (char *groupname); Удаляет вызывающий процесс из группы groupname int pvm_gsive (char *groupname); Возвращает int-значение, которое представляет собой количество членов в группе groupname int pvm_gettid (char *groupname, int inum); Возвращает int-значение, равное идентификационному номеру задачи, выполняемой процессом, который идентифицируется именем группы groupname и номером экземпляра inum int pvm_getinst (char *groupname, int taskid); Возвращает int-значение, которое представляет собой номер экземпляра, связанный с именем группы groupname и процессом, выполняющим задачу с идентификационным номером taskid int pvm_barrier (char *groupname, int count); Блокирует вызывающий процесс до тех пор, пока count членов в группе groupname не вызовут эту функцию int pvm_bcast (char *groupname, int messageid); Передает всем членам группы groupname сообщение, хранимое в активном буфере отправки, связанном с номером messageid int pvm_reduce (void *operation, void *buffer, int count, int datatype, int messageid, char *groupname, int root); Выполняет глобальную операцию operation во всех процессах группы groupname // Листинг 13.5. Определение функции operator // в классе task void task::operator(string X) { int cc; pvm_mytid; cc = pvm_spawn(const_cast N++; } Функция-оператор operator используется для порождения PVM-задач. Имя задачи содержится в элементе X. data . При обращении к функции pvm_spawn (см. листинг 13.5) создается одна задача, а ее идентификационный номер сохраняется в элементе Tid[N] . (Подробнее о функции pvm_spawn и вызове PVM-задач см. гла-вуб.) Класс task используется для создания функциональных объектов (объектов-функций). При выполнении алгоритма for_each(Solve.begin,Solve.end,Task); вызывается функция operator , которая выполняет объект Task. Эта операция заставляет активизироваться источники знаний, содержа // Листинг 13.6. Запуск PVM-задач из конструктора // класса task void task::operator(string X) { int cc; pvm_mytid; cc = pvm_spawn(const_cast }
Связь «классной доски» и источников знаний
Согласно коду, приведенному в листинге 13.6, порождается 20 источников знаний. Сначала все они выполняют одинаковый код. После их порождения «классная доска» должна отправить сообщения с указанием, какую роль они будут играть в процессе решения задачи. При использовании данной конфигурации источники знаний и «классная доска» являются частью PVM-среды. После создания источники знаний будут взаимодействовать с «классной доской» путем соединения с портом, на котором она размещается, или по ее адресу в сети intranet или Internet. Для этого источникам знаний понадобится объектная ссылка на «классную доску». Эти ссылки можно «зашить» в код источников знаний, или они могут прочитать их из файла конфигурации либо получить из службы имен. Имея ссылку, источник знаний взаимодействует с ORB-брокером (Object Request Broker — брокер объектных запросов), чтобы найти удаленный объект, содержащий реальные данные (знания) и активизировать его. Для нашего примера мы назначаем «классной доске» конкретный порт и запускаем CORBA-объект «классной доски» с помощью следующей ко blackboard -ORBIIOPAddr inet:porthos:12458 По этой команде запускается наша программа «классной доски» с подключением к порту 12458 хоста porthos. Запуск CORBA-объекта зависит от используемой CORBA-реализации. В данном случае мы используем «открытую» CORBA-реализацию Mico [25] При выполнении программы blackboard реализуется экземпляр «классной доски», который в свою очередь порождает источники знаний. В созданных источниках знаний жестко закодирован номер порта, по которому они будут связываться с «классной доской». Фрагмент кода реализации источника знаний, который связывается с CORBA - ориентированным объектом «классной доски», представлен в листинге 13.7. // Листинг 13.7. Код источника знаний, который связывается // с CORBA-ориентированной «классной доской» 1 #include «pvm3.h» 2 using namespace std; 3 #include 4 #include 5 #include 6 #include 7 #include «black_board_impl.h» 8 9 int main(int argc, char *argv[]) 10 { 11 CORBA::ORB_var Orb = CORBA::ORB_init(argc, argv,«mico-local-orb»); 12 CORBA::Object_yar Obj =Orb->bind(«IDL:black_board:1.0»,«inet:por thos:12 4 5 8»); 13 courses Courses; 14 //... 15 //... 16 black_board_var BlackBoard = black_board::_narrow(Obj); 17 18 int Pid; 19 //... 20 //... 21 22 cout « «Источник знаний создан.» « endl; 23 Courses.length(2); 24 Courses[0] = 255551; 25 Courses[l] = 253212; 26 string FileName; 27 strstream Buffer; 28 Pid = pvm_mytid; 29 Buffer « «Результат.» « Pid « ends; 30 Buffer » FileName; 31 ofstream Fout(FileName.data); 32 BlackBoard->suggestionsForMajor(Courses); 33 Fout.close; 34 pvm_exit; 35 return(0); 36 } 37 В строке 11 (см. листинг13.7) инициализируется ORB брокер . При выполнении строки 12 осу При выполнении этого вызова информация о курсах обучения записывается на «классную доску». Аналогично следующие методы courses currentDegreePlan; courses suggestedSchedule; можно использовать для считывания информации с «классной доски». Поэтому для об
Активизация источников знаний с помощью POSIX-функции spawn
Реализация источников знаний в ра В варианте 1 CORBA-объект и источники знаний размещаются на одном компьютере, и каждый источник знаний имеет собственное адресное пространство. Другими словами, каждый источник знаний порожден с помощью функции posix_spawn или семейств а функций fork-exec. В варианте 2 CORBA-объект размещается на одном компьютере, а все источники знаний — на другом, но в различных адресных пространствах. В обоих вариантах CORBA-объект действует как разновидность общей памяти для источников знаний, поскольку все они получают доступ к нему и могут обмениваться информацией через «классную доску». При этом важно помнить о существовании основного преимущества CORBA-объекта — он имеет более высокую организацию, чем простой блок памяти. «Классная доска» — это объект, который может состоять из структур данных любого типа, объектов и даже других «классных досок». Такой вид организации не может быть реализован простым использованием базовых функций доступа к общей памяти. Поэтому CORBA-реализация обеспечивает идеальный способ разделения сложных объектов между процессами. В подразделе 13.5.3.1 описано создание PVM-задач, которые реализуют источники знаний. Здесь мы изменяем конструктор, включал в него вызовы функции posix_spawn (с той же целью можно использовать алгоритм for_each и функциональный объект задачи для вызова функции posix_spawn). В варианте 1 (см. рис. 13.6) «классная доска» может порождать источники знаний при реализации конструктора. Но в варианте 2 это невозможно, поскольку «классная доска» расположена на отдельном компьютере. Поэтому в варианте 2 «классной доске» для вызова функции posix_spawn приходится прибегать к услугам посредника. Посредничество можно организовать разными способами, например, «классная доска» может вызвать другой CORBA-объект, расположенный на одном компьютере с источниками знаний. С той же целью можно использовать удаленный вызов процедуры (Remote Procedure Call — RPC) или MPI- либо PVM-задачу, которая должна вызвать программу, содержащую обращение к функции posix_spawn . (Описание вызовов функции posix_spawn приведено в главе 3.) Как можно использовать функцию posix_spawn для активизации одного из источников знаний, показано в листинге 13.8. // Листинг 13.8. Использование функции posix_spawn для // запуска источников знаний #include //.. . pid_t Pid; posix_spawnattr_t M; posix_spawn_file_actions_t N; posix_spawn_attr_init(&M); posix_spawn_file_actions_init(&N); char *const argv[] = {«knowledge_source1»,NULL}; posix_spawn(&Pid,«knowledge_source1»,&N,&M,argv,NULL); //. . . } В листинге 13.8 инициализируются атрибуты и действия, необходимые для порождения задач, после чего с помощью функции posix_spawn создается отдельный процесс, который предназначен для выполнения источника знаний knowledge_source1. После создания этого процесса «классная доска» получает к нему доступ через его идентификационный номер, сохраняемый в параметре Pid. Кроме «классной доски», используемой в качестве средства связи, возможно и стандартное межпроцессное взаимодействие (IPC), если «классная доска» расположена на одном компьютере с источниками знаний. «Классная доска» — самый простой способ взаимодействия между источниками знаний, хотя в конфигурации размещения «классной доски» на отдельном компьютере можно использовать с этой целью сокеты. В этом случае управление, осуществляемое «классной доской» над источниками знаний, будет более жестким и обусловленным в любой момент времени содержимым «классной доски», а не сообщениями, передаваемыми непосредственно источникам знаний. Прямую пересылку сообщений легче реализовать при использовании «классной доски» в сочетании с PVM-задачами. В этом случае источники знаний сами настраивают себя на основе содержимого «классной доски». Но «классная доска» все же имеет определенный «рычаг»управления источниками знаний, поскольку ей «известны» идентификационные номера всех процессов, содержащих источники знаний. Как модель MPMD (MIMD), так и модель SPMD (SIMD), также поддерживаются использованием функции posix_spawn. В листинге 13.9 представлен класс, который можно использовать в качестве объекта-функции при выполнении алгоритма for_each . // Листинг 13.9. Использование класса child_process как // объекта-функции при запуске источников // знаний class child_process{ string Command; posix_spawnattr_t M; posix_spawn_file_actions_t N; pid_t Pid; //.. . public: child_process(void); void operator(string X); void spawn(string X); }; void child_process::operator(string X) { //.. . posix_spawnattr_init(&M); posix_spawn_file_actions_init(&N); Command.append("/tmp/"); Command.append(X); char *const argv[] = {const_cast posix_spawn(&Pid,Command.data,&N,&M,argv,NULL); Command.erase(Command.begin, Command.end); //.. . } Мы инкапсулируем атрибуты, необходимые для функции posix_spawn , в классе child_process. Инкапсуляция всех данных, требуемых для вызова этой функции в классе, упро // Конструктор. //... child_process Task; for_each(Solve.begin, Solve.end, Task); При выполнении этого конструктора для каждого элемента контейнера Solve вызывается метод operator , код которого приведен в листинге 13.9. После активизации источники знаний получают доступ к ссылке на объект «классной доски» и могут приступать к решению свой части задачи. И хотя источники знаний здесь не являются PVM-задачами, они связываются с «классной доской» таким же способом (см. подраздел 13.5.3.2) и так же выполняют свою работу. Дело в том, что межпроцессное взаимодействие между стандартными UNIX/Linux-процессами отличается от межпроцессного взаимодействия, которое возможно с использованием PVM-среды. Кроме того, PVM-задачи могут располагаться на разных компьютерах, в то время как процессы, созданные с помощью функции posix_spawn, могут существовать только на одном и том же компьютере. Если процессы, созданные функцией posix_spawn (либо семейством функций fork-exec), необходимо использовать в сочетании с моделью SIMD, то в дополнение к объекту «классной доски» для назначения источникам знаний конкретных областей задачи, которые они должны решать, можно использовать параметры argc и argv. В случае, когда «классная доска» находится на одном компьютере с источниками знаний, и она активизирует источники знаний в своем конструкторе, то формально «классная доска» является для них родителем, а потомки наследуют от родителя переменные среды. Переменные среды «классной доски» можно использовать в качестве еще одного способа передачи информации источникам знаний. Этими переменными среды можно легко управлять, используя следующие функции. #include //.. . setenv; unsetenv; putenv; Если источники знаний реализуются в процессах, которые созданы с помощью функции posix_spawn (или fork-exec), то их программирование не выходит за рамки обычного CORBA-программирования с доступом ко всех средствам, предлагаемым CORBA-протоколом.
Реализация модели «классной доски» с помощью глобальных объектов
Выбор CORBA-ориентированной «классной доски» вполне естествен в условиях, когда источники знаний должны быть реализованы в среде intranet или Internet, или когда в целях соблюдения модульного принципа организации, инкапсуляции и так далее каждый источник знаний реализуется в отдельном процессе. Однако в распределении «классной доски» необходимость возникает не всегда. Если источники знаний можно реализовать в рамках одного процесса или на одном компьютере, то лучше всего в этом случае организовать несколько потоков, поскольку при таком варианте быстродействие выше, расходы системных ресурсов меньше, а сама работа (настройка) — проще. Взаимодействие между потоками легче организовать, поскольку потоки разделяют одно адресное пространство и могут использовать глобальные переменные. Ведь тогда «классную доску» можно реализовать как глобальный объект, доступный всем потокам в процессе. При реализации источников знаний в виде потоков в рамках одной программы отпадает необходимость в межпроцессном взаимодействии, использовании сокетов или какого-либо другого типа сетевой связи. Кроме того, в этом случае оказывается ненужным дополнительный уровень CORBA-протокола, поскольку можно обойтись разработкой обычных C++-классов. Если многопоточная программа рассчитана на использование одного компьютера с несколькими процессорами, то потоки могут выполняться параллельно на доступных процессорах. В SMP- и МРР-системах потоковая конфигурация «классной доски» весьма привлекательна. В общем случае при использовании потоков достигается самая высокая производительность. Потоки часто называют Поскольку «классная доска» реализована в многопоточной среде, то для синхронизации доступа к «классной доске» можно использовать Pthread-мьютексы и переменные условий, которые необходимо инкапсулировать в интерфейсных классах, как описано в главе 11. Кроме того, для координации и синхронизации работы, выполняемой источниками знаний, можно использовать функции pthread_cond_signal и pthread_cond_broadcast . Поскольку «классная доска» сама создает потоки, ей будет нетрудно получить доступ к идентификационным номерам всех источников знаний. Это означает, что «классная доска» может при необходимости аннулировать поток, используя функцию pthread_cancel . Кроме того, «классная доска» способна синхронизировать выполнение источников знаний с помощью функции pthread_join. Помимо уже перечисленных достоинств многопоточной реализации (высокое быстродействие и простота использования потоков и глобального объекта «классной доски»), существует также проблема обработки ошибок и исключительных ситуаций. В общем случае эта проблема решается проще в рамках одного процесса и одного компьютера, чем при использовании нескольких процессов и нескольких компьютеров. На рис. 13.8 показаны уровни сложности, связанные с обработкой ошибок и исключительных ситуаций при использовании различных конфигураций. Если источники знаний реализованы в отдельных потоках одного и того же процесса, то обработка возможных ошибок или исключительных ситуаций в этом случае относится к уровню сложности 2. Эту степень сложности необходимо учитывать еще на этапах проектирования и разработки программы, особенно в случае, если она требует параллельного программирования. Простейшее архитектурное решение, использующее модель «классной доски», состоит в реализации «классной доски» в виде глобального объекта, а источников знаний — в виде потоков. Рассмотрим фрагмент объявления класса blackboard. // Листинг 13.10. Фрагмент объявления класса blackboard, // разработанного для многопоточной среды class blackboard{ protected: //.. . set set set set set set mutex Mutex[10]; //.. . public: blackboard(void) ; ~blackboard(void); void suggestionsForMajor(set void suggestionsForMinor(set void suggestionsForGeneral(set void suggestionsForElectives(set set set //.. . }; Класс blackboard предназначен для реализации в качестве глобального объекта, к которому смогут получать доступ все потоки в программе. Обратите внимание на то, что класс blackboard в листинге 13.10 включает массив мьютексов. Эти мьютексы используются для защиты критических разделов «классной доски». При реализации источников знаний практически нет необходимости беспокоиться о синхронизации доступа к критическим разделам, поскольку код синхронизации инкапсулирован в классе blackboard.
Активизация источников знаний с помощью потоков
В этом разделе рассматривается реализация источников знаний в отдельных потоках. Потоки создаются здесь при выполнении конструктора класса «классной доски» (blackboard), и каждому потоку назначается конкретный источник знаний. Тем самым реализуется модель MIMD. Фрагмент кода конструктора класса blackboard приведен в листинге 13.11. // Листинг 13.11. Конструктор класса blackboard, // используемый для создания потоков, // содержащих источники знаний blackboard::blackboard(void) { pthread_t Tid[4]; //.. . try{ pthread_create(&Tid[0],NULL,suggestionForMajor, NULL); pthread_create(&Tid[l],NULL, suggestionForMinor, NULL); pthread_create(&Tid[2], NULL,suggestionForGeneral, NULL); pthread_create(&Tid[3],NULL, suggestionForElective, NULL); pthread_join(Tid[0],NULL); pthread_join(Tid[l],NULL); pthread_join(Tid[2],NULL); pthread_join(Tid[3],NULL); } //. . . } Обратите внимание на то, что конструктор вызывает функцию pthread_join. Этот вызов заставляет конструктор ожидать завершения работы всех четырех потоков. Эти потоки могут активизироваться и с помощью других функций-членов класса blackboard. Но те действия, которые выполняют источники знаний «в рамках» конструктора, представляют своего рода предварительную инициализацию «классной доски», поэтому весьма уместно не продолжать работу по созданию объекта «классной доски» до тех пор, пока эти потоки не доведут до конца свою работу. Такой подход к созданию потоков в конструкторе заставляет задуматься об обработке ошибок и исключительных ситуаций. Что произойдет, если по какой-то причине при выполнении потоков случится сбой? Поскольку конструкторы не возвращают никаких значений, то здесь просто необходимо позаботиться об обработке исключительных ситуаций. Каждый поток связывается со «своей» функцией. void *suggestionForMajor(void *X); void *suggescionForMinor(void *X); void *suggestionForGeneral(void *X); void *suggestionForElective(void *X); Эти четыре функции используются потоками для реализации действий соответствующих источников знаний. Поскольку «классная доска» является глобальным объектом, каждая из этих функций имеет непосредственный доступ к функциям-членам класса blackboard. Поэтому источники знаний могут вызывать функции-члены «классной доски» напрямую. //... Combination.generateCombinations(1,9, Courses); Result = Combination.element(9); //.. . Blackboard.suggestionsForMinor(Value); //.. . Поскольку некоторые разделы «классной доски» имеют ограниченный доступ для отдельных источников знаний, то к этим разделам можно применить CRCW-стратегию доступа (рис. 13.9). Тип параллелизма, представленный на рис. 13.9, вполне естествен для систем, реализующих модель «классной доски», поскольку «классная доска» часто делится на разделы, относящиеся к определенным частям задачи или подзадачи. Обычно одной проблемной области соответствует один источник знаний, поэтому параллельный доступ к этим разделам вполне уместен.
Резюме
Модель «классной доски» поддерживает параллелизм, который присутствует как в структуре «классной доски», так и в отношениях между «классной доской» и источниками знаний, а также между самими источниками знаний. Модель «классной доски» — это модель решения некоторой задачи. Общая задача делится на части, соответствующие конкретным областям знаний. Каждой области назначается источник знаний, или решатель задач. Источники знаний обычно отличаются самодостаточностью (автономностью) и не требуют интенсивного общения с другими источниками знаний. Необходимое взаимодействие осуществляется через «классную доску». Следовательно, источники знаний позволяют организовать обработку данных в рамках программы по модульному принципу. Такие своеобразные модули могут работать отдельно и параллельно, не требуя сложной синхронизации. «Классную доску» можно реализовать в виде CORBA-объектов. В этом случае источники знаний могут быть распределены в сетях intranet или Internet. «Классная доска» действует как разновидность общей распределенной па
Приложение A
Это приложение представляет собой краткий справочник UML-диаграмм, используемых в этой книге. Универсальный язык моделирования (Unified Modeling Language - UML) предлагает графические обозначения, используемые для проектирования, визуализации, моделирования и документирования артефактов системы программного обеспечения. Этот язык является стандартом «де-факто» для моделирования объектно-ориентированных систем. В нем используются символы и обозначения для представления артефактов системы ПО с различных точек зрения. И хотя в книге используются и другие обозначения, это приложение позволит читателю быстро ознакомиться с основными элементами и символами языка UML, которые могут понадобиться ему при составлении Документации на разрабатываемые системы ПО.
Диаграммы классов и объектов
Диаграммы классов и объектов — самые распространенные диаграммы, используемые в моделировании объектно-ориентированных систем. Диаграммы классов используются для представления классов любого типа, в том числе шаблонных и интерфейсных классов. Эти диаграммы могут содержать члены класса (атрибуты и операции). В диаграммах классов и объектов отображаются типы данных, значения переменных и типы значений, возвращаемых функциями. В диаграммах объектов можно отобразить имя объекта. В диаграммах обоих типов можно указать количество классов или объектов, используемых в системе, а также отношения между классами и объектами.
Диаграммы взаимодейс
Диаграммы взаимодействия предназначены для отображения взаимодействия
Диаграммы сотрудничества
Диаграммы сотрудничества используются для отображения объектов, работающих вместе с целью выполнения некоторой общей работы. Под сотрудничеством в системе понимается временная кооперация множества объектов. Диаграммы этого типа могут отображать организацию или структуру сотрудничества. Это подразумевает отображение всех объектов данного множества, связей между ними, а также отправляемых и получаемых ими сообщений.
Диаграммы последовательностей
Диаграммы последовательностей предназначены для отображения временного упорядочения сообщений, отправляемых и получаемых объектами в системе.
A.2.3. Диаграммы видов деятельности
Диаграммы видов деятельности отображают передачу управления от одного вида деятельности другому. Под деятельностью подразумеваются действия, выполняемые объектами. Действия включают обработку операций ввода-вывода, создание или разрушение объектов либо выполнение вычислений. Диаграммы видов деятельности подобны блок-схемам.
A.3. Диаграммы состояний
Диаграмма состояний используется для отображения последовательности изменения состояния объектов. Состояние — это условие, при котором объект занимает ту или иную позицию на своей «линии жизни». Объект за время своего существования может многократно изменять свое состояние. Объекты переходят в новое состояние, если создаются определенные условия, выполняется некоторое действие или происходит соответствующее событие.
A.4. Диаграммы пакетов
Диаграммы пакетов используются для организации элементов системы по группам
Приложение Б [26]
posix_spawn, posix_spawnp
Имя posix_spawn, posix_spawnp — функции порождения процессов (ADVANCED REALTIME) Синопсис SPN #include int posix_spawn ( pid_t *restrict const posix_spawn_file_actions_t pid_t *restrict const posix_spawn_file_actions_t Описание Функции posix_spawn и posix_spawnp предназначены для создания нового (сыновнего) процесса из заданного образа процесса. Новый образ процесса создается на основе обычного выполняе Если в качестве результата этого вызова выполняется С-програ Здесь Аргумент argv представляет собой массив символьных указателей на строки с завершающим нулем. Последний член этого массива (он не учитывается аргу Аргу Количество байтов, допустимых для обобщенного аргумента сыновнего процесса и списков строк описания конфигурации среды, составляет {ARG_MAX}. В систе Ар Пара Если пара Если пара 1. Множество открытых файловых дескрипторов дл 2. Маска сигнала, стандартные действия сигналов, а также идентификационные номера эффективного пользователя и группы для сыновнего процесса должны измениться в соответствии со значениями, заданными в объекте атрибутов, адресуемом параметром actrp. 3. Действия над файлами, заданные объектом действий для порождаемого процесса, должны быть выполнены в порядке их добавления в этот объект. 4. Любой файловый Тип объекта атрибутов posix_spawnattr_t опре Если в атрибуте В качестве специального случал, ес PS Если в атрибуте Если в атрибуте Флаг POSIX_SPAWN_RESETIDS в атрибуте Флаг POSIX__SPAWN_RESETIDS в атрибуте Если в атрибуте Если в атрибуте Сигналы, установленные для перехвата вызывающим процессом, должны быть установлены равными действиям по умолчанию в сыновнем процессе. За исключение Если сигнал SIGCHLD установлен как игнорируемый вызывающим процессом, точно не установлено, должен ли сигнал SIGCHLD игнорироваться сыновним процессом или он будет установлен равным действию по умолчанию в сыновнем процессе, если не определено иное посредством флага POSIX_SPAWN_SETSIGDEF, установленного в атрибуте spawn-flags объекта, адресуемого параметром attrp, и сигнала SIGCHLD, обозначенного в атрибуте spawn_sigdefault того же объекта. Если указатель attrp содержит значение NULL, используются значения по умолчанию. Все атрибуты процесса, на которые не было оказано влияния со стороны атрибутов, установленных в объекте, адресуемом параметром attrp (как было описано выше), или вследствие манипуляций с файловыми дескрипторами, заданных в параметре file_actions, должны присутствовать в образе нового процесса в таком виде, как будто была вызвана функция fork для создания сыновнего процесса, а затем член семейства функций exec был вызван сыновним процессом для выполнения образа нового процесса. THR Запускаются ли обработчики разветвлений при вызове функций posix_spawn или posix_spawnp , определяется конкретной реализацией. Возвращаемые значения При успешном выполнении функция posix_spawn (и функция posix_spawnp ) должна возвратить родительскому процессу идентификационный номер (ID) сыновнего процесса в переменной, адресуемой аргументом pid (если его значение не равно NULL), и нуль в качестве значения, возвращаемого функцией. В противном случае сыновний процесс не создается, значение, сохраненное в переменной, адресуемой аргументом pid (если его значение не равно NULL), не определяется, а в качестве значения, возвращаемого функцией, передается код ошибки, обозначающий ее характер. Если аргумент pid содержит нулевой указатель, значение ID сыновнего процесса инициатору вызова не возвращается. Ошибки Вызовы функций posix_spawn и posix_spawnp могут оказаться неудачными, если: [EINVAL] значение, за Если ошибка возникла после того, как вызывающий процесс успешно вернулся из функции posix_spawn или posix_spawnp , то сыновний процесс может завершиться со стагусом выхода (exit status), равным значению 127. Если неудачный исход функции posix_spawn или posix_spawnp вызван одной из причин, которые бы привели к отказу функции fork или одной из функций семейства exec, то возвращаемое значение ошибки будет соответствовать описанию для функций fork и exec соответственно (или, если ошибка возникнет после того, как вызывающий процесс успешно вернется, сыновний процесс завершится со статусом выхода, равным значению 127). Если в атрибуте PS Если в атрибуте Если в атрибуте Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не представлены во всех реализациях. Логическое обоснование Функция posix_spawn и ее «близкая родственница» функция posix_spawnp были введены для преодоления следующих ощутимых трудностей использования функции fork : функцию fork сложно (или невозможно) реализовать без обмена (подкачки) или динамической трансляции адреса. • Обмен (механизм подкачки в оперативную память недостающей страницы виртуальной памяти, затребованной программой) — в общем случае слишком медленный механизм для среды реального времени. • Осуществление динамической трансляции адреса возможно не везде, где может использоваться библиотека POSIX . • Создание процессов с помощью библиотеки POSIX не требует трансляции адресов или иных услуг, связанных с MMU (memory management unit — блок управления памятью). Таким образом, библиотека POSIX использует примитивы создания процессов и выполнения файлов, которые могут быть эффективно реализованы без трансляции адресов или иных MMU-процедур. Функция posix_spawn реализуется как библиотечная программа, но обе функции posix_spawn и posix_spawnp задуманы как операции ядра операционной системы. Несмотря на то что они могут представлять эффективную замену для многих пар функций fork /exec, их цель — обеспечить возможность создания процессов для систем, в которых возникают сложности с применением функции fork , а не полностью вытеснить функции fork / exec. Такая роль функций posix_spawn и posix_spawnp оказала влияние на их API-интерфейс. Здесь не было попытки обеспечить полную функциональность пар fork/exec, при использовании которых между созданием сыновнего процесса и выпол Функции posix_spawn и posix_spawnp обеспечивают управление шестью типами наследования: файловыми дескрипторами, идентификационным номером (ID) группы процессов, ID пользователя и группы, маской сигналов, стратегией планирования, а также управление сигналами (будет ли каждый сигнал, игнорируемый в родительском процессе, игнорироваться и в сыновнем, или же он будет установлен равным действию по умолчанию). Возможность управления файловыми дескрипторами позволяет независимо записанному образу сыновнего процесса получить доступ к потокам данных, открытым (или даже сгенерированным) либо читаемым родительским процессом, без специального программирования средств, с помощью которых можно было бы определить, какие файлы (файловые дескрипторы) используются в родительском процессе. Возможность управления идентификационным номером группы процессов позволяет установить, как механизм управления заданиями в сыновнем процессе связан с аналогичным механизмом в родительском процессе. Управления маской сигналов и установкой сигналов по умолчанию вполне достаточно для поддержки реализации функции system. Несмотря на то что поддержка функции system не является одной из явных целей для функций posix_spawn и posix_spawnp , все же эта поддержка составляет не менее 50% от общей «суммы целей». Намерение состоит в том, что обычное насле Мы Функции posix_spawn и posix_spawnp не обладают всей полнотой власти, которая характерна для функций fork / exec. И такой эффект вполне ожидаем. Функция fork — чрезвычайно мощная. Мы и не надеялись скопировать все ее возможности в простой и быстрой функции, не предъявляя специальных требований к оборудованию. Важно то, что функции posix_spawn и posix_spawnp очень близки к средствам создания процессов во многих операционных системах, отличных от UNIX. Требования К реализации функций posix_spawn и posix_spawnp предъявляются следующие требования. • Они должны быть реализованы без использования MMU (memory management unit — блок управления памятью) или какого-то иного специального оборудования. • Они должны быть совместимы с существующими POSIX -стандартами. Дополнительные требования таковы. • Они должны быть эффективными. • Их способность по замещению функции fork (в обычных условиях) должна составлять не меньше 50%. • Система, в которой реализованы функции posix_spawn и posix_spawnp , но не реализована функция fork , должна иметь достаточную эффективность, по крайней мере для приложений реального времени. • Система, в которой реализована функция fork и семейство функций exec, должна обладать способностью к реализации функций posix_spawn и posix_spawnp как библиотечных программ. Двухвариантный синтаксис POSIX-функция exec имеет несколько последовательностей вызовов с приблизительно одинаковой результативностью. Это вызвано практическими реалиями. Поскольку установившаяся практика использования функций posix_spawn существенно отличается от POSIX-варианта, мы посчитали, что простота важнее полной совместимости. Поэтому мы представили только две модификации для функции posix_spawn . Различий в списках параметров между функциями posix_spawn и posix_spawnp практически нет; при использовании функции posix_spawnp второй параметр интерпретируется более сложно, чем при использовании функции posix_spawn . Совместимость с POSIX.5 (Ada) Процедуры Start_Process и Start_Process_Search из пакета привязки языка Ada POSIX_Process_Primitives к POSIX.1 . инкапсулируют действия функций fork и ехес практически так же, как это делают функции posix_spawn и posix_spawnp. Первоначально, придерживаясь цели более простого подхода, разработчики стандарта ограничили возможности функций posix_spawn и posix_spawnp подмножеством возможностей, присущих процедурам Start_Process и Start_Process_Search, отказавшись от поддержки конкретных нестандартных средств. Но на основе пожеланий группы приема стандарта усовершенствовать отображение дескрипторов файлов или совсем отказаться от них, а также по рекомендации членов рабочей группы Ada Language Bindings разработчики стандарта решили, что функции posix_spawn и posix_spawnp должны быть в достаточной степени эффективными для реализации возможностей процедур Start_Process и Start_Process_Search. Мы исходили из того, что если привязка языка Ada к такому базовому варианту уже была одобрена в качестве стандарта IEEE, то вряд ли не будут одобрены эквивалентные части С-привязки. Среди возможностей, реализованных функциями posix_spawn и posix_spawnp, можно насчитать только следующие три пункта, которые не обеспечивались процедурами Start_Process и Start_Process_Search: необязательное задание идентификационного номера группы сыновних процессов, набор сигналов, подлежащих стандартной обработке в сыновнем процессе, а также стратегия планирования (и ее параметры). Для того чтобы привязку языка Ada в виде процедуры Start_Process можно было реализовать с помощью функции posix_spawn , функции posix_spawn пришлось бы явно передавать пустую маску сигналов и среду родительского процесса везде, где инициатор вызова процедуры Start_Process позволял установку этих аргументов по умолчанию, поскольку в функции posix_spawn такой установки аргументов по умолчанию не предусмотрено. Способность процедуры Start_Process маскировать определенные пользователем сигналы во время ее выполнения является уникальной для привязки языка Ada и должна быть обработана в самой привязке отдельно от вызова функции posix_spawn . Группа процессов Поле наследования группы процессов можно использовать для присоединения сыновнего процесса к су Потоки В системах, в которых отсутствует трансляция адресов, для представле Функции posix_spawn и posix_spawnp Асинхронное уведомление об ошибках Библиотечная реализация функций posix_spawn или posix_spawnp не позволяет выявить все возможные ошибки до создания сыновнего процесса. Стандарт IEEE Std 1003.1-2001 обеспечивает возможность индикации ошибок, возвращаемых из сыновнего процесса, которому не удалось успешно завершить операцию создания, с помощью специального статуса выхода, который можно обнаружить, используя значение статуса, возвращаемое функциями wait и waitpid . Интерфейс Разработчики стандарта для интерпретации значения stat_val предложили использовать два дополнительных макроса. Первый, WIFSPAWNFAIL, предназначен для выявления статуса, который свидетельствует о завершении сыновнего процесса по причине ошибки, обнаруженной во время выполнения операции posix_spawn или posix_spawnp , а не во время реального выполнения образа сыновнего процесса; второй макрос, WSPAWNERRNO, должен выделить значение ошибки, если макрос WIFSPAWNFAIL обнаружит сбой. К сожалению, группа приема стандарта резко возражала против этого дополнения, поскольку оно поставило бы библиотечную реализацию функции posix_spawn или posix_spawnp в зависимость от модификации функции waitpid, способной встраивать специальную информацию в значение stat_val для индикации сбоя при порождении процесса. Восьми бит статуса выхода сыновнего процесса, доступность которых для ожидающего родительского процесса гарантирована стандартом IEEE Std 1003.1-2001, недостаточно для устранения неоднозначности ошибок порождения процесса, которые может возвратить образ любого процесса. Требуется, чтобы в значении stat_val никакие другие биты статуса выхода не были видимы, поэтому упомянутые выше макросы не поддаются строгой реализации на библиотечном уровне. Резервирование значения статуса выхода 127 для таких ошибок порождения процессов согласуется с использованием этого значения функциями system и popen при пропадании сигналов в этих операциях, которые возникают после завершения функции, но перед тем, как системная оболочка сможет их отработать. Статус выхода 127 уникальным образом не идентифицирует этот класс ошибок и не предоставляет никакой детальной информации о природе сбоя. Обратите внимание на то, что разрешается (и даже поощряется) «ядерная» реализация функций posix_spawn и posix_spawnp с обеспечением возврата любых возможных ошибок в виде значений, возвращаемых этой функцией, тем самым предоставляя для родительского процесса более детальную информацию о происшедших сбоях. Таким образом, для выделения асинхронных ошибок при выполнении функций posix_spawn или posix_spawnp упомянутые выше макросы не используются. О возможных ошибках, обнаруженных в контексте сыновнего процесса до того, как выполнится образ нового процесса, уведомление происходит путем установки статуса выхода сыновнего процесса равным значению 127. Вызывающий процесс для выявления сбоев при порождении процессов может использовать макросы WIFEXITED HWEXITSTATUS и значение stat_val, сохраненное функциями wait или waitpid, в тех случаях, когда другие значения статуса, с которыми может завершиться образ сыновнего процесса (до того, как родительский процесс сможет окончательно определить, что образ сыновнего процесса начал выполняться), отличаются от статуса выхода, равного числу 127. Будущие направления Отсутствуют. Смотри также alarm, chmod, close , dup, exec, exit , fcntl , fork, kill , open ,posix_spawn_file_actions_addclose, posix_spawn_file_actions_adddup2, posix_spawn_file_actions_addopen, posix_spawn_file_actions_destroy , Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEEStdl003.1d-1999. Применяется интерпретация IEEE PASC Interpretation 1003.1 #103, которая указывает, что в пункте 2 действия, соответствующие установкам сигналов по умолчанию, изменены так же, как маска сигналов. При
posix_spawn_file_actions_addclose, posix_spawn_file_actions_addopen
Имя posix_spawn_file_actions_addclose, posix_spawn_file_actions_addopen— функции внесения в объект действий над файла Синопсис SPN #include int posix__spawn_file_actions_addclose ( posix_spawn_file_actions_t posix_spawn_file_actions_t *restrict const char *restrict Описание Эти функции добавляют в объект действий над файла Объект действий над файла Объект действий над файлами, передаваемый функции posix_spawn или posix_spawnp , определяет, как множество открытых файловых дескрипторов вызывающего процесса должно быть трансформировано во множество потенциально открытых файловых дескрипторов для порождаемого процесса. Эта трансформация должна выглядеть так, как если бы однократно была выполнена заданнал последовательность действий в контексте порожденного процесса (до выполнения образа нового процесса), причем в порядке, в котором эти действия были добавлены в объект. Кроме того, при выполнении образа нового процесса любой файловый дескриптор (из этого нового множества), у которого установлен флаг FD_CLOEXEC, должен быть закрыт (см. описание функции posix_spawn ). Функция posix_spawn_file_actions_addclose добавляет в объект, адресуемый параметром file_actions, действие по закрытию файлов close, в результате чего при порождении нового процесса с использованием объекта действий файловый дескриптор, заданный параметром fildes, будет закрыт (как если бы была вызвана функция close (fildes)). Функция posix_spawn_file_actions_addopen добавляет в объект, адресуемый параметром file_actions, действие по открытию файлов орел, в результате чего при порождении ново Строка, адресуе Возвращаемые значения При успешном завершении эти функции возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Эти функции завершатся неудачно, если: [EBADF] значение, заданное пара Выполнение этих функций [ENOMEM] для расширения содержи Не считается ошибкой, если в качестве значения аргу Примеры Отсутствуют. Замечания по использованию Эти функции яв Логическое обоснование Объект действий над фай 1. Это согласуется с эквивалентной функциональностью библиотеки POSIX .5 (Ada). 2. Это поддерживает парадигму перенаправления потоков ввода-вывода, часто применяемую POSIX-программами, предназначенными для вызова из оболочки. Если такая программа является сыновним процессом, ее можно сориентировать на самостоятельное открытие файлов. 3. Это позволяет сыновнему процессу открывать файлы, которые не должен открывать родительский процесс, поскольку операция по открытию файлов в этом случае может оказаться неудачной или нарушить права доступа к файлам (или права собственности). Относительно приведенного выше п. 2 заметим, что действие «открыть файл» создает для функций posix_spawn и posix_spawnp те же возможности, что и операторы перенаправления для функции system, но только без промежуточного выполнения оболочки. Например, так: system («myprog Относительно приведенного выше п. 3 заметим, что если вызывающему процессу нужно открыть один или несколько файлов для доступа к ним порожденного процесса, но он обладает недостаточными запасами файловых дескрипторов, то выполнение действия open необходимо позволить в контексте сыновнего процесса после того, как другие файловые дескрипторы (которые должны оставаться открытыми в родительском процессе) будут закрыты. Кроме того, если родительский процесс выполняется из файла, для которого установлен бит режима «set-user-id» (идентификационный номер пользователя установлен) и в атрибутах порожденного процесса установлен флаг POSIX_SPAWN_RESETIDS, то файл, созданный в родительско Преобразование файловых дескрипторов Разработчики стандарта первоначально предлагали использовать массив, который бы определял преобразование файловых дескрипторов сыновнего процесса в обратном направлении, т.е. при переходе к родительскому процессу. Группа приема стандарта обратила внимание на то, что невозможно произвольно перетасовывать файловые дескрипторы в библиотечной реализации функции posix_spawn или posix_spawnp , не имея запаса файловых дескрипторов (которого попросту может не быть). Такой массив требует, чтобы реализация обладала сложной стратегией достижения нужного преобразования, которая бы исключала случайное закрытие «не того» файлового дескриптора в самое неподходящее время. Одним из членов рабочей группы Ada Language Bindings было отмечено, что принятое в языке Ada семейство POSIX-примитивов Будущие направления Отсутствуют. Смотри также close , dup , open , posix_spawn , posix_spawn_file_actions_adddup2 , posix_spawn_file_actions_destroy , posix_spawnp , том Base Definitions стандарта IEEE Std 1003.1-2001, < spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основание При
posix_spawn_file_actions_adddup2
Имя posix_spawn_file_actions_adddup2— функция внесения в объект действий над файла Синопсис SPN #include int posix_spawn_file_actions_adddup2 ( posix_spawn_file_actions_t * file_aсtions, int fildes, int newfildes); Описание Функция posix_spawn_file_actions_adddup2 добавляет в объект, адресуе Объект действий над файла Возвращаемое значение При успешно Ошибки Функция posix_spawn_file_actions_adddup2 завершится неудачно, если: [EBADF] значение, заданное пара [EN0MEM] для расширения содержи Выполнение фу [EINVAL] значение, заданное пара Не считается ошибкой, если в качестве значения аргу Примеры Отсутствуют. Замечания по использованию Эта функция является частью опции Spawn и Логическое обоснование С Будущие направления Отсутствуют. Смотри также dup , posix_spawn , posix_spawn_file_actions_addclose , posix_spawn_file_actions_destroy , posix_spawnp , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6, основание При
posix_spawn_file_actions_destroy, posix_spawn_file_actions_init
Имя posix_spawn_file_actions_destroy, posix_spawn_file_actions_init — функции разрушения и инициализации объекта действий над файла Синопсис SPN #include int posix_spawn_file_actions_destroy (posix_spawn_file_actions_t *file___actions) ; int posix_spawn_file_actions_init (posix_spawn_file_actions_t *file_actions); Описание Функция posix_spawn_file_actions_destroy предназначена для разрушения объекта, адресуе Функция posix_spawn_file_actions_init используется для инициализации объекта, адресуемого параметром Объект действий над файла Возвращаемые значения При успешно Ошибки Функция posix_spawn_file_actions_init завершится неудачно, если: [EN0MEM] для Функция posix_spawn_file_actions_destroy [EINVAL] значение, заданное пара Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и Логическое обоснование С Будущие направления Отсутствуют. Смотри также posix_spawn , posix_spawnp , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основание В разделе «Синопсис» включение заголовка
posix_spawnattr_destroy, posix_spawnattr_init
Имя posix_spawnattr_destroy, posix_spawnattr_init— функции разрушения и инициализации объекта атрибутов порожденно Синопсис SPN #include int posix_spawnattr_destroy (posix_spawnattr_t *attr); int posix_spawnattr_init (posix_spawnattr_t *attr); Описание Функция posix_spawnattr_destroy предназначена для разрушения объекта атрибутов порожденного процесса. Разрушенный объект атрибутов, адресуемый параметром attr, можно снова инициализировать с помощью функции posix_spawnattr_init ; результаты ссылки на этот объект после его разрушения не определены. В конкретной реализации функция posix_spawnattr_destroy может устанавливать объект, адресуемый параметром attr, равным некоторому недействительному значению. Функция posix_spawnattr_init служит для инициализации объекта атрибутов порожденного процесса, адресуемого параметром attr, значениями, действующими по умолчанию для всех отдельных атрибутов, используемых конкретной реализацией. Результат вызова функции posix_spawnattr_init не определен, если заданный параметром attr объект атрибутов уже инициализирован. Объект атрибутов порожденного процесса имеет тип posix_spawnattr_t (определен в заголовке Для каждой реализации должны быть описаны отдельные атрибуты, которые она использует, и их стандартные значения, если они не определены стандартом IEEE Std ЮОЗ.1-2001. Атрибуты, не определенные стандартом IEEE Std 1003.1-2001, их стандартные значения и имена соответствующих функций чтения и записи этих атрибутов определяются конкретной реализацией. Результирующий объект атрибутов порожденного процесса (возможно, модифицированный путем установки значений отдельных атрибутов) используется для модификации поведения функций posix_spawn или posix_spawnp . После того как объект атрибутов был использован для порождения процесса путем вызова функции posix_spawn или posix_spawnp, любая функция, способная изменить объект атрибутов (включая функцию разрушения), не может повлиять на процесс, соз Возвращаемые значения При успешно Ошибки Функция posix_spawnattr_init завершится неудачно, если: [ ENOMEM ] для инициализации объекта атрибутов недостаточно существующей памяти. Функция posix_spawnattr_destroy [EINVAL ] з Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и Логическое обоснование Исходный интерфейс, предложенный в стандарте IEEE Std 1003.1-2001, определял атрибуты, наследуемые при выполнении операции порождения процесса, в виде структуры. Чтобы иметь возможность выделить некоторые необязательные атрибуты в отдельные опции (например, атрибуты spawn-schedparamn spawn-schedpolicy относятся к опции Process Scheduling), а также с целью расширяемости и совместимости с более новыми POSIX-интерфейсами, для интерфейса атрибутов был изменен тип данных. Этот интерфейс в настоящее время состоит из типа posix_spawnattr_t, представляющего объект атрибутов порожденного процесса, и соответствующих функций, которые позволяют инициализировать или разрушить этот объект атрибутов, а также установить или получить значение каждого отдельного атрибута. Несмотря на то что новый объектно-ориентированный интерфейс более сложен, чем исходнал структура, его проще использовать, легче наращивать и реализовывать. Будущие направления Отсутствуют. Смотри также posix_spawn , posix_spawnattr_getsigdefault , posix_spawnattr_getflags , posix_spawnattr_getpgroup , posix_spawnattr_getschedparam, posix_spawnattr_getschedpolicy , posix_spawnattr_getsigmask,posix_spawnattr_setsigdefault, posix_spawnattr_setflags, posix_spawnattr_setpgroup, posix_spawnattr_setsigmask, posix_spawnattr_setschedpolicy, posix_spawnattr_setschedparam , posix_spawnp , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999. При
posix_spawnattr_getflags, posix_spawnattr_setflags
Имя posix_spawnattr_getflags, posix_spawnattr_setflags— функции считывания и установки атрибута Синопсис SPN #include < spawn.h> int posix_spawnattr_getflags (const posix_spawnattr_t *restrict int posix_spawnattr_setflags (posix_spawnattr_t Описание Функция posix_spawnattr_getflags пре Функция posix_spawnattr_setflags пре Атрибут POSIX_SPAWN_RESETIDS POSIX_SPAWN_SETPGROUP POSIX_SPAWN_SETSIGDEF POSIX_SPAWN_SETSIGMASK PS POSIX_SPAWN_SETSCHEDPARAM POSIX_SPAWN_SETSCHEDULER Эти флаги определены в заголовке Возвращаемые значения При успешном выполнении функция posix_spawnattr_getflags возвращает нулевое значение и сохраняет значение атрибута При успешном выполнении функция posix_spawnattr_setflags возвращает нулевое значение, в противном случае — код ошибки, обозначающий ее характер. Ошибки Эти функции Функция posix_spawnattr__setflags [ EINVAL ] устанавливаемое значение атрибута недопустимо. Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не пре Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn , posix_spawnattr_destroy , posix_spawnattr_init , posix_spawnattr_getsigdefault , posix_spawnattr_getpgroup , posix_spawnattr_getschedparam, posix_spawnattr_getschedpolicy, posix_spawnattr_getsigmask , posix_spawnattr_setsigdefault , posix_spawnattr_setpgroup , posix_spawnattr_setschedparam, posix_spawnattr_setschedpolicy , posix_spawnattr_setsigmask , posix_spawnp , том Base Definitions стан Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стан
posix_spawnattr_getpgroup, posix_spawnattr_setpgroup
Имя posix_spawnattr_getpgroup, posix_spawnattr_setpgroup— функции считывания и установки атрибута Синопсис SPN #include int posix_spawnattr_getpgroup ( const posix_spawnattr_t *restrict attr, pid_t *restrict pid_t Описание Функция posix_spawnattr_getpgroup предназначена для получения значения атрибута Функция posix_spawnattr_setpgroup позволяет установить атрибут Атрибут Возвращаемые значения При успешном выполнении функция posix_spawnattr_getpgroup возвращает нулевое значение и сохраняет значение атрибута spawn-pgroup из объекта атрибутов, адресуемого параметром attr, в объекте, адресуемом параметром pgroup\ в противном случае возвращается код ошибки, обозначающий ее характер. При успешном выполнении функция posix_spawnattr_setgroup возвращает нулевое значение, в противном случае — код ошибки, обозначаю Ошибки Выполнение этих функций [EINVAL] значение, заданное пара Функция posix_spawnattr_setgroup [ EINVAL ] устанавливае Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не пре Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn, posix_spawnattr_destroy,posix_spawnattr_init, posix_spawnattr_getsigdefault, posix_spawnattr_getflags, posix_spawnattr_getschedparam, posix_spawnattr_getschedpolicy, posix_spawnattr_getsigmask, posix_spawnattr_setsigdefault, posix_spawnattr_setflags , posix_spawnattr_setschedparam, posix_spawnattr_setschedpolicy, posix_spawnattr_setsigmask, posix_spawnp , том Base Deflnitions стан Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999.
posix_spawnattr_getschedparam, posix_spawnattr_setschedparam
Имя posix_spawnattr_getschedparam, posix_spawnattr_setschedparam функции считывания и установки атрибута Синопсис SPNPS #include #include int posix_spawnattr_getschedparam (const posix_spawnattr_t *restrict attr, struct sched_param *restrict int posix_spawnattr_setschedparam (posix_spawnattr_t *restrict Описание Функция posix_spawnattr_getschedparam предназначена для получения значения атрибута spawn-schedparamn3 объекта атрибутов, адреcуемого параметром attr. Функция posix_spawnattr_setschedparam позволяет установить атрибут Атрибут Возвращаемые значения При успешном выполнении функция posix_spawnattr_getschedparam возвращает нулевое значение и сохраняет значение атрибута При успешно Ошибки Выполнение этих функций [ EINVAL] значение, заданное пара Функция posix_spawnattr_setschedparam [ EINVAL ] устанавливае Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опций Spawn и Process Scheduling и могут быть не представлены во всех реализациях. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn , posix_spawnattr_destroy , posix_spawnattr_init , posix_spawnattr_getsigdefault, posix_spawnattr_getflags, posix_spawnattr_getpgroup, posix_spawnattr_getschedpolicy, posix_spawnattr_getsigmask, posix_spawnattr_setsigdefault, posix_spawnattr_setflags, posix_spawnattr_setpgroup, posix_spawnattr_setschedpolicy, posix_spawnattr_setsigmask, posix_spawnp , том Base Definitions стан Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999.
posix_spawnattr_getschedpolicy, posix_spawnattr_setschedpolicy
Имя posix_spawnattr_getschedpolicy, posix_spawnattr_setschedpolicy — функции считывания и установки атрибута Синопсис SPN #include #include int posix_spawnattr_getschedpolicy (const posix_spawnattr_t *restrict attr, int *restrict int posix_spawnattr_setschedpolicy ( posix_spawnattr_t *attr, int Описание Функция posix_spawnattr_getschedpolicy предназначена для получения значения атрибута Функция posix_spawnattr_setschedpolicy позволяет установить атрибут Атрибут Возвращаемые значения Ошибки Выполнение этих функций Функция posix_spawnattr_setschedpolicy можетзавершиться неудачно, если: [ EINVAL ] устанавливаемое значение атрибута недопустимо. Пр Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опций Spawn и Process Scheduling и могут быть не пре Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn , posix_spawnattr_destroy , posix_spawnattr_init , posix_spawnattr_getsigdefault, posix_spawnattr_getflags, posix_spawnattr_getpgroup , posix_spawnattr_getschedparam, posix_spawnattr_getsigmask , posix_spawnattr_setsigdefault , posix_spawnattr_setflags, posix_spawnattr_setpgroup, posix_spawnattr_setschedparam, posix_spawnattr_setsigmask, posix_spawnp , том Base Definitions стандарта1ЕЕЕ Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999.
posix_spawnattr_getsigdefault, posix_spawnattr_setsigdefault
Имя posix_spawnattr_getsigdefault, posix_spawnattr_setsigdefault — функции считывания и установки атрибута Синопсис SPN #include #include int posix_spawnattr_getsigdefault ( const posix_spawnattr_t *restrict attr, sigset_t *restrict int posix_spawnattr_setsigdefault ( posix_spawnattr_t *restrict attr, const sigset_t *restrict Описание Функция posix_spawnattr_getsigdefault предназначена для получения значения атрибута Функция posix_spawnattr_setsigdefault позволяет установить атрибут Атрибут Возвращаемые значения При успешно При успешно Ошибки Выполнение этих функций Функция posix_spawnattr_setsigdefault Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn , posix_spawnattr_destroy , posix_spawnattr_init , posix_spawnattr_getflags , posix_spawnattr_getpgroup , posix_spawnattr_getschedparam, posix_spawnattr_getschedpolicy , posix_spawnattr_getsigmask , posix_spawnattr_setflags , posix_spawnattr_setpgroup , posix_spawnattr_setschedparam, posix_spawnattr_setschedpolicy, posix_spawnattr_setsigmask , posix_spawnp , том Base Definidons стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Stdl003.1d-1999.
posix_spawnattr_getsigmask, posix_spawnattr_setsigmask
Имя posix_spawnattr_getsigmask, posix_spawnattr_setsigmask— функции считывания и установки атрибута spawn-sigmask из объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include #include int posix_spawnattr_getsigmask ( const posix_spawnattr_t *restrict attr, sigset_t *restrict posix_spawnattr_t *restrict Описание Функция posix_spawnattr_getsigmask предназначена для получения значения атрибута Функция posix_spawnattr_setsigmask позволяет установить атрибут Атрибут Возвращаемые значения При успешно При успешно Ошибки Выполнение этих функций [EINVAL] значение, заданное пара Функция posix_spawnattr_setsigmask может завершиться неудачно, если: [EINVAL ] устанавливаемое значение атрибута недопустимо. Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не представлены во всех реализациях. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn , posix_spawnattr_destroy , posix_spawnattr_init , posix_spawnattr_getsigdefault, posix_spawnattr_getflags, posix_spawnattr_getpgroup , posix_spawnattr_getschedparam, posix_spawnattr_getschedpolicy, posix_spawnattr_setsigdefault, posix_spawnattr_setflags , posix_spawnattr_setpgroup , posix_spawnattr_setschedparam, posix_spawnattr_setschedpolicy, posix_spawnp , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEEStd 1003.1d-1999.
pthread_attr_destroy, pthread_attr_init
Имя pthread_attr_destroy, pthread_attr_init — функции разрушения и инициализации объекта атрибутов потока. Синопсис THR #include int pthread_attr_destroy (pthread_attr_t *attr); int pthread_attr_init (pthread_attr_t *attr); Описание Функция pthread_attr_destroy предназначена для разрушения объекта атрибутов потока. В конкретной реализации функция pthread_attr_destroy Функция pthread_attr_init позволяет инициализировать объект атрибутов потока, адресуемый параметром attr, значением, действующим по умолчанию для всех отдельных атрибутов, используемых в данной реализации. Результирующий объект атрибутов (воз Возвращаемые значения При успешно Ошибки Функция pthread_attr_init завершится неудачно, если: [ENOMEM] для инициализации объекта атрибутов потока недостаточно существующей па Эти функции не возвращают код ошибки в виде значения [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствует. Логическое обоснование Объекты атрибутов используются для потоков, мьютексов и условных переменных в качестве будущего механизма поддержки стандартизации в этих областях, не требующего изменения самих функций. Объекты атрибутов обеспечивают четкую автономность реконфигурируемых аспектов потоков. Например, важным атрибутом потока является «размер стека», который при переносе многопоточной программы с одного компьютера на другой часто приходится корректировать. Использование объектов атрибутов позволит вносить необходимые изменения в одном месте программы, а не в разных местах, «разбросанных» по всем экземплярам потоков. Объекты атрибутов можно использовать для создания классов потоков с аналогичными атрибутами; например, «потоков с большими стеками и высоким приоритетом» или «потоков с минимальными стеками». Эти классы можно определить в одном месте программы, а затем их использовать, когда понадобится создать поток. В результате значительно упростится процесс изменения «классовых» решений потоков, и не придется подробно анализировать каждый вызов функции pthread_create . Объекты атрибутов с целью потенциальной расширяемости определяются как «закрытые» типы. Если бы они были определены как «прозрачные» структуры, то при добавлении новых атрибутов (т.е. при расширении объектов атрибутов) пришлось бы перекомпилировать все многопоточные программы, что не всегда возможно, например, если различные программные компоненты приобретены у различных изготовителей. Кроме того, «непрозрачные» объекты атрибутов предоставляют возможность для повышения быстродействия. Достоверность атрибутов можно проверить один раз при их установке, а не при каждом создании потока. Ведь реализации зачастую требуют кэширования объектов ядра, создание которых считается «дорогим удовольствием». Именно «непрозрачные» объекты атрибутов позволяют вовремя определить, в какой момент кэшированные объекты становятся недействительными из-за изменения атрибутов. Поскольку оператор присваивания необязательно должен быть определен для каждого «непрозрачного» типа, значения, определяемые конкретной реализацией по умолчанию, невозможно назначать без ущерба для переносимости. Для решения этой проблемы можно позволить динамическую инициализацию объектов атрибутов с помощью соответствующих функций инициализации, и тогда значения, действующие по умолчанию, реализация сможет назначать автоматически. В качестве предполагаемой альтернативы поддержки атрибутов были представлены следующие предложения. 1. Поддерживается стиль передачи функциям инициализации (pthread_create , pthread_mutex_init , pthread_cond_init ) параметра, формируемого пу-тем применения поразрядной операции включающего ИЛИ к флагам. Содержащий эти флаги параметр (в расчете на расширяемость в булущем) должен иметь «непрозрачный» тип. Если в этом параметре флаги не установлены, то объекты создаются с использованием характеристик, действующих по умолчанию. Реализация самостоятельно может задавать значения флагов и соответствующее им поведение. 2. Если необходима дальнейшая специализация мьютексов и условных переменных, в реализациях могут быть определены дополнительные процедуры, предназначенные для выполнения действий над объектами типа pthread_mutex_t и pthread_cond_t (а не над объектами атрибутов). При внедрении этого решения возможны следующие трудности. 1. Побитовая маска не будет считаться «закрытой», если биты должны быть установлены в векторных объектах атрибутов с использованием явно закодированных поразрядных операций включающего .ИЛИ. Если количество опций превышает размер типа int, прикладные программисты должны знать местоположение каждого бита. Если биты устанавливаются или считываются путем средств инкапсуляции (т.е. с помощью функций считывания и установки), то побитовал маска будет представлять собой всего лишь реализацию объектов атрибутов без свободного доступа для программиста. 2. Многие атрибуты имеют тип, отличный от булевого, или представляют собой малые целые значения. Например, для задания стратегии планирования можно выделить 3 или 4 бит, но для приоритета потребуется 5 или больше бит, следовательно по меньшей мере 8 из 16 возможных бит (для компьютеров с 16-разрядными целочисленными значениями) уже «занято». Поэтому побитовая маска может корректно управлять только атрибутами булевого типа («установлен» или нет) и не может служить в качестве хранилища для значений иного типа. Такие значения необходимо задавать или в качестве параметров функций (которые не относятся к числу наращиваемых), или путем установки полей структуры (которые не являются «закрытыми»), или с помощью функций доступа, т.е. функций считывания и записи (которые делают побитовую маску излишним дополнением к объектам атрибутов). Размер стека определяется как необязательный атрибут, поскольку само понятие стека зависит от конкретного компьютера. Например, в одних реализациях невозможно изменить размер стека, а в других в этом вообще нет необходимости, поскольку страницы стека могут быть несмежными и выделяться (и освобождаться) по требованию. Механизм атрибутов разработан по большей мере ради расширяемости. Будущие дополнения к механизму атрибутов или любому объекту атрибутов, определенному в этом томе (разделе) стандарта IEEE Std 1003.1-2001, необходимо вносить с чрезвычайной тщательностью, чтобы они не отразились на совместимости на уровне машинных кодов. Объекты атрибутов, даже создаваемые с помощью таких функций динамического распределения памяти, как malloc, во время компиляции могут иметь фиксированный размер. Это означает, например, что функция pthread_create в реализации с дополнениями для использования типа pthread_attr_t не сможет «видеть» за пределами области, которую двоичное приложение считает допустимой. Это говорит о том, что реализации должны поддерживать в объекте атрибутов поле размера, а также информацию о версии, если дополнения приходится использовать в различных направлениях (или объединять продукты различных изготовителей). Будущие направления Отсутствуют. Смотри также pthread_attr_getstackaddr, pthread_attr_getstacksize, pthread_attr_getdetachstate , thread_create , том Base Definidons стандарта IEEEStd 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_attr_destroy и pthread_attr_init от При
pthread_attr_getdetachstate, pthread_attr__setdetachstate
Имя pthread_attr_getdetachstate, pthread_attr__setdetachstate — функции считывания и записи атрибута Синопсис THR #include int pthread_attr_getdetachstate ( const pthread_attr_t int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate) ; Описание Атрибут Функции pthread_attr_getdetachstate и pthread_attr_setdetachstate считывают и устанавливают соответственно атрибут С по С по Значение PTHREAD_CREATE_DETACHED используется для перевода всех потоков, создавае Возвращаемые значения При успешно Функция pthread_attr_getdetachstate при успешно Ошибки Функция pthread_attr_setdetachstate завершится неу Примеры Отсутствуют. Замечания по использованию Отсутствует. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy , pthread_attr_getstackaddr , pthread_attr_getstacksize , pthread_create , том Base Definidons стандарта IEEEStd 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_attr_getdetachstate и pthread_attr_setdetachstate от Раздел «Описание» был отредактирован с целью исключить из него слово «must» («должен»).
pthread_attr_getguardsize, pthread_attr_setguardsize
Имя pthread_attr_getguardsize, pthread_attr_setguardsize— функции считывания и установки значения потоково Синопсис XSI #include int pthread_attr_getguardsize ( const pthread_attr_t *restrict attr, size_t *restrict int pthread_attr_setguardsize (pthread_attr_t *attr, size_t Описание Функция pthread_attr_getguardsize используется для считывания атрибута Функция pthread_attr_setguardsize позволяет установить атрибут Атрибут Реализация может округлить значение, содержащееся в атрибуте guardsize, до числа, кратного значению реконфигурируемой системной переменной {PAGESIZE} (см. заголовок По у Если предварительно был установлен атрибут Возвращаемые значения При успешно Ошибки Функция pthread_attr_getguardsize завершится неудачно, если: [EINVAL ] значение, заданное пара [ EINVAL ] значение пара Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствует. Логическое обоснование Атрибут 1. На защиту от переполнения могут потенциально затрачиваться существенные системные ресурсы. Для приложения, в котором создается большое количество потоков и существует уверенность в том, что при выполнении потоков их стеки никогда не будут переполнены, можно сэкономить системные ресурсы, отключив выделение областей защиты. 2. Если потоки размещают в стеке большие структуры данных, то для обнаружения факта переполнения стека могут понадобиться области защиты большого объема. Будущие направления Отсутствуют. Смотри также То Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Из раздела «Ошибки» было удалено третье условие возникновения ошибки [EINVAL] , поскольку оно включается во второе условие. В целях согласования со стандарто
pthread_attr_getinheritsched, pthread_attr_setinheritsched
Имя pthread_attr_getinheritsched, pthread_attr_setinheritsched— функции считывания и установки атрибута Синопсис THRTPS #include int pthread_attr_getinheritsched ( const pthread_attr_t *restrict int pthread_attr_setinheritsched (pthread_attr_t int Описание Функции pthread_attr_getinheritsched и pthread_attr_setinheritsched используются для считывания и установки соответственно атрибута Если при вызове функции pthread_create используются объекты атрибутов, то атрибут Значение PTHREAD_INHERIT_SCHED говорит о то Значение PTHREAD_EXPLICIT_SCHED подразу Значения PTHREAD_INHERIT_SCHED и PTHREAD_EXPLICIT_SCHED определяются в заголовке От значения атрибута Возвращаемые значения При успешно Ошибки Функция pthread_attr_setinheritsched [EINVAL] значение, заданное пара [ENOTSUP] была сделана попытка установить атрибут равны Эти функции не возвра Примеры Отсутствуют. Замечания по использованию После установки этих атрибутов поток Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy , pthread_attr_getscope , pthread_attr_getschedpolicy, pthread_attr_getschedparam, pthread_create , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение От Issue 6 Функции pthread_attr_getinheritsched и pthread_attr_setinheritsched от Условие ошибки [ENOSYS] было удалено,поскольку в заглушках нет необходимости, если реализация не поддерживает опцию Thread Execution Scheduling. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_attr_getinheritsched было добавлено ключевое слово restrict.
pthread_attr_getschedparam, pthread_attr_setschedparam
Имя pthread_attr_getschedparam, pthread_attr_setschedparam— функции считывания и установки атрибута Синопсис THR #include int pthread_attr_getschedparam (const pthread_attr_t *restrict attr, struct sched_param *restrict param); int pthread_attr_setschedparam (pthread_attr_t *restrict attr,const struct sched_param *restrict param); Описание Функции pthread_attr_getschedparam и pthread_attr_setschedparam используются для считывания и установки соответственно атрибутов параметров планирования в объекте, заданном параметром attr. Содержимое структуры TSP Для установки стратегии планирования SCHED_SPORADIC необходимо установить следующие члены структуры Возвращаемые значения При успешном завершении функции pthread_attr_getschedparam и pthread_attr_setschedparam возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_attr_setschedparam [EINVAL] значение, заданное пара [ ENOTSUP ] была сделана попытка установить атрибут равны Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию После установки этих атрибутов поток Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy, pthread_attr_getscope, pthread_attr_getinheritsched, pthread_attr_getschedpolicy, pthread_create , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_attr_getschedparam и pthread_attr_setschedparam отмечены как часть опции Threads. В целях согласования со стандарто В целях согласования со стандарто
pthread_attr_getschedpolicy, pthread_attr_setschedpolicy
Имя pthread_attr_getschedpolicy, pthread_attr_setschedpolicy — функции считывания и установки атрибута Синопсис THR, TPS #include int pthread_attr_getschedpolicy (const pthread_attr_t *restrict attr, int *restrict int pthread_attr_setschedpolicy (pthread_attr_t *attr, int Описание Функции pthread_attr_getschedpolicy и pthread_attr_setschedpolicy используются для считывания и установки соответственно атрибута Для обозначения стратегии планирования поддерживаются значения SCHED_FIF0, SCHED_RR и SCHED_OTHER, которые определены в заголовке TSP Когда потоки, выполняющиеся с использованием стратегий планирования SCHED_FIFO, SCHED_RR или SCHED_SPORADIC, ожидают освобождения мьютекса, то его получение (после разблокировки) происходит согласно приоритета Возвращаемые значения При успешном завершении функции pthread_attr_getschedpolicy и pthread_attr_setschedpolicy возвращают нулевое значение; в противно Ошибки Функция pthread_attr_setschedpolicy [EINVAL] значение, заданное пара [ENOTSUP] была сделана попытка установить атрибут равным значению, которое не поддерживается реализацией. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию После установки этих атрибутов поток можно создать путем вызова функции pthread_create с использованием объекта атрибутов. Применение этих функций не оказывает влияния на поток, выполняемый в данный момент. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy, pthread_attr_getscope, pthread_attr_getinheritsched , pthread_attr_getschedparam, pthread_create , том Base Definidons стан Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение От Issue 6 Функции pthread_attr_getschedpolicy и pthread_attr_setschedpolicy от Условие ошибки [ENOSYS] было удалено, поскольку в заглушках нет необходимости, если реализация не поддерживает опцию Thread Execution Scheduling. В целях согласования со стандарто В целях согласования со стандарто
pthread_cancel
Имя pthread_cancel — функция от Синопсис THR #include int pthread_cancel (pthread_t Описание Функция pthread_cancel создает запрос на отмену потока. Когда именно отмена вступит в силу, зависит от текущего состояния потока, заданного параметром Действия, связанные с от Возвращаемое значение При успешном завершении функция pthread_cancel возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_cancel [ESRCH] не удалось найти поток, иде Функция pthread_cancel не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Для отправки потоку уведо Преимущество варианта, прелусматривающего создание нового сигнала, состояло в том, что критерии его выдачи были бы во многом идентичны тем, которые использовались при попытке выдать любой другой сигнал, поэтому сигнальный механизм уведомления об отмене казался унифицированным. И в самом деле, во многих реализациях отмена потоков осуществляется посредством специального сигнала. Однако до сих пор не существовало ни одной сигнальной функции (за исключением функции pthread_kill), которую можно было бы использовать совместно с этим новым сигналом, поскольку поведение выдаваемого сигнала отмены должно было отличаться от поведения любого из уже определенных сигналов. К достоинству варианта создания специальной функции можно отнести осознание того, что уведомление об отмене потока было бы в этом случае четко определенным. Кроме того, механизм выдачи уведомления об отмене не требует реализации в виде сигнала. Ведь если такой механизм заметно ближе к сигналам, то ему свойственны аналогии с языковым механизмом исключительных ситуаций, которые потенциально не видны. В конечном счете, поскольку необходимость обеспечивать обработку большого числа исключительных ситуаций при использовании нового сигнала с существующими сигнальными функциями может неоправданно усложнить (даже запутать) процесс отмены потока, было решено сделать выбор в пользу специальной функции, которая устраняет эту проблему. Такая функция была тщательно разработана, причем так, что любая реализация могла бы обеспечить «безоговорочное» выполнение процедуры отмены «поверх» каких бы то ни было сигналов. Наличие специальной функции отмены потока также означает, что реализации не обязаны обеспечивать процедуру отмены с помощью сигналов. Будущие направления Отсутствуют. Смотри также pthread_exit , pthread_cond_timedwait , pthread_join , pthread_setcancelstate , то Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширение Issue 6 Функция pthread_cancel от
pthread_cleanup_pop, pthread_cleanup_push
Имя pthread_cleanup_pop, pthread_cleanup_push— функции создания обработчиков запроса об от Синопсис THR #include void pthread_cleanup_pop (int execute); void pthread_cleanup_push (void (* Описание Функция pthread_cleanup_pop используется для извлечения функции, расположенной в вершине стека вызываю Функция pthread_cleanup_push позволяет поместить в стек вызывающего потока заданную функцию обработчика • поток существует (т.е. он вызывает функцию pthread_exit ); • поток действует в соответствии с запросом отмены; • поток вызывает функцию pthread_cleanup_pop с ненулевым значением аргумента execute. Эти функции можно реализовать как макросы. Приложение должно гарантировать, что они имеют форму инструкций и используются попарно в пределах одного и того же лексического контекста (чтобы макрос pthread_cleanup_push раскрывался в список лексем, начинающийся лексемой '{', а макрос pthread_cleanup_pop раскрывался в список лексем, завершающийся соответствующей лексемой '}'). Результат вызова функции longjmp или siglongjmp не определен, ec-ли имели место обращения к функции pthread_cleanup_push или pthread_cleanup_pop без соответствующего «парного» вызова по причине заполнения буфера переходов. Результат вызова функции longjmp или siglongjmp из обработчика, предназначенного для выполнения подготовительных действий по аннулированию потока, также не определен. Возвращаемые значения Функции pthread_cleanup_push и hread_cleanup_pop не возвра Ошибки Ошибки не определены. Эти функции не возвращают код ошибки [EINTR]. Примеры Следующий код представляет собой пример использования примитивов потока для реализации блокировки чтения-записи (с приоритетом для записи) с возможностью отмены. typedef struct { pthread_mutex_t lock; pthread_cond_t rcond, wcond; int lock_count; /* lock_count < 0 .. Удерживается записывающим потоком. */ /* lock_count > 0 .. Удерживается lock_count считывающими * потоками. */ /* lock_count = 0 .. Ничем не удерживается. */ int waiting_writers; /* Счетчик ожидающих записывающих * потоков. */ } rwlock; void waiting_reader_cleanup (void, *arg) { rwlock *1; 1 = (rwlock *) arg; pthread_mutex_unlock (&l->lock); } void lock_for_read (rwlock *1) { pthread_mutex_lock (&l->lock); pthread_cleanup_push (waiting_reader_cleanup, 1) ; while ((l->lock_count < 0) && (l->waiting_writers ! = 0)) pthread_cond_wait (&l->rcond, &l->lock); l->lock_count++; /* * Обратите внимание на то, что функция pthread_cleanup_pop * выполняет здесь фyнкциюwaiting_reader_cleanup. */ pthread_cleanup_pop(l); } void release_read_lock (rwlock *1) { pthread_mutex_lock (&l->lock); if (--l->lock_count == 0) pthread_cond_signal (&l->wcond); pthread_mutex_unlock (1); void waiting_writer_cleanup (void *arg) { rwlock *1; 1 = (rwlock *) arg; if ((—l->waiting_writers == О) && (l->lock_count >= 0)) { /* * Это происходит только в случае отмены потока. */ pthread_cond_broadcast (&l->wcond); } pthread_mutex_unlock (&l->lock); } void lock_for_write (rwlock *1) { pthread_mutex_lock (&l->lock),-l->waiting_writers++; pthread_cleanup_push (waiting_writer_cleanup, 1); while (l->lock_count ! = О) pthread_cond_wait (&l->wcond, &l->lock); l->lock_count = -1; /* * Обратите внимание на то, что функция pthread_cleanup_pop * выполняет здесь функцию waiting_writer_cleanup. */ pthread_cleanup_pop (1); } void release_write_lock (rwlock *1) { pthread_mutex_lock (&l->lock); l->lock_count = 0; if (l->waiting_writers == О) pthread_cond_broadcast (&l->rcond) else pthread_cond_signal (&l->wcond); pthread_mutex_unlock (&l->lock); } /* * Эта функция вызывается для инициализации блокировки * чтения-записи. */ void initialize_rwlock (rwlock *1) { pthread_mutex_init (&l->lock, pthread_mutexattr_default); pthread_cond_init (&l->wcond, pthread_condattr_default); pthread_cond_init (&l->rcond, pthread_condattr_default); l->lock_count = О; l->waiting_writers = О; \ Приложение Б 559 } reader_thread { lock_for_read (&lock); pthread_cleanup_push (release_read_lock, &lock); /* * Поток устанавливает блокировку для чтения. */ pthread_cleanup_pop (1); } writer_thread { lock_for_write (&lock); pthread_cleanup_push (release_write_lock, &lock); /* * Поток устанавливает блокировку для записи. */ pthread_cleanup_pop (1) ; } Замечания по использованию Две описываемые здесь функции, pthread_cleanup_push и pthread_cleanup_pop , которые помещают и извлекают из стека обработчики запроса на отмену потока, можно сравнить с левой и правой круглыми скобками. Их нужно всегда использовать «в паре». Логическое обоснование Ограничение, налагае #define pthread_cleanup_push (rtn, arg) { \ struct _pthread_handler_rec _cleanup_handler, **_head; \ _cleanup_handler.rtn = rtn; \ _cleanup_handler.arg = arg; \ (void) pthread_getspecific (_pthread_handler_key, &_head); \ _cleanup_handler.next = *_head; \ *_head = &_cleanup_handler; #define pthread_cleanup_pop (ex) \ *_head = _cleanup_handler.next; \ if (ex) (*_cleanup_handler.rtn) (_cleanup_handler.arg); \ } Возможна даже более «смелая» реализация этих функций, которая позволит компилятору «считать» обработчик запроса на отмену константой, значение которой можно «встраивать» в код. В данном томе стандарта IEEE Std 1003.1-2001 пока оставлен неопределенным результат вызова функции longjmp из обработчика сигнала, выполняемого в функции библиотеки POSIX System Interfaces. Если в какой-то реализации потребуется разрешить этот вызов и придать ему надлежащее поведение, функция longjmp должна в этом случае вызвать все обработчики запроса на отмену, которые были помещены в стек (но еще не извлечены из него) с момента вызова функции setjmp . Рассмотрим многопоточную функцию, вызываемую одним потоком, который использует сигналы. Если бы сигнал был выдан обработчику сигналов во время операции qsort, и этому обработчику пришлось бы вызвать функцию longjmp (которая в свою очередь не вызывала бы обработчики запроса на отмену), то вспомогательные потоки, создаваемые функцией qsort , не были бы аннулированы. Они бы продолжали выполняться и осуществляли запись в массив аргументов даже в том случае, если этот массив был к тому времени извлечен из стека. Обратите внимание на то, что такой механизм обработки запросов на отмену особенно тесно связан с языком С, и, несмотря на требование независимости языка, предъявляемое к любому унифицированному механизму выполнения «очистительно-восстановительных работ», подобный механизм, выраженный в других языках, может быть совершенно иным. Кроме того, необходимость этого механизма в действительности связана только с отсутствием реального механизма обработки исключительных ситуаций в языке С, который был бы идеальным решением. Здесь отсутствуют замечания о функции безопасной отмены потока. Если приложение в своих обработчиках сигналов не имеет точек отмены, блокирует любой сигнал, обработчик которого может иметь точки отмены (несмотря на вызов асинхронно-опасных функций), или запрещает отмену (несмотря на вызов асинхронно-опасных функций), все функции можно безопасно вызывать из функций обработки запросов на отмену потоков. Будущие направления Отсутствуют. Смотри также pthread_cancel, pthread_setcancelstate, то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_cleanup_pop и pthread_cleanup_push от Добавлен раздел «За Раздел «Описание» был отредактирован с целью исключить из него слово « must» («должен»).
pthread_cond_broadcast,pthread_cond_signal
Имя pthread_cond_broadcast,pthread_cond_signal Описание Эти функции используются для разблокировки потоков, заблокированных с помощью переменной условия. Функция pthread_cond_broadcast позволяет разблокировать все потоки, заблокированные в данный момент с использованием переменной условия, заданной параметром Функция pthread_cond_signal используется для разблокировки по крайней мере одного из потоков, заблокированных с использованием условной переменной, заданной параметром cond (если таковые существуют). Если с использованием этой переменной условия заблокировано несколько потоков, то порядок разблокировки будет определен в соответствии с их стратегией планирования. Когда каждый поток, разблокированный в результате вызова функции pthread_cond_broadcast или pthread_cond_signal, вернется из вызванной им функции pthread_cond_wait или pthread_cond_timedwait, этот поток получит мьютекс, с которым была вызвана функция pthread_cond_wait или pthread_cond_timedwait. Разблокированные потоки будут состязаться за мьютекс в соответствии с их стратегией планирования (если это имеет смысл), как будто каждый из них вызвал функцию pthread_mutex_lock . Функции pthread_cond_broadcast и pthread_cond_signal могут быть вызваны потоком, владеющим (или нет) в данный момент мьютексом. При этом потоки, вызвавшие функцию pthread_cond_wait или pthread_cond_timedwait , связали во время ожидания этот мьютекс с условной переменной. Однако, если необходимо обеспечить прогнозируемое поведение, этот мьютекс может быть заблокирован потоком, вызвавшим функцию pthread_cond_broadcast или pthread_cond_signal . Функции pthread_cond_broadcast и pthread_cond_signal не будут иметь результата, если в данный момент не су Возвращаемые значения При успешном завершении функции pthread_cond_broadcast и pthread_ cond_signal возвра pthread_cond_broadcast, pthread_cond_signal — функции разблокировки потоков, заблокированных с по Синопсис THR #include int pthread_cond_broadcast (pthread_cond_t int pthread_cond_signal (pthread_cond_t |Ошибки Функции pthread_cond_broadcast и pthread_cond_signal [EINVALJ значение, заданное параметром cond, не ссылается на инициализированную условную переменную. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Функция pthread_cond_broadcast используется при изменении состояния общей переменной в ситуации, когда выполняется сразу несколько потоков. Рассмотрим задачу с участием одного «изготовителя» и нескольких «потребителей», в которой «изготовитель» может вставить в список несколько элементов, к которым могут получать доступ «потребители» (по одному элементу за раз). Путем вызова функции pthread_cond_broadcast «изготовитель» уведомляет о своем действии всех «потребителей», которые, возможно, находятся в состоянии ожидания, и, таким образом, при использовании мультипроцессора приложение может достичь более высокой пропускной способности. Кроме того, функция pthread_cond_broadcast позволяет упростить реализацию блокировки чтения-записи. Функция pthread_cond_broadcast весьма полезна, когда записывающий поток освобождает блокировку, и нужно «запустить» все «читающие» потоки, находящиеся в состоянии ожидания. Наконец, эту широковещательную функцию можно использовать в двухфазном алгоритме фиксации для уведомления всех клиентов о предстоящей фиксации транзакции. Функцию pthread_cond_signal небезопасно использовать в обработчике сигналов, который вызывается асинхронно. Даже если это было бы безопасно, имела бы место «гонка» данных между проверками булевой функции pthread_cond_ wait , которую невозможно эффективно устранить. Следовательно, мьютексы и переменные условий не подходят для освобождения ожидающего потока путем сигнализации из кода обработчика сигналов. Логическое обоснование Несколько запусков по условному сигналу Для мультипроцессора, скорее всего, невозможно применить функцию pthread_cond_signal, чтобы избежать разблокировки нескольких потоков, заблокированных с использованием условной переменной. Рассмотрим, например, следующую частичную реализацию функций pthread_cond_wait и pthread_cond_ signal, выполняемых потоками в заданном порядке. Один поток пытается «дождаться» нужного значения условной переменной, другой при этом выполняет функцию pthread_cond_signal, в то время как третий поток уже находится в состоянии ожидания. pthread_cond_wait(mutex, cond): value = cond->value; /* 1 */ pthread_mutex_unlock (mutex); /* 2 */ pthread_mutex_lock (cond->mutex); /* 10 */ if (value == cond->value) { /* 11 */ me->next_cond = cond->waiter; cond->waiter = me; pthread_mutex_unlock(cond->mutex); unable_to_run (me); } else pthread_mutex_unlock (cond->mutex); /* 12 */ pthread_mutex_lock (mutex); /* 13 * / pthread_cond_signal (cond): pthread_mutex_lock (cond->mutex); /* 3 */ cond->value++; /* 4 */ if (cond->waiter) { /* 5 */ sleeper = cond->waiter; /* 6 */ cond->waiter = sleeper->next_cond; /* 7 */ able_to_run (sleeper); /* 8 */ } pthread_mutex_unlock (cond->mutex); /* 9 */ Итак, в результате одного обращения к функции pthread_cond_signal сразу несколько потоков могут вернуться из вызова функции pthread_cond_wait или pthread_cond_timedwait . Такой эффект называется «фиктивным запуском». Обратите внимание на то, что подобная ситуация является самокорректирующейся благодаря тому, что количество потоков, «пробуждающихся» таким путем, ограничено; например, следующий поток, который вызывает функцию pthread_cond_wait , после определенной последовательности событий блокируется. Несмотря на то что эту проблему можно было бы решить, потеря эффективности ради обработки дополнительного условия, которое возникает лишь иногда, неприемлема, особенно в случае, когда нужно протестировать предикат, связанный с условной переменной. Корректировка этой проблемы слишком уж понизила бы уровень параллелизма в этом базовом стандартном блоке при выполнении всех высокоуровневых операций синхронизации. В разрешении «фиктивных запусков» есть одно дополнительное преимущество: знал о них, разработчикам приложений придется прелусмотреть цикл тестирования предиката при ожидании наступления нужного условия. Это также вынудит приложение «терпеливо» отнестись к распространению «лишних» условных сигналов, связанных с одной и той же условной переменной, формирование которых может быть закодировано в какой-то другой части приложения. В результате приложения станут более устойчивыми. Поэтому в стандарте IEEE Std 1003.1-2001 в прямой форме отмечена возможность возникновения «фиктивных запусков». Будущие направления Отсутствуют. Смотри также pthread_cond_destroy , pthread_cond_timedwait , том Base Definitions cтaндapтa IEEEStd 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_cond_broadcast и pthread_cond_signal от Добавлен раздел «Замечания по использованию» (APPLICATION USAGE).
pthread_cond_destroy, pthread_cond_init
Имя pthread_cond_destroy, pthread_cond_init Синопсис THR #include int pthread_cond_destroy (pthread_cond_t *cond); int pthread_cond_init ( pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); pthread_cond_t cond = PTHREAD_COND_INITIALIZER; Описание Функция pthread_cond_destroy используется для разрушения условной пере Нет никакой опасности в разрушении инициализированной условной переменной, по которой не заблокирован в данный момент ни один поток. Попытка же разрушить условную переменную, по которой заблокированы в данный момент другие потоки, может привести к неопределенному поведению. Функция pthread_cond_init используется для инициализации условной пере Для осуществления синхронизации используется только сама условная переменная Если атрибуты условной переменной, действующие по умолчанию, заранее определены, для инициализации условных переменных, которые создаются статически, можно использовать макрос PTHREAD_COND_INITIALIZER. Результат в это Возвращаемые значения При успешно Проверка на наличие ошибок с кодами [EBUSY] и [EINVAL] реализована так (если реализована вообще), как будто она выполняется в самом начале работы каждой функции, и код ошибки в случае ее обнаружения возвращается до модификации состояния условной переменной, заданной параметром Ошибки Функция pthread_cond_destroy может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку разрушить объект, адресуемый параметром cond, который относится к другому потоку (например, при использовании в функциях pthread_cond_wait или pthread_cond_timedwait ); [EINVAL] значение, заданное пара Функция pthread_cond_init завершится неудачно, если: [EAGAIN] система испытывает недостаток в ресурсах (не имеется в виду память), необходимых для инициализации еще одной условной переменной; [ENOMEM] для инициализации условной переменной недостаточно существующей памяти. Функция pthread_cond_init может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку повторно инициализировать объект условной переменной, адресуемый параметром cond, которой был ранее инициализирован, но еще не разрушен; [ EINVAL ] значение, заданное параметром аttr, недействительно. Примеры Условную пере struct list { pthread_mutex_t lm; } struct elt { key k; int busy; pthread_cond_t notbusy; } /* Находим элемент списка и сохраняем его. */ struct elt * list_find (struct list *lp, key k) { struct elt *ep; pthread_mutex_lock (&lp->lm); while ((ep = find_elt (1, к) ! = NULL) && ep->busy) pthread_cond_wait (&ep->notbusy, &lp->lm); if (ер != NULL) ep->busy = 1; pthread_mutex_unlock (&lp->lm) ; return (ер); } delete_elt (struct list *lp, struct elt *ep) { pthread_mutex_lock (&lp->lm); assert (ep->busy); //... удаляем элемент ер из списка … ep->busy = 0; /* Paranoid. */ (A) pthread_cond_broadcast (&ep->notbusy); pthread_mutex_unlock (&lp->lm); (B) pthread_cond_destroy (&rp->notbusy); free (ер); } В этом примере условную переменную и ее элемент списка можно освободить (строка В) сразу после того, как все потоки, ожидающие соответствующего значения условной переменной, будут «разбужены» (строка А), поскольку мьютекс и этот код гарантируют, что никакой другой поток не сможет ссылаться на удаляемый элемент. Замечания по использованию Отсутствуют. Логическое обоснование С Будущие направления Отсутствуют. Смотри также pthread_cond_broadcast , pthread_cond_signal , pthread_cond_timedwait , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_cond_destroy и pthread_cond_init от Раздел «Описание» был отредактирован путе В целях согласования со стандарто
pthread_cond_timedwait, pthread_cond_wait
Имя pthread_cond_timedwait, pthread_cond_wait — функции ожидания условия. Синопсис THR #include int pthread_cond_timedwait ( pthread_cond_t *restrict int pthread_cond_wait (pthread_cond_t *restrict Описание Функции pthread_cond_timedwait и pthread_cond_wait используются для блокирования потоков по условной переменной. Они вызываются с использованием мьютекса Эти функции автоматически освобождают мьютекс mutex и обеспечивают блокирование вызывающего потока по условной переменной При успешном выполнении мьютекс будет заблокирован, а владеть им будет вызывающий поток. При использовании условных переменных всегда существует булев предикат, совместно используемый этими переменными, которые связаны с каждым ожидаемым условием. Это условие становится истинным, если поток должен продолжать выполнение. При использовании функций pthread_cond_timedwait или pthread_cond_wait возможны фиктивные запуски. Поскольку возврат из этих функций не подразумевает ничего, кроме оценки значения упомянутого выше предиката, он должен вычисляться после каждого такого выхода из функции. Результат использования нескольких мьютексов для параллельно выполняемых операций pthread_cond_timedwait или pthread_cond_wait по одной и той же условной переменной не определен; другими словами, условная переменная связывается с уникальным мьютексом, когда поток ожидает заданного значения условной переменной, и это (динамическое) связывание завершится вместе с завершением ожидания. Ожидание условия (синхронизированное или нет) представляет собой «точку отмены». Если статус возможности аннулирования дл Поток, который был разблокирован по причине отмены в то время, пока он был заблокирован в вызове функции pthread_cond_timedwait или pthread_cond_wait , не будет использовать условный сигнал, который можно направить параллельно на условную переменную, если существуют другие потоки, заблокированные по этой условной переменной. Функция pthread_cond_timedwait эквивалентна функции pthread_cond_wait , за исключением того, что она возвращает код ошибки, если абсолютное время, заданное пара C S Если поддерживается опция Clock Selection, условная переменная будет иметь атрибут часов, определяющий механизм, который предназначен для измерения времени, заданного параметром Если потоку, ожидающему значения условной переменной, передается сигнал, то при возврате из обработчика сигнала поток возобновит ожидание этой условной переменной (как будто не было никакого прерывания на обработку сигнала) или возвратит нуль вследствие фиктивного запуска. Возвращаемые значения За исключением кода ошибки [ETIMEDOUT], все проверки на наличие ошибок реализованы так, как если бы они были выполнены в самом начале работы каждой функции, и код ошибки в случае ее обнаружения возвращается до модификации состояния мьютекса, заданного пара При успешном завершении возвращается нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_cond_timedwait завершится неудачно, если: [ETIMEDOUT] вре Функции pthread_cond_timedwait и pthread_cond_wait [EINVAL] значение, заданное хотя бы одни [EINVAL] для выполнения параллельных операций pthread_cond_timedwait или pthread_cond_wait по одной и той же условной пере [EPERM] во вре Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Семантика ожидания по условию Важно от В некоторых реализациях, в частности мультипроцессорных, иногда возможно пробуждение сразу нескольких потоков, если сигнал об изменении состояния условной переменной генерируется одновременно на различных процессорах. В общем случае при каждом завершении ожидания по условию поток должен оценивать значение предиката, связанного с ожиданием по условию, чтобы узнать, может ли он безопасно продолжать выполнение, ожидать или объявить тайм-аут. Возврат из состояния ожидания не означает, что соответствующий предикат имеет конкретное значение (ЛОЖЬ или ИСТИНА). Поэтому рекомендуется ожидание по условию выражать в коде, эквивалентно Семантика ожидания по времени Абсолютное время было выбрано для задания параметра лимита времени по двум причинам. Во-первых, несмотря на то, что измерение относительного времени нетрудно реализовать в начале функции, для которой задается абсолютное время, с заданием абсолютного времени в начале функции, которая определяет относительное время, связано условие «гонок». Предположим, например, что функция clock_gettime возвращает текущее время, а функция cond_relative_timed_wait использует относительное время. clock_gettime(CLOCK_REALTIME, &now) reltime = sleep_til_this_absolute_time -now; cond_relative_timed_wait (с, m, &reltime); Если поток выгружается между первой и последней инструкциями, поток блокируется слишком надолго. Однако блокирование несущественно, если используется абсолютное время. Кроме того, абсолютное время не нужно пересчитывать, если оно используется в цикле несколько раз. Для случаев, когда системные часы работают дискретно, можно предполагать, что реализации обработают любые ожидания по времени, истекающие в промежутке между дискретными состояниями, так, как если бы нужное время уже наступило. Аннулирование потока и ожидание по условию Ожидание по условию, синхронизированное или нет, является точкой отмены (аннулирования) потока. Другими словами, функции pthread_cond_wait или pthread_cond_timedwait представляют собой точки, в которых обнаружен необработанный запрос на отмену. Дело в том, что в этих точках возможно бесконечное ожидание, т.е. какое бы событие ни ожидалось, даже при совершенно корректной программе оно может никогда не произойти; например, входные данные, получения которых ожидает программа, могут быть никогда не отправлены. Сделав же ожидание по условию точкой отмены, поток можно безопасно аннулировать и выполнить соответствующие обработчики даже в случае, если программа «увязнет» в бесконечном ожидании. Побочный эффект обработки запроса на от Следовательно, поскольку в случае, когда запрос на отмену приходит во время ожидания, в отношении состояния блокировки должна быть выполнена определенная инструкция, при этом должно быть выбрано такое определение, которое сделает кодирование приложения наиболее удобным и свободным от ошибок. При выполнении действий, связанных с получение Быстродействие мьютексов и условных переменных Предполагается, что мьютексы должны блокироваться только для нескольких инструкций. Такая практика почти автоматически вытекает из желания программистов избегать длинных последовательностей программных инструкций (которые способны снизить общую эффективность параллелизма). При использовании мьютексов и условных переменных всегда пытаются обеспечить последовательность, которая считается обычным случаем: заблокировать мьютекс, получить доступ к общим данным и разблокировать мьютекс. Ожидание по условной переменной — относительно редкая ситуация. Например, при реализации блокировки чтения-записи коду, который получает блокировку чтения, обычно нужно лишь инкрементировать счетчик считывающих потоков (при взаимном исключении доступа). Вызывающий поток будет реально ожидать по условной переменной только тогда, когда уже существует активный записывающий поток. Поэтому эффективность операции синхронизации связана с «ценой» блокировки-разблокировки мьютекса, а не с ожиданием по условию. Обратите внимание на то, что в обычном случае переключения контекста не происходит. Из вышесказанного отнюдь не следует, что эффективность ожидания по условию не важна. Поскольку существует потребность по крайней мере в одном переключении контекста на рандеву (взаимодействие между параллельными процессами), то эффективность ожидания по условию также важна. Цена ожидания по условной переменной должна быть намного меньше минимальной цены одного переключения контекста и времени, затрачиваемого на разблокировку и блокировку мьютекса. Особенности мьютексов и условных переменных Было предложено отделить захват и освобождение мьютекса от ожидания по условию. Но это предложение было отклонено, по причине «сборной природы» этой операции, которая в действительности упрощает реализации реального времени. Такие реализации могут незаметно перемещать высокоприоритетный поток между условной переменной и мьютексом, тем самым предотвращал излишние переключения контекстов и обеспечивал более детерминированное владение мьютексом при получении сигнала ожидающим потоком. Таким образом, вопросы равнодоступности и приоритетности могут быть решены непосредственно самой дисциплиной планирования. К тому же, широко распространенная операция ожидания по условию соответствует существующей практике. Планирование поведения мьютексов и условных переменных Примитивы (базовые элементы) синхронизации, которые могут противоречить используемой стратегии планирования путем установки «своего» правила упорядочения, считаются нежелательными. Выбор среди потоков, ожидающих освобождения мьютексов и условных переменных, происходит в порядке, который зависит именно от стратегии планирования, а не от какой-то другой дисциплины, устанавливающей некий фиксированный порядок (имеется в виду, например, FIFO-дисциплина или учет приоритетов). Таким образом, только стратегия планирования определяет, какой поток (потоки) будет запущен для продолжения работы. Синхронизированное ожидание по условию Функция pthread_cond_timedwait позволяет приложению прервать ожидание наступления конкретного условия после истечения заданного интервала времени. Рассмотрим следующий пример. (void) pthread_mutex_lock (&t. mn); t.waiters++; clock_gettime (CLOCK_REALTIME, &ts) ; ts.tv_sec += 5; rc = 0; while (! mypredicate (&t) && rc == 0) rc = pthread_cond_timedwait (&t.cond, &t.mn, &ts); t.waiters--; if (rc == 0) setmystate (&t); (void) pthread_mutex_unlock (&t.mn); Абсолютный параметр времени ожидания позволяет не пересчитывать его значение каждый раз, когда программа проверяет значение предиката блокирования. Если бы время ожидания было задано относительной величиной, соответствующий пересчет пришлось бы делать перед каждым вызовом функции. Это было бы особенно трудно сделать, поскольку такому колу пришлось бы учитывать возможность дополнительных запусков вследствие дополнительной сигнализации по условной переменной, которые могут происходить до того, как предикат станет истинным или истечет время ожидания. Будущие направления Отсутствуют. Смотри также pthread_cond_signal , pthread_cond_broadcast , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_cond_timedwait и pthread_cond_wait от К описанию прототипа функции pthread_cond_wait был приложен список опечаток Open Group Corrigendum U021/9. Для согласования со стандартом IEEE Std 1003.1j-2000 раздел «Описание» был отредактирован путем добавления семантики для опции Clock Selection. В раздел «Ошибки» внесен еще один код ошибки [EPERM] в ответ на включение интерпретации IEEE PASC Interpretation 1003.1с #28. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототипы функций pthread_cond_timedwait и pthread_cond_wait было добавлено ключевое слово restrict.
pthread_condattr_destroy, pthread_condattr_init
Имя pthread_condattr_destroy, pthread_condattr_init — функции разрушения и инициализации объекта атрибутов условной пере Синопсис THR #include int pthread_condattr_destroy (pthread_condattr_t *attr); int pthread_condattr_init (pthread_condattr_t *attr); Описание Функция pthread_condattr_destroy используется для разрушения объекта атрибутов условной переменной, в результате чего он становится неинициализированным. В конкретной реализации функция pthread_condattr_destroy может устанавливать объект, адресуемый параметром Функция pthread_condattr_init предназначена для инициализации объекта атрибутов условной пере Если функция pthread_condattr_init вызывается для уже инициализированного объекта атрибутов После того как объект атрибутов условной пере Этот то Дополнительные атрибуты, их значения по умолчанию и имена соответствующих функций доступа, которые считывают и устанавливают эти значения атрибутов, определяются конкретной реализацией. Возвращаемые значения При успешно Ошибки Функция pthread_condattr_destroy может завершиться неудачно, если: [EINVAL] значение, заданное параметром аttr, недействительно. Функция pthread_condattr_init завершится неудачно, если: [ENOMEM] для инициализации объекта атрибутов условной переменной недостаточно существующей памяти. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование С Будущие направления Отсутствуют. Смотри также pthread_attr_destroy , pthread_cond_destroy , pthread_condattr_getpshared, pthread_create, pthread_mutex_destroy , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_condattr_destroy и pthread_condattr_init от
pthread_condattr_getpshared, pthread_condattr_setpshared
Имя pthread_condattr_getpshared, pthread_condattr_setpshared — функции считывания и установки атрибутаусловной пере Синопсис THR TSH #include int pthread_condattr_getpshared (const pthread_condattr_t *restrict attr, int *restrict int pthread_condattr_setpshared (pthread_conda 11 r_t * аttr, int Описание Функция pthread_condattr_getpshared используется для получения значения атрибута Атрибут Возвращаемые значения При успешном завершении функция pthread_condattr_setpshared возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. При успешном завершении функция pthread_condattr_getpshared возвращает нулевое значение и сохраняет считанное значение атрибута Ошибки Функции pthread_condattr_getpshared и pthread_condattr_setpshared [EINVAL] значение, заданное пара Функция pthread_condattr_setpshared [EINVAL] новое значение, заданное для атрибута, не попадает в диапазон значений, действительных для этого атрибута. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_create , pthread_cond_destroy , pthread_condattr_destroy , pthread_mutex_destroy , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_condattr_getpshared и pthread_condattr_setpshared от В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_condattr_getpshared было добавлено ключевое слово restrict.
pthread_create
Имя pthread_create — функция создания потока. Синопсис THR #include int pthread_create (pthread_t *restrict Описание Функция pthread_create используется для создания в процессе нового потока с атрибутами, заданными параметром При создании потока выполняется функция start_routine, которая вызывается с единственным аргументом Статус сигналов для нового потока будет инициализирован следующим образом: • маска сигналов будет унаследована от создающего потока; • множество необработанных сигналов для нового потока будет пустым. Среда обработки данных с плавающей точкой будет унаследована от создающего потока. При неудачном выполнении функции pthread_create поток не создается, а содержимое области, адресуемое параметром thread, остается неопределенным. TCT Если определено значение _POSIX_THREAD_CPUTIME, новый поток получит доступ к таймеру центрального процессора (CPU-time clock), и начальное значение для этих часов будет установлено равным нулю. Возвращаемое значение При успешном завершении функция pthread_create возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_create завершится неудачно, если: [EAGAIN] в системе недостаточно ресурсов, необходимых для создания еще одного потока, или был превышен предел ({PTHREAD_THREADS_MAX}), установленный в системе для общего количества потоков в процессе; [EINVAL] значение, заданное параметром [EPERM] инициатор вызова не имеет соответствующего разрешения на установку требуемых параметров планирования или стратегии планирования. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование В качестве альтернативного решения для функции pthread_create предлагалось определить две отдельные операции: «создать» и «запустить». Для некоторых приложений такое поведение было бы более естественным. В среде Ada, в частности, отделено «создание» задачи от ее «активизации». Разбиение этой операции на две части разработчиками стандарта было отклонено по нескольким причинам. • Количество вызовов, требуемых для запуска потока, в этом случае возросло бы от одного до двух, что, таким образом, возложило бы излишние расходы на приложения, которым не нужна дополнительная синхронизация. Однако второго вызова можно было бы избежать за счет усложнения атрибута состояния запуска. • Для потока пришлось бы вводить дополнительное состояние, которое можно определить как «созданный, но не активизированный». Это потребовало бы введения стандарта для определения поведения операций потока в случае, когда поток еще не начал выполняться. • Для приложений, которым подходит именно такое поведение, можно сымитировать два отдельных действия с использованием существующих средств. Функцию start_routine можно синхронизировать путем организации ожидания по условной переменной, сигнализируемой операцией активизации потока. При реализации Ada-приложений можно создавать потоки в любой из двух точек Ada-программы: при создании объекта задачи или при ее активизации. В случае принятия первого варианта функции start_routine пришлось бы ожидать по условной переменной получения «приказа» начать активизацию. Второй вариант не требует использования условной переменной или дополнительной синхронизации. В любом случае при создании объекта задачи потребовалось бы создание отдельного блока управления Ada-задачей, чтобы поддерживать рандеву-очереди. Расширение упомянутой модели позволило бы модифицировать состояние потока между созданием и активизацией, и, следовательно, удалить объект атрибутов потока. Это предложение было отвергнуто по таким причинам. • Должна существовать возможность установки любого состояния в объекте атрибутов потока. Это потребовало бы определения функций для модификации атрибутов потока, что не уменьшило бы количество вызовов, необходимых для установки потока. На самом деле для приложения, которое создает все потоки с использованием идентичных атрибутов, количество вызовов функций, необходимых для установки потоков, резко бы возросло. Использование объектов атрибутов потока позволяет приложению создать один набор вызовов функций установки атрибутов. В противном случае набор вызовов функций установки атрибутов пришлось бы делать для создания каждого потока. • В зависимости от архитектурного решения функции установки состояния потока потребовали бы вызовов функций ядра системы или (по каким-то иным причинам) не могли быть реализованы как макросы, что увеличило бы расходы ресурсов на создание потока. • Была бы утеряна возможность «классовой» организации потоков для приложений. Предлагалась еще одна альтернатива, в которой рассматривалось использование модели, аналогичной созданию процессов, — «разветвление потока». Семантика разветвления обеспечивала бы большую гибкость, и функцию создания можно было реализовать в виде простого разветвления потока, за которым немедленно следовал вызов требуемой «запускающей» функции. Этот вариант имел такие недостатки. • Для многих реализаций внутренний стек вызывающего потока пришлось бы дублировать, поскольку во многих архитектурах нет возможности определить размер вызывающего фрейма. • Эффективность снизилась бы, поскольку пришлось бы копировать по крайней мере некоторую часть стека, несмотря на то, что в большинстве случаев после вызова нужной «запускающей» функции потоку уже не требуется скопированный контекст. Будущие направления Отсутствуют. Смотри также fork , pthread_exit , pthread_join , то Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширением POSIX Threads Extension. Issue 6 Функция pthread_create от В результате согласования со спецификацией Single UNIX Specification был добавлен обязательный код ошибки [EPERM]. С целью согласования со ста Для согласования со стандарто В раздел «Описание» внесено явное утверждение о то
pthread_detach
Имя pthread_detach — функция отсоединения потока. Синопсис THR #include int pthread_detach (pthread_t Описание Функция pthread_detach уведомляет реализацию о том, что область памяти для потока thread может быть восстановлена, когда он завершит выполнение. Если поток не завершается, функция pthread_detach не служит причиной для его завершения. Результат нескольких вызовов функции pthread_detach для одного и того же потока не определен. Возвращаемое значение При успешном завершении функция pthread_detach возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_detach завершится неудачно, если: [EINVAL] реализация обнаружила, что значение, заданное параметром thread, не относится к присоединенному потоку; [ESRCH] не был найден ни один поток, соответствующий заданному идентификационному номеру потока ID. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Функции pthread_join или pthread_detach должны вызываться для каждого потока, который создается, чтобы можно было снова использовать область памяти, связанную с потоком. Высказывалось мнение о необязательности использования функции pthread_detach : поскольку поток никогда динамически не отсоединяется, то достаточно использовать атрибут создания потока detachstate. Однако необходимость в этой функции возникает по крайней мере в двух случалх. 1. В обработчике запроса на отмену для функции присоединения потока (pthread__join) важно иметь функцию pthread_detach, чтобы отсоединить поток. Без нее обработчик вынужден был бы выполнить еще раз функцию pthread_j oin , чтобы попытаться отсоединить поток, который не только задерживает процедуру отмены в течение неограниченного времени, но и вносит новый вызов функции pthread_join. В этом случае есть смысл говорить о динамическом отсоединении. 2. Чтобы отсоединить «исходный поток» (это может понадобиться в процессах, которые создают потоки сервера). Будущие направления Отсутствуют. Смотри также pthread_join , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена Issue 6 Функция pthread_detach отмечена как часть опции Threads.
pthread_exit
Имя pthread_exit — функция завершения потока. Синопсис THR #include void pthread_exit (void *va2ue_ptr); Описание Функция pthread_exit завершает вызывающий поток и делает значение Когда из функции запуска возвращается поток, отличный от того, в котором была изначально вызвана функция main, делается неявное обращение к функции pthread_exit. Значение, возвращаемое этой функцией, служит в качестве состояния выхода этого потока. Поведение функции pthread_exit не определено, если она вызвана из обработчика запроса на отмену потока или функции деструктора, к которой было сделано обращение в результате явного или неявного вызова функции pthread_exit . После завершения потока результат доступа к локальным переменным потока не определен. Таким образом, ссылки на локальные переменные существующего потока не следует использовать для функции pthread_exit в качестве значения параметра value_ptr. После завершения процесс будет иметь состояние выхода, равное нулю, после того, как завершится его последний поток. Поведение при этом будет таким, как если бы во время завершения потока была вызвана функция exit с нулевым аргументом. Возвращаемое значение Функция pthread_exit не возвращается к инициатору ее вызова. Ошибки Ошибки не определены. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Нормальный механизм завершения потока состоит в возвращении из функции, которая была задана в вызове функции pthread_create . Функция pthread_exit обеспечивает возможность завершения потока без обязательного выхода из стартовой функции этого потока и, следовательно, служит аналогом функции exit . Независимо от метода завершения потока любые обработчики отмены, которые были помещены в стек, но еще не извлечены из него, будут выполнены, а также вызваны деструкторы для любых существующих данных потока. Этот том стандарта IEEE Std 1003.1-2001 требует, чтобы обработчики отмены извлекались из стека и выполнялись по порядку. После выполнения всех обработчиков отмены для каждого элемента потоковых данных вызываются соответствующие функции деструкторов (в неопределенном порядке). Такая последовательность действий обязательна, поскольку обработчики отмены могут использовать данные потока. Поскольку значение состояния выхода определяется приложением (за исключением случаев, когда поток был отменен, т.е. в случаях отмены используется значение PTHREAD_CANCELED), реализации не известно, что следует понимать под недействительным значением состояния, поэтому проверка на наличие ошибок не выполняется. Будущие направления Отсутствуют. Смотри также exit , pthread_create , pthread_join , том Base Definitions стан Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для со Issue 6 Функция pthread_exit от
pthread_getconcurrency, pthread_setconcurrency
Имя pthread_getconcurrency, pthread_setconcurrency — функции считывания и установки уровня параллелизма. Синопсис XSI #include int pthread_getconcurrency (void); int pthread_setconcurrency (int Описание Несвязанные потоки в процессе выполняются (или не выполняются) одновременно. По умолчанию реализация потоков гарантирует активность достаточного количества потоков для того, чтобы процесс мог успешно продолжать выполнение. И хотя такой подход сохраняет системные ресурсы, он может не обеспечить наиболее эффективный уровень параллелизма. Функция pthread_setconcurrency позволяет приложению с помощью пара Функция pthread_getconcurrency возвращает значение, установленное в результате предыдущего обращения к функции pthread_setconcurrency . Если «предыдущего» вызова этой функции не было, функция pthread_getconcurrency возвращает нуль, который означает, что реализация поддерживает заданный уровень параллелизма. Обращение к функции pthread_setconcurrency информирует реализацию о желаемом уровне параллелизма, а реализация использует его как совет, а не требование. Если реализация не поддерживает мультиплексирование пользовательских потоков, то функции pthread_setconcurrency и pthread_getconcurrency используются ради совместимости исходного кода, но не дают никакого эффекта при вызове. Для поддержки семантики функций параметр new_level сохраняется при вызове функции pthread_setconcurrency , чтобы послелующее обращение к функции pthread_getconcurrency могло вернуть то же значение. Возвращаемые значения При успешном выполнении функция pthread_setconcurrency возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Функция pthread_getconcurrency всегда возвращает уровень параллелизма, установленный в результате предыдущего обращения к функции pthread_setconcurrency . Если «предыдущего» вызова этой функции не было, функция pthread_getconcurrency возвращает нуль. Ошибки Фу [EINVAL ] значение, заданное пара [EAGAIN] значение, заданное пара Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Использование этих функций изменяет состояние базового уровня параллелизма, от которого зависит работа приложения. Разработчикам библиотек рекомендуется не использовать функции pthread_getconcurrency и pthread_setconcurrency, поскольку это может привести к конфликту с их использованием в приложении. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также То Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5.
pthread_getschedparam, pthread_setschedparam
Имя pthread_getschedparam, pthread_setschedparam — функции динамического доступа к параметрам стратегии планирования потока (REALTIME THREADS). Синопсис THR TPS #include int pthread_getschedparam (pthread_t int pthread_setschedparam (pthread_t Описание Функции pthread_getschedparam и pthread_setschedparam используются для считывания и установки соответственно значений стратегии планирования и параметров отдельных потоков многопоточного процесса. Для значений стратегии планирования SCHED_FIFO и SCHED_RR в структуре sched_param должен быть установлен только один ее член Функция pthread_getschedparam пре Параметр TSP Если определено значение _POSIX_THREAD_SPORADIC_SERVER, аргу При неудачном завершении функции pthread_setschedparam параметры планирования для заданного потока изменены не будут. Возвращаемые значения Ошибки Функци [ESRCH] з ному из существующих потоков. Функция pthread_setschedparam может завершиться неудачно, если: [EINVAL] значение, заданное параметром policy, или значение одного из параметров планирования, связанных со значением стратегии планирования policy, недействительно; была сделана попытка установить для стратегии планирования или ее параметров неподдерживаемые значения; была сделана попытка динамически изменить стратегию планирования, установив для нее значение SCHED_SPORADIC, при том, что реализация не поддерживает такое изменение; инициатор вызова не имеет соответствующего разрешения устанавливать параметры планирования или стратегию планирования для заданного потока; реализация не позволяет приложению модифицировать один из параметров в соответствии с заданным значением; значение, заданное пара Эти функции не возвращают код ошибки [EINTR]. [ENOTSUP] TSP [ENOTSUP] [EPERM] [EPERM] [ESRCH] Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_setschedprio , sched_getparam, sched_getscheduler , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_getschedparam и pthread_setschedparam от Код ошибки [ENOSYS] был исключен, поскольку е К описанию прототипа функции pthread_setschedparam был приложен список опечаток Open Group Corrigendum U026/2, чтобы второй ар Для согласования со стандартом IEEE Std 1003.1d-1999 было добавлено значение стратегии планирования SCHED_SPORADIC. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_getschedparam было добавлено ключевое слово restrict. Был добавлен список опечаток Open Group Corrigendum U047/1. Быладобавлена интерпретация IEEE PASC 1тегрге1а1юп 1003.1 #96, отмечающая» что значения приоритета также можно установить путем вызова функции pthread_setschedprio.
pthread_join
Имя pthread_join — функция ожидания завершения потока. Синопсис THR #include int pthread_join (pthread_t Описание Функция pthread_join приостанавливает выполнение вызывающего потока до тех пор, пока не завершится заданный поток (если он еще не завершился). Если после удачного возвращения из функции pthread_join параметр Не определено, учитывается ли в значении {PTHREAD_THREADS_MAX} поток, который завершился, но остался отсоединенным. Возвращаемые значения При успешном завершении функция pthread_join возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_join завершится неудачно, если: [EINVAL] реализация обнаружила, что значение, заданное параметром [ESRCH] не найден ни один поток, идентификационный номер которого (ID) соответствовал бы заданному потоку. Функция pthread_join может завершиться неудачно, если: [EDEADLK] была обнаружена взаимоблокировка или значение параметра Функция pthread_join не возвращает код ошибки [EINTR]. Примеры Ниже приведен пример создания потока и его удаления. typedef struct { int *ar; long n; } subarray; void *incer (void *arg) { long i; for (i = О; i < ((subarray *)arg) ->n; i++) ((subarray *) arg) ->ar[i]++; } int main (void) { int ar[1000000]; pthread_t th1, th2; subarray sbl, sb2; sbl.ar = &ar[О]; sbl.n = 500000; (void) pthread_create(&thl, NULL, incer, &sbl); sb2.ar = &ar[500000]; sb2.n = 500000; (void) pthread_create(&th2, NULL, incer, &sb2); (void) pthread_join(thl, NULL); (void) pthread_join(th2, NULL); return 0; } Замечания по использованию Отсутствуют. Логическое обоснование Функция pthread_join представляет собой удобное и полезное средство для использования в многопоточных приложениях. Конечно, программист мог бы сымитировать эту функцию, если бы она не существовала, другими средствами, например, путем передачи функции start_routine дополнительного состояния как части аргумента. Завершающийся поток в этом случае установил бы флаг, означающий завершение, и отправил бы условную переменную, которая является частью этого состояния, а присоединяющий поток ожидал бы получения этой условной переменной. Несмотря на то что такой метод позволил бы организовать ожидание наступления более сложных условий (например, завершения сразу нескольких потоков), ожидание завершения одного потока— весьма распространенная ситуация, и поэтому «заслуживает» отдельной функции. Кроме того, включение в библиотеку функции pthread_join никоим образом не мешает программисту самому кодировать такие сложные ожидания. Таким образом, включение функции pthread_join в этот том стандарта IEEE Std 1003.1-2001 считается весьма полезным. Функция pthread_join обеспечивает простой механизм, позволяющий приложению ожидать завершения потока. После того как поток завершится, приложение может приступать к освобождению ресурсов, которые использовались этим потоком. Например, после возвращения функции pthread_join может быть восстановлена любая область памяти, предоставленная приложением под стек. Функции pthread_join или pthread_detach должны в конце концов быть вызваны для каждого потока, который создается с атрибутом detachstate, равным значению PTHREAD_CREATE_JOINABLE , чтобы Взаимодействие между функцией pthread_join и механизмом отмены потока хорошо определено по следующим причинам: • функция pthread_join , как и все остальные не асинхронные функции безопасной отмены потоков, можно вызывать только при возможности отложенного типа отмены. • отмена потока не может происходить в состоянии запрещения отмены. Таким образом, имеет смысл рассматривать только стандартное состояние возможности отмены. Итак, вызов функции pthread_join либо отменяется, либо успешно завершается. Для приложения это различие очевидно, поскольку либо выполняется обработчик запроса на отмену, либо возвращается функция pthread_join . В этом случае условия «гонок» не возникают, поскольку функция pthread_join вызывается в состоянии отложенного запроса на отмену. Будущие направления Отсутствуют. Смотри также pthread_create, wait, том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширение Issue 6 Функция pthread_join отмечена как часть опции Threads.
pthread_mutex_destroy, pthread_mutex_init
Имя pthread_mutex_destroy, pthread_mutex_init — функции разруше Синопсис THR #include int pthread_mutex_destroy (pthread_mutex_t *^utex); int pthread_mutex_init ( pthread_mutex_t *restrict jnutex, const pthread_mutexattr_t *restrict attr); pthread_mutex_t Описание Функция pthread_mutex_destroy используется для разрушения объекта мьютекса, адресуемого параметром mutex, в результате чего этот объект мьютекса становится неинициализированным. В конкретной реализации функция pthread_mutex_destroy может устанавливать объект, адресуемый параметром Нет никакой опасности в разрушении инициализированного объекта мьютекса, по которому не заблокирован в данный момент ни один поток. Попытка же разрушить заблокированный мьютекс может привести к неопределенно Функция pthread_mutex_init используется для инициализации Для осуществления синхронизации используется только сам объект, адресуемый параметром mutex. Результат ссылки на копии объекта mutex в обращениях к функциям pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock и pthread_mutex_destroy не определен. Попытка инициализировать уже инициализированный объект мьютекса приведет к неопределенному поведению. В случаях, когда атрибуты мьютекса, действующие по умолчанию, заранее определены, для инициализации мьютексов, которые создаются статически, можно использовать макрос PTHREAD_MUTEX_INITIALIZER. Резу Возвращаемые значения При успешном завершении функции pthread_mutex_destroy и pthread_ mutex_init возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Проверка на наличие ошибок с кодами [EBUSY] и [EINVAL] реализована так (если реализована вообще), как будто она выполняется в самом начале работы каждой функции, и код ошибки в случае ее обнаружения возвращается до модификации состояния мьютекса, заданного параметром mutex. Ошибки Функция pthread_mutex_destroy может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку разрушить объект, адресуе [EINVAL] значение, заданное пара Функция pthread_mutex_init завершится неудачно, если: [EAGAIN] система испытывает недостаток ресурсов (не имеется в виду память), необходимых для инициализации еще одного мьютекса; [ENOMEM] для инициализации мьютекса недостаточно существующей памяти; [EPERM] инициатор вызова функции не имеет привилегий для выполнения этой операции. Функция pthread_mutex_init [EBUSY] реализация обнаружила попытку повторно инициализировать объект мьютекса, адресуемый параметром mutex, которой был ранее инициализирован, но еще не разрушен; [EINVAL ] значение, заданное пара Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Возможность альтернативных реализаций Данный том стандарта IEEE Std 1003.1-2001 поддерживает несколько альтернативных реализаций мьютексов. Реализация может сохранять блокировку непосредственно в объекте типа pthread_mutex_t. Возможно также хранение блокировки в «куче», а указателя, дескриптора или уникального ID — в объекте мьютекса. Каждая реализация обладает различными достоинствами в зависимости от определенных конфигураций оборудования. Поэтому, чтобы написать код, который не нужно будет изменять в зависимости от выбранной реализации, в данном томе стандарта IEEE Std 1003.1-2001 жестко не определяется тип хранения блокировки и термин «инициализировать» используется для усиления утверждения о том, что блокировка может в действительности располагаться в самом объекте мьютекса. Обратите вни В реализации разрешается, чтобы при выполнении функции pthread_mutex_destroy в мьютексе хранилось недействительное значение. Это позволит выявить ошибочные программы, которые пытаются заблокировать уже разрушенный мьютекс (или по крайней мере сослаться на него). Компромисс между контролем за ошибками и производительностью Существует множество случаев, когда можно обойтись без проверки на наличие ошибок ради достижения более высокой производительности. Полнота применения контроля за ошибками должна соответствовать нуждам конкретных приложений и возможностям сред выполнения. В общем случае об ошибках или ошибочных условиях, вызванных системными причинами (например, недостаточностью памяти), необходимо уведомлять всегда, но необязательно сообщать об ошибках, связанных с некорректностью кода приложения (например, при неудачной попытке обеспечить адекватную синхронизацию, используемую при защите мьютекса от удаления). Таким образом, возможен широкий диапазон реализаций. Например, реализация, предназначенная для отладки приложений, может включать все возможные проверки ошибок, в то время как реализация, выполняющая на встроенном компьютере одно-единственное уже отлаженное приложение при очень строгих требованиях к производительности, может содержать лишь минимальный набор проверок на наличие ошибок. Более того, реализация может быть представлена даже в двух версиях подобно опциям, предоставляемым компиляторами: в версии с полным объемом проверок ошибок (но более медленной) и в версии с ограниченным объемом проверок ошибок (но более быстрой). Запретить возможность необязательности контроля за ошибками значило бы оказать пользователю медвежью услугу. Предусмотрительно ограничивая использование понятия «неопределенное поведение» только случаями ошибочных действий самого приложения (по причине недостаточно продуманного кода) и обязательно определяя ошибки, связанные с недоступностью системных ресурсов, данный том стандарта IEEE Std 1003.1-2001 гарантирует, что любое корректно написанное приложение переносимо в полном диапазоне реализаций, но не обязывает все реализации нести дополнительные затраты на проверку многочисленных условий, которые корректно написанная программа никогда не создаст. Почему не определяются предельные значения Определение символьных значений для использования в качестве максимального числа мьютексов и условных переменных рассматривалось, но было отвергнуто, поскольку количество этих объектов может изменяться динамически. Более того, многие реализации размещают эти объекты в памяти приложения, следовательно, говорить о необходимости явного определения максимума нет никакого смысла. Статические инициализаторы для мьютексов и условных переменных Обеспечение статической инициализации статически размещаемых в памяти объектов синхронизации позволяет в модулях, содержащих закрытые статические переменные синхронизации, избежать тестирования и соответствующих затрат, связанных с динамической инициализацией. Более того, это упрощает кодирование Без применения статической инициализации функция самоинициализации foo может иметь следующий вид. static pthread_once_t foo_once = PTHREAD_ONCE_INIT; static pthread_mutex_t foo_mutex; void foo_init { pthread_mutex_init (&foo_mutex, NULL); } void foo { pthread_once(&foo_once, foo_init); pthread_mutex_lock (&foo_mutex); /* Выполнение действий. */ pthread_mutex_unlock (&foo_mutex); } С применением статической инициализации ту же функцию самоинициализации foo static pthread_mutex_t foo_mutex = PTHREAD_MUTEX_INITIALIZER; void foo { pthread_mutex_lock(&foo_mutex) ; /* Выполнение действий. */ pthread_mutex_unlock(&foo_mutex); } Обратите внимание на то, что статическая инициализация устраняет необходимость в тестировании, проводимом в функции pthread_once , и получении значения адреса &foo_mutex, передаваемого функции pthread_mutex_lock или pthread_mutex_unlock . Таким образом, С-код, написанный для инициализации статических объектов, проще во всех системах и работает быстрее на большом классе систем, в которых объект (внутренней) синхронизации можно хранить в памяти приложения. До сих пор вопрос о быстродействии блокировок поднимался для машин, которые требовали, чтобы для мьютексов выделялась специальная память. В действительности в таких машинах мьютексы и, возможно, условные переменные должны были содержать указатели на реальные аппаратные средства защиты. Для того чтобы на таких машинах работала статическая инициализация, функция pthread_mutex_lock также должна проверять, выделена ли память для указателя на реальный объект блокировки. Если не выделена, функция pthread_mutex_lock , прежде чем его использовать, должна его инициализировать. Резервирование таких ресурсов можно выполнить при загрузке программы, и поэтому для мьютексов и условных переменных не были введены дополнительные коды ошибок, означающие неудачное выполнение инициализации. Такое динамическое тестирование в функции pthread_mutex_lock, которое позволяет узнать, был ли инициализирован указатель, могло показаться на первый взгляд лишним. На большинстве компьютеров это было бы реализовано в виде считывания его значения, сравнения с нулем и использования по назначению при условии получения нужного результата сравнения. Несмотря на то что это тестирование кажется лишним, дополнительные затраты (на тестирование содержимого регистра) обычно незначительны, поскольку в действительности никакие дополнительные ссылки на память не делаются. Так как все больше и больше компьютеров оснащаются кэш-памятью (быстродействующей буферной памятью большой емкости), то реальные издержки представляют собой отработку ссылок, а не выполнение инструкций. В качестве альтернативного варианта (в зависимости от архитектуры компьютера) можно в наиболее важных случаях ликвидировать все расходы системных ресурсов на операции блокировки, которые выполняются после инициализации средств блокировки. Это можно сделать путем перехода от более затратных к редко выполняемым операциям, т.е. перенести весь «груз расходов» на однократно выполняемую инициализацию. Поскольку «внешняя» (т.е. выполняемая вне основной программы) инициализация мьютекса также означает, что для получения реальной блокировки адрес должен быть разыменован, один из широко применяемых методов при статической инициализации состоит в сохранении фиктивного значения для этого адреса; в частности, адреса, который вызывает сбой в работе компьютера. При возникновении такого сбоя во время первой попытки заблокировать мьютекс можно сделать проверку достоверности, а затем для реальной блокировки использовать корректный адрес. Последующие операции, связанные с блокировкой, не будут сопряжены с дополнительными расходами, поскольку они уже не являются «сбойными». Это — всего лишь метод, который можно использовать для поддержки статической инициализации, несмотря на то, что он неблагоприятно отражается на скорости захвата блокировки. Безусловно, существуют и другие методы, которые в высокой степени зависят от архитектуры компьютера. Расходы на блокировку для компьютеров, выполняющих «внешнюю» инициализацию мьютекса, сравнимы с расходами для модулей, инициализируемых неявным образом (имеются в виду те из них, где достигнута «внутренняя» инициализация мьютексов). Безусловно, «внутренняя» инициализация выполняется гораздо быстрее, но «внешняя» ненамного хуже. Помимо вопроса быстродействия блокировки, нас беспокоит то, что потоки могут соперничать за блокировки при попытке завершить инициализацию статически размещаемых в памяти мьютексов. (Такое завершение обычно включает захват внутренней блокировки, выделение памяти для структуры, сохранение указателя на эту структуру в мьютексе и освобождение внутренней блокировки.) Во-первых, многие реализации могут сократить эту последовательность действий путем хеширования по адресу мьютекса. Во-вторых, количество таких «сериалов» может быть весьма ограниченным. В частности, их может быть столько, сколько создается статически размещаемых объектов синхронизации. Динамически же создаваемые объекты по-прежнему инициализируются с помощью функций pthread_mutex_init или pthread_cond_init . Наконец, если ни один из описанных выше методов оптимизации для «внешнего» размещения объектов синхронизации не позволяет достичь нужной производительности приложения при использовании определенной реализации, приложение может избежать статической инициализации, явным образом инициализируя все объекты синхронизации c помощью соответствующих функций Разрушение мьютексов Мьютекс можно разрушить сразу после разблокировки. Например, рассмотрим следующий код. struct obj { pthread_mutex_t om; int refcnt; }; obj_done (struct obj *op) { pthread_mutex_lock (&op- >om); if (—op- >refcnt == 0) { pthread_mutex_unlock (&op- >om); (A) pthread_mutex_destroy (&op- >om); (B) free(op); } else (С) pthread_mutex_unlock (&op->om); } В данном случае структура obj служит для учета количества ссылок, а функция obj_done вызывается всякий раз, когда удаляется ссылка на объект. Реализации должны позволить разрушение объекта и освобождение занимаемых им ресурсов (см. строки А и В) сразу после его разблокировки (строка С). Будущие направления Отсутствуют. Смотри также pthread_mutex_getprioceiling , pthread_mutex_lock , pthread_mutex_timedlock , pthread_mutexattr_getpshared , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_mutex_destroy и pthread_mutex_init от В целях согласования со стандарто Раздел «Описание» б В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_mutex_init было добавлено ключевое слово restrict.
pthread_mutex_getprioceiling, pthread_mutex_setprioceiling
Имя THR TPP pthread_mutex_getprioceiling, pthread_mutex_setprioceiling — функции считывания и установки предельного значения приоритета мьютекса (REALTIME THREADS). Синопсис #include int pthread_mutex_getprioceiling ( const pthread_mutex_t *restrict pthread_mutex_t *restrict Описание Функция pthread_mutex_getprioceiling используется для считывания теку При неудачном выполнении функции pthread_mutex_setprioceiling предельное значение приоритета мьютекса не будет изменено. Возвращаемые значения При успешном завершении функции pthread_mutex_getprioceiling и pthread_mutex_setprioceiling возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функции pthread_mutex_getprioceiling и pthread_mutex_setprioceiling могут завершиться неудачно, если: [EINVAL] приоритет, заданный пара [EINVAL] значение, заданное пара [ EPERM] инициатор вызова не и Эти функции не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_mutex_destroy,pthread_mutex_lock, pthread_mutex_timedlock , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_mutex_getprioceiling и pthread_mutex_setprioceiling отмечены как часть опций Threads и Thread Execution Scheduling. Код ошибки [ENOSYS] был исключен, поскольку его нет смысла учитывать, если реализация не поддерживает опцию Thread Priority Protection. Код ошибки [ENOSYS], обозначающий отсутствие поддержки протокола учета приоритета для мьютексов, был исключен. Дело в том, что если реализация предоставляет эти функции (независимо от того, определено ли значение _POSIX_PTHREAD_PRIO_PROTECT), они должны работать так, как отмечено в разделе «Описание», т.е. протокол учета приоритета для мьютексов должен поддерживаться. В целях согласования со стандартом IEEE Std 1003.1d-1999 в раздел «Смотри также была добавлена функция pthread_mutex_timedlock . В целях согласования со стандартом ISO/IEC 9899: 1999 в прототипы функции pthread_mutex_getprioceiling и pthread_mutex_setprioceiling было добавлено ключевое слово restrict.
pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock
Имя pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock — функции блокировки и разблокировки мьютекса. Синопсис THR #include int pthread_mutex_lock (pthread_mutex_t *.mutex) ; int pthread_mutex_trylock (pthread_mutex_t Описание Объект мьютекса, адресуемый параметром mutex, блокируется путем вызова функции pthread_mutex_lock. Если мьютекс уже заблокирован, вызывающий поток блокируется до тех пор, пока мьютекс не станет доступным. При завершении этой операции объект мьютекса, адресуемый параметром mutex, находится в состоянии блокировки, а вызывающий поток является его владельцем. XSI Ес Для мьютексов типа PTHREAD_MUTEX_ERRORCHECK предусмотрена проверка на наличие ошибок. Если поток попытается заблокировать мьютекс, который уже заблокирован, возвращается ошибка. Если поток попытается разблокировать мьютекс, который не заблокирован, возвращается ошибка. Если мьютекс имеет тип PTHREAD_MUTEX_RECURSIVE, мьютекс должен поддерживать концепцию подсчета блокировок. При первом успешном блокировании мьютекса счетчик блокировок устанавливается равным единице. При каждом очередном блокировании этого мьютекса счетчик блокировок инкрементируется, а при каждом разблокировании — декрементируется. Когда счетчик блокировок достигает нулевого значения, мьютекс становится доступным для других потоков. Если поток попытается разблокировать мьютекс, который не заблокирован, возвращается ошибка. Если Функция pthread_mutex_trylock эквивалентна функции pthread_mutex_lock , за исключением того, что если объект мьютекса, адресуемый параметром mutex, в данный момент заблокирован (любым потоком, включал текущий), эта функция немедленно завершится. Если мьютекс имеет тип PTHREAD_MUTEX_RECURSIVE , и в данный момент мьютексом владеет вызывающий поток, счетчик блокировок этого мьютекса инкрементируется, а функция pthread_mutex_trylock немедленно возвращает признак успешного завершения. Функция pthread_mutex_unlock освобождает объект XSI Способ освобождения зависит от атрибута типа Если при вызове функции pthread_mutex_unlock , в результате которого мьютекс стал доступным, существуют потоки, заблокированные по объекту мьютекса, адресуемому параметром мьютекс, то поток-владелец этого мьютекса будет установлен стратегией планирования. XSI (Для Если к потоку, ожидающему освобождения мьютекса, поступает сигнал, то после выполнения обработчика этого сигнала поток снова перейдет в состояние ожидания, как если бы он и не прерывался на обработку сигнала. Возвращаемые значения При успешном завершении функции pthread_mutex_lock npthread_mutex_unlock возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Функция pthread_mutex_trylock возвращает нулевое значение, если выполнена блокировка по объекту мьютекса, адресуемому параметром mutex. В противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функции pthread_mutex_lock и pthread_mutex_trylock завершатся неудачно, если: [EINVAL] мьютекс был создан с использованием атрибута protocol, имеющего значение PTHREAD_PRIO_PROTECT, а приоритет вызывающего потока выше текущего значения предельного приоритета мьютекса. Функция pthread_mutex_trylock завершится неудачно, если: [EBUSY] мьютекс остался недоступным, поскольку он был уже заблокирован. Функции pthread_mutex_lock , pthread_mutex_trylock и pthread_mutex_unlock [EINVAL] значение, заданное пара XSI [EAGAIN] мьютекс остался недоступным, поскольку было превышено максимальное количество рекурсивных блокировок для мью-текса, заданного параметром mutex. Функция pthread_mutex_lock [ EDEADLK ] текущий поток уже владеет мьютексом. Функция pthread_mutex_unlock [ EPERM ] текущий поток не владеет мьютексом. Эти функции не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Объекты мьютексов служат в качестве базовых элементов низкого уровня, на основе которых можно построить другие функции синхронизации потоков. Поэтому реализация мьютексов должна быть максимально эффективной. Функции управления мьютексами и, в частности, устанавливаемые по умолчанию значения атрибутов мьютексов позволяют по желанию организовать быстродействующие встроенные реализации блокировок и разблокировок мьютексов. Например, тупиковая ситуация при двойной блокировке— это явным образом разрешенное поведение, которое позволяет избежать внесения в базовый механизм больших затрат. (Более «дружественные» мьютексы, которые обнаруживают взаимоблокировку или позволяют множественное блокирование одним и тем же потоком, пользователь может легко создать с помощью других механизмов. Например, для регистрации владельцев мьютекса можно использовать функцию pthread_self.) Реализации путем использования специальных атрибутов мьютексов также могут предоставлять дополнительные возможности в виде опций. Поскольку большинство атрибутов проверяется перед тем, как поток должен быть заблокирован, их использование не замедляет процесс блокирования мьютекса. Более того, несмотря на возможность выделить идентификационный номер (ID) владельца мьютекса, это потребовало бы сохранения текущего ID потока при каждом блокировании мьютекса, что связано с неприемлемым уровнем затрат. Аналогичные аргументы применимы и к операции mutex_tryunlock. Будущие направления Отсутствуют. Смотри также pthread_mutex_destroy , pthread_mutex_timedlock , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования c расширением POSIX Threads Extension. Issue 6 Функции pthread_mutex_lock , pthread_mutex_trylock и pthread_mutex_ unlock отмечены как часть опции Threads. В результате согласования со спецификацией Single UNIX Specification было определено поведение при попытке повторно заблокировать мьютекс. В целях согласования со стандартом IEEE Std 1003.1d-1999 в раздел «Смотри также» была добавлена функция pthread_mutex_timedlock . Пр
pthread_mutex_timedlock
Имя pthread_mutex_timedlock — функция блокировки мьютекса (ADVANCED REALTIME). Синопсис THR #include TMO #include int pthread_mutex_timedlock ( pthread_mutex_t *restrict Описание Функция pthread_mutex_timedlock используется для блокирования объекта мьютекса, адресуемого параметром Заданный интервал времени истекает, когда наступит абсолютное время, заданное параметром TMR Если поддерживается опция Timers, отсчет интервала вре Разрешение для интервала времени определяется разрешением часов, которые используются для его отсчета. Тип данных timespec определяется в заголовке Ни при каких условиях эта функция не завершится неудачно, если мьютекс может быть заблокирован немедленно. В проверке У правил наследования приоритета (для мьютексов, инициализированных с использованием протокола PRIO_INHERIT) есть следствие: если ожидание мьютекса, действующего с ограничением по времени, завершается по причине исчерпания заданного интервала времени, то приоритет владельца мьютекса будет откорректирован таким образом, чтобы отражать факт того, что данный поток больше не относится к числу потоков, ожидающих заданный мьютекс. Возвращаемое значение При успешном завершении функция pthread_mutex_timedlock возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_mutex_timedlock завершится неудачно, если: [EINVALJ мьютекс был создан с использованием атрибута [EINVAL] процесс или поток заблокирован, а пара [ETIMEDOUT] мьютекс не удалось заблокировать до истечения заданного интервала времени. Функция pthread_mutex_timedlock может завершиться неудачно, если: [EINVAL] значение, заданное пара [ EDEADLK] текущий поток уже владеет мьютексом. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Функция pthread_mutex_timedlock является частью опций Threads и Timeouts и Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_mutex_destroy , pthread_mutex_lock, pthread_mutex_trylock, time , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEEStd 1003.1d-1999.
pthread_mutexattr_destroy
Имя pthread_mutexattr_destroy Синопсис THR #include int pthread_mutexattr_destroy ( pthread_mutexattr_t Описание Функция pthread_mutexattr_destroy используется для разрушения объекта атрибутов Результаты не определены, если функция pthread_mutexattr_init вызывается, ссылаясь на уже инициализированный объект атрибутов attr. После того как объект атрибутов мьютекса был использован для инициализации одного или нескольких мьютексов, Любая функция, которая оказывает влияние на объект атрибутов (включал деструктор), никак не отразится на ранее инициализированных мьютексах. Возвращаемые значения При успешно Ошибки Функция pthread_mutexattr_destroy [EINVAL ] значение, заданное параметром attr, недействительно. Функция pthread_mutexattr_init завершится неудачно, если: [ENOMEM] для инициализации объекта атрибутов Эти функции не возвра pthread_mutexattr_destroy, pthread_mutexattr_init — функции разрушения и инициализации объекта атрибутов Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Для получения общих разъяснений назначения атрибутов см. описание функции pthread_attr_init . Объекты атрибутов позволяют реализациям экспериментировать с полезными расширениями и разрешают использовать расширение этого тома стандарта IEEE Std 1003.1-2001, не изменяя существующих функций. Таким образом, они обеспечивают возможности для будущего расширения этого тома стандарта IEEE Std 1003.1-2001 и уменьшают соблазн преждевременно стандартизировать семантику, которая еще широко не реализована или не до конца понята. Рассматривалась возможность использования таких дополнительных атрибутов мьютексов, как spin__only, limited spin, no__spin, recursive и metered. (Считаем необходимым разъяснить назначение таких атрибутов, как recursive nmetered: рекурсивные мьютексы позволяют выполнение нескольких повторных блокировок со стороны текущего владельца; мьютексы с регистрирацией фиксируют длину очереди, время ожидания и т.д.) Поскольку еще нет достаточных данных о том, насколько полезны эти атрибуты, в данном томе стандарта IEEE Std 1003.1-2001 они не определены. Однако объекты атрибутов мьютексов позволяют проверить эти идеи на предмет возможной их стандартизации в будущем. Атрибуты мьютекса и производительность Необходимо позаботиться о том, чтобы действующие по умолчанию значения атрибутов мьютекса были определены таким образом, чтобы мьютексы, инициализированные этими значениями, имели достаточно простую семантику, согласно которой блокирование и разблокирование можно было бы выполнить с помощью инструкций, эквивалентных операциям тестирования и установки значений (и, возможно, еще некоторых других базовых инструкций). Существует по крайней мере один метод реализации, который можно использовать для сокращения расходов в период блокирования на проверку того, имеет ли мьютекс нестандартные атрибуты. Один такой метод заключается в том, чтобы предварительно заблокировать любые мьютексы, которые инициализированы нестандартными атрибутами. Любая попытка позже заблокировать такой мьютекс заставит реализацию перейти на «медленный путь», как если бы мьютекс был недоступен; затем реализация могла бы «по-настоящему» заблокировать «нестандартный» мьютекс. Базовая операция разблокировки более сложна, поскольку реализация никогда в действительности не желает освобождать мьютекс, который был предварительно заблокирован. Это показывает, что (в зависимости от оборудования) существует необходимость применения оптимизаций для более эффективной обработки часто используемых атрибутов мьютекса. Использование общей памяти и синхронизация процессов Существование функций распределения памяти в этом томе стандарта IEEE Std 1003.1-2001 дает приложению возможность выделять память объектам синхронизации из того раздела, который доступен многим процессам (а следовательно, и потокам многих процессов). Чтобы реализовать такую возможность при эффективной поддержке обычного (т.е. однопроцессорного) случая, был определен атрибут Если реализация по Для того чтобы объекты синхронизации по у /* sem.h */ struct semaphore { pthread_mutex_t lock; pthread_cond_t nonzero; unsigned count; }; typedef struct semaphore semaphore_t; semaphore_t *semaphore_create (char *semaphore_name); semaphore_t *semaphore_open (char *semaphore_name); void semaphore_post (semaphore_t *semap); void semaphore_wait (semaphore_t *semap); void semaphore_close (semaphore_t *semap); /* sem.c */ #include semaphore_t * semaphore_create (char * semaphore_name) t int fd; semaphore_t * semap; pthread_mutexattr_t psharedm; pthread_condattr_t psharedc; fd = open(semaphore_name, O_RDWR | O_CREAT | O_EXCL, Оббб); if (fd <0) return (NULL); (void) ftruncate (fd, sizeof (semaphore_t)); (void) pthread_mutexattr_init (&psharedm); (void) pthread_mutexattr_setpshared(&psharedm, PTHREAD_PROCESS_SHARED) ; (void) pthread_condattr_init (&psharedc); (void) pthread_condattr_setpshared (&psharedc PTHREAD_PROCESS_SHARED); semap = (semaphore_t *) mmap (NULL, sizeof (semaphore_t), PR0T_READ | PROT_WRITE, MAP_SHARED, fd, О); close (fd); (void) pthread_mutex_init (&semap->lock, &psharedm); (void) pthread_cond_init (&semap->nonzero, &psharedc); semap->count = 0; return (semap); } semaphore_t * semaphore_open (char *semaphore_name) { int fd; semaphore_t *semap; fd = open (semaphore_name, O_RDWR, 0666); if (fd <0) return (NULL); semap = (semaphore_t *) mmap (NULL, sizeof (semaphore_t), PROT_READ | PROT_WRITE, MAP_SHARED, f d, 0) ; close (fd); return (semap); } void semaphore_post (semaphore_t *semap) { pthread_mutex_lock (&semap->lock); if (semap->count == 0) pthread_cond_signal (&semapx->nonzero); semap->count++; pthread_mutex_unlock (&semap->lock); } void semaphore_wait (semaphore_t * semap) { pthread_mutex_lock (&semap->lock); while (semap->count == 0) pthread_cond_wait (&semap->nonzero, &semap->lock); semap->count--; pthread_mutex_unlock (&semap->lock); } void semaphore_close (semaphore_t *semap) { munmap ((void *) semap, sizeof (semaphore_t)); } Следующий код обеспечивает выполнение трех отдельных процессов, которые создают семафор в файле /tmp/semaphore, отправляют сигналы и ожидают его освобождения. После того как семафор создан, программы сигнализации и ожидания инкрементируют и декрементируют счетчик семафора, несмотря на то, что они сами не инициализировали семафор. /* create.c */ # include «pthread. h» #include «sem.h» int main { semaphore_t * semap; semap = semaphore_create («/ tmp/semaphore») ; if (semap == NULL) exit(l); semaphore_close (semap) ,-return (0); } /* post */ # include «pthread. h» #include «sem.h» int main { semaphore_t *semap; semap = semaphore_open ("/tmp/semaphore»); if (semap == NULL) exit (1); semaphore_post (semap); semaphore_close (semap); return (0); } /* wait */ #include «pthread.h» #include «sem.h» int main { semaphore_t *semap; semap = semaphore_open ("/tmp/semaphore 11 ); if (semap == NULL) exit (1); semaphore_wait (semap); semaphore_close (semap); return (0); } Будущие направления Отсутствуют. Смотри также pthread_cond_destroy , pthread_create , pthread_mutex_destroy , pthread_mutexattr_destroy , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_mutexattr_destroy и pthread_mutexattr_init отмечены как часть опции Threads. Раздел «Ошибки» был отредактирован путем при
pthread_mutexattr_getprioceiling, pthread_mutexattr_setprioceiling
Имя pthread_mutexattr_getprioceiling, pthread_mutexattr_setprioceiling Синопсис THR #include int pthread_mutexattr_getprioceiling ( const pthread_mutexattr_t *restrict attr, int *restrict pthread_mutexattr_t Описание Функции pthread_mutexattr_getprioceiling и pthread_mute-xattr_setprioceiling используются для считывания и установки соответственно атрибута Атрибут Значение атрибута Возвращаемые значения При успешно Ошибки Функции pthread_mutexattr_getprioceiling и pthread_mutexattr_setprioceiling могут завершиться неудачно, если: [EINVAL] значение, заданное пара [EPERM] инициатор вызова не обладает привеле Эти функции не возвра pthread_mutexattr_getprioceiling, pthread_mutexattr_setprioceiling — функции считывания и установки атрибута Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_cond_destroy, pthread_create, pthread_mutex_destroy, том Base Definitions стандарта1ЕЕЕ Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для со Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_mutexattr_getprioceiling и pthread_mutexattr_setp-rioceiling отмечены как часть опций Threads и Thread Priority Protection. Код ошибки [ENOSYS] был исключен, поскольку его нет смысла учитывать, если реализация не под Ко В целях согласования со стан
pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol
Имя pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol Синопсис THR #include TPP|TPI int pthread_mutexattr_getprotocol (const pthread_mutexattr_t *restrict attr, int *restrict int pthread_mutexattr_setprotocol ( pthread_mutexattr_t * attr, int Описание Функции pthread_mutexattr_getprotocol и pthread_mutexattr_setprotocol используются для считывания и установки соответственно атрибута Параметр PTHREAD_PRIO_NONE TPI PTHREAD_PRIO_INHERIT TPP PTHREAD_PRIO_PROTECT Если поток владеет мьютексом с использованием значения PTHREAD_PRIO_NONE для атрибута TPI Если поток блокирует потоки с более высоким приоритетом благодаря тому, что он владеет одним или несколькими мьютексами, у которых атрибут TPP Если поток владеет одни Пока поток удерживает Если поток одновре TPI Если поток обращается к функции pthread_mutex_lock , а атрибут Возвращаемые значения При успешно Ошибки Функция pthread_mutexattr_setprotocol завершится неудачно, если: [ ENOTSUP ] значение, заданное пара Функции pthread_mutexattr_getprotocol и pthread_mutexattr_setprotocol [EINVAL] значение, заданное пара [EPERM] инициатор вызова не обладает привиле Эти функции не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_cond_destroy , pthread_create , pthread_mutex_destroy , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_mutexattr_getprotocol и pthread_mutexattr_setprotocol от Код ошибки [ENOSYS] был исключен, поскольку его нет с В целях согласования со стан
pthread_mutexattr_getpshared, pthread_mutexattr_setpshared
Имя pthread_mutexattr_getpshared, pthread_mutexattr_setpshared — функ-ции считывания и установки атрибута Синопсис THR #include int pthread_mutexattr_getpshared ( const pthread_mutexattr_t *restrict attr, int *restrict pthread_mutexattr_t *attr, int Описание Функция pthread_mutexattr_getpshared используется дл Атрибут Возвращаемые значения При успешном завершении функция pthread_mutexattr_setpshared возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функции pthread_mutexattr_getpshared и pthread_mutexattr_setpshared [ EINVAL] значение, заданное параметром attr, недействительно. Функция pthread_mutexattr_setpshared [EINVAL] новое значение, за Эти функции не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_cond_destroy, pthread_create, pthread_mutex_destroy, pthread_mutexattr_destroy , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_mutexattr_getpshared и pthread_mutexattr_setpshared от В целях согласования со стандарто
pthread_mutexattr_gettype, pthread_mutexattr_settype
Имя pthread_mutexattr_gettype, pthread_mutexattr_settype — функции считывания и установки атрибута type. Синопсис XSI #include int pthread_mutexattr_gettype ( const pthread_mutexattr_t *restrict attr, int *restrict type); int pthread_mutexattr_settype ( pthread_mutexattr_t *attr, int type); Описание Функции pthread_mutexattr_gettype и pthread_mutexattr_settype используются для считывания и установки соответственно атрибута type. Этот атрибут задается при вызове этих функций в пара Атрибут type содержит тип PTHREAD_MUTEX_NORMAL Мьютекс этого типа не обнаруживает взаи PTHREAD_MUTEX_ERRORCHECK Мьютекс этого типа выполняет проверку на наличие ошибок. Поток, пытаясь перезаблокировать такой мьютекс без первоначального его разблокирования, генерирует код ошибки. При попытке разблокировать мьютекс, заблокированный другим потоком, генерируется код ошибки. При попытке разблокировать незаблокированный мьютекс также генерируется код ошибки. PTHREAD_MUTEX_RECURS IVE PTHREAD_MUTEX_DEFAULT Попытка рекурсивного блокирования мьютекса этого типа приводит к неопределенному поведению. Попытка разблокировать мьютекс, не заблокированный вызывающим потоком, приводит к неопределенному поведению. Попытка разблокировать незаблокированный мьютекс также приводит к неопределенному поведению. Реализация может преобразовать мьютекс этого типа в один из других типов мьютексов. Возвращаемые значения При успешном завершении функция pthread_mutexattr_gettype возвращает нулевое значение и сохраняет значение атрибута type, считанное из объекта attr, в объекте, адресуемом параметром type; в противном случае она возвращает код ошибки, обозначающий ее характер. При успешном завершении функция pthread_mutexattr_settype возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_mutexattr_settype завершится неудачно, если: [EINVAL] значение, заданное пара Функции pthread_mutexattr_gettype и pthread_mutexattr_settype могутзавершиться неудачно, если: [EINVAL] значение, заданное пара Эти функции не возвра Примеры Отсутствуют. Замечания по использованию В приложениях пре Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_cond_timedwait, том Base Definitions стандарта IEEE Std 1003.1-200l, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Приложен список опечаток Open Group Corrigendum U033/3. Был отредактирован раздел «Синопсис» для функции pthread_mutexattr_gettype , в результате чего первый аргумент получил тип const pthread_mutexattr_t*. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_mutexattr_gettype было добавлено ключевое слово restrict.
pthread_once
Имя pthread_once — функция Синопсис THR #include int pthread_once (pthread_once_t Описание При перво Функция pthread_once не является точкой от Константа PTHREAD_ONCE_INIT определяется в заголовке Поведение функции pthread_once будет неопределенны Возвращаемое значение При успешно Ошибки Функция pthread_once [EINVAL] значения, заданные пара Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Некоторые библиотеки С разработаны для дина static int random_is_initialized = 0; extern int initialize_random ; int random_function { if (random_is_initialized == 0) { initialize_random ; random_is_initialized = 1; } ... /* Операции, выполняемые после инициализации. */ } Чтобы хранить такую же структуру в многопоточной программе, нужно использовать новый примитив. В противном случае инициализация библиотеки должна быть выполнена путем явного вызова экспортированной функции инициализации до какого бы то ни было использования этой библиотеки. Для динамической инициализации в многопоточном процессе недостаточно простого флага инициализации; этот флаг необходимо защищать от модификации данных со стороны нескольких потоков, одновременно обращающихся к библиотеке. Защита флага требует использования мьютекса, однако мьютексы должны быть инициализированы до их использования. Для гарантии того, что мьютекс инициализируется только единожды, требуется рекурсивное решение этой проблемы. Использование функции pthread_once не только предоставляет гарантированные реализацией средства дина #include static pthread_once_t random_is_initialized =PTHREAD_ONCE_INIT; extern int initialize_random; int random_function { (void) pthread_once (&random_is_initialized,initialize_random); ... /* Операции, выполняемые после инициализации. */ } Обратите вни Будущие направления Отсутствуют. Смотри также Том Base Definitions стандарта1ЕЕЕStd 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функция pthread_once от Был добавлен код ошибки [EINVAL], возвращаемый при неудачном завершении функции в случае, если хотя бы один из аргументов недействителен.
pthread_rwlock_destroy, pthread_rwlock_init
Имя pthread_rwlock_destroy, pthread_rwlock_init — функции разрушения и инициализации объекта блокировки для чтения и записи. Синопсис THR #include int pthread_rvlock_destroy(pthread_rwlock_t *rwlock); int pthread_rwlock_init( pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); Описание Функция pthread_rwlock_destroy используется для разрушения объекта блокировки чтения и записи, адресуемого параметром Функция pthread_rwlock_init выделяет любые ресурсы, необходимые для использования объекта блокировки для чтения и записи, адресуемого пара При неудачном выполнении функции pthread_rwlock_init объект, адресуемый параметро Для выполнения синхронизации можно использовать только объект, адресуемый параметром Возвращаемые значения При успешно Проверка на наличие ошибок с кода Ошибки Функция pthread_rwlock_destroy [EBUSY] реализация обнаружила попытку разрушить заблокированный объект, адресуе [EINVAL] значение, за Функция pthread_rwlock_init завершится неу [EAGAIN] систе [ENOMEM] для инициализации объекта блокировки для чтения и записи недостаточно существующей памяти; [EPERM] инициатор вызова не обладает привилегиями для выполнения этой операции. Функция pthread_rwlock_init [EBUSY] реализация обнаружила попытку повторно инициализировать объект блокировки, адресуе [EINVAL] значение, заданное пара Эти функции не возвращают ко Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_rdlock , pthread_rwlock_timedrdlock , pthread_rwlock_timedwrlock , pthread_rwlock_tryrdlock , pthread_rwlock_trywrlock, pthread_rwlock_unlock, pthread_rwlock_wrlock , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1j-2000 были внесены следующие изменения. • В разделе «Синопсис» изменена метка. Новая метка THR обозначает, что рассматриваемые функции теперь являются частью опции Threads (ранее они относились к опции Read-Write Locks стандарта IEEE Std 1003.1j-2000, а также считались частью дополнения XSI). В раздел «Синопсис» также не входит макрос инициализации. • Раздел «Описание» отредактирован следующим образом: — явно отмечено выделение ресурсов при инициализации объекта блокировки для чтения и записи; — добавлен абзац, в котором указывается, что копии объекта блокировки для чтения и записи использовать нельзя. • В раздел «Ошибки» добавлен код ошибки [EINVAL] , означающий, что при вызове функции pthread_rwlock_init значение, заданное пара • Отредактирован раздел «Смотри также». В целях согласования со стандарто
pthread_rwlock_rdlock, pthread_rwlock_tryrdlock
Имя pthread_rwlock_rdlock, pthread_rwlock_tryrdlock— функции блокирования объекта блокировки чтения-записи для обеспечения чтения. Синопсис THR #include int pthread_rwlock_rdlock (pthread_rwlock_t Описание Функция pthread_rwlock_rdlock при TPS Если поддерживается опция Thread Execution Scheduling и потоки, участвующие в данной блокировке, выполняются с использованием стратегий планирования SCHED_FIFO или SCHED_RR, то вызывающий поток не получит эту блокировку, если ее удерживает записывающий поток или если по этому объекту блокировки заблокированы записывающие потоки такого же или более высокого приоритета; в противном случае вызывающий поток получит блокировку. TSP TSP Если поддерживается опция Thread Execution Scheduling и потоки, участвующие в данной блокировке, выполняются с использованием стратегии планирования SCHED_SPORADIC, то вызывающий поток не получит эту блокировку, если ее удерживает записывающий поток или если по этому объекту блокировки заблокированы записывающие потоки такого же или более высокого приоритета; в противном случае вызывающий поток получит блокировку. Если опция Thread Execution Scheduling не поддерживается, то только конкретнал реализация определяет, получит ли вызывающий поток эту блокировку, если никакой записывающий поток не удерживает этот объект блокировки и существуют другие записывающие потоки, заблокированные по этому объекту. Если записывающий поток удерживает этот объект блокировки, вызывающий поток не получит блокировку для чтения. Если блокировка для чтения не предоставлена, вызывающий поток блокируется до тех пор, пока он не получит блокировку. Вызывающий поток может попасть в ловушку взаимоблокировки, если во время вызова он удерживает блокировку для обеспечения записи. Поток Максимальное количество одновременных (и гарантированно успешных) блокировок для чтения, которое может быть применено к объекту блокировки чтения-записи, определяется конкретной реализацией. В случае превышения этого максимума функция pthread_rwlock_rdlock может завершиться неудачно. Функция pthread_rwlock_tryrdlock при Результаты выполнения этих функций не определены, если любая из них вызывается с неинициализированным объектом блокировки чтения-записи. Если потоку, ожидающему освобождения блокировки чтения-записи для обеспечения блокировки чтения передается сигнал, то после его обработки поток возобновит ожидание освобождения блокировки, как если бы оно и не прерывалось. Возвращаемые значения При успешном завершении функция pthread_rwlock_rdlock возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Функция pthread_rwlock_tryrdlock возвращает нулевое значение, если блокировка для чтения по объекту блокировки чтения-записи, адресуемому параметром Ошибки Функция pthread_rwlock_tryrdlock завершится неудачно, если: [EBUSY] блокировка чтения-записи не могла быть предоставлена для чтения, поскольку удерживает блокировку записывающий поток, или по этому объекту заблокирован записывающий поток с соответствующим приоритетом Функции pthread_rwlock_rdlock и pthread_rwlock_tryrdlock [EINVAL] значение, заданное пара [EAGAIN] блокировка не Функция pthread_rwlock_rdlock [EDEADLK] теку Примеры Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy , pthread_rwlock_timedrdlock , pthread_rwlock_timedwrlock , pthread_rwlock_trywrlock , pthread_rwlock_unlock , pthread_rwlock_wrlock , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандарто • В разделе «Синопсис» была из • Раздел «Описание» был отредактирован следую - заданы условия, при которых записываю - разъяснена воз - добавлен абзац, в которо • Был • Был отредактирован раздел «Смотри также». Замечания по использованию Как упо
pthread_rwlock_timedrdlock
Имя pthread_rwlock_timedrdlock— функция, блокирующал объект блокировки чтения-записи для обеспечения чтения. Синопсис THR #include int pthread_rwlock_timedrdlock ( pthread_rwlock_t *restrict const struct timespec *restrict Описание Функция pthread_rwlock_timedrdlock при TMR Если по Если опция Timers не поддерживается, отсчет интервала времени происходит с использованием системных часов, значение которых возвращает функция time . Разрешение для интервала времени определяется разрешением часов, которые используются для его отсчета. Тип данных timespec определяется в заголовке Если потоку, заблокированно Вызывающий поток может попасть в ловушку взаимоблокировки, если во время вызова он удерживает блокировку для обеспечения записи по объекту, адресуемому параметром Возвращаемое значение Функция pthread_rwlock_timedrdlock возвра Ошибки Функция pthread_rwlock_timedrdlock завершится неудачно, если: [ETIMEDOUT] блокировка не Функция pthread_rwlock_timedrdlock [EAGAIN] блокировка превышено [EDEADLK] вызываю [EINVAL] значение, заданное параметром rwlock, не относится к инициализированному объекту блокировки чтения-записи, или значение abs_timeout, выраженное в наносекундах, меньше нуля либо больше или равно 1000 миллионам. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Как упо Функция pthread_rwlock_timedrdlock является частью опций Threads и Timeouts и может быть не пре Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy, pthread_rwlock_rdlock, pthread_rwlock_timedwrlock, pthread_rwlock_tryrdlock, pthread_rwlock_trywrlock, pthread_rwlock_unlock, pthread_rwlock_wrlock , то Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6, основание
pthread_rwlock_timedwrlock
Имя pthread_rwlock_timedwrlock — функция, блокирующая объект блокировки чтения-записи для обеспечения записи. Синопсис THR TMO #include #include int pthread_rwlock_timedwrlock ( pthread_rwlock_t *restrict const struct timespec *restrict Описание Функция pthread_rwlock_timedwrlock при TMR Если поддерживается опция Timers, отсчет интервала вре Если опция Timers не поддерживается, отсчет интервала времени происходит с использованием системных часов, значение которых возвращает функция time . Разрешение для интервала времени определяется разрешением часов, которые используются для его отсчета. Тип данных timespec определяется в заголовке Если потоку, заблокированно Вызывающий поток может попасть в ловушку взаимоблокировки, если во время вызова он удерживает блокировку чтения-записи по объекту, адресуемому параметро Возвращаемое значение Функция pthread_rwlock_timedwrlock возвра Ошибки Фу [ETIMEDOUT] блокировка не Функция pthread_rwlock_timedwrlock [EDEADLK] вызываю адресуе [EINVAL] значение, заданное пара Эта функция не возвра Примеры Отсутствуют. Замечания по использованию Как упоминалось в томе Base Definitions стандарта IEEE Std 1003.1-2001 (Section 3.285, Priority Inversion), приложения, которые используют эту функцию, могут подвергнуться инверсии приоритетов. Функция pthread_rwlock_timedwrlock является частью опций Threads и Timeouts и может быть не предоставлена во всех реализациях. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy,pthread_rwlock_rdlock, pthread_rwlock_timedrdlock, pthread_rwlock_tryrdlock, pthread_rwlock_trywrlock , pthread_rwlock_unlock , pthread_rwlock_wrlock , том Base Definitions стандарта IEEE Std 1003.1-2001, Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6, основание
pthread_rwlock_trywrlock, pthread_rwlock_wrlock
Имя pthread_rwlock_trywrlock, pthread_rwlock_wrlock — функции, блокирующие объект блокировки чтения-записи для обеспечения записи. Синопсис THR #include int pthread_rwlock_trywrlock (pthread_rwlock_t int pthread_rwlock_wrlock (pthread_rwlock_t Описание Функция pthread_rwlock_trywrlock при Функция pthread_rwlock_wrlock при Реализации могут благоприятствовать записывающим потокам перед считывающими, чтобы избежать зависания записывающего потока. Результаты не определены, если Любая из этих функций вызывается с неинициализированным объектом блокировки чтения-записи. Если потоку, ожидающему блокировки для обеспечения записи, передается сигнал, то после его обработки поток возобновит ожидание освобождения блокировки, как если бы оно и не прерывалось. Возвращаемые значения Функция pthread_rwlock_trywrlock возвра При успешном завершении функция pthread_rwlock_wrlock возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_rwlock_trywrlock завершится неудачно, если: [EBUSY] блокировка чтения-записи не Функции pthread_rwlock_wrlock и pthread_rwlock_trywrlock [EINVAL] значение, заданное пара Функция pthread_rwlock_wrlock [EDEADLK] теку Эти функции не возвра Примеры Отсутствуют. Замечания по использованию Как упо Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy,pthread_rwlock_rdlock, pthread_rwlock_timedrdlock , pthread_rwlock_timedwrlock , pthread_rwlock_tryrdlock,pthread_rwlock_unlock, том Base Definitions cтaндapтaIEEEStd 1003.1-2001, Последовательность внесения изменений Функции впервые реализованы в вылуске Issue 5. Issue 6 Для согласования со стандарто • В разделе «Синопсис» была из носились к опции Read-Write Locks стандарта IEEE Std 1003.1j-2000, а также считались частью дополнения XSI). • Из раздела «Ошибки» удален абзац, посвященный описанию кода ошибки [EDEADLK] , возвращаемому функцией pthread_rwlock_trywrlock . • Был отредактирован раздел «С
pthread_rwlock_unlock
Имя pthread_rwlock_unlock— функция разблокирования объекта блокировки чтения-записи. Синопсис THR #include int pthread_rwlock_unlock(pthread_rwlock_t Описание Функция pthread_rwlock_unlock используется для освобождения блокировки, удерживае Если эта функция вызывается, чтобы освободить блокировку для обеспечения чтения, и существуют другие блокировки чтения, удерживаемые в данный момент по этому объекту блокировки чтения-записи, то он (объект) останется в состоянии блокирования для обеспечения чтения. Если с помощью этой функции освобождается последняя блокировка для чтения по заданному объекту блокировки чтения-записи, то этот объект перейдет в разблокированное состояние и, соответственно, не будет иметь владельцев. Если эта функция вызывается, чтобы освободить блокировку для обеспечения записи по заданному объекту блокировки чтения-записи, то этот объект перейдет в разблокированное состояние. Если существуют потоки, заблокированные по этому объекту блокировки, то при его освобождении именно стратегия планирования определяет, какой поток (потоки) получит блокировку. TPS Если потоки, ожидающие освобождения блокировки, выполняются в соответствии со стратегиями планирования SCHED_FIFO, SCHED_RR или SCHED_SPORADIC, то при поддержке опции Thread Execution Scheduling после освобождения этой блокировки потоки получат блокировку в порядке следования их приоритетов. Для потоков с одинаковыми приоритетами блокировки для записи имеют преимущество перед блокировками для чтения. Если опция Thread Execution Scheduling не поддерживается, то будут ли блокировки для записи иметь преимущество перед блокировками для чтения, определяется конкретной реализацией. Результаты не определены, если эта функция вызывается с неинициализированным объектом блокировки чтения-записи. Возвращаемое значение При успешном завершении функция pthread_rwlock_unlock возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_rwlock_unlock [EINVAL] значение, заданное пара [EPERM] текущий поток не удерживает объект блокировки чтения-записи для обеспечения записи или чтения. Функция pthread_rwlock_unlock не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy, pthread_rwlock_rdlock, pthread_rwlock_timedrdlock , pthread_rwlock_timedwrlock , pthread_rwlock_tryrdlock, pthread_rwlock_trywrlock, pthread_rwlock_wrlock , то Последовательность внесения изменений Функции впервые реализованы в вылуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1j-2000 были внесены следующие изменения. • В разделе «Синопсис» была изменена метка. Новал метка THR означает, что рассматриваемые функции теперь являются частью опции Threads (ранее они относились к опции Read-Write Locks стандарта IEEE Std 1003.1j-2000, а также считались частью дополнения XSI). • Раздел «Описание» был отредактирован следующим образом: — заданы условия, при которых записывающие потоки имеют преимущество перед считывающими; — удалена концепция владельца блокировки чтения-записи. • Был отредактирован раздел «Смотри также».
pthread_rwlockattr_destroy, pthread_rwlockattr_init
Имя pthread_rwlockattr_destroy, pthread_rwlockattr_init— функции разрушения и инициализации объекта атрибутов для блокировки чтения-записи. Синопсис THR #include int pthread_rwlockattr_destroy( pthread_rwlockattr_t *attr); int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); Описание Функция pthread_rwlockattr_destroy используется для разрушения объекта атрибутов для блокировки чтения-записи. Разрушенный объект атрибутов, адресуемый параметром attr, можно инициализировать повторно с помощью функции pthread_rwlockattr_init ; результаты ссылки на этот объект после его разрушения не определены. В конкретной реализации функция pthread_rwlockattr_destroy может устанавливать объект, адресуемый параметром attr, равным недействительному значению. Функция pthread_rwlockattr_init предназначена для инициализации объекта атрибутов блокировки чтения-записи attr значением, действующим по умолчанию для всех атрибутов, определенных конкретной реализацией. Если функция pthread_rwlockattr_init вызывается для уже инициализированного объекта атрибутов attr, то результаты вызова этой функции не определены. После того как объект атрибутов блокировки чтения-записи уже был использован для инициализации одной или нескольких блокировок чтения-записи, Любая функция, которая оказывает влияние на объект атрибутов (включал деструктор), никак не отразится на ранее инициализированных блокировках чтения-записи. Возвращаемые значения При успешно Ошибки Функция pthread_rwlockattr_destroy может завершиться неудачно, если: [EINVAL] значение, заданное параметром attr, недействительно. Функция pthread_rwlockattr_init завершится неудачно, если: [ENOMEM] для инициализации объекта атрибутов блокировки чтения-записи недостаточно существующей памяти. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy, pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1j-2000 были внесены следующие изменения. • В разделе «Синопсис» была изменена метка. Новал метка THR означает, что рассматриваемые функции теперь являются частью опции Threads (ранее они относились к опции Read-Write Locks стандарта IEEE Std 1003.1j-2000, а также считались частью дополнения XSI). • Был отредактирован раздел «Смотри также».
pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared
Имя pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared —функции считывания и установки атрибута Синопсис THRTSH #include int pthread_rwlockattr_getpshared( const pthread_rwlockattr_t *restrict attr, int *restrict pthread_rwlockattr_t * attr, int Описание Функция pthread_rwlockattr_getpshared используется для получения значения атрибута Атрибут process-sharedycтанaвливaeтcя равны Дополнительные атрибуты, их значения по умолчанию и имена соответствующих функций считывания и установки значений этих атрибутов определяются конкретной реализацией. Возвращаемые значения При успешно При успешно Ошибки Функции pthread_rwlockattr_getpshared и pthread_rwlockattr_ setpshared [ EINVAL ] значение, заданное пара Функция pthread_rwlockattr_setpshared [EINVAL] новое значение, заданное для атрибута, попа Эти функции не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy,pthread_rwlockattr_destroy, pthread_rwlockattr_init , то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стан • В разделе «Синопсис» была изменена метка. Новая метка THR означает, что расс • В разделе «Описание» отмечено, что дополнительные атрибуты определяются конкретной реализацией. • Был отредактирован раздел «Смотри также». В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_rwlockattr_getpshared было добавлено ключевое слово restrict.
pthread_self
Имя pthread_self — функция получения и Синопсис THR #include pthread_t pthread_self {void); Описание Функция pthread_self возвра Возвращаемое значение См. раз Ошибки Ко Функция pthread_self не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Функция pthread_self обеспечивает воз Будущие направления Отсутствуют. Смотри также pthread_create , pthread_equal , то Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширение Issue 6 Функция pthread_self от
pthread_setcancelstate, pthread_setcanceltype, pthread_testcancel
Имя pthread_setcancelstate, pthread_setcanceltype, pthread_testcancel— функции установки состояния от Синопсис THR #include int pthread_setcancelstate(int int pthread_setcanceltype(int void pthread_testcancel(void); Описание Функция pthread_setcancelstate о Функция pthread_setcanceltype о Состояние и тип отмены любых создаваемых потоков, включал поток, в которо Функция pthread_testcancel пре Возвращаемые значения При успешно Ошибки Функция pthread_setcancelstate [EINVAL] за Функция pthread_setcanceltype [EINVAL] за Эти функции не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Функции pthread_setcancelstate и pthread_setcanceltype позволяют управлять точка Объект можно рассматривать как обобщение некоторой процедуры. Вернее, он представляет собой множество процедур и глобальных переменных, организованных в виде одного модуля, вызываемого клиентами, не известными для этого объекта, причем одни объекты могут зависеть от других. Во-первых, на входе в объект возможность отмены должна быть запрещена (никогда явно не разрешена). На выходе из объекта состояние отмены должно быть всегда восстановлено до значения, которое оно имело на входе в этот объект. Это следует из принципа модульности: если клиент объекта (или клиент объекта, использующего данный объект) запретил возможность отмены, это означает, что клиент не желает проведения очистительно-восстановительных операций в случае, если поток будет отменен во время выполнения некоторой важной последовательности действий. Если объект вызывается в таком состоянии и предоставляет возможность отмены, а запрос на отмену задерживается для этого потока, то такой поток отменяется вопреки желанию клиента (т.е. вопреки запрету на отмену). Во-вторых, на входе в объект тип отмены можно установить явным образом (равным либо «отложенному», либо «асинхронному» значению). Но, как и для состояния отмены, на выходе из объекта тип отмены должен быть всегда восстановлен до значения, которое он имел на входе в этот объект. Наконец, из потока, который позволяет асинхронную от Будущие направления Отсутствуют. Смотри также pthread_cancel, то Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширение Issue 6 Функции pthread_setcancelstate , pthread_setcanceltype и pthread_ testcancel от
pthread_setschedprio
Имя pthread_setschedprio — функция Синопсис THRTPS #include int pthread_setschedprio(pthread_t Описание Функция pthread_setschedprio используется В случае неудачного завершения функции pthread_setschedprio приоритет планирования заданного потока останется без из Возвращаемое значение При успешно Ошибки Функция pthread_setschedprio [EINVAL] значение пара [ENOTSUP] была сделана попытка установить приоритет равны [EPERM] инициатор вызова не и [EPERM] реализация не позволяет приложению [ESRCH] значение, за Функция pthread_setschedprio не возвра Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Функция pthread_setschedprio обеспечивает для приложения воз Нес Будущие направления Отсутствуют. Смотри также pthread_getschedparam, то Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6. Включена в качестве реакции на интерпретацию IEEE PASC Interpretation 1003.1 #96. СПИСОК ЛИТЕРАТУРЫ 1. Audi, Robert. Action, Intention, andReason. Ithaca, N. Y.: Cornell University Press, 1993. 2. Axford, Tom. Concurrent Programming: Fundamental Techniques for Real-Time and ParalMSoftwareDesign, Chichester, U. K.:JohnWiley, 1989. 3. Baase, Sarah. ComputerAlgorithms: Introduction to Design andAnalysis. 2nd ed. Reading, Mass.:Addison-Wesley, 1988. 4. Barfield, Woodrow, and Thomas A. Furnell III. Virtual Environments and Advanced InterfaceDesign. New York: Oxford University Press, 1995. 5. Binkley, Robert, Bronaugh, Richard, and Ausonio Marras. Agent, Action, andReason. Toronto: UniversityofToronto Press, 1971. 6. Booch, Grady, James Rumbaugh, and IvarJacobson. The Unified Modeling Language UserGuide. Boston: Addison-Wesley, 1999. 7. Bowan, Howard, andJohn Derrick. FormalMethods forDistributedProcessing: A Survey of Object-OrientedApproaches. NewYork: Cambridge University Press, 2001. 8. Brewka, Gerhard,Jurgen Diz, and Kurt Konolige. Nonmonotonic Reasoning. Stanford, Calif.: CSLI Publications, 1997. 9. Carroll, Martin D., and Margaret A. Ellis. Designing and CodingReusabh C++. Reading, Mass.: Addison-Wesley, 1995. 10. Cassell, Justine, Joseph Sullivan, Scott Prevost, and Elizabeth Churchill. Embodied ConversationalAgents. Cambridge, Mass.: MIT Press, 2000. 11. Chellas, Brian F. ModalLogic: An Introduction. New York: Cambridge University Press, 1980. 12. Coplien,James O. MuUi-Paradigm Design for C++. Reading, Mass.: Addison-Wesley, 1999. 13. Cormen, Thomas, Charles Leiserson, and Ronald Rivet. Introduction to Algorithms. Cambridge, Mass.: MIT Press, 1995. 14. Englemore, Robert, and Tony Morgan. BUickboard Systems. Wokingham, England: Addison-Wesley, 1988. 15. Garg, Vijay K. Principhs ofDistnbutedSystems. Norwell, Mass.: KluwerAcademic, 1996. 16. Geist, A1, Adam Beguelin, Jack Dongarra, Weicheng Jiang, Robert Manchek, and VaidySinderman. PVM:ParaUelVirtualMachine. London, England: MITPress, 1994. 17. Goodheart, Berny, andJames Cox. The Magic Garden ExpUiined: The Internak of Unix System VReUase4. New York: Prentice Hall, 1994. 18. Gropp, William, Steven Huss-Lederman, Andrew Lumsdaine, Ewing Lusk, Bill Nitzberg, William Saphir, and Marc Snir. MPI: The Compkte Reference. Vol. 2. Cambridge, Mass.: MIT Press, 1998. 19. Heath, Michael T. Scientific Computing: An Introduction Survey. New York: McGraw-Hill. 20. Henning, Michi, and Steve Vinoski. Advanced COBRA Programming with C++. Reading, Mass.: Addison-Wesley, 1999. 21. Hintikka,Jakko, and Merrill Hintikka. The Logic ofEpistemoU>gy and the EpistemoU>gy of LogTC.Amsterdam: KluwerAcademic, 1989. 22. Horty,John F. Agency andDeonticLogic. New York: Oxford University Press, 2001. 23. Hughes, Cameron, and Tracey Hughes. Mastering the Standard C++ CJmses. New York: JohnWiley, 1990. 24. Hughes, Cameron, and Tracey Hughes. Object-OrientedMuUithreading Using C++. New York:JohnWiley, 1997. 25. Hughes, Cameron, and Tracey Hughes. Linux Rapid Application Devebpment. Foster City, Calif.: M & T Books, 2000. 26. International Standard Organization. Information Technobgy: Portabk Operating System Interface. Pt. 1 System Application Program Interface. 2nd ed. Std 1003.1 ANSI/IEEE. 1996. 27. Josuttis, Nicolai M. The C++ Standard Boston: Addison-Wesley, 1999. 28. Koeing, Andrew, and Barbara Moo. Ruminations on C++. Reading, Mass.: Addison-Wesley, 1997. 29. Krishnamoorthy, C. S., and S. Rajeev. Artificial Intelligence and Expert Systems for Engineers. Boca Raton, Fla.: CRC Press, 1996. 30. Lewis, Ted, Glenn Andert, Paul Calder, Erich Gamma, Wolfgang Press, Larcy Rosenstein, and Kraus, Sarit. Strategic Negotiation in MuUitangent Environments. London: МГГ Press, 2001. 31. Luger, George F. ArtificialInteUigence. 4th ed. England: Addison-Wesley, 2002. 32. Mandrioli Dino, and Carlo Ghezzi. Theoretical Foundations of Computer Science. New York:JohnWiley, 1987. 33. Nielsen, Michael A., and Isaac L. Chuang. Quantum Computation and Quantum Information. New York: Cambridge University Press, 2000. 34. Patel, Mukesh J., Vasant Honavar, and Karthik Balakrishnan. Advances in the Evolutionary SynthesisofIntelligentAgents. Cambridge, Mass.: МГГ Press, 2001. 35. Picard, Rosalind. Affective Computing. London: MIT Press, 1997. 36. Rescher, Nicholas, and Alasdir Urquhart. Temporal Logic. New York: Springer-Verlag, 1971. 37. Robbins, Kay A., and Steven Robbins. Practical Unix Programming. Upper Saddle River, N.J.: Prentice Hall, 1996. 38. Schmucker, Kurt, Ander Weinand, andJohn M. VUssides. Object-OrientedApplication Frameworks. Greenwich, Conn.: ManningPublications, 1995. 39. Singh, Наггу. Progressing to Distributed Multiprocessing. Upper Saddle River, N.J.: Prentice Hall, 1999. 40. Skillicorn, David. Foundations of ParaUel Programming. New York: Cambridge University Press, 1994. 41. Soukup, Jiri. Taming C++: Pattern Ckisses and Persistence for Large Projects. Reading, Mass.:Addison-Wesley, 1994. 42. Sterling, Thomas L.,John Salmon, DonaldJ. Becker, and Daniel F. Savarese. How to Build a Bewoulf: A Guide to ImpUmentation and Application of PC Clusters. London: MITPress, 1999. 43. Stevens, Richard W. UNIX Network Programming: Interprocess Communications. Vol. 2, 2nd ed. Upper Saddle River: Prentice Hall, 1999. 44. Stroustrup, Bjarne. TheDesign andEvohUion of C++. Reading, Mass.: Addison-Wesley, 1994. 45. Subrahmanian, V.S., Piero Bonatti,Jurgen Dix, Thomas Eiter, Sarit Kraus, Fatma Ozcan, and Robert Ross. HeterogeneousAgentSystems. Cambridge, Mass.: МГГ Press, 2000. 46. Tel, Gerard. Introduction to Distributed Algorithms. 2nd ed. New York: Cambridge University Press, 2000. 47. Thompson, WilliamJ. ComputingforScientists andEngineers. New York:John Wiley, 1992. 48. Tomas, Gerald, and Christoph W. Uebeerhuber. Visualization of Scientific ParaUel Programming. New York: Springer-Verlag, 1994. 49. Tracy, Kim W. and Peter Bouthoorn. Object-Oriented: Artificial InteVigence Using C++. NewYork: ComputerScience Press, 1997. 50. Weiss, Gerhard. MultitagentSystems. Cambridge, Mass.: MFTPress, 1999. 51. Wooldridge, Michael. ReasoningAboutRationalAgents. London: MIT Press, 2000. notes
Notes
1
POSIX— Portable Operating System Interface for computer environments— интерфейс переносимой операционной системы (набор стандартов IEEE, описывающих интерфейсы ОС для UNIX).
2
IEEE— профессиональное объединение, выпускающие свои собственные стандарты; членами IEEE являются ANSI и ISO.
3
) В оригинале написано «On the other hand, the distributed application in Figure 1-1 consists of three separate programs with each program executing on a separate computer». Что можно перевести как «С другой стороны, распределенное приложение на рисунке 1-1 состоит из трех отдельных программ, каждая из которых выполняется на отдельном компьютере» (Примечание пирата)
4
M.J. Flynn. Very high-speed computers. Из сборников объединения IEEE, 54, 1901-1909 (декабрь 1966).
5
MPP- Massively Parallel Processors (процессоры с массовым параллелизмом), SMP- symmetric multi-processor(симметричный мультипроцессор).
6
В оригинале «text segment», что принято переводить как «сегмент кода»
7
В оригинале - «Some hardware resources
8
В оригинале - «Therefore, once the thread exits, its resources, namely thread id, can be instantly reused» Exist & exits — все-таки разные слова
9
В оригинале следующее - «If the terminating thread did not make a call to pthread_exit, then the exit status will be the return value of the function, if it has one; otherwise, the exit status is NULL» Под
10
11
Не макроса, а инициализации предопределенной константой, вообще-то
12
Правда, это не обеспечивает автоматически того, что исключение не будет возбуждено какой-либо функцией, вызванной данным методом.
13
Мы не включаем многопоточные программы в категорию распределенных
14
Все примеры использования CORBA-компонентов в этой книге реализованы с использованием версии MICO 2.3.3 в операционной системе SuSE Linux и версии MICO 2.3.7 в операционной системе Solaris 8.
15
Вызовы удаленных объектов вносят задержку во времени, необходимость выполнять требования безопасности и возможность возникновения частичных отказов.
16
wCorba - это стандарт CORBA для беспроводного взаимодействия удаленных объектов. Материалы по стандарту wCORBA доступны по адресу: www.omg.org.
17
Все МРI- примеры в этой книге реализованы с использованием версий MPICH 1.1.2 и MPICH 1.2.4 в среде Linux.
18
Вообще-то «API-интерфейс» - это «масло масляное»
19
При использовании термина объект в определении агента мы включаем родственные для него понятия из области искусственного интеллекта:
20
Мы намеренно избегаем термина
21
Из нашего определения когнитивных структур данных намеренно исключены такие относящиеся к психике человека понятия, как воображение, паранойя, беспокойство, счастье, грусть и т.п. Нас интересует рациональное эпистемологическое, а не интеллектуальное
22
Несмотря на то что «классную доску»можно использовать для решения для многих аналогичных задач, вряд ли это возможно для совершенно различных классов задач, т.е. многократное использование «классной доски» обычно ограничено близкими по своей сути задачами. Дело в том, что пространство решений в этом случае тесно связано с конкретной задачей, а компонент правил тесно связан с пространством решений, что не позволяет использовать «классную доску» для решения задач более широкого диапазона.
23
На практике каждый сегмент источника знаний должен содержать один или несколько стандартных контейнерных С++-классов, используемых в качестве очередей данных и очередей событий. Безопасность каждого контейнера обеспечивается за счет компонентов синхронизации.
24
Эта конфигурация обусловлена тем, что Prolog имеет множество таких встроенных средств, как операция унификации, возврат к предыдущему состоянию и поддержка логики предикатов, которые в противном случае (без использования языка Prolog) пришлось бы реализовать в С++ «с нуля». В этой книге для примеров, в которых мы «смешиваем» С++ с языком Prolog, используется версия SWI-Prolog (разработка университета в Амстердаме) и ее С++ библиотека интерфейсов.
25
Для всех CORBA-примеров этой книги мы использовали реализацию Mico 2.3.3 в среде Linux и Mico 2.3.7 в ОС Solaris 8.
26
Дословный перевод man pages
27
Точнее — это единственный член, который должен быть установлен