Текст
                    Параллельное
и распределенное
программирование
с использованием
I Трейс
Камерон Хьюз
Хьюз I


п араллельное и распределенное программирование с использованием I I
p arallel and Distributed Programming Using r-'fLj ■.../■:■! Cameron Hughes • Tracey Hughes AAddison-Wesley Pearson Education Boston • San Fransisco • New York • Toronto • Montreal London • Munich • Paris • Madrid Cape Town • Sydney • Tokyo • Singapore • Mexico City
Параллельное и распределенное программирование с использованием Камерон Хьюз • Трейси Хьюз иилыишс Ш Москва • Санкт-Петербург • Киев 2004
ББК 32.973.26-018.2.75 Х98 УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией С. Н. Тригуб Перевод с английского и редакция Н. М. Ручко По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу: info@dialektika.com, http://www.dialektika.com Хьюз, Камерон, Хьюз, Трейси. Х98 Параллельное и распределенное программирование на C++. : Пер. с англ. — М. : Издательский дом "Вильяме", 2004. — 672 с.: ил. — Парал. тит. англ. ISBN 5-8459-0686-5 (рус.) В книге представлен архитектурный подход к распределенному и параллельному программированию с использованием языка C++. Здесь описаны простые методы программирования параллельных виртуальных машин и основы разработки кластерных приложений. Эта книга не только научит писать программные компоненты, предназначенные для совместной работы в сетевой среде, но и послужит надежным "путеводителем" по стандартам для программистов, которые занимаются многозадачными и многопоточными приложениями. Многолетний опыт работы привел авторов книги к использованию агентно-ориентированной архитектуры, а для минимизации затрат на обеспечение связей между объектами системы они предлагают применить методологию "классной доски". Эта книга адресована программистам, проектировщикам и разработчикам программных продуктов, а также научным работникам, преподавателям и студентам, которых интересует введение в параллельное и распределенное программирование с использованием языка C++. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни бьыо средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Addison-Wesley Publishing Company, Inc. Authorized translation from the English language edition published by Addison-Wesley Publishing Company, Inc , Copyright © 2004 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2004 ISBN 5-8459-0686-5 (рус.) © Издательский дом "Вильяме", 2004 ISBN 0-13-101376-9 (англ.) © Pearson Education, Inc., 2004
ОГЛАВЛЕНИЕ Введение 1 б Глава 1. Преимущества параллельного программирования 23 Глава 2. Проблемы параллельного и распределенного программирования 42 Глава 3. Разбиение С++-программ на множество задач 57 Глава 4. Разбиение С++-программ на множество потоков 111 Глава 5. Синхронизация параллельно выполняемых задач 183 Глава 6. Объединение возможностей параллельного программирования и С++-средств на основе PVM 211 Глава 7. Обработка ошибок, исключительных ситуаций и надежность программного обеспечения 245 Глава 8. Распределенное объектно-ориентированное программирование в C++ 268 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов и MPI-программирования 312 Глава 10. Визуализация проектов параллельных и распределенных систем 336 Глава 11. Проектирование компонентов для поддержки параллелизма 377 Глава 12. Реализация агентно-ориентированных архитектур 427 Глава 13. Реализация технологии "классной доски" с использованием pvm-средств, потоков и компонентов C++ 463 Приложение А 497 Приложение Б 507 Список литературы 657 Предметный указатель 660
СОДЕРЖАНИЕ ^'^\>Ш^^ # Введение 16 Этапы большого пути 17 Подход 18 Почему именно C++ 18 Библиотеки для параллельного и распределенного программирования 19 Новый единый стандарт спецификаций UNIX 19 Для кого написана эта книга 19 Среды разработки 20 Дополнительный материал 20 Глава 1. Преимущества паралельного программирования 23 1.1. Что такое параллелизм 25 1.1.1. Два основных подхода к достижению параллельности 26 1-2. Преимущества параллельного программирования 28 12.1. Простейшая модель параллельного программирования (PRAM) 29 1.2.2. Простейшая классификация схем параллелизма 30 1.3. Преимущества распределенного программирования 31 1.3.1. Простейшие модели распределенного программирования 32 1.3.2. Мультиагентные распределенные системы 32 1-4. Минимальные требования 33 1.4.1. Декомпозиция 33 1.4.2. Связь 34 1.4.3. Синхронизация 34 1 -5. Базовые уровни программного параллелизма 34 1.5.1. Параллелизм на уровне инструкций 34 1.5.2. Параллелизм на уровне подпрограмм 35
8 Содержание 1.5.3. Параллелизм на уровне объектов 35 1.5.4. Параллелизм на уровне приложений 35 1.6. Отсутствие языковой поддержки параллелизма в C++ 36 1.6.1. Варианты реализации параллелизма с помощью C++ 36 1.6.2. Стандарт MPI 37 1.6.3. Pvm: стандарт для кластерного программирования 38 1.6.4. Стандарт CORBA 38 1.6.5. Реализации библиотек на основе стандартов 39 1.7. Среды для параллельного и распределенного программирования 40 1.8. Резюме 40 Глава 2. Проблемы параллельного и распределенного программирования 42 2.1. Кардинальное изменение парадигмы 44 2.2. Проблемы координации 44 2.3. Отказы оборудования и поведение ПО 51 2.4. Негативные последствия излишнего параллелизма и распределения 52 2.5. Выбор архитектуры 53 2.6. Различные методы тестирования и отладки 54 2.7. Связь между параллельным и распределенным проектами 55 2.8. Резюме 56 Глава 3. Разбиение С++-программ на множество задач 57 3.1. Определение процесса 58 3.1.1. Два вида процессов 59 3.1.2. Блок управления процессами 59 3.2. Анатомия процесса 60 3.3. Состояния процессов 63 3.4. Планирование процессов 66 3.4.1. Стратегия планирования 67 3.4.2. Использование утилиты PS 69 3.4.3. Установка и получение приоритета процесса 71 3.5. Переключение контекста 73 3.6. Создание процесса 74 3.6.1. Отношения между родительскими и сыновними процессами 74 3.6.2. Использование системной функции fork() 78 3.6.3. Использование семейства системных функций exec 78 3.6.4. Использование функции system() для порождения процессов 83 3.6.5. Использование posix-функций для порождения процессов 83
Содержание £ 3.6.6. Идентификация родительских и сыновних процессов с помощью функций управления процессами 8£ 3.7. Завершение процесса 8£ 3.7.1. Функции exit(), kill() и abort() 90 3.8. Ресурсы процессов 91 3.8.1. Типы ресурсов 93 3.8.2. Posix-функции для установки ограничений доступа к ресурсам 94 3.9. Асинхронные и синхронные процессы 97 3.9.1. Создание синхронных и асинхронных процессов с помощью функций fork(), exec(), system() и posix_spawn() 99 3.9.2. Функция wait() 99 3.10. Разбиение программы на задачи 101 3.10.1. Линии видимого контура 108 3.11. Резюме 109 Глава 4. Разбиение С++-программ на множество потоков in 4.1. Определение потока 113 4.1.1. Контекстные требования потока 114 4.1.2. Сравнение потоков и процессов 115 4.1.3. Преимущества использования потоков 116 4.1.4. Недостатки использования потоков 117 4.2. Анатомия потока 119 4.2.1. Атрибуты потока 121 4.3. Планирование потоков 123 4.3.1. Состояния потоков 124 4.3.2. Планирование потоков и область конкуренции 125 4.3.3. Стратегия планирования и приоритет 125 4.4. Ресурсы потоков 128 4.5. Модели создания и функционирования потоков 129 4.5.1. Модель делегирования 130 4.5.2. Модель с равноправными узлами 132 4.5.3. Модель конвейера 132 4.5.4. Модель "изготовитель-потребитель" 133 4.5.5. Модели SPMD и MPMD для потоков 133 4.6. Введение в библиотеку PTHREAD 134 4.7. Анатомия простой многопоточной программы 136 4.7.1. Компиляция и компоновка многопоточных программ 137 4-8. Создание потоков 138 4.8.1. Получение идентификатора потока 141 4.8.2. Присоединение потоков 141
10 Содержание 4.8.3. Создание открепленных потоков 142 4.8.4. Использование объекта атрибутов 143 4.9. Управление потоками 145 4.9.1. Завершение потоков 145 4.9.2. Управление стеком потока 154 4.9.3. Установка атрибутов планирования и свойств потоков 157 4.9.4. Использование функции sysconf() 162 4.9.5. Управление критическими разделами 164 4.10. Безопасность использования потоков и библиотек 170 4.11. Разбиение программы на несколько потоков 172 4.11.1. Использование модели делегирования 173 4.11.2. Использование модели сети с равноправными узлами 177 4.11.3. Использование модели конвейера 177 4.11.4. Использование модели "изготовитель-потребитель" 178 4.11.5. Создание многопоточных объектов 180 Резюме 181 Глава 5. Синхронизация параллельно выполняемых задач 183 5.1. Координация порядка выполнения потоков 185 5.1.1. Взаимоотношения между синхронизируемыми задачами 185 5.1.2. Отношения типа старт-старт (СС) 186 5.1.3. Отношения типа финиш-старт (ФС) 187 5.1.4. Отношения типа старт-финиш (СФ) 188 5.1.5. Отношения типа финиш-финиш (ФФ) 188 5.2. Синхронизация доступа к данным 189 5.2.1. Модель PRAM 189 5.3. Что такое семафоры 193 5.3.1. Операции по управлению семафором 193 5.3.2. Мьютексные семафоры 194 5.3.3. Блокировки для чтения и записи 201 5.3.4. Условные переменные 205 5.4. Объектно-ориентированный подход к синхронизации 210 5.5. Резюме 210 Глава 6. Объединение возможностей параллельного программирования и С++-средств на основе PVM 2i i 6.1. Классические модели параллелизма, поддерживаемые системой PVM 213 6.2. Библиотека PVM для языка C++ 214 6.2.1. Компиляция и компоновка С++/РУМ-программ 217 6.2.2. Выполнение PVM-программы в виде двоичного файла 218
Содержание 11 6.2.3. Требования к PVM-программам 220 6.2.4. Объединение динамической С++-библиотеки с библиотекой PVM 222 6.2.5. Методы использования PVM-задач 223 6.3. Базовые механизмы PVM 233 6.3.1. Функции управления процессами 234 6.3.2. Упаковка и отправка сообщений 236 6.4. Доступ к стандартному входному потоку (STDIN) и стандартному выходному потоку (STDOUT) со стороны pvm-задач 242 6.4.1. Получение доступа к стандартному выходному потоку (COUT) из сыновней задачи 243 6.5. Резюме 243 Глава 7. Обработка ошибок, исключительных ситуаций и надежность программного обеспечения 245 7.1. Надежность программного обеспечения 247 7.2. Отказы в программных и аппаратных компонентах 249 7.3. Определение дефектов в зависимости от спецификаций ПО 251 7.4. Обработка ошибок или обработка исключительных ситуаций? 251 7.5. Надежность ПО: простой план 255 7.5.1. План А: модель возобновления, план Б: модель завершения 255 7.6. Использование объектов отображения для обработки ошибок 256 7.7. Механизмы обработки исключительных ситуаций в C++ 259 7.7.1. Классы исключений 260 7.8. Диаграммы событий, логические выражения и логические схемы 264 7.9. Резюме 267 Глава 8. Распределенное объектно-ориентированное программирование в C++ 268 8.1. Декомпозиция задачи и инкапсуляция ее решения 271 8.1.1. Взаимодействие между распределенными объектами 272 8.1.2. Синхронизация взаимодействия локальных и удаленных объектов 274 8.1.3. Обработка ошибок и исключений в распределенной среде 275 8-2. Доступ к объектам из других адресных пространств 275 8.2.1. IOR-доступ к удаленным объектам 277 8.2.2. Брокеры объектных запросов (ORB) 278 8.2.3. Язык описания интерфейсов (IDL): более "пристальный" взгляд на CORBA-объекты 282 8.3. Анатомия базовой CORBA-программы потребителя 286 8.4. Анатомия базовой CORBA-программы изготовителя 288 8.5. Базовый проект CORBA-приложения 289
12 Содержание 8.5.1. IDL-компилятор 292 8.5.2. Получение IOR-ссылки для удаленных объектов 293 8.6. Служба имен 294 8.6.1. Использование службы имен и создание именных контекстов 298 8.6.2. Служба имен "потребитель-клиент" 300 8.7. Подробнее об объектных адаптерах 303 8.8. Хранилища реализаций и интерфейсов 305 8.9. Простые распределенные Web-службы, использующие CORBA-спецификацию 306 8.10. Маклерская служба 307 8.11. Парадигма "клиент-сервер" 309 8.12. Резюме 311 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов и МР1-программирования 312 9.1. Декомпозиция работ для МР1-интерфейса 314 9.1.1. Дифференциация задач по рангу 315 9.1.2. Группирование задач по коммуникаторам 317 9.1.3. Анатомия МР1-задачи 319 9.2. Использование шаблонных функций для представления МР1-задач 320 9.2.1. Реализация шаблонов и модель SPMD (типы данных) 321 9.2.2. Использование полиморфизма для реализации MPMD-модели 322 9.2.3. Введение MPMD-модели с помощью функций-объектов 327 9.3. Как упростить взаимодействие между MPI-задачами 328 9.3.1. Перегрузка операторов "«" и "»" для организации взаимодействия между MPI-задачами 332 9.4. Резюме 335 Глава 10. Визуализация проектов параллельных и распределенных систем ззб 10.1. Визуализация структур 338 10.1.1. Классы и объекты 338 10.1.2. Отношения между классами и объектами 347 10.1.3. Организация интерактивных объектов 353 10.2. Отображение параллельного поведения 354 10.2.1. Сотрудничество объектов 354 10.2.2. Последовательность передачи сообщений между объектами 359 10.2.3. Деятельность объектов 360 10.2.4. Конечные автоматы 364 10.2.5. Распределенные объекты 371
Содержание 13 10.3. Визуализация всей системы 371 10.3.1. Визуализация развертывания систем 372 10.3.2. Архитектура системы 373 10.4. Резюме 374 Глава 11 - Проектирование компонентов для поддержки параллелизма 377 11.1. Как воспользоваться преимуществами интерфейсных классов 380 11.2. Подробнее об объектно-ориентированном взаимном исключении и интерфейсных классах 385 11.2.1. "Полуширокие" интерфейсы 386 11.3. Поддержка потокового представления 393 11.3.1. Перегрузка операторов "«" и "»" для PVM-потоков данных 395 11.4. Пользовательские классы, создаваемые для обработки PVM-потоков данных 398 11.5. Объектно-ориентированные каналы и FIFO-очереди как базовые элементы низкого уровня 401 11.5.1. Связь каналов с iostream-объектами с помощью дескрипторов файлов 405 11.5.2. Доступ к анонимным каналам с использованием итератора OSTREAMJTERATOR 408 11.5.3. FIFO-очереди (именованные каналы), iostreams-классы и итераторы типа ostream_iterator 415 11.6. Каркасные классы 421 11.7. Резюме 425 Глава 12. Реализация агентно-ориентированных архитектур 427 12.1. Что такое агенты 429 12.1.1. Агенты: исходное определение 429 12.1.2. Типы агентов 430 12.1.3. В чем состоит разница между объектами и агентами 431 12.2. Понятие об агентно-ориентированном программировании 432 12.2.1. Роль агентов в распределенном программировании 434 12.2.2. Агенты и параллельное программирование 436 12.3. Базовые компоненты агентов 437 12.3.1. Когнитивные структуры данных 438 12.4. Реализация агентов в C++ 444 12.4.1. Типы данных предположений и структуры убеждений 444 12.4.2. Класс агента 449 12.4.3. Простая автономность 460 12.5. Мультиагентные системы 461 12.6. Резюме 462
14 Содержание Глава 13. Реализация технологии "классной доски" с использованием PVM-средств, потоков и компонентов C++ 463 13.1. Модель "классной доски" 465 13.2. Методы структурирования "классной доски" 467 13.3. Анатомия источника знаний 470 13.4. Стратегии управления для "классной доски" 471 13.5. Реализация модели "классной доски" с помощью CORBA-объектов 475 13.5.1. Пример использования corba-объекта "классной доски" 475 13.5.2. Реализация интерфейсного класса blackboard 478 13.5.3. Порождение источников знаний в конструкторе "классной доски" 480 13.6. Реализация модели "классной доски" с помощью глобальных объектов 490 13.7. Активизация источников знаний с помощью потоков 494 13.8. Резюме 495 Приложение А 497 А. 1. Диаграммы классов и объектов 497 А.2. Диаграммы взаимодействия 499 А.2.1. Диаграммы сотрудничества 499 А.2.2. Диаграммы последовательностей 499 А.2.3. Диаграммы видов деятельности 501 А.З. Диаграммы состояний 505 А.4. Диаграммы пакетов 506 Приложение Б 507 Список литературы 657 Предметный указатель 660
Эта книга посвящена всем программистам, "безвредным11 хакерам, инженерам-полуночникам и бесчисленным добровольцам, которые без устали и сожаления отдают свой талант, мастерство, опыт и время, чтобы сделать открытые программные продукты реальностью и совершить революцию в Linux. Без их вклада кластерное, МРР-, SMP- и распределенное программирование не было бы столь доступным для всех желающих, каким оно стало в настоящее время.
ВВЕДЕНИЕ В этой книге представлен архитектурный подход к распределенному и параллельному программированию с использованием языка C++. Особое внимание уделяется применению стандартной С++-библиотеки, алгоритмов и контейнерных классов в распределенных и параллельных средах. Кроме того, мы подробно разъясняем методы расширения возможностей языка C++, направленные на решение задач программирования этой категории, с помощью библиотек классов и функций. При этом нас больше всего интересует характер взаимодействия средств C++ с новыми стандартами POSIX и Single UNIX применительно к организации многопоточной обработки. Здесь рассматриваются вопросы объединения С++-программ с программами, написанными на других языках программирования, для поиска "многоязычных" решений проблем распределенного и параллельного программирования, а также некоторые методы организации программного обеспечения, предназначенные для поддержки этого вида программирования. В книге показано, как преодолеть основные трудности параллелизма, и описано, что понимается под производным распараллеливанием. Мы сознательно уделяем внимание не методам оптимизации, аппаратным характеристикам или производительности, а способам структуризации компьютерных программ и программных систем ради получения преимуществ от параллелизма. Более того, мы не пытаемся применить методы параллельного программирования к сложным научным и математическим алгоритмам, а хотим познакомить читателя с мультипарадигматическим подходом к решению некоторых проблем, которые присущи распределенному и параллельному программированию. Чтобы эффективно решать эти задачи, необходимо сочетать различные программные и инженерные подходы. Например, методы объектно-ориентированного программирования используются для решения проблем "гонки" данных и синхронизации их обработки. При многозадачном и многопоточном управлении мы считаем наиболее перспективной агентно-ориентированную архитектуру. А для минимизации затрат на обеспечение связей между объектами мы привлекаем методологию "классной доски" (стратегия решения сложных системных задач с использованием разнородных источников знаний, взаимодействующих через общее информационное поле). Помимо объектно-ориентированного, агентно-
Введение 17 ориентированного и AI-ориентированного (AI — сокр. от artificial intelligence— искусственный интеллект) программирования, мы используем параметризованное (настраиваемое) программирование для реализации обобщенных алгоритмов, которые применяются именно там, где нужен параллелизм. Опыт разработки программного обеспечения всевозможных форм и объемов позволил нам убедиться в том, что для успешного проектирования программных средств и эффективной их реализации без разносторонности (универсальности) применяемых средств уже не обойтись. Предложения, идеи и решения, представленные в этой книге, отражают практические результаты нашей работы. Этапы большого пути При написании параллельных или распределенных программ, как правило, необходимо "пройти" следующие три основных этапа. 1. Идентификация естественного параллелизма, который существует в контексте предметной области. 2. Разбиение задачи, стоящей перед программным обеспечением, на несколько подзадач, которые можно выполнять одновременно, чтобы достичь требуемого уровня параллелизма. 3. Координация этих задач, позволяющая обеспечить корректную и эффективную работу программных средств в соответствии с их назначением. Эти три этапа достигаются при условии параллельного решения следующих проблем: "гонка" данных обнаружение взаимоблокировки частичный отказ бесконечное ожидание взаимоблокировка отказ средств коммуникации регистрация завершения работы отсутствие глобального состояния проблема многофазной синхронизации несоответствие протоколов локализация ошибок отсутствие средств централизованного распределения ресурсов В этой книге разъясняются все названные проблемы, причины их возникновения и возможные пути решения. Наконец, в некоторых механизмах, выбранных нами для обеспечения параллелизма, в качестве протокола используется TCP/IP (Transmission Control Protocol/Internet Protocol— протокол управления передачей/протокол Internet). В частности, имеются в виду следующие механизмы: библиотека MPI (Message Passing Interface — интерфейс для передачи сообщений), библиотека PVM (Parallel Virtual Machine — параллельная виртуальная машина) и библиотека MICO (или CORBA — Common Object Request Broker Architecture — технология построения распределенных объектных приложений). Эти механизмы позволяют использовать наши подходы в среде Internet/Intranet, а это значит, что программы, работающие параллельно, могут выполняться на различных сайтах Internet (или корпоративной сети intranet) и общаться между собой посредством передачи сообщений. Многие эти идеи служат
18 Введение в качестве основы для построения инфраструктуры Web-служб. В дополнение к MPI- и PVM-процедурам, используемые нами CORBA-объекты, размещенные на различных серверах, могут взаимодействовать друг с другом через Internet. Эти компоненты можно использовать для обеспечения различных Internet/Intranet-служб. Подход При решении проблем, которые встречаются при написании параллельных или распределенных программ, мы придерживаемся компонентного подхода. Наша главная цель — использовать в качестве строительных блоков параллелизма каркасные классы. Каркасные классы поддерживаются объектно-ориентированными мьютексами, семафорами, конвейерами и сокетами. С помощью интерфейсных классов удается значительно снизить сложность синхронизации задач и их взаимодействия. Для того чтобы упростить управление потоками и процессами, мы используем агентно-ориентированные потоки и процессы. Наш основной подход к глобальному состоянию и связанные с ним проблемы включают применение методологии "классной доски". Для получения мультипарадигмати- ческих решений мы сочетаем агентно-ориентированные и объектно- ориентированные архитектуры. Такой мультипарадигматический подход обеспечивают средства, которыми обладает язык C++ для объектно-ориентированного, параметризованного и структурного программирования. Почему именно C++ Существуют С++-компиляторы, которые работают практически на всех известных платформах и в операционных средах. Национальный Институт Стандартизации США (American National Standards Institute — ANSI) и Международная организация по стандартизации (International Organization for Standardization — ISO) определили стандарты для языка C++ и его библиотеки. Существуют устойчиво работающие, так называемые открытые (open source) (т.е. лицензионные программы вместе с их исходными текстами, не связанные ограничениями на дальнейшую модификацию и распространение с сохранением информации о первичном авторстве и внесенных изменениях), а также коммерческие реализации этого языка. Язык C++ был быстро освоен научными работниками, проектировщиками и профессиональными разработчиками всего мира. Его использовали для решения самых разных (по объему и форме) проблем: для написания как отдельных драйверов устройств, так и крупномасштабных промышленных приложений. Язык C++ поддерживает мультипарадигматический подход к разработке программных продуктов и библиотек, которые делают средства параллельного и распределенного программирования легко доступными.
Введение 19 Библиотеки для параллельного и распределенного программирования Для параллельного программирования на основе C++ используются такие библиотеки, как MPICH (реализация библиотеки MPI), PVM и Pthreads (POSIX1 Threads). Для распределенного программирования применяется библиотека MICO (С++-реализация стандарта CORBA). Стандартная библиотека C++ (C++ Standard Library) в сочетании с CORBA и библиотекой Pthreads обеспечивает поддержку концепций агентно- ориентированного программирования и программирования на основе методологии "классной доски", которые рассматриваются в этой книге. Новый единый стандарт спецификаций UNIX Новый единый стандарт спецификаций UNIX (Single UNIX Specifications Standard) версии З — совместный труд Института инженеров по электротехнике и электронике (Institute of Electrical and Electronics Engineers — IEEE2) и организации Open Group — был выпущен в декабре 2001 года. Новый единый стандарт спецификаций UNIX реализует стандарты POSIX и способствует повышению уровня переносимости программных продуктов. Его основное назначение — дать разработчикам программного обеспечения единый набор API-функций (Application Programming Interface — интерфейс прикладного программирования, т.е. набор функций, предоставляемый для использования в прикладных программах), поддерживаемых каждой UNIX-системой. Этот документ обеспечивает надежный "путеводитель" по стандартам для программистов, которые занимаются многозадачными и многопоточными приложениями. В этой книге, рассматривая темы создания процессов, управления процессами, использования библиотеки Pthreads, новых процедур posix_spawn(), POSIX-семафоров и FIFO-очередей (/irst-in, yirst-out— "первым поступил, первым обслужен"), мы опираемся исключительно на новый единый стандарт спецификаций UNIX. В приложении Б представлены выдержки из этого стандарта, которые могут быть использованы в качестве справочника для изложенного нами материала. Для кого написана эта книга Эта книга предназначена для проектировщиков и разработчиков программного обеспечения, прикладных программистов и научных работников, преподавателей и студентов, которых интересует введение в параллельное и распределенное программирование с использованием языка C++. Для освоения материала этой книги читателю необходимо иметь базовые знания языка C++ и стандартной С++-библиотеки классов, поскольку учебный курс по программированию на C++ и по объектно-1 ориентированному программированию здесь не предусмотрен. Предполагается, что читатель должен иметь общее представление о таких принципах объектно-! POSIX— Portable Operating System Interface for computer environments— интерфейс переносимой от рационной системы (набор стандартов IEEE, описывающих интерфейсы ОС для UNIX). IEEE— профессиональное объединение, выпускающие свои собственные стандарты; членами IEEi являются ANSI и ISO.
20 Введение ориентированного программирования, как инкапсуляция, наследование и полиморфизм. В настоящей книге излагаются основы параллельного и распределенного программирования в контексте C++. Среды разработки Примеры и программы, представленные в этой книге, разработаны и протестированы в Linux- и UNIX-средах, а именно — под управлением Solaris 8, Aix и Linux (SuSE, Red Hat). MPI- и PVM-код разработан и протестирован на 32-узловом Linux-ориентированном кластере. Многие программы протестированы на серверах семейства Sun Enterprise 450. Мы использовали Sun C++ Workshop (С++-компилятор компании Portland Group) и проект по свободному распространению программного обеспечения GNU C++. Большинство примеров должны выполняться как в UNIX-, так и Linux- средах. Если конкретный пример не предназначен для выполнения в обеих названных средах, этот факт отмечается в разделе "Профиль программы", которым снабжаются все законченные примеры программ этой книги. Дополнительный материал Диаграммы UML Для построения многих диаграмм в этой книге применяется стандарт UML (Unified Modeling Language — унифицированный язык моделирования). В частности, для описания важных архитектур параллелизма и межклассовых взаимоотношений используются диаграммы действий, развертывания (внедрения), классов и состояний. И хотя знание языка UML не является необходимым условием, все же некоторый уровень осведомленности в этом вопросе окажется весьма полезным. Описание и разъяснение символов и самого языка UML приведено в приложении А. Профили программы Каждая законченная программа в этой книге сопровождается разделом "Профиль программы", который содержит описание таких особенностей реализации, как требуемые заголовки, библиотеки, инструкции по компиляции и компоновке. Профиль программы также включает подраздел "Примечания", содержащий специальную информацию, которую необходимо принять во внимание при выполнении данной программы. Если код не сопровождается профилем программы, значит, он предназначен только для демонстрации. Параграфы Мы посчитали лишним включать сугубо теоретические замечания в такую книгу- введение, как эта. Но в некоторых случаях без теоретических или математических выкладок было не обойтись, и тогда мы сопровождали такие выкладки подробными разъяснениями, оформленными в виде параграфов (например, § 6.1).
Введение 21 Тестирование кода и его надежность Несмотря на то что все примеры и приложения, приведенные в этой книге, были протестированы для подтверждения их корректности, мы не даем никаких гарантий, что эти программы полностью лишены изъянов или ошибок, совместимы с любым конкретным стандартом, годятся для продажи или отвечают вашим конкретным требованиям. На эти программы не следует полагаться при решении проблем, если существует вероятность, что некорректный способ получения результатов может привести к материальном}7 ущербу. Авторы и издатели этой книги не признают какую бы то ни было ответственность за прямой или косвенный ущерб, который может явиться результатом использования примеров, программ или приложений, представленных в этой книге. Ждем ваших отзывов! Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравит ся или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: info@dialektika.com WWW: http://www.dialektika.com Информация для писем из: России: 115419, Москва, а/я 783 Украины: 03150, Киев, а/я 152
Благодарности Мы никогда бы не смогли "вытянуть" этот проект без помощи, поддержки, конструктивной критики и материальных ресурсов многих наших друзей и коллег. В частности, мы хотели бы поблагодарить Терри Льюиса (Terry Lewis) и Дага Джонсона (Doug Johnson) из компании OSC (Ohio Super-Computing) за предоставление доступа к 32-узловому Linux-ориентированному кластеру; Марка Уэлтона (Mark Welton) из компании YSU за экспертный анализ и помощь при конфигурировании кластера для поддержки наших PVM- и MPI-программ; Сэлу Сандерс (Sal Sanders) из компании YSU, позволившую нам работать на Power-PC с установленными Mac OSX и Adobe Illustrator; Брайана Нельсона (Brian Nelson) из YSU за разрешение протестировать многие наши многопоточные и распределенные программы на многопроцессорных вычислительных машинах Sun E-250 и Е-450. Мы также признательны Мэри Энн Джонсон (Mary Ann Johnson) и Джеффри Тримблу (Jeffrey Trimble) из YSU MAAG за помощь в получении справочной информации; Клавдию М. Стэнзиоло (Claudio M. Stanziola), Полетт Голдвебер (Paulette Goldweber) и Жаклин Хэнсон (Jacqueline Hansson) из объединения IEEE Standards and Licensing and Contracts Office за получение разрешения на переиздание фрагментов нового стандарта Single-UNIX/POSIX; Эндрю Джози (Andrew Josey) и Джину Пирсу (Gene Pierce) из организации Open Group за аналогичное содействие. Большое спасибо Тревору Уоткинсу (Trevor Watkins) из организации Z-Group за помощь в тестировании примеров программ; использование его распределенной Linux-среды было особенно важным фактором в процессе тестирования. Особую благодарность заслужили Стив Тарасвеки (Steve Tarasweki) за согласие написать рецензию на эту книгу (несмотря на то, что она была еще в черновом варианте); доктор Юджин Сантос (Eugene Santos) за то, что он указал нужное направление при составлении категорий структур данных, которые можно использовать в PVM (Parallel Virtual Machine — параллельная виртуальная машина); доктор Майк Кресиманно (Mike Crescimanno) из организации Advanced Computing Work Group (ACWG) при компании YSU за разрешение представить некоторые материалы из этой книги на одном из совещаний ACWG. Наконец, мы хотим выразить признательность Полю Петрелия (Paul Petralia) и всему составу производственной группы (особенно Гейлу Кокеру-Богусу (Gail Cocker-Bogusz)) из компании Prentice Hall за их терпение, поддержку, энтузиазм и высокий профессионализм.
ПРЕИМУЩЕСТВА ПАРАЛЛЕЛЬНОГО ПРОГРАММИРОВАНИЯ В этой главе... 1.1. Что такое параллелизм 1.2. Преимущества параллельного программирования 1.3. Преимущества распределенного программирования 1.4. Минимальные требования 1.5. Базовые уровни программного параллелизма 1.6. Отсутствие языковой поддержки параллелизма в C++ 1.7. Среды для параллельного и распределенного программирования 1.8. Резюме
J~y\3J^J^J "Я допускаю, что параллелизм лучше всего поддерживать с помощью библиотеки, причем такую библиотеку можно реализовать без существенных расширений самого языка программирования." — Бьерн Страуструп, создатель языка C++ Для того чтобы в настоящее время разрабатывать программное обеспечение, необходимы практические знания параллельного и распределенного программирования. Теперь перед разработчиками приложений все чаще ставится задача, чтобы отдельные программные составляющие надлежащим образом выполнялись в Internet или Intranet. Если программа (или ее часть) развернута в одной или нескольких таких средах, то к ней предъявляются самые жесткие требования по части производительности. Пользователь всегда надеется, что результаты работы программ будут мгновенными и надежными. Во многих ситуациях заказчик хотел бы, чтобы программное обеспечение удовлетворяло сразу многим требованиям. Зачастую пользователь не видит ничего необычного в своих намерениях одновременно загружать программные продукты и данные из Internet. Программное обеспечение, предназначенное для приема телетекста, также должно быть способно на гладкое воспроизведение графических изображений и звука после цифровой обработки (причем без прерывания). Программное обеспечение Web-сервера нередко выдерживает сотни тысяч посещений в день, а часто посещаемые почтовые серверы— порядка миллиона отправляемых и получаемых сообщений. При этом важно не только количество обрабатываемых сообщений, но и их содержимое. Например, передача данных, содержащих оцифрованные музыку, видео или графические изображения, может "поглотить" всю пропускную способность сети и причинить серьезные неприятности программному обеспечению сервера, которое не было спроектировано должным образом. Обычно мы имеем дело с сетевой вычислительной
1.1. Что такое параллелизм 25 средой, состоящей из компьютеров с несколькими процессорами. Чем больше функций возлагается на программное обеспечение, тем больше к нему предъявляется требований. Чтобы удовлетворить минимальные требования пользователя, современные программы должны быть еще более производительными и интеллектуальными. Программное обеспечение следует проектировать так, чтобы можно было воспользоваться преимуществами компьютеров, оснащенных несколькими процессорами. А поскольку сетевые компьютеры — это скорее правило, чем исключение, то целью проектирования программного обеспечения должно быть его корректное и эффективное выполнение при условии, что некоторые его составляющие будут одновременно выполняться на различных компьютерах. В некоторых случаях используемые компьютеры могут иметь совершенно различные операционные системы с разными сетевыми протоколами! Чтобы справиться с описанными реалиями, ассортимент разработок программных продуктов должен включать методы реализации параллелизма посредством параллельного и распределенного программирования. 1.1. Что такое параллелизм Два события называют одновременными, если они происходят в течение одного и того же временного интервала. Если несколько задач выполняются в течение одного и того же временного интервала, то говорят, что они выполняются параллельно. Для нас термин параллельно необязательно означает "точно в один момент". Например, две задачи могут выполняться параллельно в течение одной и той же секунды, но при этом каждая из них выполняется в различные доли этой секунды. Так, первая задача может отработать в первую десятую часть секунды и приостановиться, затем вторая может отработать в следующую десятую часть секунды и приостановиться, после чего первая задача может возобновить выполнение в течение третьей доли секунды, и т.д. Таким образом, эти задачи могут выполняться по очереди, но поскольку продолжительность секунды с точки зрения человека весьма коротка, то кажется, что они выполняются одновременно. Понятие одновременности (параллельности) можно распространить и на более длинные интервалы времени. Так, две программы, выполняющие некоторую задачу в течение одного и того же часа, постепенно приближаясь к своей конечной цели в течение этого часа, могут (или могут не) работать точно в одни и те же моменты времени. Мы говорим, что данные две программы для этого часа выполняются параллельно, или одновременно. Другими словами, задачи, которые существуют в одно и то же время и выполняются в течение одного и того же интервала времени, являются параллельными. Параллельные задачи могут выполняться в одно- или многопроцессорной среде. В однопроцессорной среде параллельные задачи существуют в одно и то же время и выполняются в течение одного и того же интервала времени за счет контекстного переключения. В многопроцессорной среде, если свободно достаточное количество процессоров, параллельные задачи могут выполняться в одни и те же моменты времени в течение одного и того же периода времени. Основной фактор, влияющий на степень приемлемости для параллелизма того или иного интервала времени, определяется конкретным приложением. Цель технологий параллелизма— обеспечить условия, позволяющие компьютерным программам делать больший объем работы за тот же интервал времени. Поэтому проектирование программ должно ориентироваться не на выполнение одной задачи в некоторый промежуток времени, а на одновременное выполнение нескольких задач, на которые предварительно должна быть разбита программа. Возможны ситуации, когда целью является не выполнение большего объема работы в течение того же
26 Глава 1. Преимущества параллельного программирования интервала времени, а упрощение решения с точки зрения программирования. Иногда имеет смысл думать о решении проблемы как о множестве параллельно выполняемых задач. Например (если взять для сравнения вполне житейскую ситуацию), проблему снижения веса лучше всего представить в виде двух параллельно выполняемых задач: диета и физическая нагрузка. Иначе говоря, для решения этой проблемы предполагается применение строгой диеты и физических упражнений в один и тот же интервал времени (необязательно точно в одни и те же моменты времени). Обычно не слишком полезно (или эффективно) выполнять одну подзадачу в один период времени, а другую — совершенно в другой. Именно параллельность обоих процессов дает естественную форму искомого решения проблемы. Иногда к параллельности прибегают, чтобы увеличить быстродействие программы или приблизить момент ее завершения. В других случаях параллельность используется для увеличения продуктивности программы (объема выполняемой ею работы) за тот же период времени при вторичности скорости ее работы. Например, для некоторых Web-сайтов важно как можно дольше удерживать пользователей. Поэтому здесь имеет значение не то, насколько быстро будет происходить подключение (регистрация) и отключение пользователей, а сколько пользователей сможет этот сайт обслуживать одновременно. Следовательно, цель проектирования программного обеспечения такого сайта — обрабатывать максимальное количество подключений за как можно больший промежуток времени. Наконец, параллельность упрощает само программное обеспечение. Зачастую сложную последовательность операций можно упростить, организовав ее в виде ряда небольших параллельно выполняемых операций. Независимо от частной цели (ускорение работы программ, обработка увеличенной нагрузки или упрощение реализации программы), наша главная цель— усовершенствовать программное обеспечение, воспользовавшись принципом параллельности. 1.1.1. Два основных подхода к достижению параллельности Параллельное и распределенное программирование— это два базовых подхода к достижению параллельного выполнения составляющих программного обеспечения (ПО). Они представляют собой две различные парадигмы программирования, которые иногда пересекаются. Методы параллельного программирования позволяют распределить работу программы между двумя (или больше) процессорами в рамках одного физического или одного виртуального компьютера. Методы распределенного программирования позволяют распределить работу программы между двумя (или больше) процессами, причем процессы могут существовать на одном и том же компьютере или на разных. Другими словами, части распределенной программы зачастую выполняются на разных компьютерах, связываемых по сети, или по крайней мере в различных процессах. Программа, содержащая параллелизм, выполняется на одном и том же физическом или виртуальном компьютере. Такую программу можно разбить на процессы (process) или потоки (thread). Процессы мы рассмотрим в главе 3, а потоки— в главе 4. В изложении материала этой книги мы будем придерживаться того, что распределенные программы разбиваются только на процессы. Многопоточность ограничивается параллелизмом. Формально параллельные программы иногда бывают распределенными, например, при PVM-программировании (Parallel Virtual Machine — параллельная виртуальная машина). Распределенное программирование иногда используется для реализации параллелизма, как в случае с MPI-программированием (Message Passing Interface — интерфейс для
1.1. Что такое параллелизм 27 передачи сообщений). Однако не все распределенные программы включают параллелизм. Части распределенной программы могут выполняться по различным запросам и в различные периоды времени. Например, программу календаря можно разделить на две составляющие. Одна часть должна обеспечивать пользователя информацией, присущей календарю, и способом записи данных о важных для него встречах, а другая часть должна предоставлять пользователю набор сигналов для разных типов встреч. Пользователь составляет расписание встреч, используя одну часть ПО, в то время как другая его часть выполняется независимо от первой. Набор сигналов и компонентов расписания вместе образуют единое приложение, которое разделено на две части, выполняемые по отдельности. При чистом параллелизме одновременно выполняемые части являются компонентами одной и той же программы. Части распределенных приложений обычно реализуются как отдельные программы. Типичная архитектура построения параллельной и распределенной программ показана на рис. 1.2. ПАРАЛЛЕЛЬНА ВЫПОЛНЯЕМЫЕ ПРИЛОЖЕНИЯ I *Ч единый физический или виртуальный 1 Программа Задача А Задача В Задача С Задача D КОМПЬЮТЕР ' :: р. s : "ТТ Р2 . -*~-р> J P, *rrw 4 1 РАСПРЕДЕЛЕННОЕ ПРИЛОЖЕНИЕ Рис1-1 ■ Типичная архитектура построения параллельной и распределенной программ
28 Глава 1. Преимущества параллельного программирования Параллельное приложение, показанное на рис. 1.2, состоит из одной программы, разделенной на четыре задачи. Каждая задача выполняется на отдельном процессоре, следовательно, все они могут выполняться одновременно. Эти задачи можно реализовать в 1.2, состоит из трех отдельных программ, каждая из которых выполняется на отдельном компьютере. При этом программа 3 состоит из двух отдельных частей (задачи А и задачи D), выполняющихся на одном компьютере. Несмотря на это, задачи А и D являются распределенными, поскольку они реализованы как два отдельных процесса. Задачи параллельной программы более тесно связаны, чем задачи распределенного приложения. В общем случае процессоры, связанные с распределенными программами, находятся на различных компьютерах, в то время как процессоры, связанные с программами, реализующими параллелизм, находятся на одном и том же компьютере. Конечно же, существуют гибридные приложения, которые являются и параллельными, и распределенными одновременно. Именно такие гибридные объединения становятся нормой. 1.2. Преимущества параллельного программирования Программы, надлежащее качество проектирования которых позволяет воспользоваться преимуществами параллелизма, могут выполняться быстрее, чем их последовательные эквиваленты, что повышает их рыночную стоимость. Иногда скорость может спасти жизнь. В таких случаях быстрее означает лучше. Иногда решение некоторых проблем представляется естественнее в виде коллекции одновременно выполняемых задач. Это характерно для таких областей, как научное программирование, математическое и программирование искусственного интеллекта. Это означает, что в некоторых ситуациях технологии параллельного программирования снижают трудозатраты разработчика ПО, позволяя ему напрямую реализовать структуры данных, алгоритмы и эвристические методы, разрабатываемые учеными. При этом используется специализированное оборудование. Например, в мультимедийной программе с широкими функциональными возможностями с целью получения более высокой производительности ее логика может быть распределена между такими специализированными процессорами, как микросхемы компьютерной графики, цифровые звуковые процессоры и математические спецпроцессоры. К таким процессорам обычно обеспечивается одновременный доступ. МРР-компьютеры (Massively Parallel Processors — процессоры с массовым параллелизмом) имеют сотни, а иногда и тысячи процессоров, что позволяет их использовать для решения проблем, которые просто не реально решить последовательными методами. Однако при использовании МРР-компьютеров (т.е. при объединении скорости и "грубой силы") невозможное становится возможным. К категории применимости МРР-компьютеров можно отнести моделирование экологической системы (или моделирование влияния различных факторов на окружающую среду), исследование космического пространства и ряд тем из области биологических исследований, например проект моделирования генома человека. Применение более совершенных технологий параллельного программирования открывает двери к архитектурам ПО, которые специально разрабатываются для параллельных сред. Например, существуют специальные муль- тиагентные архитектуры и архитектуры, использующие методологию "классной доски", разработанные специально для среды с параллельными процессорами.
1.2. Преимущества параллельного программирования 29 1.2.1. Простейшая модель параллельного программирования (PRAM) В качестве простейшей модели, отражающей базовые концепции параллельного программирования, рассмотрим модель PRAM (Parallel Random Access Machine — параллельная машина с произвольным доступом). PRAM — это упрощенная теоретическая модель с п процессорами, которые используют общую глобальную память. Простая модель PRAM изображена на рис. 1.2. Все процессоры имеют доступ для чтения и записи к общей глобальной памяти. В PRAM-среде возможен одновременный доступ. Предположим, что все процессоры могут параллельно выполнять различные арифметические и логические операции. Кроме того, каждый из теоретических процессоров (см. рис. 1.2) может обращаться к общей памяти в одну непрерываемую единицу времени. PRAM-модель обладает как параллельными, так и исключающими алгоритмами считывания данных. Параллельные алгоритмы считывания данных позволяют одновременно обращаться к одной и той же области памяти без искажения (порчи) данных. Исключающие алгоритмы считывания данных используются в случае, когда необходима гарантия того, что никакие два процесса никогда не будут считывать данные из одной и той же области памяти одновременно. PRAM- модель также обладает параллельными и исключающими алгоритмами записи данных. Параллельные алгоритмы позволяют нескольким процессам одновременно записывать данные в одну и ту же область памяти, в то время как исключающие алгоритмы гарантируют, что никакие два процесса не будут записывать данные в одну и ту же область памяти одновременно. Четыре основных алгоритма считывания и записи данных перечислены в табл. 1.1. 1P1 р2 Рз Рл « * Общая память Рис. 1.2. Простая модель PRAM Таблица 1.1. Четыре базовых алгоритма считывания и записи данных Типы алгоритмов Описание EREW CREW ERCW CRCW Исключающее считывание/исключающая запись Параллельное считывание/исключающая запись Исключающее считывание/параллельная запись Параллельное считывание/параллельная запись В этой книге мы будем часто обращаться к этим типам алгоритмов для реализации параллельных архитектур. Архитектура, построенная на основе технологии "классной Доски", — это одна из важных архитектур, которую мы реализуем с помощью PRAM- Модели (см. главу 13). Необходимо отметить, что хотя PRAM- это упрощенная теоретическая модель, она успешно используется для разработки практических программ, и эти программы могут соперничать по производительности с программами, которые были разработаны с использованием более сложных моделей параллелизма.
30 Глава 1. Преимущества параллельного программирования 1.2.2. Простейшая классификация схем параллелизма PRAM — это способ построения простой модели, которая позволяет представить, как компьютеры можно условно разбить на процессоры и память и как эти процессоры получают доступ к памяти. Упрощенная классификации схем функционирования параллельных компьютеров была предложена М. Флинном (М. J. Flynn)1. Согласно этой классификации различались две схемы: SIMD (Single-Instruction, Multiple-Data — архитектура с одним потоком команд и многими потоками данных) и MIMD (Multiple-Instruction, Multiple-Data — архитектура со множеством потоков команд и множеством потоков данных). Несколько позже эти схемы были расширены до SPMD (Single-Program, Multiple-Data — одна программа, несколько потоков данных) и MPMD (Multiple-Programs, Multiple-Data — множество программ, множество потоков данных) соответственно. Схема SPMD (SIMD) позволяет нескольким процессорам выполнять одну и ту же инструкцию или программу при условии, что каждый процессор получает доступ к различным данным. Схема MPMD (MIMD) позволяет работать нескольким процессорам, причем все они выполняют различные программы или инструкции и пользуются собственными данными. Таким образом, в одной схеме все процессоры выполняют одну и ту же программу или инструкцию, а в другой все процессоры выполняют различные программы или инструкции. Конечно же, возможны гибриды этих моделей, в которых процессоры могут быть разделены на группы, из которых одни образуют SPMD-модель, а другие — MPMD- модель. При использовании схемы SPMD все процессоры просто выполняют одни и те же операции, но с различными данными. Например, мы можем разбить одну задачу на группы и назначить для каждой группы отдельный процессор. В этом случае каждый процессор при решении задачи будет применять одинаковые правила, обрабатывая при этом различные части этой задачи. Когда все процессоры справятся со своими участками работы, мы получим решение всей задачи. Если же применяется схема MPMD, все процессоры выполняют различные виды работы, и, хотя при этом все они вместе пытаются решить одну проблему, каждому из них выделяется свой аспект этой проблемы. Например, разделим задачу по обеспечению безопасности Web-сервера по схеме MPMD. В этом случае каждому процессору ставится своя подзадача. Предположим, один процессор будет отслеживать работу портов, другой — курировать процесс регистрации пользователей, а третий — анализировать содержимое пакетов и т.д. Таким образом, каждый процессор работает с нужными ему данными. И хотя различные процессоры выполняют разные виды работы, используя различные данные, все они вместе работают в одном направлении — обеспечивают безопасность Web-сервера. Принципы параллельного программирования, рассматриваемые в этой книге, нетрудно описать, используя модели PRAM, SPMD (SIMD) и MPMD (MIMD). И в самом деле, эти схемы и модели успешно используются для реализации практических мелко- и среднемасштабных приложений и вполне могут вас устраивать до тех пор, пока вы не подготовитесь к параллельному программированию более высокой степени организации. M.J. Flynn. Very high-speed computers. Из сборников объединения IEEE, 54, 1901-1909 (декабрь 1966).
1.3. Преимущества распределенного программирования 31 1.3. Преимущества распределенного программирования Методы распределенного программирования позволяют воспользоваться преимуществами ресурсов, размещенных в Internet, в корпоративных Intranet и локальных сетях. Распределенное программирование обычно включает сетевое программирование в той или иной форме. Это означает, что программе, которая выполняется на одном компьютере в одной сети, требуется некоторый аппаратный или программный ресурс, который принадлежит другому компьютеру в той же или удаленной сети. Распределенное программирование подразумевает общение одной программы с другой через сетевое соединение, которое включает соответствующее оборудование (от модемов до спутников). Отличительной чертой распределенных программ является то, что они разбиваются на части. Эти части обычно реализуются как отдельные программы, которые, как правило, выполняются на разных компьютерах и взаимодействуют друг с другом через сеть. Методы распределенного программирования предоставляют доступ к ресурсам, которые географически могут находиться на большом расстоянии друг от друга. Например, распределенная программа, разделенная на компонент Web-сервера и компонент Web-клиента, может выполняться на двух различных компьютерах. Компонент Web-сервера может располагаться, допустим, в Африке, а компонент Web-клиента — в Японии. Часть Web-клиента может использовать программные и аппаратные ресурсы компонента Web-сервера, несмотря на то, что их разделяет огромное расстояние, и почти наверняка они относятся к различным сетям, функционирующим под управлением различных операционных сред. Методы распределенного программирования предусматривают совместный доступ к дорогостоящим программным и аппаратным ресурсам. Например, высококачественный го- лографический принтер может обладать специальным программным обеспечением сервера печати, которое предоставляет соответствующие услуги для ПО клиента. ПО клиента печати размещается на одном компьютере, а ПО сервера печати — на другом. Как правило, для обслуживания множества клиентов печати достаточно только одного сервера печати. Распределенные вычисления можно использовать для создания определенного уровня избыточности вычислительных средств на случай аварии. Если разделить программу на несколько частей, каждая из которых будет выполняться на отдельном компьютере, то некоторым из этих частей мы можем поручить одну и ту же работу. Если по какой-то причине один компьютер откажет, его программу заменит аналогичная программа, выполняющаяся на другом компьютере. Ни для кого не секрет, что базы данных способны хранить миллионы, триллионы и даже квадриллионы единиц информации. И, конечно же, нереально каждому пользователю иметь копию подобной базы данных. А ведь пользователи и компьютер, содержащий базу данных, зачастую находятся не просто в разных зданиях, а в разных городах или даже странах. Но именно методы распределенного программирования дают возможность пользователям (независимо от их местонахождения) обращаться к таким базам данных.
32 Глава 1. Преимущества параллельного программирования 1.3.1. Простейшие модели распределенного программирования Возможно, самой простой и распространенной моделью распределенной обработки данных является модель типа "клиент/сервер". В этой модели программа разбивается на две части: одна часть называется сервером, а другая— клиентом. Сервер имеет прямой доступ к некоторым аппаратным и программным ресурсам, которые желает использовать клиент. В большинстве случаев сервер и клиент располагаются на разных компьютерах. Обычно между клиентом и сервером существует отношение типа "множество-к-одному", т.е., как правило, один сервер отвечает на запросы многих клиентов. Сервер часто обеспечивает опосредованный доступ к огромной базе данных, дорогостоящему оборудованию или некоторой коллекции приложений. Клиент может запросить интересующие его данные, сделать запрос на выполнение вычислительной процедуры или обработку другого типа. В качестве примера приложения типа "клиент/сервер" приведем механизм поиска (search engine). Механизмы (или машины) поиска используются для поиска заданной информации в Internet или корпоративной Intranet. Клиент служит для получения ключевого слова или фразы, которая интересует пользователя. Часть ПО клиента затем передает сформированный запрос той части ПО сервера, которая обладает средствами поиска информации по заданному пользователем ключевому слову или фразе. Сервер либо имеет прямой доступ к информации, либо связан с другими серверами, которые имеют его. В идеальном случае сервер находит запрошенное пользователем ключевое слово или фразу и возвращает найденную информацию клиенту. Несмотря на то что клиент и сервер представляют собой отдельные программы, выполняющиеся на разных компьютерах, вместе они составляют единое приложение. Разделение ПО на части клиента и сервера и есть основной метод распределенного программирования. Модель типа "клиент/сервер" также имеет другие формы, которые зависят от конкретной среды. Например, термин "изготовитель-потребитель" (producer-consumer) можно считать близким родственником термина "клиент/сервер". Обычно клиент-серверными приложениями называют большие программы, а термин "изготовитель-потребитель" относят к программам меньшего объема. Если программы имеют уровень операционной системы или ниже, к ним применяют термин "изготовитель-потребитель", если выше — то термин "клиент/сервер" (конечно же, исключения есть из всякого правила). 1.3.2. Мультиагентные распределенные системы Несмотря на то что модель типа "клиент/сервер" — самая распространенная модель распределенного программирования, все же она не единственная. Используются также агенты — рациональные компоненты ПО, которые характеризуются самонаведением и автономностью и могут постоянно находиться в состоянии выполнения. Агенты могут как создавать запросы к другим программным компонентам, так и отвечать на запросы, полученные от других программных компонентов. Агенты сотрудничают в пределах групп для коллективного выполнения определенных задач. В такой модели не существует конкретного клиента или сервера. Это — модель сети с равноправными узлами (peer-to-peer), в которой все компоненты имеют одинаковые права, и при этом у каждого компонента есть что предложить другому. Например, агент, который назначает цены на восстановление старинных спортивных машин, может работать вместе с другими агентами. Один агент может быть специалистом по мо-
1.4. Минимальные требования 33 торам, другой — по кузовам, а третий предпочитает работать как дизайнер по интерьерам. Эти агенты могут совместно оценить стоимость работ по восстановлению автомобиля. Агенты являются распределенными, поскольку все они размещаются на разных серверах в Internet. Для связи агенты используют согласованный Internet-протокол. Для одних типов распределенного программирования лучше подходит модель типа "клиент/сервер", а для других — модель равноправных агентов. В этой книге рассматриваются обе модели. Большинство требований, предъявляемых к распределенному программированию, удовлетворяется моделями "клиент/сервер" и равноправных агентов. 1.4. Минимальные требования Параллельное и распределенное программирование требует определенных затрат. Несмотря на описанные выше преимущества, написание параллельных и распределенных программ не обходится без проблем и необходимости наличия предпосылок. О проблемах мы поговорим в главе 2, а предпосылки рассмотрим в следующих разделах. Написанию программы или разработке отдельной части ПО должен предшествовать процесс проектирования. Что касается параллельных и распределенных программ, то процесс проектирования должен включать три составляющих: декомпозиция, связь и синхронизация (ДСС). 1.4.1. Декомпозиция Декомпозиция — это процесс разбиения задачи и ее решения на части. Иногда части группируются в логические области (т.е. поиск, сортировка, вычисление, ввод и вывод данных и т.д.). В других случаях части группируются по логическим ресурсам (т.е. файл, связь, принтер, база данных и т.д.). Декомпозиция программного решения часто сводится к декомпозиции работ (work breakdown structure— WBS). Декомпозиция работ определяет, что должны делать разные части ПО. Одна из основных проблем параллельного программирования— идентификация естественной декомпозиции работ для программного решения. Не существует простого и однозначного подхода к идентификации WBS. Разработка ПО — это процесс перевода принципов, идей, шаблонов, правил, алгоритмов или формул в набор инструкций, которые выполняются, и данных, которые обрабатываются компьютером. Это, в основном, и составляет процесс моделирования. Программные модели — это воспроизведение в виде ПО некоторой реальной задачи, процесса или идеала. Цель модели— сымитировать или скопировать поведение и характеристики некоторой реальной сущности в конкретной предметной области. Процесс моделирования вскрывает естественную декомпозицию работ программного решения. Чем лучше модель понята и разработана, тем более естественной будет декомпозиция работ. Наша цель — обнаружить параллелизм и распределение с помощью моделирования. Если естественный параллелизм не наблюдается, не стоит его навязывать насильно. На вопрос, как разбить приложение на параллельно выполняемые части, необходимо найти ответ в период проектирования, и правильность этого ответа должна стать очевидной в модели решения. Если модель задачи и решения не предполагает параллелизма и распределения, следует попытаться найти последовательное решение. Если последовательное решение оказывается неудачным, эта неудача может дать ключ к нужному параллельному решению.
34 Глава 1. Преимущества параллельного программирования 1.4.2. Связь После декомпозиции программного решения на ряд параллельно выполняемых частей обычно возникает вопрос о связи этих частей между собой. Как же реализовать связь, если эти части разнесены по различным процессам или различным компьютерам? Должны ли различные части ПО совместно использовать общую область памяти? Каким образом одна часть ПО узнает о том, что другая справилась со своей задачей? Какая часть должна первой приступить к работе? Откуда один компонент узнает об отказе другого компонента? На эти и многие другие вопросы необходимо найти ответы при проектировании параллельных и распределенных систем. Если отдельным частям ПО не нужно связываться между собой, значит, они в действительности не образуют единое приложение. 1.4.3. Синхронизация Декомпозиция работ, как уже было отмечено выше, определяет, что должны делать разные части ПО. Когда множество компонентов ПО работают в рамках одной задачи, их функционирование необходимо координировать. Определенный компонент должен "уметь" определить, когда достигается решение всей задачи. Необходимо также скоординировать порядок выполнения компонентов. При этом возникает множество вопросов. Все ли части ПО должны одновременно приступать к работе или только некоторые, а остальные могут находиться пока в состоянии ожидания? Каким двум (или больше) компонентам необходим доступ к одному и тому же ресурсу? Кто имеет право получить его первым? Если некоторые части ПО завершат свою работу гораздо раньше других, то нужно ли им "поручать" новую работу? Кто должен давать новую работу в таких случаях? ДСС (декомпозиция, связь и синхронизация) — это тот минимум вопросов, которые необходимо решить, приступая к параллельному или распределенному программированию. Помимо сути проблем, составляющих ДСС, важно также рассмотреть их привязку. Существует несколько уровней параллелизма в разработке приложений, и в каждом из них ДСС-составляющие применяются по-разному. 1.5. Базовые уровни программного параллелизма В этой книге мы исследуем возможности параллелизма в пределах приложения (в противоположность параллелизму на уровне операционной системы или аппаратных средств). Несмотря на то что параллелизм на уровне операционной системы или аппаратных средств поддерживает параллелизм приложения, нас все же интересует само приложение. Итак, параллелизм можно обеспечить на уровне: • инструкций; • подпрограмм (функций или процедур); • объектов; • приложений. 1.5.1. Параллелизм на уровне инструкций Параллелизм на уровне инструкций возникает, если несколько частей одной инструкции могут выполняться одновременно. На рис. 1.3 показан пример декомпозиции одной инструкции с целью достижения параллелизма выполнения отдельных операций.
1.5. Базовые уровни программного параллелизма 35 На рис. 1.3 компонент (А + В) можно вычислить одновременно с компонентом /С _ d) • Этот вид параллелизма обычно поддерживается директивами компилятора и не попадает под управление С++-программиста. X = (A + B)*(C-D) х^а + в хг-c-D Параллельное выполнение j 1 ' | Синхронизация X = Х2 * Х2 Рис. 1.3. Декомпозиция одной инструкции 1.5.2. Параллелизм на уровне подпрограмм ДСС-структуру программы можно представить в виде ряда функций, т.е. сумма работ, из которых состоит программное решение, разбивается на некоторое количество функций. Если эти функции распределить по потокам, то каждую функцию в этом случае можно выполнить на отдельном процессоре, и, если в вашем распоряжении будет достаточно процессоров, то все функции смогут выполняться одновременно. Подробнее потоки описываются в главе 4. 1.5.3. Параллелизм на уровне объектов ДСС-структуру программного решения можно распределить между объектами. Каждый объект можно назначить отдельному потоку или процессу. Используя стандарт CORBA (Common Object Request Broker Architecture — технология построения распределенных объектных приложений), все объекты можно назначить различным компьютерам одной сети или различным компьютерам различных сетей. Более детально технология CORBA рассматривается в главе 8. Объекты, реализованные в различных потоках или процессах, могут выполнять свои методы параллельно. 1.5.4. Параллелизм на уровне приложений Несколько приложений могут сообща решать некоторую проблему. Несмотря на то что какое-то приложение первоначально предназначалось для выполнения отдельной задачи, принципы многократного использования кода позволяют приложениям сотрудничать. В таких случаях два отдельных приложения эффективно работают вместе подобно единому распределенному приложению. Например, буфер обмена (Clipboard) не предназначался для работы ни с каким конкретным приложением, но его успешно использует множество приложений рабочего стола. О некоторых вариантах применения буфера обмена его создатели в процессе разработки даже и не мечтали. Второй и третий уровни — это основные уровни параллелизма, поэтому методам их реализации и уделяется основное внимание в этой книге. Уровня операционной системы и аппаратных средств мы коснемся только в том случае, когда это будет необходимо в контексте проектирования приложений. Получив соответствующую ДСС- структуру для проекта, предусматривающего параллельное или распределенное программирование, можно переходить к следующему этапу— рассмотрению возможности его реализации в C++.
36 Глава 1. Преимущества параллельного программирования 1.6. Отсутствие языковой поддержки параллелизма в C++ Язык C++ не содержит никаких синтаксических примитивов для параллелизма. С++-стандарт ISO также отмалчивается на тему многопоточности. В языке C++ не предусмотрено никаких средств, чтобы указать, что заданные инструкции должны выполняться параллельно. Включение встроенных средств параллелизма в других языках представляется как их особое достоинство. Бьерн Страуструп, создатель языка C++, имел свое мнение на этот счет: Можно организовать поддержку параллелизма средствами библиотек, которые будут приближаться к встроенным средствам параллелизма как по эффективности, так и по удобству применения. Опираясь на такие библиотеки, можно поддерживать различные модели, а не только одну, как при использования встроенных средств параллелизма. Я полагаю, что большинство программистов согласятся со мной, что именно такое направление (создание набора библиотек поддержки параллелизма) позволит решить проблемы переносимости, используя тонкий слой интерфейсных классов. Более того, Страуструп говорит: "Я считаю, что параллелизм в C++ должен быть представлен библиотеками, а не как языковое средство". Авторы этой книги находят позицию Страуструпа и его рекомендации по реализации параллелизма в качестве библиотечного средства наиболее подходящими с практической точки зрения. В настоящей книге рассмотрен только этот вариант, и такой выбор объясняется доступностью высококачественных библиотек, которые успешно можно использовать для решения задач параллельного и распределенного программирования. Библиотеки, которые мы используем для усиления языка C++ с этой целью, реализуют национальные и международные стандарты и используются тысячами С++-программистов во всем мире. 1.6.1. Варианты реализации параллелизма с помощью C++ Несмотря на существование специальных версий языка C++, предусматривающих "встроенные" средства параллельной обработки данных, мы представляем методы реализации параллелизма с использованием стандарта ISO (International Organization for Standardization — Международная организация по стандартизации) для C++. Мы находим библиотечный подход к параллелизму (при котором используются как системные, так и пользовательские библиотеки) наиболее гибким. Системные библиотеки предоставляются средой операционной системы. Например, поточно- ориентированная библиотека POSIX (Portable Operating System Interface— интерфейс переносимой операционной системы) содержит набор системных функций, которые в сочетании с языковыми средствами C++ успешно используются для поддержки параллелизма. Библиотека POSIX Threads является частью нового единого стандарта спецификаций UNIX (Single UNIX Specifications Standard) и включена в набор стандартов IEEE, описывающих интерфейсы ОС для UNIX (IEEE Std. 1003.1-2001). Создание нового единого стандарта спецификаций UNIX финансируется организацией Open Group, а его разработка поручена организации Austin Common Standards Revision Group. В соответствии с документами Open Group новый единый стандарт спецификаций UNIX:
1.6. Отсутствие языковой поддержки параллелизма в C++ 37 • предоставляет разработчикам ПО единый набор API-функций, которые должны поддерживаться каждой UNIX-системой; • смещает акцент с несовместимых реализаций систем UNIX на соответствие единому набору функций API; • представляет собой кодификацию и юридическую стандартизацию общего ядра системы UNIX; • в качестве основной цели преследует достижение переносимости исходного кода приложения. Новый единый стандарт спецификаций UNIX, версия 3, включает стандарт IEEE Std. 1003.1-2001 и спецификации Open Group Base Specifications Issue 6. Стандарты IEEE POSIX в настоящее время представляют собой часть единой спецификации UNIX, и наоборот. Сейчас действует единый международный стандарт для интерфейса переносимой операционной системы. С++-разработчикам это только на руку, поскольку данный стандарт содержит API-функции, которые позволяют создавать потоки и процессы. За исключением параллелизма на уровне инструкций, единственным способом достижения параллелизма с помощью C++ является разбиение программы на потоки или процессы. Именно эти средства и предоставляет новый стандарт. Разработчик может использовать: • библиотеку POSIX Threads (или Pthreads); • POSIX-функцию spawn (); • семейство функций exec (). Все эти средства поддерживаются системными API-функциями и системными библиотеками. Если операционная система отвечает 3-й версии нового единого стандарта UNIX, то С++-разработчику будут доступны эти API-функции (они рассматриваются в главах 3 и 4 и используются во многих примерах этой книги). Помимо библиотек системного уровня, для поддержки параллелизма в C++ могут применяться такие библиотеки пользовательского уровня, как MPI (Message Passing Interface— интерфейс для передачи сообщений), PVM (Parallel Virtual Machine— параллельная виртуальная машина) и CORBA (Common Object Request Broker Architecture— технология построения распределенных объектных приложений). 1.6.2. Стандарт MPI Интерфейс MPI — стандартная спецификация на передачу сообщений — был раз- раоотан с целью достижения высокой производительности на компьютерах с массовым параллелизмом и кластерах рабочих станций (рабочая станция — это сетевой компьютер, использующий ресурсы сервера). В этой книге используется MPICH-реализация стандарта MPI. MPICH — это свободно распространяемая переносимая реализация интерфейса MPI. MPICH предоставляет С++-программисту набор API-функций и библиотек, которые поддерживают параллельное программирование. Интерфейс МР1 особенно полезен для программирования моделей SPMD (Single-Program, Multiple- Uata — одна програмхма, несколько потоков данных) и MPMD (Multiple-Program, Multiple-Data— множество программ, множество потоков данных). Авторы этой книги используют MPICH-реализацию библиотеки MPI для 32-узлового Linux-ориенти- рованного кластера и 8-узлового кластера, управляемого операционными системами
38 Глава 1. Преимущества параллельного программирования Linux и Solaris. И хотя в C++ нет встроенных примитивов параллельного программирования, С++-программист может воспользоваться средствами обеспечения параллелизма, предоставляемыми библиотекой MPICH. В этом и состоит одно из достоинств языка C++, которое заключается в его фантастической гибкости. 1.6.3. PVM: стандарт для кластерного программирования Программный пакет PVM позволяет связывать гетерогенную (неоднородную) коллекцию компьютеров в сеть для использования ее в качестве единого мощного параллельного компьютера. Общая цель PVM-системы — получить возможность совместно использовать коллекцию компьютеров для организации одновременной или параллельной обработки данных. Реализация библиотеки PVM поддерживает: • гетерогенность по компьютерам, сетям и приложениям; • подробно разработанную модель передачи сообщений; • обработку данных на основе выполнения процессов; • мультипроцессорную обработку данных (МРР, SMP)'; • "полупрозрачный" доступ к оборудованию (т.е. приложения могут либо игнорировать, либо использовать преимущества различий в аппаратных средствах); • динамически настраиваемый пул (процессоры могут добавляться или удаляться динамически, возможен также их смешанный состав). PVM — это самая простая (по использованию) и наиболее гибкая среда, доступная для решения задач параллельного программирования, которые требуют применения различных типов компьютеров, работающих под управлением различных операционных систем. PVM-библиотека особенно полезна для объединения в сеть нескольких однопроцессорных систем с целью образования виртуальной машины с параллельно работающими процессорами. Методы использования библиотеки PVM в С++-коде мы рассмотрим в главе 6. PVM — это фактический стандарт для реализации гетерогенных кластеров, который легко доступен и широко распространен. PVM прекрасно поддерживает модели параллельного программирования MPMD (MIMD) и SPMD (SIMD). Авторы этой книги для решения небольших и средних по объему задач параллельного программирования используют PVM-библиотеку, а для более сложных и объемных — MPI-библиотеку. Обе библиотеки PVM и MPI можно успешно сочетать с C++ для программирования кластеров. 1.6.4. Стандарт CORBA CORBA— это стандарт для распределенного кроссплатформенного объектно- ориентированного программирования. Выше упоминалось о применении CORBA для поддержки параллелизма, поскольку реализации стандарта CORBA можно использовать для разработки мультиагентных систем. Мультиагентные системы предлагают важные сетевые модели распределенного программирования с равноправными узлами МРР - Massively Parallel Processors (процессоры с массовым параллелизмом), SMP - symmetric multiprocessor (симметричный мультипроцессор).
1.6. Отсутствие языковой поддержки параллелизма в C++ 39 foeer-to-peer). В мультиагентных системах работа может быть организована параллельно. 9то одна из областей, в которых параллельное и распределенное программирование пе- крываются. Несмотря на то что агенты выполняются на различных компьютерах, это происходит в течение одного и того же промежутка времени, т.е. агенты совместно работают над общей проблемой. Стандарт CORBA обеспечивает открытую, независимую от изготовителя архитектуру и инфраструктуру, которую компьютерные приложения используют для совместного функционирования в сети. Используя стандартный протокол ПОР (Internet InterORB Protocol — протокол, определяющий передачу сообщений между сетевыми объектами по TCP/IP), CORBA-ориентированная программа (созданная любым производителем на любом языке программирования, выполняемая практически на любом компьютере под управлением любой операционной системы в любой сети) может взаимодействовать с другой CORBA-ориентированной программой (созданной тем же или другим производителем на любом другом языке программирования, выполняемой практически на любом компьютере под управлением любой операционной системы в любой сети). В этой книге мы используем MICO-реализацию стандарта CORBA. MICO— свободно распространяемая и полностью соответствующая требованиям реализация стандарта CORBA, которая поддерживает язык C++. 1.6.5. Реализации библиотек на основе стандартов Библиотеки MPICH, PVM, MICO и POSIX Threads реализованы на основе стандартов. Это означает, что разработчики ПО могут быть уверены, что эти реализации широко доступны и переносимы с одной платформы на другую. Эти библиотеки используются многими разработчиками ПО во всем мире. Библиотеку POSIX Threads можно использовать с C++ для реализации многопоточного программирования. Если про-; грамма выполняется на компьютере с несколькими процессорами, то каждый поток может выполняться на отдельном процессоре, что позволяет говорить о реальной параллельности программирования. Если же компьютер содержит только один про-! цессор, то иллюзия параллелизма обеспечивается за счет процесса переключения контекстов. Библиотека POSIX Threads позволяет реализовать, возможно, самый! простой способ введения параллелизма в С++-программу. Если для использования библиотек MPICH, PVM и MICO необходимо предварительно побеспокоиться об их] установке, то в отношении библиотеки POSIX Threads это излишне, поскольку среда любой операционной системы, которая согласована с POSIX-стандартом или новой] спецификацией UNIX (версия 3), оснащена реализацией библиотеки POSIX Threads Все библиотеки предлагают модели параллелизма, которые имеют незначительные) различия. В табл. 1.2 показано, как каждую библиотеку можно использовать с C++. [Табл Таблица 1.2. Использование библиотек MPICH, PVM, MICO и POSIX Threads с C++ Библиотека Использование с C++ MPICH Поддерживает крупномасштабное сложное программирование кластеров. Предпочтительно используется для модели SPMD. Также поддерживает SMP-, МРР- и многопользовательские конфигурации PVM Поддерживает кластерное программирование гетерогенных сред. Легко используется для однопользовательских (мелко- и среднемасштабных) кластерных приложений. Также поддерживает МРР-конфигурации
40 Глава 1. Преимущества параллельного программирования Окончание табл. 1.2 Библиотека Использование с C++ MICO Поддерживает и распределенное, и параллельное программирование. Содержит эффективные средства поддержки агентно- ориентированного и мультиагентного программирования POSIX Поддерживает параллельную обработку данных в одном приложении на уровне функций или объектов. Позволяет воспользоваться преимуществами SMP- и МРР-конфигурации В то время как языки со встроенной поддержкой параллелизма ограничены применением конкретных моделей, С++-разработчик волен смешивать различные модели параллельного программирования. При изменении структуры приложения С++- разработчик в случае необходимости выбирает другие библиотеки, соответствующие новому сценарию работы. 1.7. Среды для параллельного и распределенного программирования Наиболее распространенными средами для параллельного и распределенного программирования являются кластеры, SMP- и МРР-компьютеры. Кластеры — это коллекции, состоящие из нескольких компьютеров, объединенных сетью для создания единой логической системы. С точки зрения приложения такая группа компьютеров выглядит как один виртуальный компьютер. Под МРР- конфигурацией (Massively Parallel Processors — процессоры с массовым параллелизмом) понимается один компьютер, содержащий сотни процессоров, а под SMP-конфи- гурацией (symmetric multiprocessor — симметричный мультипроцессор) — единая система, в которой тесно связанные процессоры совместно используют общую память и информационный канал. SMP-процессоры разделяют общие ресурсы и являются объектами управления одной операционной системы. Поскольку эта книга представляет собой введение в параллельное и распределенное программирование, нас будут интересовать небольшие кластеры, состоящие из 8-32 процессоров, и многопроцессорные компьютеры с двумя-четырьмя процессорами. И хотя многие рассматриваемые здесь методы можно использовать в МРР- или больших SMP-средах, мы в основном уделяем внимание системам среднего масштаба. 1.8. Резюме В этой книге представлен архитектурный подход к параллельному и распределенному программированию. При этом акцент ставится на определении естественного параллелизма в самой задаче и ее решении, который закрепляется в программной модели решения. Мы предлагаем использовать объектно-ориентированные методы, которые бы позволили справиться со сложностью параллельного и распределенного программирования, и придерживаемся следующего принципа: функция следует за формой. В отношении языка C++ используется библиотечный подход к обеспечению
1.8. Резюме 41 поддержки параллелизма. Рекомендуемые нами библиотеки базируются на национальных и международных стандартах. Каждая библиотека легко доступна и широко используется программистами во всем мире. Методы и идеи, представленные в этой книге, не зависят от конкретных изготовителей программных и аппаратных средств, общедоступны и опираются на открытые стандарты и открытые архитектуры. С++-программист и разработчик ПО может использовать различные модели параллелизма, поскольку каждая такая модель обусловливается библиотечными средствами. Библиотечный подход к параллельному и распределенному программированию дает С++-программисту гораздо большую степень гибкости по сравнению с использованием встроенных средств языка. Наряду с достоинствами, параллельное и распределенное программирование не лишено многих проблем, которые рассматриваются в следующей главе.
ПРОБЛЕМЫ ПАРАЛЛЕЛЬНОГО И РАСПРЕДЕЛЕННОГО ПРОГРАММИРОВАНИЯ В этой главе... 2.1. Кардинальное изменение парадигмы 2.2. Проблемы координации 2.3. Отказы оборудования и поведение ПО 2.4. Негативные последствия излишнего параллелизма и распределения 2.5. Выбор архитектуры 2.6. Различные методы тестирования и отладки 2.7. Связь между параллельным и распределенным проектами 2.8. Резюме
Slj\3JS33J J Я1;*\ "Стремление обозначать точные значения любой физической величины (температура, плотность, напряженность потенциального поля или что-либо еще...) есть не что иное как смелая экстраполяция." — Эрвин Шредингер (Erwin Shrodinger), Causality and Wave Mechanics В базовой последовательной модели программирования инструкции компьютерной программы выполняются поочередно. Программа выглядит как кулинарный рецепт, в соответствии с которым для каждого действия компьютера задан порядок и объемы используемых "ингредиентов". Разработчик программы разбивает основную задачу ПО на коллекцию подзадач. Все задачи выполняются по порядку, и каждая из них должна ожидать своей очереди. Все программы имеют начало, середину и конец. Разработчик представляет каждую программу в виде линейной последовательности задач. Эти задачи необязательно должны находиться в одном файле, но их следует связать между собой так, чтобы, если первая задача по какой-то причине не завершила свою работу, то вторая вообще не начинала выполнение. Другими словами, каждая задача, прежде чем приступить к своей работе, должна ожидать до тех пор, пока не получит результатов выполнения предыдущей. В последовательной Модели зачастую устанавливается последовательная зависимость задач. Это означает, что задаче А необходимы результаты выполнения задачи В, а задаче В нужны результаты выполнения задачи С, которой требуется что-то от задачи D и т.д. Если при выполнении задачи В по какой-то причине произойдет сбой, задачи С и D никогда не Приступят к работе. В таком последовательном мире разработчик привычно ориентирует ПО сначала на выполнение действия 1, затем — действия 2, за которым должно следовать действие 3 и т.д. Подобная последовательная модель настолько закрепилась
44 Глава 2. Проблемы параллельного и распределенного программирования в процессе проектирования и разработки ПО, что многие программисты считают ее незыблемой и не допускают мысли о возможности иного положения вещей. Решение каждой проблемы, разработка каждого алгоритма и планирование каждой структуры данных — все это делалось с мыслью о последовательном доступе компьютера к каждой инструкции или ячейке данных. 2.1. Кардинальное изменение парадигмы В мире параллельного программирования все обстоит по-другому. Здесь сразу несколько инструкций могут выполняться в один и тот же момент времени. Одна инструкция разбивается на несколько мелких частей, которые будут выполняться одновременно. Программа разбивается на множество параллельных задач. Программа может состоять из сотен или даже тысяч выполняющихся одновременно подпрограмм. В мире параллельного программирования последовательность и местоположение составляющих ПО не всегда предсказуемы. Несколько задач могут одновременно начать выполнение на любом процессоре без какой бы то ни было гарантии того, что задачи закреплены за определенными процессорами, или такая-то задача завершится первой, или все они завершатся в таком-то порядке. Помимо параллельного выполнения задач, здесь возможно параллельное выполнение частей (подзадач) одной задачи. В некоторых конфигурациях не исключена возможность выполнения подзадач на различных процессорах или даже различных компьютерах. На рис. 2.1 показаны три уровня параллелизма, которые могут присутствовать в одной компьютерной программе. Модель программы, показанная на рис. 2.1, отражает кардинальное изменение парадигмы программирования, которая была характерна для "раннего" сознания программистов и разработчиков. Здесь отображены три уровня параллелизма и их распределение по нескольким процессорам. Сочетание этих трех уровней с базовыми параллельными конфигурациями процессоров показано на рис. 2.2. Обратите внимание на то, что несколько задач может выполняться на одном процессоре даже при наличии в компьютере нескольких процессоров. Такая ситуация создается системными стратегиями планирования. На длительность выполнения задач, подзадач и инструкций оказывают влияние и выбранные стратегии планирования, и приоритеты процессов, и приоритеты потоков, и быстродействие устройств ввода-вывода. На рис. 2.2 следует обратить внимание на различные архитектуры, которые программист должен учитывать при переходе от последовательной модели программирования к параллельной. Основное различие в моделях состоит в переходе от строго упорядоченной последовательности задач к лишь частично упорядоченной (или вовсе неупорядоченной) коллекции задач. Параллелизм превращает ранее известные величины (порядок выполнения, время выполнения и место выполнения) в неизвестные. Любая комбинация этих неизвестных величин является причиной изменения значений программы, причем зачастую непредсказуемым образом. 2.2. Проблемы координации Если программа содержит подпрограммы, которые могут выполняться параллельно, и эти подпрограммы совместно используют некоторые файлы, устройства или области памяти, то неизбежно возникают проблемы координации. Предположим, у нас
2.2. Проблемы координации 45 программа поддержки электронного банка, которая позволяет снимать деньги со Чета и класть их на депозит. Допустим, что эта программа разделена на три задачи (обозначим их А, В и С), которые могут выполняться параллельно. Программа/ приложение Параллелизм на уровне задач Все задачи выполняются параллельно. Задача. А Задача В Параллелизм на уровне подзадач Задача разбивается на параллельно выполняемые подзадачи. Поток 1, Поток 2. Поток 1с Поток 2, Параллелизм на' уровне'инструкций Это компоненты инструкции могут выполняться параллельно. d ,ё Г Рис. 2.1. Три уровня параллелизма, которые возможны в одной компьютерной программе Задача А получает запросы от задачи Б на выполнение операций снятия денег со счета. Задача А также получает запросы от задачи С положить деньги на депозит. Задача А принимает запросы и обрабатывает их по принципу "первым пришел — первым обслужен". Предположим, на счете имеется 1000 долл., при этом задача С требует Положить на депозит 100 долл., а задача В желает снять со счета 1100 долл. Что произойдет, если обе задачи Б и С попытаются обновить один и тот же счет одновременно?
46 Глава 2. Проблемы параллельного и распределенного программирования Параллелизм на уровне задач Программа/ приложение Задача А Задача В Задача С Задач'а В Задача А: Задача С Процессор, Процессор Компьютер 1 Однопроцессорный компьютер Параллелизм на уровне подзадач Программа/ приложение Задача С I Задача I I Задача I I A I I С J I 1 I Поток 1. Поток 2 I AN All Тг Поток 2* Поток 1, Процессор ^^____р Задачз С Процессор..,] Компьютер 3 PVM-среда с несколькими однопроцессорными компьютерами Параллелизм на уровне инструкций Программа/ приложение Задача А Поток 1я Компьютер 5 PVM-среда с несколькими многопроцессорными компьютерами Рис. 2.2. Три уровня параллелизма в сочетании с базовыми параллельными конфигурациями процессоров
2.2. Проблемы координации 47 Каким будет остаток на счете? Очевидно, остаток на счете в каждый момент времени может иметь более одного значения. Задача А применительно к счету должна вы- олнять одновременно только одну транзакцию, т.е. мы сталкиваемся с проблемой координации задач. Если запрос задачи В будет выполнен на какую-то долю секунды быстрее, чем запрос задачи С, то счет приобретет отрицательный баланс. Но если задача С получит первой право на обновление счета, то этого не произойдет. Таким об- пазом, остаток на счете зависит от того, какой задаче (В или С) первой удастся сделать запрос к задаче А. Более того, мы можем выполнять задачи В и С несколько раз с одними и теми же значениями, и при этом иногда запрос задачи В будет произведен на какую-то долю секунды быстрее, чем запрос задачи С, а иногда — наоборот. Очевидно, что необходимо организовать надлежащую координацию действий. Для координации задач, выполняемых параллельно, требуется обеспечить связь между ними и синхронизацию их работы. При некорректной связи или синхронизации обычно возникает четыре типа проблем. Проблема №1: "гонка" данных Если несколько задач одновременно попытаются изменить некоторую общую область данных, а конечное значение данных при этом будет зависеть от того, какая задача обратится к этой области первой, возникнет ситуация, которую называют состоянием "гонок" (race condition). В случае, когда несколько задач попытаются обновить один и тот же ресурс данных, такое состояние "гонок" называют "гонкой" данных (data race). Какая задача в нашей программе поддержки электронного банка первой получит доступ к остатку на счете, определяется результатом работы планировщика задач операционной системы, состоянием процессоров, временем ожидания и случайными причинами. В такой ситуации создается состояние "гонок". И какое значение в этом случае должен сообщать банк в качестве реального остатка на счете? Итак, несмотря на то, что мы хотели бы, чтобы наша программа позволяла одновременно обрабатывать множество операций по снятию денег со счета и вложению их на депозит, нам нужно координировать эти задачи в случае, если окажется, что операции снятия и вложения денег должны быть применены к одному и тому же счету. Всякий раз когда задачи одновременно используют модифицируемый ресурс, к ресурсному доступу этих задач должны быть применены определенные правила и стратегии. Например, в нашей программе поддержки банковских операций со счетами мы могли бы всегда выполнять любые операции по вложению денег до выполнения каких бы то ни было операций по их снятию. Мы могли бы установить правило, в соответствии с которым доступ к счету одновременно могла получать только одна транзакция. И если окажется, что к одному и тому же счету одновременно обращается сразу несколько транзакций, их необходимо задержать, организовать их выполнение в соответствии с некоторым правилом очередности, а затем предоставлять им доступ к счету по одной (в порядке очереди). Такие правила организации позволяют добиться надлежащей синхронизации действий. Проблема №2: бесконечная отсрочка Такое планирование, при котором одна или несколько задач должны ожидать до тех Пор, пока не произойдет некоторое событие или не создадутся определенные условия, Может оказаться довольно непростым для реализации. Во-первых, ожидаемое событие
48 Глава 2. Проблемы параллельного и распределенного программирования или условие должно отличаться регулярностью. Во-вторых, между задачами следует наладить связи. Если одна или несколько задач ожидают сеанса связи до своего выполнения, то в случае, если ожидаемый сеанс связи не состоится, состоится слишком поздно или не полностью, эти задачи могут так никогда и не выполниться. И точно так же, если ожидаемое событие или условие, которое (по нашему мнению) должно произойти (или наступить), но в действительности не происходит (или не наступает), то приостановленные нами задачи будут вечно находиться в состоянии ожидания. Если мы приостановим одну или несколько задач до наступления события (или условия), которое никогда не произойдет, возникнет ситуация, называемая бесконечной отсрочкой (indefinite postponement). Возвращаясь к нашему примеру электронного банка, предположим, что, если мы установим правила, предписывающие всем задачам снятия денег со счета находиться в состоянии ожидания до тех пор, пока не будут выполнены все задачи вложения денег на счет, то задачи снятия денег рискуют стать бесконечно отсроченными. Мы исходили из предположения о гарантированном существовании задач вложения денег на счет. Но если ни один из запросов на пополнение счетов не поступит, то что тогда заставит выполниться задачи снятия денег? И, наоборот, что, если будут без конца поступать запросы на пополнение одного и того же счета? Ведь тогда не сможет "пробиться" к счету ни один из запросов на снятие денег. Такая ситуация также может вызвать бесконечную отсрочку задач снятия денег. Бесконечная отсрочка возникает при отсутствии задач вложения денег на счет или их постоянном поступлении. Необходимо также предусмотреть ситуацию, когда запросы на вложение денег поступают корректно, но нам не удается надлежащим образом организовать связь между событиями и задачами. По мере того как мы будем пытаться скоординировать доступ параллельных задач к некоторому общему ресурсу данных, следует предусмотреть все ситуации, в которых возможно создание бесконечной отсрочки. Методы, позволяющие избежать бесконечных отсрочек, рассматриваются в главе 5. Проблема №3: взаимоблокировка Взаимоблокировка— это еще одна "ловушка", связанная с ожиданием. Для демонстрации взаимоблокировки предположим, что в нашей программе поддержки электронного банка три задачи работают не с одним, а с двумя счетами. Вспомним, что задача А получает запросы от задачи В на снятие денег со счета, а от задачи С — запросы на вложение денег на депозит. Задачи А, В и С могут выполняться параллельно. Однако задачи В и С могут обновлять одновременно только один счет. Задача А предоставляет доступ задач В и С к нужному счету по принципу "первым пришел — первым обслужен". Предположим также, что задача В имеет монопольный доступ к счету 1, а задача С — монопольный доступ к счету 2. При этом задаче В для выполнения соответствующей обработки также нужен доступ к счету 2 и задаче С — доступ к счету 1. Задача В удерживает счет 1, ожидая, пока задача С не освободит счет 2. Аналогично задача С удерживает счет 2, ожидая, пока задача В не освободит счет 1. Тем самым задачи В и С рискуют попасть в тупиковую ситуацию, которую в данном случае можно назвать взаимоблокировкой (deadlock). Ситуация взаимоблокировки между задачами В и С схематично показана на рис. 2.3. Форма взаимоблокировки в данном случае объясняется наличием параллельно выполняемых задач, имеющих доступ к совместно используемым данным, которые им разрешено обновлять. Здесь возможна ситуация, когда каждая из задач будет ожидать до тех пор, пока другая не освободит доступ к общим данным (общими данными здесь являются счет 1 и счет 2). Обе задачи имеют доступ к обоим счетам. Может случиться так,
2.2. Проблемы координации 49 вместо получения доступа одной задачи к двум счетам, каждая задача получит доступ ному из счетов. Поскольку задача В не может освободить счет 1, пока не получит к счету 2, а задача С не может освободить счет 2, пока не получит доступ к счету 1, опэамма обслуживания счетов электронного банка будет оставаться заблокированной. Обоатите внимание на то, что задачи В и С могут ввести в состояние бесконечной отсрочки и другие задачи (если таковые имеются в системе). Если другие задачи ожидают получения доступа к счетам 1 или 2, а задачи В и С "скованы" взаимоблокировкой, то те другие задачи будут ожидать условия, которое никогда не выполнится. При координации параллельно выполняемых задач необходимо помнить, что взаимоблокировка и бесконечная отсрочка — это самые опасные преграды, которые нужно предусмотреть и избежать. Счет №1 Доступ к счету №1 предоставлен задаче В Задача С запрашивает доступ к счету №1 Задача В запрашивает доступ к счету №2 Доступ к счету №2 предоставлен задаче С Рис. 2.3. Ситуация взаимоблокировки между задачами Б и С Проблема №4: трудности организации связи Многие распространенные параллельные среды (например, кластеры) зачастую состоят из гетерогенных компьютерных сетей. Гетерогенные компьютерные сети— это системы, которые состоят из компьютеров различных типов, работающих в общем случае под управлением различных операционных систем и использующих различные сетевые протоколы. Их процессоры могут иметь различную архитектуру, обрабатывать слова различной длины и использовать различные машинные языки. Помимо разных операционных систем, компьютеры могут различаться используемыми стратегиями планирования и системами приоритетов. Хуже того, все системы могут различаться параметрами передачи данных. Это делает обработку ошибок и исключительных ситуаций (исключений) особенно трудной. Неоднородность системы может усугубляться и другими различиями. Например, может возникнуть необходимость в организации совместного использования данных программами, написанными на различных языках или разработанных с использованием различных моделей ПО. Ведь общее системное решение может быть реализовано по частям, написанным на языках Fortran, C++ и Java. Это вносит проблемы межъязыковой связи. И даже если распределенная или параллельная среда не является гетерогенной, остается проблема
50 Глава 2. Проблемы параллельного и распределенного программирования взаимодействия между несколькими процессами или потоками. Поскольку каждый процесс имеет собственное адресное пространство, то для совместного использования переменных, параметров и значений, возвращаемых функциями, необходимо применять технологию межпроцессного взаимодействия (interprocess communication— IPC), или МПВ-технологию. И хотя реализация МПВ-методов необязательно является самой трудной частью разработки системы ПО, тем не менее они образуют дополнительный уровень проектирования, тестирования и отладки в создании системы. POSIX-спецификация поддерживает пять базовых механизмов, используемых для реализации взаимодействия между процессами: • файлы со средствами блокировки и разблокировки; • каналы (неименованные, именованные и FIFO-очереди); • общая память и сообщения; • сокеты; • семафоры. Каждый из этих механизмов имеет достоинства, недостатки, ловушки и тупики, которые проектировщики и разработчики ПО должны обязательно учитывать, если хотят создать надежную и эффективную связь между несколькими процессами. Организовать взаимодействие между несколькими потоками (которые иногда называются облегченными процессами) обычно проще, чем между процессами, так как потоки используют общее адресное пространство. Это означает, что каждый поток в программе может легко передавать параметры, принимать значения, возвращаемые функциями, и получать доступ к глобальным данным. Но если взаимодействие процессов или потоков не спроектировано должным образом, возникают такие проблемы, как взаимоблокировки, бесконечные отсрочки и другие ситуации "гонки" данных. Необходимо отметить, что перечисленные выше проблемы характерны как для распределенного, так и для параллельного программирования. Несмотря на то что системы с исключительно параллельной обработкой отличаются от систем с исключительно распределенной обработкой, мы намеренно не проводили границ)7 между проблемами координации в распределенных и параллельных системах. Частично мы можем объяснить это некоторым перекрытием существующих проблем, и частично тем, что некоторые решения проблем в одной области часто применимы к проблемам в другой. Но главная причина нашего "обобщенного" подхода состоит в том, что в последнее время гибридные (параллельно-распределенные) системы становятся нормой. Современное положение в параллельном способе обработке данных определяют кластеры и сетки. Причудливые кластерные конфигурации составляют из готовых продуктов. Такие архитектуры включают множество компьютеров со многими процессорами, а однопроцессорные системы уже уходят в прошлое. В будущем предполагается, что чисто распределенные системы будут встраиваться в виде компьютеров с несколькими процессорами. Это означает, что на практике проектировщик или разработчик ПО будет теперь все чаще сталкиваться с проблемами распределения и параллелизма. Вот потому-то мы и рассматриваем все эти проблемы в одном пространстве. В табл. 2.1 представлены комбинации параллельного и распределенного программирования с различными конфигурациями аппаратного обеспечения. В табл. 2.1 обратите внимание на то, что существуют конфигурации, в которых параллелизм достигается за счет использования нескольких компьютеров. В этом случае подходит применение библиотеки PVM. И точно так же существуют конфигурации,
2.3. Отказы оборудования и поведение ПО 51 оторых распределение может быть достигнуто лишь на одном компьютере за счет биения Логики ПО на несколько процессов или потоков. Именно факт использо- ания множества процессов или потоков говорит о том, что работа программы носит "оаспределенный" характер. Комбинации параллельного и распределенного программирования, представленные в табл. 2.1, подразумевают, что проблемы конфигурации, обычно присущие распределенному программированию, могут возникнуть «ситуациях, обусловленных параллельным программированием, и, наоборот, проблемы конфигурации, обычно связанные с параллельным программированием, могут возникнуть в ситуациях, обусловленных распределенным программированием. Таблица 2.1. Комбинации параллельного и распределенного программирования с различными конфигурациями аппаратного обеспечения Один компьютер Множество компьютеров Параллельное программирование Распределенное программирование Оснащен множеством процессоров. Использует логическое разбиение на несколько потоков или процессов. Потоки или процессы могут выполняться на различных процессорах. Для координации задач требуется МПВ-технология Наличие нескольких процессоров не является обязательным. Логика ПО может быть разбита на несколько процессов или потоков. Для координации задач требуется МПВ- технология Использует такие библиотеки, как PVM. Требует организации взаимодействия посредством передачи сообщений, что обычно связано с распределенным программированием Реализуется с помощью сокетов и таких компонентов, как CORBA ORB (Object Request Broker — брокер объектных запросов). Может использовать тип взаимодействия, который обычно связан с параллельным программированием Независимо от используемой конфигурации аппаратных средств, существует два базовых механизма, обеспечивающих взаимодействие нескольких задач: общая память и средства передачи сообщений. Для эффективного использования механизма общей памяти программисту необходимо предусмотреть решение проблем "гонки" Данных, взаимоблокировки и бесконечных отсрочек. Схема передачи сообщений Должна предполагать возникновение таких "накладок", как прерывистые передачи, бессмысленные (искаженные), утерянные, ошибочные, слишком длинные, просроченные (с нарушением сроков), преждевременные сообщения и т.п. Эффективное использование обоих механизмов подробно рассматривается ниже в этой книге. 2.3. Отказы оборудования и поведение ПО При совместной работе множества процессоров над решением некоторой задачи возможен отказ одного или нескольких процессоров. Каким в этом случае должно °Ь1ть поведение ПО? Программа должна остановиться или возможно перераспределение работы? Что случится, если при использовании мультикомпыотерной системы Канал связи между несколькими компьютерами временно выйдет из строя? Что произойдет, если поток данных будет настолько медленным, что процессы на каждом
52 Глава 2. Проблемы параллельного и распределенного программирования конце связи превысят выделенный им лимит времени? Как ПО должно реагировать на подобные ситуации? Если, предположим, во время работы системы, состоящей из 50 компьютеров, совместно работающих над решением некоторой проблемы, произойдет отказ двух компьютеров, то должны ли остальные 48 взять на себя их функции? Если в нашей программе электронного банка при одновременном выполнении задач по снятию и вложению денег на счет две задачи попадут в ситуацию взаимоблокировки, то нужно ли прекратить работу серверной задачи? И что тогда делать с заблокированными задачами? А как быть, если задачи по снятию и вложению денег на счет будут работать надлежащим образом, но по какой-то причине будет "парализована" серверная задача? Следует ли в этом случае прекратить выполнение всех "повисших" задач по снятию и вложению денег на счет? Что делать с частичными отказами или прерывистой работой? Подобные вопросы обычно не возникают при работе последовательных программ в одно-компьютерных средах. Иногда отказ системы является следствием административной политики или стратегии безопасности. Например, предположим, что система содержит 1000 подпрограмм, и некоторым из них требуется доступ к файлу для записи в него информации, но они по какой-то причине не могут его получить. В результате возможны взаимоблокировка, бесконечная отсрочка или частичный отказ. А как быть, если некоторые подпрограммы блокируются из-за отсутствия у них прав доступа к нужным ресурсам? Должна ли в таких случаях "вырубаться" вся система целиком? Насколько можно доверять обработанной информации, если в системе произошли сбои в оборудовании, отказ каналов связи или их работа была прерывистой? Тем не менее эти ситуации очень даже характерны (можно сказать, являются нормой) для распределенных или параллельных сред. В этой книге мы рассмотрим ряд архитектурных решений и технологий программирования, которые позволят программному обеспечению системы справляться с подобными ситуациями. 2.4. Негативные последствия излишнего параллелизма и распределения При внедрении технологии параллелизма всегда существует некоторая "точка насыщения", по "ту сторону" которой затраты на управление множеством процессоров превышают эффект от увеличения быстродействия и других достоинств параллелизма. Старая поговорка "процессоров никогда не бывает много" попросту не соответствует истине. Затраты на организацию взаимодействия между компьютерами или обеспечение синхронизации процессоров выливаются "в копеечку". Сложность синхронизации или уровень связи между процессорами может потребовать таких затрат вычислительных ресурсов, что они отрицательно скажутся на производительности задач, совместно выполняющих общую работу. Как узнать, на сколько процессов, задач или потоков следует разделить программу? И, вообще, существует ли оптимальное количество процессоров для любой заданной параллельной программы? В какой "точке" увеличение процессоров или компьютеров в системе приведет к замедлению ее работы, а не к ускорению? Нетрудно предположить, что рассматриваемые числа зависят от конкретной программы. В некоторых областях имитационного моделирования максимальное число процессоров может достигать нескольких тысяч, в то время как в коммерческих приложениях можно ограничиться несколькими сотнями. Для ряда клиент-серверных конфигураций зачастую оптимальное количество составляет восемь процессоров, а добавление девятого уже способно ухудшить работу сервера.
2.5. Выбор архитектуры 53 Всегда необходимо отличать работу и ресурсы, задействованные в управлении па- лельными аппаратными средствами, от работы, направленной на управление па- аллельно выполняемыми процессами и потоками в ПО. Предел числа программных пооцессов может быть достигнут задолго до того, как будет достигнуто оптимальное количество процессоров или компьютеров. И точно так же можно наблюдать снижение эффективности оборудования еще до достижения оптимального количества параллельно выполняемых задач. 2.5. Выбор архитектуры Существует множество архитектурных решений, которые поддерживают параллелизм. Архитектурное решение можно считать корректным, если оно соответствует декомпозиции работ (work breakdown structure— WBR) программного обеспечения (ДРПО). Параллельные и распределенные архитектуры могут быть самыми разнообразными. В то время как некоторые распределенные архитектуры прекрасно работают в Web-среде, они практически обречены на неудачу в среде с реальным масштабом времени. Например, распределенные архитектуры, которые рассчитаны на длинные временные задержки, вполне приемлемы для Web-среды и совершенно неприемлемы для многих сред реального времени. Достаточно сравнить распределенную обработку данных в Web-ориентированной системе функционирования электронной почты с распределенной обработкой данных в банкоматах, или автоматических кассовых машинах (automated teller machine— ATM). Задержка (время ожидания), которая присутствует во многих почтовых Web-системах, была бы попросту губительной для таких систем реального времени, как банкоматы. Одни распределенные архитектуры (имеются в виду некоторые асинхронные модели) справляются с временными задержками лучше, чем другие. Кроме того, необходимо самым серьезным образом подходить к выбору соответствующих архитектур параллельной обработки данных. Например, методы векторной обработки данных наилучшим образом подходят для решения определенных математических задач и проблем имитационного моделирования, но они совершенно неэффективны в применении к мультиагентным алгоритмам планирования. Распространенные архитектуры ПО, которые поддерживают параллельное и распределенное программирование, показаны в табл. 2.2. Четыре базовые модели, перечисленные в табл. 2.2, и их вариации обеспечивают основу для всех параллельных типов архитектур (т.е. объектно-ориентированного, агентно-ориентированного и "классной доски"), которые рассматриваются в этой книге. РазработчикахМ ПО необходимо подробно ознакомиться с каждой из этих моделей и их приложением к параллельному и распределенному программированию. Мы считаем своИхМ долгом предоставить читателю введение в эти модели и дать библиографические сведения по материалам, которые позволят найти о них более детальную информацию. В каждой работе или при решении проблемы лучше всего искать естественный или присущий им параллелизм, а выбранный тип архитектуры Должен максимально соответствовать этому естественному параллелиЗхМу. Например, Параллелизм в решении, возможно, лучше описывать с помощью симметричной модели, или модели сети с равноправными узлами (peer-to-peer model), в которой все сотрудники (исполнители) считаются равноправными, в отличие от несимметричной модели "управляющий/рабочий", в которой существует главный (ведущий) процесс, Управляющий всеми остальными процессами как подчиненными.
54 Глава 2. Проблемы параллельного и распределенного программирования Таблица 2.2. Распространенные архитектуры ПО, используемые для поддержки параллельного и распределенного программирования Модель Архитектура Распределенное Параллельное программирование программирование Модель ведущего узла, именуемая также: • главный/подчиненный; • управляющий/рабочий; • клиент/сервер Модель равноправных узлов Векторная или конвейерная (поточная)обработка Дерево с родительскими и дочерними элементами Главный узел управляет задачами, т.е. контролирует их выполнение и передает работу подчиненным задачам Все задачи, в основном, имеют одинаковый ранг, и работа между ними распределяется равномерно Один исполнительный узел соответствует каждому элементу массива (вектора) или шагу конвейера Динамически генерируемые исполнители в отношении типа "родитель/потомок". Этот тип архитектуры полезно использовать в алгоритмах следующих типов: • рекурсия; • "разделяй и властвуй"; •И/ИЛИ • древовидная обработка S • S 2.6. Различные методы тестирования и отладки При тестировании последовательной программы разработчик может отследить ее логику в пошаговом режиме. Если он будет начинать тестирование с одних и тех же данных при условии, что система каждый раз будет пребывать в одном и том же состоянии, то результаты выполнения программы или ее логические цепочки будут вполне предсказуемыми. Программист может отыскать ошибки в программе, используя соответствующие входные данные и исходное состояние программы, путем проверки ее логики в пошаговом режиме. Тестирование и отладка в последовательной модели зависят от степени предсказуемости начального и текущего состояний программы, определяемых заданными входными данными. С параллельным и распределенньш программированием все обстоит иначе. Здесь трудно воспроизвести точный контекст параллельных или распределенных задач из- за разных стратегий планирования, применяемых в операционной системе, динамически меняющейся рабочей нагрузки, квантов процессорного времени, приоритетов процессов и потоков, времени ых задержек при их взаимодействии и собственно
2.7. Связь между параллельным и распределенным проектами 55 шолнении, а также различных случайных изменений ситуаций, характерных для оаллельных или распределенных контекстов. Чтобы воспроизвести точное состоя- в котором находилась среда при тестировании и отладке, необходимо воссоздать аждую задачу, выполнением которой была занята операционная система. При этом лолжен быть известен режим планирования процессорного времени и точно воспроизведены состояние виртуальной памяти и переключение контекстов. Кроме того, следует воссоздать условия возникновения прерываний и формирования сигналов, а в некоторых случаях— даже рабочую нагрузку сети. При этом нужно понимать, что и сами средства тестирования и отладки оказывают немалое влияние на состояние среды. Это означает, что создание одинаковой последовательности событий для тестирования и отладки зачастую невозможно. Необходимость воссоздания всех перечисленных выше условий обусловлено тем, что они позволяют определить, какие процессы или потоки следует выполнять и на каких именно процессорах. Смешанное выполнение процессов и потоков (в некоторой неудачной "пропорции") часто является причиной возникновения взаимоблокировок, бесконечных отсрочек, "гонки" данных и других проблем. И хотя некоторые из этих проблем встречаются и в последовательном программировании, они не в силах зачеркнуть допущения, сделанные при построении последовательной модели. Тот уровень предсказуемости, который имеет место в последовательной модели, недоступен для параллельного программирования. Это заставляет разработчика овладевать новыми тактическими приемами для тестирования и отладки параллельных и распределенных программ, а также требует от него поиска новых способов доказательства корректности его програМхМ. 2.7. Связь между параллельным и распределенным проектами При создании документации на проектирование параллельного или распределенного ПО необходимо описать декомпозицию работ и их синхронизацию, а также взаимодействие между задачами, объектами, процессами и потоками. При этом проектировщики должны тесно контактировать с разработчиками, а разработчики — с теми, кто будет поддерживать систему и заниматься ее администрированием. В идеале это взаимодействие должно осуществляться по действующим стандартам. Однако найти единый язык, понятный всем сторонам и позволяющий четко представить мультипара- Дигматическую природу всех этих систем, — труднодостижимая цель. Мы остановили свой выбор на языке UML (Unified Modeling Language — унифицированный язык моделирования). В табл. 2.3 перечислено семь UML-диаграмм, которые часто используются При создании многопоточных, параллельных или распределенных программ. Семь диаграмм, перечисленных в табл. 2.3, представляют собой лишь подмножество диаграмм, которые предусмотрены языком UML, но они наиболее всего подходят к том)-, что мы хотим подчеркнуть в наших проектах параллельного ПО. В частности, UML-диаграМхМЫ деятельности, развертывания и состояний весьма полезны для °Писания взаимодействующего поведения параллельной и распределенной подсистем обработки данных. Поскольку UML— это фактический стандарт, используемый пРи создании взаимодействующих объектно-ориентированных и агентно- °рИентированных проектов, при изложении материала в этой книге мы опираемся иМенно на него. Описание обозначений и символов, используемых в перечисленных вЬ1Ше диаграммах, содержится в приложении А.
56 Глава 2. Проблемы параллельного и распределенного программирования Таблица 2.3. UML-диаграммы, используемые при создании многопоточных, параллельных или распределенных программ UML-диаграммы Описание Диаграмма (видов) деятельности Диаграмма взаимодействия Диаграмма (параллельных) состояний Диаграмма последовательностей Диаграмма сотрудничества Диаграмма развертывания (внедрения) Диаграмма компонентов Разновидность диаграммы состояний, в которой большинство состояний (или все) представляют виды деятельности, а большинство переходов (или все) активизируются при выполнении некоторого действия в исходных состояниях Тип диаграммы, которая отображает взаимодействие между объектами. Взаимодействия описываются в виде сообщений, которыми они обмениваются. К диаграммам взаимодействия относятся диаграммы сотрудничества, диаграммы последовательностей и диаграммы (видов) деятельности Диаграмма, которая показывает последовательность преобразований объекта в процессе его реакции на события. При использовании диаграммы параллельных состояний эти преобразования могут происходить в течение одного и того же интервала времени Диаграмма взаимодействия, в которой отображается организация структуры объектов, принимающих или отправляющих сообщения (с акцентом на упорядочении сообщений по времени) Диаграмма взаимодействия, в которой отображается организация структуры объектов, принимающих или отправляющих сообщения (с акцентом на структурной организации) Диаграмма, которая показывает динамическую конфигурацию узлов обработки, аппаратных средств и программных компонентов в системе Диаграмма взаимодействия, в которой отображается организация физических модулей программного кода (пакетов) в системе и зависимости между ними 2.8. Резюме При создании параллельного и распределенного ПО разработчиков ожидает множество проблем. Поэтому при проектировании ПО им необходимо искать новые архитектурные подходы и технологии. Многие фундаментальные допущения, которых придерживались разработчики при построении последовательных моделей программирования, совершенно неприемлемы в области создания параллельного и распределенного ПО. В программах, включающих элементы параллелизма, программисты чаще всего сталкиваются со следующими четырьмя проблемами координации: "гонка" данных, бесконечная отсрочка, взаимоблокировка и проблемы синхронизации при взаимодействии задач. Наличие параллелизма и распределения оказывает огромное влияние на все аспекты жизненного цикла разработки ПО: начиная эскизным проектом и заканчивая тестированием готовой системы и подготовкой документации. В этой книге мы представляем архитектурные подходы к решению многих упомянутых проблем, используя преимущества мультипарадигматических средств языка C++, которые позволяют справиться со сложностью параллельных и распределенных программ.
РАЗБИЕНИЕ С++- ПР0ГРАММ НА МНОЖЕСТВО ЗАДАЧ В этой главе... 3.1. Определение процесса 3.2. Анатомия процесса 3.3. Состояния процессов 3.4. Планирование процессов 3.5. Переключение контекста 3.6. Создание процесса 3.7. Завершение процесса 3.8. Ресурсы процессов 3.9. Асинхронные и синхронные процессы 3.10. Разбиение программы на задачи 3.11. Резюме
J~1/\ZJSJZJ Коль выполнение параллельных процессов возможно на более низком (нейронном) уровне, то на символическом уровне мышление человека с принципиальной точки зрения можно рассматривать как последовательную машину, которая использует временно создаваемые последовательности процессов, выполнение которых длится сотни миллисекунд. — Герберт Саймон (Herbert A. Simon), The Machine As Mind Параллельность в С++-программе достигается путем ее (программы) разложения на несколько процессов или потоков. Несмотря на существование различных вариантов организации логики С++-программы (например, с помощью объектов, функций или обобщенных шаблонов), под параллелизмом все же понимается использование множества процессов и потоков. Прочитав эту главу, вы поймете, что такое процесс и как С++-программы можно разделить на несколько процессов. 3.1. Определение процесса Процесс (process) — это некоторая часть (единица) работы, создаваемая операционной системой. Важно отметить, что процессы и программы — необязательно эквивалентные понятия. Программа может состоять из нескольких процессов. В некоторых ситуациях процесс может быть не связан с конкретной программой. Процессы — это артефакты операционной системы, а программы — это артефакты разработчика. Такие операционные системы, как UNIX/Linux позволяют управлять сотнями или даже тысячами параллельно загружаемых процессов.
3.1. Определение процесса 59 Чтобы некоторую часть работы можно было назвать процессом, она должна иметь оесное пространство, назначаемое операционной системой, и идентификатор, или идентификационный номер (^процесса). Процесс должен обладать определенным статусом и иметь свой элемент в таблице процессов. В соответствии со стандартом POSIX он должен содержать один или несколько потоков управления, выполняющихся в рамках его адресного пространства, и использовать системные ресурсы, требуемые для этих потоков. Процесс состоит из множества выполняющихся инструкций, размещенных в адресном пространстве этого процесса. Адресное пространство процесса распределяется между инструкциями, данными, принадлежащими процессу, и стеками, обеспечивающими вызовы функций и хранение локальных переменных. 3.1.1. Два вида процессов При выполнении процесса операционная система назначает ему некоторый процессор. Процесс выполняет свои инструкции в течение некоторого периода времени. Затем он выгружается, освобождая процессор для другого процесса. Планировщик операционной системы переключается с кода одного процесса на код другого, предоставляя каждому процессу шанс выполнить свои инструкции. Различают пользовательские процессы и системные. Процессы, которые выполняют системный код, называются системными и применяются к системе в целом. Они занимаются выполнением таких служебных задач, как распределение памяти, обмен страницами между внутренним и вспомогательным запоминающими устройствами, контроль устройств и т.п. Они также выполняют некоторые задачи "по поручению" пользовательских процессов, например, делают запросы на ввод-вывод данных, выделяют память и т.д. Пользовательские процессы выполняют собственный код и иногда обращаются к системным функциям. Выполняя собственный код, пользовательский процесс пребывает в пользовательском режиме (user mode). В пользовательском режиме процесс не может выполнять определенные привилегированные машинные команды. При вызове системных функций (например read(), write () или open ()) пользовательский процесс выполняет инструкции операционной системы. При этом пользовательский процесс "удерживает" процессор до тех пор, пока не будет выполнен системный вызов. Для выполнения системного вызова процессор обращается к ядру операционной системы. В это время о пользовательском процессе говорят, что он пребывает в привилегированном режиме, или режиме ядра (kernel mode), и не может быть выгружен никаким другим пользовательским процессом. 3.1.2. Блок управления процессами Процессы имеют характеристики, используемые для идентификации и определения их поведения. Ядро поддерживает необходимые структуры данных и предоставляет системные функции, которые дают возможность пользователю получить доступ к этой информации. Некоторые данные хранятся в блоках управления процессами (process control block— PCB), или БУЛ. Данные, хранимые в БУП-блоках, описывают Процесс с точки зрения потребностей операционной системы. С помощью этой информации операционная система может управлять каждым процессом. Когда операци- °нная система переключается с одного процесса на другой, она сохраняет текущее со- стояние выполняющегося процесса и его контекст в области сохранения БУП-блока, т°бы надлежащим образом возобновить выполнение этого процесса в следующий раз,
60 Глава 3. Разбиение С++-программ на множество задач когда ему снова будет выделен центральный процессор (ЦП). БУП-блок считывается и обновляется различными модулями операционной системы. Модули "отвечают" за контроль производительности операционной системы, планирование, распределение ресурсов и доступ к механизму обработки прерываний и/или модифицируют БУП-блок. Блок БУП содержит следующую информацию: • текущее состояние и приоритет процесса; • идентификатор процесса, а также идентификаторы родительского и сыновнего процессов; • указатели на выделенные ресурсы; • указатели на область памяти процесса; • указатели на родительский и сыновний процесс; • процессор, занятый процессом; • регистры управления и состояния; • стековые указатели. Среди данных, содержащихся в БУП-блоке, есть такие, которые "отвечают" за управление процессом, т.е. отражают его текущее состояние и приоритет, указывают на БУП-блоки родительского и сыновнего процессов, а также выделенные ресурсы и память. Кроме того, этот блок включает информацию, связанную с планированием, привилегиями процессов, флагами, сообщениями и сигналами, которыми обмениваются процессы (имеется в виду межпроцессное взаимодействие— mter/?rocess communication, или IPC). С помощью информации, связанной с управлением процессами, операционная система может координировать параллельно выполняемые процессы. Стековые указатели и содержимое регистров пользователя, управления и состояния содержат информацию, связанную с состоянием процессора. При выполнении процесса соответствующая информация размещается в регистрах ЦП. При переключении операционной системы с одного процесса на другой вся информация из этих регистров сохраняется. Когда процесс снова получает ЦП во "временное пользование", ранее сохраненная информация может быть восстановлена. Есть еще один вид информации, который связан с идентификацией процесса. Имеется в виду идентификатор процесса (id), или PID, и идентификатор родительского процесса (PPID). Эти идентификационные номера (которые представлены положительными целочисленными значениями) уникальны для каждого процесса. 3.2. Анатомия процесса Адресное пространство процесса делится на три логических раздела: текстовый (для кода программы), информационный (для данных программы) и стековый (для стеков программы). Логическая структура процесса показана на рис. 3.1. Текстовый раздел (расположенный в нижней части адресного пространства) содержит подлежащие выполнению инструкции, которые называются программным кодом. Раздел данных (расположенный над текстовым разделом) содержит инициализированные глобальные, внешние и статические переменные процесса. Раздел стеков содержит локально создаваемые переменные и параметры, передаваемые функциям. Поскольку процесс может
3.2. Анатомия процесса 61 вызывать как системные функции, так и функции, определенные пользователем, в стековом разделе поддерживаются два стека: стек пользователя и стек ядра. При вызове функции создается стековый фрейм функции, который помещается в стек пользователя или стек ядра в зависимости от того, в каком режиме пребывает процесс в данный момент: в пользовательском или привилегированном (режиме ядра). Стековый раздел имеет тенденцию расти в направлении раздела данных. При выходе из функции ее стековый фрейм извлекается из стека. Разделы кода, данных и стеков, а также блок управления процессом образуют часть того, из чего складывается образ процесса (process image). БУП ОБРАЗ ПРОЦЕССА ИДЕНТИФИКАЦИЯ ПРОЦЕССА ИНФОРМАЦИЯ 0 СОСТОЯНИИ ПРОЦЕССА ИНФОРМАЦИЯ ОБ УПРАВЛЕНИИ ПРОЦЕССОМ РАЗДЕЛ СТЕКОВ Стек ядра Стек пользователя РАЗДЕЛ ДАННЫХ • инициализированные глобальные переменные • внешние переменные « статические переменные РАЗДЕЛ КОДА • код программы Рис. 3.1. Адресное пространство процесса делится на три логических раздела: текстовый, информационный и стековый. Так выглядит логическая структура процесса Адресное пространство процесса виртуально. Применение виртуальной памяти позволяет отделить адреса, используемые в текущем процессе, от адресов, реально доступных во внутренней памяти. Тем самым значительно увеличивается задействованное пространство адресов памяти по сравнению с реально доступными адресами. Разделы виртуального адресного пространства процесса представляют собой смежные блоки памяти. Каждый такой раздел и физическое адресное пространство разделены на участки памяти, именуемые страницами. У каждой страницы есть уникальный номер страничного шока (page frame number). В качестве индекса для входа в таблицы страничных блоков Ipage frame table) используется номер виртуального страничного блока. Каждый элемент таб- ицы страничных блоков содержит номер физического страничного блока, что позволяет установить соответствие между виртуальными и физическими страничными блоками. Это °ответствие отображено на рис. 3.2. Как видите, виртуальное адресное пространство епрерывно, но устанавливаемое с его помощью соответствие физическим страницам е является упорядоченным. Другими словами, при последовательных виртуальных ад- Р сах соответствующие им физические страницы не будут последовательными.
62 Глава 3. Разбиение С++-программ на множество задач Несмотря на то что виртуальное адресное пространство каждого процесса защищено, т.е. приняты меры по предотвращению доступа к нему со стороны другого процесса, текстовый раздел процесса может совместно использоваться несколькими процессами. На рис. 3.2 также показано, как два процесса могут разделять один и тот же программный код. При этом в элементах таблиц страничных блоков обоих процессов хранится один и тот же номер физического страничного блока. Как показано на рис. 3.2, виртуальный страничный блок с номером 0 процесса А соответствует физическому страничному блоку с номером 5, что также справедливо и для виртуального страничного блока с номером 2 процесса В. ВИРТУАЛЬНОЕ АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В ТАБЛИЦЫ СТРАНИЧНЫХ БЛОКОВ ПРОЦЕССА В ФИЗИЧЕСКАЯ ПАМЯТЬ НСБЗ НСБ35 НСБ18 НСБ13 НСБ5 НСБ11 I НВСБ 8 НВСБ 7 НВСБ 6 НВСБ 5 НВСБ 4 НВСБЗ 1 НВСБ 2 НВСБ 1 НВСБ 0 НСБ 4 НС6 5 НСБ6 НСБ 7 НСБ 8 НСБ 9 НВСБ 8 I НВСБ 7 I НВСБ 6 НВСБ 5 I НВСБ 4 | НВСБ 3 | НВСБ 2 НВСБ 1 НВСБ 0 1 НСБ 5 НСБ 16 НСБЗ НСБ 26 НСБ1 ТАБЛИЦЫ СТРАНИЧНЫХ БЛОКОВ ПРОЦЕССА А ВИРТУАЛЬНОЕ АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А РАЗДЕЛ СТЕКОВ h РАЗДЕЛ ДАННЫХ РАЗДЕЛ КОДА Рис. 3.2. Соответствие последовательных виртуальных страничных блоков страницам физической памяти (НСБ— номер страничного блока; НВСБ— номер виртуального страничного блока)
3.3. Состояния процессов 63 Чтобы операционная система могла управлять всеми процессами, хранимыми во внутренней памяти, она создает и поддерживает таблицы процессов (process table). В действительности операционная система содержит отдельные таблицы для всех объектов, которыми она управляет. Следует иметь в виду, что операционная система управляет не только процессами, но и всеми ресурсами компьютера, т.е. устройствами ввода-вывода, памятью и файлами. Часть памяти, устройств и файлов управляется от имени пользовательских процессов. Эта информация отмечена в БУП-блоках как ресурсы, выделенные процессу. Таблица процессов должна иметь соответствующую структуру для каждого образа процесса в памяти. Каждая такая структура содержит идентификаторы (id) самого процесса и родительского процесса, идентификаторы реального и эффективного пользователей, идентификатор группы, список подвешенных сигналов, местоположение текстового, информационного и стекового разделов, а также текущее состояние процесса. Если операционной системе нужен доступ к определенному процессу, в таблице процессов разыскивается информация о нем, а затем в памяти размещается его образ (рис. 3.3). Память < Файлы < Устройства < Процессы < >— 1—► г* > ► Таблицы использования памяти Таблицы файлов Таблицы устройств ввода-вывода PID PPID STAT 19 45 12 30 6 90 15 9 D S R S Свободный элемент таблицы 42 30 S Образ j процесса : cPID^ 19! Образ : процесса ; с PiD - 45 ! I Образ процесса с РГО = 42 ТАБЛИЦЫ ПРОЦЕССОВ Рис. 3.3. Операционная система управляет таблицами. Каждая структура в массиве таблиц процессов представляет процесс в системе 3.3. Состояния процессов Во время выполнения процесса его состояние изменяется. Под состоянием процесса подразумевается его текущий режим, или статус. В среде UNIX процесс может пребывать в одном из следующих состояний: • выполнения; работоспособности (готовности);
64 Глава 3. Разбиение С++-программ на множество задач • "зомби"; • ожидания (блокирования); • останова. Состояние процесса меняется при определенных обстоятельствах, создаваемых существованием процесса или операционной системы. Под сменой состояний, или переходом из одного состояния в другое, понимают обстоятельства, которые заставляют процесс изменить свое состояние. На рис. 3.4 отображена диаграмма состояний для среды UNIX. Диаграмма состояний содержит узлы и направленные ребра, соединяющие эти узлы. Каждый узел представляет состояние процесса, а направленные ребра между узлами — переходы из одного состояния в другое. Возможные смены состояний (с их кратким описанием) перечислены в табл. 3.1. На рис. 3.4 и в табл. 3.1 показано, что между состояниями разрешены только определенные переходы. Например, между состояниями готовности и выполнения существует переход (ребро диаграммы), а между состояниями ожидания и выполнения— нет. Это означает, что возможны обстоятельства, заставляющие процесс перейти из состояния готовности в состояние выполнения, но нет обстоятельств, которые могут заставить процесс перейти в состояние выполнения из состояния ожидания. ОСТАНОВЛЕН Выдан сигнал/ Выдан сигнал Выгрузка Вх°Д ^/ ГОТОВ ^1 (работоспособен) Произошло * событие или\ завершена операция ввода-вывода Загрузка ВЫПОЛНЯЕТСЯ Выход J<oHei4 кванта времени Ожидание события или у завершения операции ввода-вывода ОЖИДАЕТ ] " Прекращение k Выход "ЗОМБИ" Рис. 3.4. Состояния процессов и переходы между ними в средах UNIX/Linux Когда процесс только создается, он готов к выполнению своих инструкций, но должен ожидать "своего часа" до тех пор, пока не освободится процессор. Каждому процессу единолично разрешается использовать процессор в течение дискретного временного интервала, именуемого квантом времени (time slice). Процессы, ожидающие использования процессора, "занимают" очередь, т.е. помещаются в очереди готовых процессов. Только из таких очередей планировщик выбирает процесс, который будет использовать процессорное время. Процессы, находящиеся в очередях готовых процессов, пребывают в состоянии работоспособности. Когда процессор становится доступным,
3.3. Состояния процессов 65 Таблица 3.1. Переходы процессов из одного состояние в другое Переходы между состояниями Описание ГОТОБЫЙ-^ВЫПОЛНЯЮЩИИСЯ (загрузка) выполняющийся^готовый (конец кванта времени) ВЫПОЛНЯЮЩИЙСЯ->ГОТОВЫЙ (досрочная выгрузка) выполняющийся-» ОЖИДАЮЩИЙ (блокировка) ОЖИДАЮЩИЙ->ГОТОВЫЙ (разблокировка) ВЫПОЛНЯЮЩИЙСЯ-» ОСТАНОВЛЕННЫЙ ОСТАНОВЛЕННЫЙ->ГОТОВЫЙ ВЫПОЛНЯЮЩИЙСЯ->"ЗОМБИ" "ЗОМБИ"->ВЫХОД ВЫПОЛНЯЮЩИЙСЯ->ВЫХОД Процесс назначается процессору Квант времени процесса, который назначен процессору, истек. Процесс возвращается назад в очередь готовых процессов Процесс выгружается до истечения его кванта времени. (Это возможно в случае, если стал готовым процесс с более высоким приоритетом.) Выгруженный процесс помещается назад в очередь готовых процессов Процесс отказывается от процессора до истечения его кванта времени. Процессу, возможно, нужно подождать наступления некоторого события, или он вызывает системную функцию, например, делает запрос на ввод-вывод данных. Процесс помещается в очередь ждущих процессов Событие, наступления которого ожидал процесс, произошло, или завершилось выполнение системной функции, например, удовлетворен запрос на ввод-вывод данных Процесс отказывается от процессора из-за получения им сигнала останова Процесс получил сигнал продолжать и возвращается назад в очередь готовых процессов Процесс прекращен и ожидает, пока родительский процесс не извлечет из таблицы процессов его статус завершения Родительский процесс извлекает из таблицы процессов статус завершения и процесс-зомби покидает систему Процесс завершен, но он покидает систему после того как родительский процесс извлечет из таблицы процессов его статус завершения Диспетчер (dispatcher) назначает его работоспособному (готовому) процессу, который занимает его в течение своего кванта времени. По истечении этого кванта времени процесс покидает процессор, независимо от того, выполнил он все свои инструкции или нет. Этот процесс снова помещается в очередь готовых процессов (как в "зал ожидания") ожидать следующего сеанса работы процессора. Тем временем из очереди выбирается новый процесс, котором)7 выделяется его квант процессорного времени. Системные процессы не выгружаются, т.е., "заполучив" процессор, они выполняются до полного завершения. Если квант времени еще не исчерпан, но процесс не в состоянии Продолжить выполнение, он может добровольно отказаться от процессорного времени.
66 Глава 3. Разбиение С++-программ на множество задач Причины отказа могут быть разными. Например, процесс может сделать запрос на получение доступа к устройству ввода-вывода, вызвав системную функцию, или ему необходимо подождать освобождения объекта (переменной) синхронизации. Процессы, которые не могут продолжать выполнение из-за необходимости ожидать некоторого события, "засыпают", т.е. переходят в состояние ожидания. Они помещаются в очередь ждущих процессов. После наступления ожидаемого ими события они удаляются из этой очереди и возвращаются в очередь готовых процессов. Текущий процесс, т.е. процесс, занимающий процессорное время, может быть лишен его еще до исчерпания кванта времени, если заявит о своей готовности процесс с более высоким приоритетом (например, системный процесс). Выгруженный досрочно процесс сохраняет статус работоспособного и поэтому снова помещается в очередь готовых процессов. Выполняющийся процесс может получить сигнал остановить выполнение. Состояние останова отличается от состояния ожидания, потому что при этом не был исчерпан квант времени и процесс не делал никакого системного запроса. Процесс мог получить сигнал остановиться либо по причине пребывания в режиме отладки, либо из- за возникновения особой ситуации в системе. Получив сигнал остановиться, процесс переходит из состояния выполнения в состояние останова. Позже процесс может быть "разбужен" или ликвидирован. Выполнив все свои инструкции, процесс покидает систему. В этом случае процесс удаляется из таблицы процессов, его БУП-блок разрушается, и все занимаемые им ресурсы освобождаются и возвращаются в системный пул доступных ресурсов. Процесс, который неспособен продолжать выполнение, но при этом не может выйти из системы, считается "зомбированным". Зомбированный процесс не использует никаких системных ресурсов, но сохраняет свою структуру в таблице процессов. Если в таблице процессов окажется слишком много зомбированных процессов, это негативно отразится на производительности системы и может вызвать ее перезагрузку. 3.4. Планирование процессов Если готовых к выполнению процессов больше одного, планировщик должен определить, какой из них первым назначить процессору. С этой целью планировщик поддерживает структуры данных, которые позволяют наиболее эффективным образом распределять между процессами процессорное время. Каждый процесс получает класс (тип) приоритета и размещается в соответствующей очереди вместе с другими работоспособными процессами того же приоритетного класса. Поэтому существует несколько приоритетных очередей, которые представляют различные классы приоритетов, используемые системой. Эти приоритетные очереди упорядочиваются и помещаются в массив распределения, именуемый также многоуровневой приоритетной очередью (multilevel priority queue), показанной на рис. 3.5. Каждый элемент этого массива связан с конкретной приоритетной очередью. Для выполнения процессором планировщик назначает тот процесс, который стоит в головной части непустой очереди, имеющей самый высокий приоритет. Приоритеты могут быть динамическими или статическими. Однажды установленный статический приоритет процесса изменить нельзя, а динамические — можно. Процессы с самым высоким приоритетом могут монополизировать использование процессора. Если же приоритет процессора динамический, то его начальный уровень может быть заменен более высоким значением, в результате чего такой процесс будет
3.4. Планирование процессов 67 пе еден в очередь с более высоким приоритетом. Кроме того, процесс, который 1 полИзирует процессор, может получить более низкий приоритет, или же другие иессы могут получить более высокий приоритет, чем процесс-монополист. В сре- UNIX/Linux для уровней приоритетов предусмотрен диапазон от -20 до 19. Чем е значение уровня, тем ниже приоритет процесса. При назначении приоритета пользовательскому процессу следует учитывать, на что нно эТОТ процесс тратит большую часть времени. Одни процессы отличаются повышенной интенсивностью использования процессорного времени (они используют процессор в течение всего кванта процессорного времени). У других же большая часть времени уходит на ожидание выполнения операций ввода-вывода или наступления некоторых иных событий. Если такой процесс готов к использованию процессора, ему следует немедленно предоставить процессор, чтобы он мог сделать следующий запрос к устройствам ввода-вывода. Процессы, которые взаимодействуют между собой, могут требовать довольно высокий приоритет, чтобы рассчитывать на приличное время реакции. Системные процессы имеют более высокий приоритет, чем пользовательские. Процессор PID71 i выполняется к ДИСПЕТЧЕР 2 1 0 -1 -2 -3 • PID12 PID90 |РЮ71 L PID17 PID43 PID35 PID10 PID63 PID50 Рис. 3.5. Многоуровневая приоритетная очередь (массив распределения), каждый элемент которой указывает на очередь готовых процессов с одинаковым уровнем приоритета 3.4.1. Стратегия планирования Процессы размещаются в приоритетных очередях в соответствии со стратегией фланирования. В системах UNIX/Linux используются две стратегии планирования: '1FO (сокр. от First In First Out, т.е. первым прибыл, первым обслужен) и RR (сокр. от
68 Глава 3. Разбиение С++-программ на множество задач round-robin, т.е. циклическая). Схема действия стратегии FIFO показана на рис. 3.6, а. При использовании стратегии FIFO процессы назначаются процессору в соответствии со временем поступления в очередь. После истечения кванта времени процесс помещается в начало (головную часть) своей приоритетной очереди. Когда ждущий процесс становится работоспособным (готовым к выполнению), он помещается в конец своей приоритетной очереди. Процесс может вызвать системную функцию и отказаться от процессора в пользу другого процесса с таким же уровнем приоритета. Такой процесс также будет помещен в конец своей приоритетной очереди. а) FIFO-планирование Выход Процессор1 PID71 выполняется назначен. ОЧЕРЕДЬ ГОТОВЫХ ПРОЦЕССОВ Прибыл первъ!М_ Прибыл последним PID71 L —ж — Конец кванта времени Запрос на операцию ввода-вывода PID90 PID43 PID10 PID50 ОЧЕРЕДЬ ЖДУЩИХ ПРОЦЕССОВ I Операция _' ввода-вывода завершена б) RR-планирование ОЧЕРЕДЬ ГОТОВЫХ ПРОЦЕССОВ Выход Процессор1 РЮ71 выполняется Прибыл первым Прибыл последним назначен ЦП I Запрос на операцию ввода-вывода ОЧЕРЕДЬ ЖДУЩИХ ПРОЦЕССОВ Операция ввода-вывода завершена Рис. 3.6. Схемы действия FIFO- и RR-стратегий планирования При использовании стратегии FIFO процессы назначаются процессору в соответствии со временем поступления в очередь. При использовании стратегии RR процессы назначаются процессору по правилам FIFO-стратегии, но с одним отличием: после истечения кванта времени процесс помещается не в начало, а в конец своей приоритетной очереди
3.4. Планирование процессов 69 В соответствии с циклической стратегией планирования (RR) все процессы счи- аются равноправными (см. рис. 3.6, б). RR-планирование совпадает с FIFO-плани- ованием с одним исключением: после истечения кванта времени процесс помещается не в начало, а в конец своей приоритетной очереди, и процессору назначается следующий (по очереди) процесс. 3.4.2. Использование утилиты ps Утилита ps генерирует отчет, который содержит статистические данные о выполнении текущих процессов. Эту информацию можно использовать для контроля за их состоянием. В табл. 3.8 перечислены общие заголовки и описаны выходные данные, генерируемые утилитой ps для сред Solaris/Linux. В любой многопроцессорной среде утилита ps успешно применяется для мониторинга состояния процессов, степени использования ЦП и памяти, приоритетов и времени запуска текущих процессов. Ниже приведены командные опции, которые позволяют управлять информацией, содержащейся в отчете (с их помощью можно уточнить, что именно и какие процессы вас интересуют). В среде Solaris по умолчанию (без командных опций) отображается информация о процессах с тем же идентификатором эффективного пользователя и управляющим терминалом инициатора вызова. В среде Linux по умолчанию отображается информация о процессах, id пользователя которых совпадает с id инициатора запуска. В обеих средах в этом случае отображаемая информация, ограниченная следующими составляющими: PID, TTY, TIME и COMMAND. Перечислим опции, которые позволяют получить информацию о нужных процессах. -t term Список процессов, связанных с терминалом, заданным значением term ~е Все текущие процессы "а (Linux) Все процессы с терминалом tty за исключением лидеров сеанса (Solaris) Большинство часто запрашиваемых процессов за исключением лидеров группы и процессов, не связанных с терминалом Все текущие процессы за исключением лидеров сеанса (Linux) Все процессы, связанные с данным терминалом (Linux) Все процессы, включая процессы остальных пользователей (Linux) Только выполняющиеся процессы -d Т а г Таблица 3.2. Общие заголовки, используемые для утилиты ps в средах Solaris/Linux Заголовки Описание Пользовательское имя владельца процесса ID процесса ID родительского процесса ID лидирующего процесса в группе ID лидера сеанса
70 Глава 3. Разбиение С++-программ на множество задач Окончание таил. 3.2 Описание %CPU Коэффициент использования времени ЦП (в процентах) процессом в течение последней минуты RSS Объем реального ОЗУ, занимаемый процессом в данный момент (в Кбайт) %МЕМ Коэффициент использования реального ОЗУ процессом в течение последней минуты SZ Размер виртуальной памяти, занимаемой данными и стеком процесса (в Кбайт или страницах) WCHAN Адрес события, в ожидании которого процесс пребывает в состоянии ожидания COMMAND Имя команды и аргументы CMD ТТ, TTY Управляющий терминал процесса S, STAT Текущее состояние процесса TIME Общее время ЦП, используемое процессом (HH:MM:SS) STIME, START Время или дата старта процесса N1 Фактор уступчивости процесса PRI Приоритет процесса С, СР Коэффициент краткосрочного использования ЦП для вычисления планировщиком значения PRI ADDR Адрес памяти, выделенной процессу LWP ID потока NLWP Количество потоков Синопсис (Linux) ps -[опции в стиле [опции в стиле --[GNU-опции в (Solaris) ps [-aAdeflcjLPy][ [-G grouplist][ Unix98] BSD] длинном формате] -о format][-t -p proclist][- termlist][-u -g pgrplist] [- userlist] -s sidlist] J В следующий список включены командные опции, которые используются для управления отображаемой информацией о процессах: - f полные распечатки -1 в длинном формате -j в формате задания Приведем пример использования утилиты ps в средах Solaris/Linux: ps -f По этой команде будет отображена полная информация о процессах, которая выводится по умолчанию в каждой среде. На рис. 3.7 показан результат выполнения этой команды Заголовки
3.4. Планирование процессов 71 де Solaris. Командные опции можно использовать тандемом (одна за другой). На с 3 7 также показан результат совместного использования опций -1 и - f в среде Solaris: ps -If Командная опция 1 позволяет отобразить дополнительные заголовки: F, S, С, PRI, N1, ADDR и WCHAN. При использовании командной опции Р отображается заголовок PSR, означающий номер процессора, которому назначается (или за которым закрепляется) процесс. //SOLARIS $ ps -f UID PID PPID C STIME TTY TIME CMD cameron 2214 2212 0 21:03:35 pts/12 0:00 -ksh cameron 2396 2214 2 11:55:49 pts/12 0:01 nedit $ ps -If F s UID PID PPID С PRI N1 ADDR SZ WCHAN STIME TTY TIME CMD 8 S cameron 2214 2212 0 51 20 70e80f00 230 70e80f6c 21:03:35 pts/12 0:00 -ksh 8 S cameron 2396 2214 1 53 24 70d747b8 843 70152aba 11:55:49 pts/12 0:01 nedit Рис. 3.7. Результат выполнения команд ps -f и ps -lfB среде Solaris На рис. 3.8 показан результат выполнения утилиты ps с использованием командных опций Тих в среде Linux. Данные, выводимые с помощью заголовков %CPU, %MEM и STAT, отображаются для процессов. В многопроцессорной среде с помощью этой информации можно узнать, какие процессы являются доминирующими с точки зрения использования времени ЦП и памяти. Заголовок STAT отображает состояние или статус процесса. Ниже приведены символы, обозначающие статус, и дано соответствующее описание. Заголовок STAT позволяет узнать дополнительную информацию о статусе процесса. D (BSD) Ожидание доступа к диску Р (BSD) Ожидание доступа к странице X (System V) Ожидание доступа к памяти W (BSD) Процесс выгружен на диск К (AIX) Доступный процесс ядра N (BSD) Приоритет выполнения понижен > (BSD) Приоритет выполнения повышен искусственно < (Linux) Процесс с высоким приоритетом L (Linux) Страницы заблокированы в памяти Эти символы должны предшествовать коду статуса. Например, е>сли перед кодом статуса стоит символ N, значит, процесс выполняется с более низким уровнем приоритета. Если код статуса процесса отображен символами SW<, это означает, что процесс пребывает в ждущем режиме, выгружен и имеет высокий уровень приоритета. 3.4.3. Установка и получение приоритета процесса Уровень приоритета процесса можно изменить с помощью функции nice (). Каж- Дьш процесс имеет фактор уступчивости (nice value), который используется для выделения уровня приоритета вызывающего процесса. Процесс наследует приоритет Р°Цесса, который его создал. Чтобы понизить приоритет процесса, следует увели- 1Ть его фактор уступчивости. Лишь процессы привилегированных пользователей яДра системы могут увеличивать уровни своих приоритетов.
72 Глава 3. Разбиение С++-программ на множество задач //Linux [tdhughes@colony]$ ps USER PID tdhughes 19259 tdhughes 19334 taiughes 19336 tdhughes 19337 tdhughes 19338 taiughes 19341 taiughes 19400 taiughes 19401 %CPU 0.0 0.0 0.0 18.0 18.0 17.9 0.0 0.0 Tux %MEM 0.1 0.0 0.0 2.4 2.3 2.3 0.0 0.1 vsz 2448 1732 1928 26872 26872 26872 2544 2448 RSS TTY STAT 1356 pts/4 S 860 pts/4 S 780 pts/4 S 24856 pts/4 R 24696 pts/4 R 24556 pts/4 R 692 pts/4 R 1356 pts/4 R START 20:29 20:33 20:33 20:33 20:33 20:33 20:38 20:38 Рис. З.8. Результат выполнения команды ps Tux в среде Linux Синопсис #include <unistd. int nice(int incr h> jj TIME 0:00 0:00 0:00 0:47 0:47 0:47 0:00 0:00 COMMAND -bash /home/tdhughes/pv / home / taiughes /pv / home / taiughes/pv /home/tdhughes/pv /home/tdhughes/pv ps Tux -bash Чем ниже фактор уступчивости, тем выше уровень приоритета процесса. Параметр incr содержит значение, добавляемое к текущему фактору уступчивости вызывающего процесса. Значение параметра incr может быть отрицательным или положительным, а фактор уступчивости представляет собой неотрицательное число. Положительное значение incr увеличивает фактор уступчивости, а значит, понижает уровень приоритета. Отрицательное значение incr уменьшает фактор уступчивости, тем самым повышая уровень приоритета. Если значение incr изменяет фактор уступчивости выше или ниже соответствующих предельных величин, он будет установлен равным самому высокому или самому низкому пределу соответственно. При успешном выполнении функция nice () возвращает новый фактор уступчивости процесса, в противном случае — число -1, а прежнее значение фактора уступчивости при этом не изменяется. Синопсис #include <sys/resource.h> int getpriority(int which, id_t who); int setpriority(int which, id_t who, int value); Функция setpriority () устанавливает фактор уступчивости для заданного процесса, группы процессов или пользователя. Функция getpriority () возвращает приоритет заданного процесса, группы процессов или пользователя. Синтаксис использования функций setpriority () и getpriority () для установки и считывания фактора уступчивости текущего процесса демонстрируется в листинге 3.1. // Листинг 3.1. Использование функций setpriority() и // getpriority() #include <sys/resource.h> //. . . id_t pid = 0; int which = PRIO_PROCESS; int value = 10; int nice_value; int ret; nice_value = getpriority(which,pid) ,
3.5. Переключение контекста 73 if<nice_value < value){ t = setpriority(which,pid,value); } В листинге 3.1 возвращается и устанавливается приоритет вызывающего процесса. Если фактор уступчивости вызывающего процесса оказывается меньше 10, он устанавливается равным 10. Процесс задается значениями, хранимыми в параметрах which и who (см. соответствующий синопсис). Параметр which может определять процесс, группу процессов или пользователя и иметь следующие значения. PRI0_PR0CESS Означает процесс PRICL.PGRP Означает группу процессов PRICMJSER Означает пользователя В зависимости от значения параметра which параметр who содержит идентификационный номер (id) процесса, группы процессов или эффективного пользователя. В листинге 3.1 параметру which присваивается значение PRIO_PROCESS. В листинге 3.1 параметр who устанавливается равным 0, означая тем самым текущий процесс. Параметр value для функции setpriority () определяет новое значение фактора уступчивости для заданного процесса, группы процессов или пользователя. Факторы уступчивости в среде Linux должны находиться в диапазоне от -20 до 19. В листинге 3.1 фактор уступчивости устанавливается равным 10, если текущее его значение оказывается меньше 10. В отличие от функции nice (), значение, передаваемое функции setpriority (), является фактическим значением фактора уступчивости, а не смещением, которое суммируется с текущим фактором уступчивости. Если процесс имеет несколько потоков, модификация приоритета процесса повлияет на приоритет всех его потоков. При успешном выполнении функции getpriority () возвращается фактор уступчивости заданного процесса, а при успешном выполнении функции setpriority () — значение 0. В случае неудачи обе функции возвращают число -1. Однако число -1 является допустимым значением фактора уступчивости для любого процесса. Чтобы уточнить, не было ли ошибок при выполнении функции getpriority (), имеет смысл протестировать внешнюю переменную errno. 3.5. Переключение контекста Переключение контекста происходит в момент, когда процессор переключается с одного процесса на другой. При переключении контекста система сохраняет контекст текущего процесса и восстанавливает контекст следующего процесса, выбранного для использования процессора. БУП-блок прерванного процесса при этом обновляется, а также изменяется значение поля состояния процесса (т.е. признак состояния выполнения заменяется признаком другого состояния: готовности, локирования или "зомби"). Сохраняется и обновляется содержимое регистров процессора, состояние стека, данные об идентификации (и привилегиях) пользователя процесса, а также о стратегии планирования и учетная информация. Система должна отслеживать статус устройств ввода-вывода процесса и других ресурсов, а также состояние всех структур данных, связанных с управлением памятью. Сгруженный (прерванный) процесс помещается в соответствующую очередь.
74 Глава 3. Разбиение С++-программ на множество задач Переключение контекста происходит в случаях, когда: • процесс выгружается; • процесс добровольно отказывается от процессора; • процесс делает запрос к устройству ввода-вывода или должен ожидать наступления события; • процесс переходит из пользовательского режима в режим ядра. Когда выгруженный процесс снова выбирается для использования процессора, его контекст восстанавливается, и выполнение продолжается с точки, на которой он был прерван в предыдущем сеансе. 3.6. Создание процесса Чтобы выполнить любую программу, операционная система должна сначала создать процесс. При создании нового процесса в главной таблице процессов создается новая структура. Создается и инициализируется новый блок БУП, и в его раздел идентификации процесса записывается уникальный идентификационный номер процесса (id) и id родительского процесса. Программный счетчик устанавливается указателем на входную точку программы, а указатели системных стеков устанавливаются таким образом, чтобы определить стековые границы для процесса. Процесс инициализируется любыми требуемыми атрибутами. Если процессу не присвоено значение приоритета, то по умолчанию ему присваивается самое низкое значение. Изначально процесс не обладает никакими ресурсами, если нет явного запроса на ресурсы или если они не были унаследованы от процесса-создателя. Процесс "входит" в состояние выполнения и помещается в очередь готовых к выполнению процессов. Для него выделяется адресное пространство, размер которого определяется по умолчанию на основе типа процесса. Кроме того, размер можно установить по запросу от создателя процесса. Процесс-создатель может передать системе размер адресного пространства в момент создания процесса. 3.6.1. Отношения между родительскими и сыновними процессами Процесс, который создает, или порождает, другой процесс, является родительским (parent) процессом по отношению к порожденному, или сыновнему (child) процессу. Процесс init— родитель (или предок) всех пользовательских процессов— первый процесс, видимый системой UNIX после ее загрузки. Процесс init организует систему, при необходимости выполняет другие программы и запускает демон-программы (daemon), т.е. сетевые программы, работающие в фоновом режиме. Идентификатор процесса init (PID) равен 1. Сыновний процесс имеет собственный уникальный идентификатор PID, БУП-блок и отдельную структуру в таблице процессов. Сыновний процесс также может породить новый процесс. Выполняющееся приложение может создать дерево процессов. Например, родительский процесс выполняет поиск накопителя на жестких дисках для заданного HTML-документа. Имя этого HTML- документа записано в глобальной структуре данных, подобной списку, который содержит все запросы на документы. После успешного обнаружения документ удаляется из списка запросов, и его путь (маршрут в сети) записывается в другую глобальную структуру данных, которая содержит пути найденных документов. Чтобы обеспечить
3.6. Создание процесса 75 Приемлемую реакцию на пользовательские запросы, для процесса предусматривается 0граничение в виде пяти необработанных запросов в списке. По достижении этого Предела порождаются два новых процесса. Если порожденный процесс в свою очередь достигнет установленного предела, он создаст еще два новых процесса. Создаваемое таким способом дерево процессов показано на рис. 3.9. Любой процесс может иметь только один родительский, но множество сыновних процессов. PID 1 Процесс in it PID 12 Поиск HTML-документов При R > 5 порождение 2 процессов При R > 54 порождение 2 процессов PID76 Поиск HTML-документов PID89 Поиск HTML-документов Рис. 3.9. Дерево процессов. При определенных условиях процесс порождает два новых потомка Сыновний процесс может быть создан с собственным исполняемым образом или в виде дубликата родительского процесса. При создании в качестве дубликата предка сыновний процесс наследует множество его атрибутов, включая среду, приоритет, стратегию планирования, ограничения по peqpcaM, открытые файлы и разделы общей памяти. Если сыновний процесс перемещает указатель текущей позиции в файле или закрывает файл, то результаты этих действий будут видны родительскому процессу. Если Родителю выделяются любые дополнительные ресурсы уже после создания процесса- °томка, то °ни не будут доступны потомку. В свою очередь, если сыновний процесс ис- °льзует какие-либо ресурсы, они также будут недоступны для процесса-родителя. Некоторые атрибуты родителя не наследуются потомком. Как упоминалось выше, !Новний процесс не наследует PID родителя и его БУП-блок. Потомок не наследует каких файловых блокировок, созданных родителем или необработанными сигна- ли. Для сыновнего процесса используются собственные значения таких времениых рактеристик, как коэффициент загрузки процессора и время создания. Несмотря на то сыновние процессы связаны определенными отношениями с родителями, они
76 Глава 3. Разбиение С++-программ на множество задач все же функционируют как отдельные процессы. Их программные и стековые счетчики действуют раздельно. Поскольку разделы данных копируются, а не используются совместно, процесс-потомок может изменять значения своих переменных, не оказывая влияния на родительскую копию данных. Родительский и сыновний процесс совместно используют раздел программного кода и выполняют инструкции, расположенные непосредственно после вызова системной функции, создавшей сыновний процесс. Они не выполняют эти инструкции на этапе блокировки из-за соперничества за процессор со всеми остальными процессами, загруженными в память. После создания образ сыновнего процесса может быть заменен другим исполняемым образом. Разделы программного кода, данных и стеков, а также его "куча" памяти перезаписывается новым образом процесса. Новый процесс сохраняет свои идентификационные номера (PID и PPID). Атрибуты, сохраняемые новым процессом после замены его исполняемого образа, перечислены в табл. 3.3. В ней также указаны системные функции, которые возвращают эти атрибуты. Переменные среды также сохраняются, если во время замены исполняемого образа процесса не были заданы новые переменные среды. Файлы, которые были открыты до момента замены исполняемого образа, остаются открытыми. Новый процесс будет создавать файлы с теми же файловыми разрешениями. Время ЦП при этом не сбрасывается. Таблица 3.3. Атрибуты, сохраняемые новым процессом после замены его исполняемого образа образом нового процесса Сохраняемые атрибуты Функция Идентификатор (ID) процесса ID родительского процесса ID группы процессов Сеансовое членство Идентификатор эффективного пользователя Идентификатор эффективной группы Дополнительные ID групп Время, оставшееся до сигнала тревоги Фактор уступчивости Время, используемое до настоящего момента Маска сигналов процесса Ожидающие сигналы Предельный размер файла Предельный объем ресурсов Маска создания файлового режима Текущий рабочий каталог Корневой каталог getpidO getppid() getpgidO getsid() getuid() getgidO getgroups() alarm() nice() times () sigprocmask() sigpending() ulimit() getrlimit() umask() getcwd()
3.6. Создание процесса 77 3.6.1.1- Утилита pstree Утилита pstree в среде Linux отображает дерево процессов (точнее, она отображает выполняющиеся процессы в форме древовидной структуры). Корнем этого дерева является процесс ini t. Синопсис pstree [-а] [-с] [-h | [pid | user] pstree -V -Hpid] [-1] [-n] [-p] [-u] [-G] | -U] При вызове этой утилиты можно использовать следующие опции. -а Отобразить аргументы командной строки. -h Выделить текущий процесс и его предков. -Н Аналогично опции -п, но выделению подлежит заданный процесс. -п Отсортировать процессы с одинаковым предком по значению PID, а не по имени. -р Отобразить значения PID. На рис. 3.10 показан результат выполнения команды pstree -h в среде Linux. ka:~ # pstree -h init-+-applix l-atd I-axmain -axnet -cron l-gpm I-inetd I-9*[kdeinit] j-kdeinit -+-kdeinit I | -kdeinit bash gimp script-fu j '-kdeinit bash -+-man sh sh less | *-pstree I-kdeinit cat I-kdm-+-X I * - kdm kde ksms erver -kflushd -khubd -klogd I-knotify I-kswapd -kupdate I -login bash |-lpd I-mdrecoveryd I-5*[mingetty] -nscd nscd 5* [nscd] I-sshd I-syslogd |-usbmgr *-xconsole Ис- 3.10. Результат выполнения команды pstree -h в среде Linux
78 Глава 3. Разбиение С++-программ на множество задач 3.6.2. Использование системной функции fork() Системная функция (или системный вызов) fork () создает новый процесс, который представляет собой дубликат вызывающего процесса, т.е. его родителя. При успешном выполнении функция fork () возвращает родительскому и сыновнему процессам два различных значения. Сыновнему возвращается число 0, а родительскому — значение PID сыновнего процесса. Родительский и сыновний процессы продолжают выполняться с инструкции, непосредственно следующей за функцией fork (). В случае неудачного выполнения (оно выражается в том, что сыновний процесс не был создан) родительскому процессу возвращается число -1. Синопсис #include <unistd pid_t fork(void), h> Неудачный исход функции fork () возможен в случае, если система не обладает ресурсами для создания еще одного процесса. Это происходит при превышении ограничения (если оно существует) на количество сыновних процессов, которое может порождать родитель, или на количество выполняющихся процессов в масштабе всей системы. В этом случае устанавливается переменная errno, которая означает наличие ошибки. 3.6.3. Использование семейства системных функций exec Семейство функций exec предназначено для замены образа вызывающего процесса образом нового процесса. При вызове функции fork () создается новый процесс, который является точной копией родительского процесса, а функция exec () заменяет образ "скопированного" процесса образом копии. Образ нового процесса представляет собой обычный выполняемый файл, который немедленно запускается на выполнение. Этот файл можно задать с помощью имени и пути доступа к нему. Функции семейства exec могут передать новому процессу аргументы командной строки, а также установить переменные среды. Если функция выполнилась успешно, она не возвращает никакого значения, поскольку образ процесса, который содержал обращение к функции exec, уже перезаписан. В случае неудачи вызывающему процессу возвращается число -1. Все функции exec () могут иметь неудачный исход при следующих условиях: • разрешения не признаны; разрешение на поиск отвергается для каталога выполняемых файлов; разрешение на выполнение отвергается для выполняемого файла; • файлы не существуют, выполняемый файл не существует; каталог не существует; • файл невозможно выполнить; файл невозможно выполнить, поскольку он открыт для записи другим процессом; файл не является выполняемым;
3.6. Создание процесса 79 проблемы с символическими ссылками; при анализе пути к исполняемому файлу символические ссылки образуют циклы; символические ссылки делают путь к исполняемому файлу слишком длинным. функции семейства exec используются совместно с функцией fork (). Функция f rk () создает и инициализирует сыновний процесс "по образу и подобию" роди- льского. Образ сыновнего процесса затем заменяет образ своего предка посредством вызова функции exec (). Пример использования функций fork () и exec () показан в листинге 3.2. II листинг 3.2. Использование системных функций II fork() и exec() RtValue = fork(); if(RtValue == 0){ execl("/path/direct","direct","."); > В листинге 3.2 демонстрируется вызов функции fork (). Значение, которое она возвращает, сохраняется в переменной RtValue. Если значение RtValue равно 0, значит, это— сыновний процесс, и в нем вызывается функция execl () с параметрами. Первый параметр содержит путь к выполняемому модулю, второй — инструкцию для выполнения, а третий — аргумент. Второй параметр, direct, представляет собой имя утилиты, которая перечисляет все каталоги и подкаталоги из данного каталога. Всего существует шесть версий функций exec, предназначенных для использования различных соглашений о вызовах. 3.6.3.1. Функции execl () Функции execl (), execle () и execlp () передают аргументы командной строки в виде списка. Количество аргументов командной строки должно быть известно во время компиляции. • int execl(const char *path,const char *arg0,.../*, (char *)0 */); Здесь path — путевое имя выполняемой программы. Его можно задать в виде полного составного имени либо относительного составного имени из текущего каталога. Последующие параметры представляют собой список аргументов командной строки, от argO до argn. Всего может быть п аргументов. Этот список завершается NULL-указателем. • int execle(const char *path,const char *arg0#.../*, (char *)0 *, char *const envp[]*/); Эта функция аналогична функции execl () с одним отличием: она имеет дополнительный параметр, envp [ ]. Этот параметр указывает на новую среду для нового процесса, т.е. envp [ ] — это указатель на строковый массив с завершающим нулевым символом. Каждая его строка, также завершающаяся нулевым символом, имеет следующую форму: name=value
80 Глава 3. Разбиение С++-программ на множество задач Здесь name — имя переменной среды, a value — сохраняемая строка. Значение параметру envp [ ] можно присвоить следующим образом: char *const envp[] = {"PATH=/opt/kde2:/sbin", "HOME=/home",NULL}; Здесь PATH и HOME — переменные среды. • int execlp(const char *file,const char *arg0, .. ./*, (char *)0 */) ; Здесь file — имя выполняемой программы. Для определения местоположения выполняемых программ используется переменная среды PATH. Остальные параметры представляют собой список аргументов командной строки (см. описание функции execl ()). Вот примеры применения синтаксиса функций execl () с различными аргументами: char *const args[] = {"direct",".",NULL}; char *const envp[] = {"files=50",NULL}; execl("/path/direct","direct",".".NULL); execle("/path/direct","direct", " . ",NULL,envp); execlp("direct","direct",".",NULL); Здесь в каждом примере вызова execl-функции активизированный процесс выполняет программу direct. Синопсис #include <unistd.h> int execl(const char *path,const char *arg0,.../*, (char *)0 */); int execle(const char *path,const char *arg0,.../*, (char *)0 *,char *const envp[]*/); int execlp(const char *file,const char *arg0,.../*, (char *)0 */); int execv(const char *path/char *const arg[]); int execve(const char *path,char *const arg[], char *const envp[]); Iint execvp(const char *file,char *const arg[]); | 3.6.3.2. Функции execv () Функции execv (), execve () и execvp () передают аргументы командной строки в векторе указателей на строки с завершающим нулевым символом. Количество аргументов командной строки должно быть известно во время компиляции. Элемент argv [ 0 ] обычно представляет собой команду. • int execv(const char *path/char *const arg[]); Здесь path — путевое имя выполняемой программы. Его можно задать в виде полного составного имени либо относительного составного имени из текущего каталога. Последующий параметр представляет вектор (с завершающим нулевым символом), содержащий аргументы командной строки представленные в виде строк с завершающими нулевыми символами. Всего может быть п аргу-
3.6. Создание процесса 81 ментов. Этот вектор завершается NULL-указателем. Элементу arg[] можно присвоить значение таким образом: char *const arg[] = {"traverse",".", ">","1000",NULL}; Бот пример вызова этой функции: execv("traverse", arg) ; Б этом случае утилита traverse перечислит все файлы в текущем каталоге, размер которых превышает 1000 байт. • int execve(const char *path,char *const arg[], char *const envp[]); Эта функция аналогична функции execv (), с одним отличием: она имеет дополнительный параметр, envp [ ], который описан выше. • int execvp(const char *file/char *const arg[]); Здесь file— имя выполняемой программы. Последующий параметр представляет собой вектор (с завершающим нулевым символом), содержащий аргументы командной строки, представленные в виде строк с завершающими нулевыми символами. Всего может быть п аргументов. Этот вектор завершается NULL-указателем. Вот примеры применения синтаксиса функций execv () с различными аргументами: char *const arg[] = {"traverse"/.", ">", " 1000" ,NULL} ; char *const envp[] = {"files=50",NULL}; execv("/path/traverse",arg); execve("/path/traverse",arg,envp); execvp("traverse",arg); Здесь в каждом примере вызова execv-функции активизированный процесс выполняет программу traverse. 3.6.3.3. Определение ограничений для функций exec () Существуют ограничения на размеры вектора argv [ ] и массива envp [ ], передаваемые функциям семейства exec. Для определения максимального размера аргументов командной строки и размера переменных среды при использовании ехес- функций (которые принимают параметр envp [ ]) можно использовать функцию sysconf (). Чтобы эта функция возвратила размер, ее параметру паше необходимо Передать значение _SC_ARG_MAX. Синопсис #include <unistd.h> Ipng sysconf(int name) Еще одним ограничением при использовании функций семейства exec и других Функций, применяемых для создания процессов, является максимальное количество Дновременно выполняемых процессов, которое допустимо для одного пользователя. тобы функция sysconf () возвратила это число, ее параметру name необходимо передать значение _SC_CHILD_MAX.
82 Глава 3. Разбиение С++-программ на множество задач 3.6.3.4. Чтение и установка переменных среды Переменные среды представляют собой строки с завершающими нулевыми символами, в которых хранится такая системная информация, как пути к каталогам, содержащим команды, библиотеки, функции и процедуры, используемые процессом. Их также можно использовать для передачи любых определенных пользователем данных между родительскими и сыновними процессами. Они обеспечивают механизм предоставления процессу специальной информации без необходимости жесткого ее связывания с кодом программы. Переменные среды предопределены системой и совместно используются всеми ее оболочками и процессами. Эти переменные инициализируются файлами запуска. Чаще всего используются следующие системные переменные. $НОМЕ Полное составное имя каталога пользователя. $РАТН Список каталогов для поиска выполняемых файлов при выполнении команд. $MAIL Полное составное имя почтового ящика пользователя. $USER Идентификатор (id) пользователя. $ SHELL Полное составное имя командной оболочки зарегистрированного пользователя. $TERM Тип терминала пользователя. Переменные среды могут храниться в файле или в списке, принадлежащем среде. Этот список среды содержит указатели на строки с завершающими нулевыми символами. Когда процесс начинает выполняться, переменная extern char **environ будет указывать на список среды. Строки, составляющие список среды, имеют следующий формат: name=value Процессы, инициализированные с помощью функций execl(), execlpO, execv () и execvp (), наследуют конфигурацию среды родительского процесса. Процессы, инициализированные с помощью функций execve () и execle (), сами устанавливают среду. Существуют функции и утилиты, которые позволяют опросить, добавить или модифицировать переменные среды. Функция getenv() используется для определения факта установки заданной переменной. Интересующая вас переменная задается с помощью параметра name. Если заданная переменная не установлена, функция возвращает значение NULL. В противном случае (если переменная установлена), функция возвращает указатель на строку, содержащую ее значение. Синопсис #include <stdlib.h> char *getenv(const char *name); int setenv(const char *name, const char *value, int overwrite); void unsetenv(const char *name); Рассмотрим пример: string Path; Path = getenv("PATH")
3.6. Создание процесса 83 Члесь строке Path присваивается значение, содержащееся во встроенной переменной среды PATH. функция setenvO используется для изменения значения существующей переменной среды или добавления новой переменной в среду вызывающего процесса. Параметр name содержит имя переменной среды, которую надлежит добавить или изменить. Заданной переменной присваивается значение, переданное в параметре value. Если переменная, заданная параметром name, уже существует, ее прежнее значение заменяется значением, заданным параметром value при условии, если параметр overwrite содержит ненулевое значение. Если же значение overwrite равно 0, содержимое заданной переменной среды не модифицируется. Функция setenv () возвращает 0 при успешном выполнении, в противном случае — значение - 1. функция unsetenv () удаляет переменную среды, заданную параметром name. 3.6.4. Использование функции system () для порождения процессов Функция system () используется для выполнения команды или запуска программы. Функция system () выполняет функцию f ork(), а затем сыновний процесс вызывает функцию exec () с оболочкой, выполняя заданную команду или программу. Синопсис #include <stdlib.h> int system(const char *string); В качестве параметра string можно передать системную команду или имя выполняемого файла. При удачном исходе функция возвращает статус завершения команды или значение, возвращаемое программой (если таковое предусмотрено). Ошибки могут возникнуть на нескольких уровнях, т.е. ошибка может произойти при выполнении функции fork () или exec () либо заданная оболочка может оказаться неподходящей для выполнения команды или программы. Функция system () возвращает значение родительскому процессу. При неудачном исходе функции exec () возвращается число 127, а при обнаружении других ошибок — число -1. Эта функция не влияет на состояние ожидания сыновних процессов. 3.6.5. Использование POSIX-функций для порождения процессов Подобно созданию процессов с помощью функций system () и fork-exec, функции posix_spawn() создают новые сыновние процессы из заданных образов процессов. Однако функции posix_spawn() позволяют при этом реализовать более * Ногослойные "рычаги" управления, т.е. они управляют следующими атрибутами сы- °вних процессов, унаследованных от родительского процесса: • Дескрипторы файлов; • стратегия планирования; • идентификатор группы процессов;
84 Глава 3. Разбиение С++-программ на множество задач • идентификатор пользователя и группы; • маска сигналов. Функции posix_spawn() позволяют управлять тем, будут ли сигналы, проигнорированные родительским процессом, игнорироваться его потомком или устанавливаться для выполнения действий, заданных по умолчанию. Управление дескрипторами файлов позволяет сыновнему процессу получить самостоятельный доступ к потоку данных, независимо открытому родителем. Возможность установить для сыновнего процесса идентификатор группы повлияет на то, как управление сыновней задачей будет связано с управлением родителем. Наконец, стратегию планирования сыновнего процесса можно установить отличной от стратегии планирования его родителя. IСинопсис I #include <spawn.h> int posix_spawn(pid_t *restrict pid, const char *restrict path, const posix_spawn_file_actions_t *file_actions, const posix__spawnattr_t *restrict attrp, char *const argv[restrict] , char *const envp[restrict]); int posix_spawnp(pid_t *restrict pid, const char *restrict file, const posix__spawn_file_actions_t *file__actions, const posix__spawnattr_t *restrict attrp, char *const argv[restrict], char *const envp[restrict]) ; Различие между этими двумя функциями состоит в том, что функции posix_spawn () передается параметр path, а функции posix__spawnp () — параметр file. Параметр path в функции posix_spawn() принимает полное или относительное составное имя выполняемого файла, а параметр file в функции posix_spawnp () — только имя выполняемой программы. Если этот параметр содержит символ "косая черта", то содержимое параметра file используется в качестве составного путевого имени. В противном случае путь к выполняемому файлу определяется с помощью переменной среды PATH. Параметр file_actions представляет собой указатель на структуру posix_spawn_file_actions__t: struct posix_spawn_file_actions_t{ { int allocated; int used; struct spawn_action *actions; int pad [16] ; }; Структура posix_spawn_f ile_actions_t содержит информацию о действиях, выполняемых в новом процессе над дескрипторами файлов. Параметр f ile_actions используется для преобразования родительского набора дескрипторов открытых файлов в набор дескрипторов файлов для порожденного сыновнего процесса. Эта структура может содержать ряд файловых операций, предназначенных для выполнения в последовательности, в которой они были добавлены в объект действий над файлами. Эти файловые операции выполняются над дескрипторами открытых файлов родительского процесса и позволяют копировать, добавлять, удалять
3.6. Создание процесса 85 иЛи закрывать дескрипторы заданных файлов от имени сыновнего процесса даже до его создания. Если параметр f ile_actions содержит нулевой указатель, то дескрипторы файлов, открытые родительским процессом, останутся открытыми для его потомка без каких-либо модификаций. Функции, используемые для добавления действий над файлами в объект типа posix_spawn_f ile_actions, перечислены в табл. 3.4. Таблица 3.4. Функции, используемыедля добавления действий над файлами в объект типа posix_spawn_f ile_actions Функции Описание int posix_spawn_f ile_actions_addclose (posix_spawn_f ile_actions_t *file_actions, int fildes); int posix_spawn_f ile_actions_addopen (posix_spawn_f ile_actions_t *file_actions, int fildes, const char *restrict path, int oflag, mode_t mode); int posix_spawn_f ile_actions_adddup2 (posix_spawn_f ile_actions_t *file_actions, int fildes, int new fildes); int posix_spawn_file_actions_destroy (posix_spawn_f ile_actions_t *file_actions); int Posix_spawn_file_actions_init (posix_spawn_file_actions_t *file_actions) , Добавляет действие close () в объект действий над файлами, заданный параметром f ile_actions. В результате при порождении нового процесса с помощью этого объекта будет закрыт файловый дескриптор fildes Добавляет действие open () в объект действий над файлами, заданный параметром f ile_actions. В результате при порождении нового процесса с помощью этого объекта будет открыт файл, заданный параметром path, с использованием дескриптора fildes Добавляет действие dup2 () в объект действий над файлами, заданный параметром f ile_actions. В результате при порождении нового процесса с помощью этого объекта будет создан дубликат файлового дескриптора fildes с использованием файлового дескриптора newf ildes Разрушает объект, заданный параметром f ile_actions, что приводит к деинициализации этого объекта. Затем его можно инициализировать повторно с помощью функции posix_spawn_f ile_actions_init () Инициализирует объект, заданный параметром f ile_actions. После инициализации этот объект не будет содержать действий, предназначенных для выполнения над файлами Параметр attrp указывает на структуру posix_spawnattr_t: struct posix_spawnattr_t short int flags; Pid_t pgrp;
86 Глава 3. Разбиение С++-программ на множество задач sigset_t sd; sigset_t ss; struct sched_param sp; int policy; int pad [16] ; }; Эта структура содержит информацию о стратегии планирования, группе процессов, сигналах и флагах для нового процесса. Ниже следует описание отдельных атрибутов этой структуры. flags Используется для индикации того, какие атрибуты процесса должны быть модифицированы в порожденном процессе. Эти атрибуты организованы поразрядно по принципу включающего ИЛИ: POSIX_SPAWN_RESETIDS POSIX_SPAWN_SETPGROUP POSIX_SPAWN_SETSIGDEF POSIX_SPAWN_SETSIGMASK POSIX_SPAWN_SETSCHEDPARAM POSIX_SPAWN_SETSCHEDULER pgrp Идентификатор группы процессов, подлежащих объединению с новым процессом. sd Представляет множество сигналов, подлежащих обработке по умолчанию новым процессом. ss Представляет маску сигналов, подлежащую использованию новым процессом. sp Представляет параметр планирования, подлежащий назначению новому процессу. —policy Представляет стратегию планирования, предназначенную для нового процесса. Функции, используемые для установки и считывания отдельных атрибутов, содержащихся в структуре posix_spawnattr_t, перечислены в табл. 3.5. Таблица 3.5. Функции, используемые для установки и считывания отдельных атрибутов структуры posix_spawnattr_t Функции Описание int posix_spawnattr_getflags Возвращает значение атрибута flags, xpa- (const posix_spawnattr_t нимого в объекте, заданном параметром attr *restrict attr, short *restrict flags); int posix_spawnattr_setflags Устанавливает значение атрибута flags, (posix_spawnattr_t *attr, хранимого в объекте, заданном параметром short flags) ; attr, равным значению параметра flags _
3.6. Создание процесса 87 Продолжение табл. 3.5 Функць Описание int posix_spawnattr__getpgroup (const posix_spawnattr_t *restrict attr, pid_t *restrict pgroup); int posix_spawnattr_setpgroup (posix_spawnattr_t ♦attr, pid_t pgroup); int posix_spawnattr_getschedparam (const posix_spawnattr_t ♦restrict attr, struct sched_param ♦restrict schedparam); int posix_spawnattr_setschedparam (posix_spawnattr_t *attrf const struct sched_param ♦restrict schedparam); int pos ix_spawna t tr_gets chedpolicy (const posix_spawnattr_t ♦restrict attr, int ♦restrict schedpolicy); int pos ix_spawnattr_setschedpolicy (posix_spawnattr_t ♦attr, int schedpolicy); int posix_spawnattr_getsigdefault (const posix_spawnattr_t ♦restrict attr, sigset_t ♦restrict sigdefault); int Posix_spawnattr_setsigdefault (posix_spawnattr_t *attr, const sigset_t ♦restrict sigdefault); lnt P°six_spawnattr_getsigmask (const posix_spawnattr_t ♦restrict attr, sigset_t ♦restrict sigmask); Возвращает значение атрибута pgroup, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре pgroup Устанавливает значение атрибута pgroup, хранимого в объекте, заданном параметром attr, равным параметру pgroup, если в атрибуте flags установлен признак POSIX_SPAWN_SETPGROUP Возвращает значение атрибута sp, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре schedparam Устанавливает значение атрибута sp, хранимого в объекте, заданном параметром attr, равным параметру schedparam, если в атрибуте flags установлен признак POSIX_SPAWN_SETSCHEDPARAM Возвращает значение атрибута policy, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре schedpolicy Устанавливает значение атрибута policy, хранимого в объекте, заданном параметром attr, равным параметру schedpolicy, если в атрибуте flags установлен признак POSIX_SPAWN_SETSCHEDULER Возвращает значение атрибута sd, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре sigdef ault Устанавливает значение атрибута sd, хранимого в объекте, заданном параметром attr, равным параметру sigde fault, если в атрибуте flags установлен признак POSIX_SPAWN_SETSIGDEF Возвращает значение атрибута ss, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре sigmask
88 Глава 3. Разбиение С++-программ на множество задач Окончание табл. 3.5 Функции Описание int posix_spawnattr_setsigmask Устанавливает значение атрибута ss, xpa- (posix_spawnattr_t нимого в объекте, заданном параметром *restrict attrf attr, равным параметру sigmask, если в ат- const slf^ £ siamask) . рибуте _flags установлен признак restrict sigmask), POSIX_SPAWN_SETSIGMASK int posix_spawnattr_destroy Разрушает объект, заданный параметром (posix_spawnattr_t *attr) ; attr. Этот объект можно затем снова инициализировать с помощью функции posix_spawnattr_init() int posix_spawnattr_init Инициализирует объект, заданный парамет- (posix_spawnattr_t *attr) ; pOM attr, значениями, действующими по умолчанию для всех атрибутов, содержащихся в этой структуре Пример использования функции posix_spawn () для создания процесса приведен в листинге 3.3. // Листинг 3.3. Порождение процесса с помощью // функции posix_spawn(), которая // вызывает утилиту ps #include <spawn.h> #include <stdio.h> #include <errno.h> #include <iostream> { //. .. posix_spawnattr_t X; posix_spawn_file_actions_t Y; pid_t Pid; char *const argv[] = {"/bin/ps","-If",NULL}; char *const envp[] = {"PROCESSES=2"}; posix_spawnattr_init(&X); posix_spawn_file_actions_init(&Y); posix_spawn(&Pid,"/bin/ps", &Y,&X,argv,envp); perror("posix_spawn"); cout « "spawned PID: " « Pid « endl; //. . . return(0) ; } В листинге 3.3 инициализируются объекты posix_spawnattr_t и posix_spawn_ f ile_actions_t. Функция posix_spawn() вызывается с такими аргументами: Pid, путь " /bin/ps", Y, X, массив argv (который содержит команду в качестве первого элемента и опцию в качестве второго) и массив envp, содержащий список переменных среды. При успешном выполнении функции posix_spawn() значение, хранимое в параметре Pid, станет идентификатором (PID) порожденного процесса, а функция perror () отобразит следующий результат: posix_spawn: Success
3.7. Завершение процесса 89 Чатем будет выведено значение Pid. В данном случае порожденный процесс выполняет следующую команду: /bin/ps -If При успешном выполнении POSIX-функции возвращают (обычным путем) число О и в параметре pid идентификатор (id) сыновнего процесса (для родительского процесса). В случае неудачи сыновний процесс не создается, следовательно, значение pid не имеет смысла, а сама функция возвращает значение ошибки. При использовании spawn-функций ошибки могут возникать на трех уровнях. Во- первых, это возможно, если окажутся недействительными объекты f ile_actions или attr objects. Если это произойдет после успешного (казалось бы) завершения функции (т.е. после порождения сыновнего процесса), такой сыновний процесс может получить статус завершения со значением 127. Если ошибка связана с функциями управления атрибутами порожденных процессов, возвращается код ошибки, сгенерированный конкретной функцией (см. табл. 3.4 и 3.5). Если spawn-функция успела успешно завершиться, то сыновний процесс может иметь статус завершения со значением 127. Ошибки также возникают при попытке породить сыновний процесс. Эти ошибки будут такими же, как при выполнении функций fork () или exec (). В этом случае они (ошибки) займут место значений, возвращаемых spawn-функциями. Если сыновний процесс генерирует ошибку, родительский процесс не получает "дурного известия" автоматически. Для извещения родителя об ошибке сыновнего процесса необходимо использовать другие механизмы, поскольку информация об этом не сохраняется в статусе завершения потомка. С этой целью можно использовать механизм межпроцессного взаимодействия либо специальный флаг, устанавливаемый сыновним процессом и видимый для его родителя. 3.6.6. Идентификация родительских и сыновних процессов с помощью функций управления процессами Существуют две функции, которые возвращают значение идентификатора (PID) вызывающего процесса и значение идентификатора (PPID) родительского процесса. Функция getpid () возвращает идентификатор вызывающего процесса, а функция getppid () — идентификатор процесса, который является родительским для вызывающего процесса, эти функции всегда завершаются успешно, поэтому коды ошибок не определены. Синопсис #include <unistd.h> Pid-t getpid(void); £jg-t getppid(void); 3.7. Завершение процесса Когда процесс завершается, его блок БУП разрушается, а используемое им адрес- °е пространство и ресурсы освобождаются. Код завершения помещается в главную ЛИцУ процессов. Как только родительский процесс примет этот код, соответст-
90 Глава 3. Разбиение С++-программ на множество задач вующая структура таблицы процессов будет удалена. Процесс завершается, если соблюдены следующие требования. • Все инструкции выполнены. • Процесс явным образом передает управление родительскому процессу или вызывает системную функцию, которая завершает процесс. • Сыновние процессы могут завершаться автоматически при завершении родительского процесса. • Родительский процесс посылает сигнал о завершении своих сыновних процессов. Аварийное завершение процесса может произойти в случае, если процесс выполняет недопустимые действия. • Процесс требует больше памяти, чем система может ему предоставить. • Процесс пытается получить доступ к неразрешенным ресурсам. • Процесс пытается выполнить некорректную инструкцию или запрещенные вычисления. Завершение процесса может быть инициировано пользователем, если этот процесс является интерактивным. Родительский процесс несет ответственность за завершение (освобождение) своих потомков. Родительский процесс должен ожидать до тех пор, пока не завершатся все его сыновние процессы. Если родительский процесс выполнит считывание кода завершения сыновнего процесса, процесс-потомок покидает систему нормально. Процесс остается в "зомбированном" состоянии до тех пор, пока его родитель не примет соответствующий сигнал. Если родитель никогда не примет сигнал (поскольку он уже успел сам завершиться и выйти из системы или не ожидал завершения сыновнего процесса), процесс-потомок остается в "зомбированном" состоянии до тех пор, пока процесс init (исходный системный процесс) не примет его код завершения. Большое количество "зомбированных" процессов может негативно отразиться на производительности системы. 3.7.1. Функции exit (), kill () и abort () Для самостоятельного завершения процесс может вызвать одну из двух функций: exit () и abort О . Функция exit() обеспечивает нормальное завершение вызывающего процесса. При этом будут закрыты все дескрипторы открытых файлов, связанные с процессом. Функция exit () сбросит на диск все открытые потоки, содержащие еще не переписанные буферизованные данные, после чего открытые потоки будут закрыты. Параметр status принимает статус завершения процесса, возвращаемый ожидающему родительскому процессу, который затем перезапускается. Параметр status может принимать такие значения: 0, EXIT_FAILURE или EXIT_SUCCESS. Значение 0 говорит об успешном завершении процесса. Ожидающий родительский процесс имеет доступ только к младшим восьми битам значения параметра status. Если родительский процесс не ожидает завершения сыновнего процесса, его (ставшего "зомбированным") "усыновляет" процесс init. Функция abort () вызывает аварийное окончание вызывающего процесса, что по последствиям равноценно результату выполнения функции fcloseO для всех открытых потоков. При этом ожидающий родительский процесс получит сигнал о пре-
3.8. Ресурсы процессов 91 крашении выполнения сыновнего процесса. Процесс может прибегнуть к преждевременному прекращению только в случае, если он обнаружит ошибку, с которой не сможет справиться программным путем. Синопсис #include <stdlib.h> void exit(int status); void abort(void) ; функцию kill () можно использовать для принудительного завершения другого процесса. Эта функция отправляет сигнал процессам, заданным параметром pid. Параметр sig — это сигнал, предназначенный для отправки заданному процессу. Возможные сигналы перечислены в заголовке <signal.h>. Для уничтожения процесса параметр sig должен иметь значение SIGKILL. Чтобы иметь право отсылать сигнал процессу, вызывающий процесс должен обладать соответствующими привилегиями, либо его реальный или идентификатор эффективного пользователя должен совпадать с реальным или сохраненным пользовательским идентификатором процесса, который принимает этот сигнал. Вызывающий процесс может иметь разрешение на отправку процессам только определенных (а не любых) сигналов. При успешной отправке сигнала функция возвращает вызывающему процессу значение 0, в противном случае — число -1. Вызывающий процесс может отправить сигнал одному или нескольким процессам при таких условиях. pid > 0 Сигнал будет отослан процессу, идентификатор (PID) которого равен значению параметра pid. pid = 0 Сигнал будет отослан всем процессам, у которых идентификатор группы процессов совпадает с идентификатором вызывающего процесса. pid = -1 Сигнал будет отослан всем процессам, для которых вызывающий процесс имеет разрешение отправлять этот сигнал. Сигнал будет отослан всем процессам, у которых идентификатор группы процессов равен абсолютному значению параметра pid, и для которых вызывающий процесс имеет разрешение отправлять этот сигнал. pid < -1 Синопсис #include <signal.h> Apt kill(pjd_t pid, int sig); 3.8. Ресурсы процессов При выполнении возложенной на процесс задачи часто приходится записывать Данные в файл, отправлять их на принтер или отображать полученные результаты на кране. Процессу могут понадобиться данные, вводимые пользователем с клавиатуры и с°Держащиеся в файле. Кроме того, процессы в качестве ресурса могут использо- ать другие процессы, например, подпрограммы. Подпрограммы, файлы, семафоры, Ыотексы, клавиатуры и экраны дисплеев — все это примеры ресурсов, которые мо-
92 Глава 3. Разбиение С++-программ на множество задач жет затребовать процесс. Под ресурсом понимается все то, что использует процесс в любое заданное время в качестве источника данных, средств обработки, вычислений или отображения информации. Чтобы процесс получил доступ к ресурсу, он должен сначала сделать запрос, обратившись с ним к операционной системе. Если ресурс свободен, операционная система позволит процессу его использовать. После использования ресурса процесс освобождает его, чтобы он стал доступным для других процессов. Если ресурс недоступен, запрос отвергается, и процесс должен подождать его освобождения. Как только ресурс станет доступным, процесс активизируется. Таков базовый подход к распределению ресурсов между процессами. На рис. 3.11 показан граф распределения ресурсов, по которому можно понять, какие процессы удерживают ресурсы, а какие их ожидают. Так, процесс В делает запрос на ресурс 2, который удерживается процессом С. Процесс С делает запрос на ресурс 3, который удерживается процессом D. Рис. 3.11. Граф распределения ресурсов, который показывает, какие процессы удерживают ресурсы, а какие их запрашивают Если удовлетворяется сразу несколько запросов на получение доступа к ресурсу, этот ресурс является совместно используемым, или разделяемым (эта ситуация также отображена на рис. 3.11). Процесс А разделяет ресурс R, с процессом D. Разделяемые ресурсы могут допускать параллельный доступ сразу нескольких процессов или разрешать доступ только одному процессу в течение ограниченного промежутка времени, после чего аналогичным доступом сможет воспользоваться другой процесс. Примером такого типа разделяемых ресурсов может служить процессор. Сначала процессор назначается одному процессу в течение короткого интервала времени, а затем процессор "получает" другой процесс. Если удовлетворяется только один запрос на получение доступа к ресурсу, и это происходит после того, как ресурс освободит другой процесс, такой ресурс является неразделяемым, а о процессе говорят, что он имеет монопольный доступ (exclusive access) к ресурсу. В многопроцессорной среде важно знать, какой доступ можно организовать к разделяемому ресурсу: параллельный или последовательный (передавая "эстафету" поочередно от ресурса к ресурсу). Это позволит избежать ловушек, присущих параллелизму. Одни ресурсы могут изменяться или модифицироваться процессами, а другие — нет. Поведение разделяемых модифицируемых или немодифицируемых ресурсов определяется типом ресурса.
3.8. Ресурсы процессов 93 fe 3.1 -Граф распределения ресурсов , Гоафы распределения ресурсов — это направленные графы, которые показывают, как Определяются реСурСЫ в системе. Такой граф состоит из множества вершин V множества ребер Е. Множество вершин делится на две категории: р1{р,.рг....р.} r-{R,.R. »»> Множество Р— это множество всех процессов, a R— это множество всех ресурсов в системе. Ребро, направленное от процесса к ресурсу, называется ребром запроса, а ребро, направленное от ресурса к процессу, называется ребром назначения. Направленные ребра обозначаются следующим образом: р _» r Ребро запроса: процесс Р{ запрашивает экземпляр типа ресурса R. r _> р. Ребро назначения: экземпляр типа ресурса R. выделен процессу R Каждый процесс в графе распределения ресурсов отображается кругом, а каждый ресурс — прямоугольником. Поскольку может быть много экземпляров одного типа ресурса, то каждый из них представляется точкой внутри прямоугольника. Ребро запроса указывает на периметр прямоугольника ресурса, а ребро назначения берет начало из точки и касается периметра круга процесса. Граф распределения ресурсов, показанный на рис. 3.11, отображает следующее. Множества Р, R и Е P-{P..PbiPefPd} R={RI,R2,R,} E-{R,->P4tR1->PdtPb^R2tRf->PctPc->RvR3^Pd} 3.8.1. Типы ресурсов Существуют три основных типа ресурсов: аппаратные, информационные и программные. Аппаратные ресурсы представляют собой физические устройства, подключенные к компьютеру (например, процессоры, основная память и все устройства ввода-вывода, включая принтеры, жесткий диск, накопитель на магнитной ленте, дисковод с zip-архивом, мониторы, клавиатуры, звуковые, сетевые и графические карты, а также модемы. Все эти устройства могут совместно использовать несколько процессов. Некоторые аппаратные ресурсы прерываются, чтобы разрешить доступ к ним различных процессов. Например, прерывания процессора позволяют различным процессам выполняться по очереди. Оперативное запоминающее устройство, или ОЗУ \КАМ),— это еще один пример ресурса, разделяемого посредством прерываний. Кода процесс не выполняется, некоторые страничные блоки, которые он занимает, мо- Ут быть выгружены во вспомогательное запоминающее устройство, а на их место за- РУжены данные, относящиеся к другому процессу. В любой момент времени весь Д*апазон памяти может быть занят страничными блоками только одного процесса, римером разделяемого, но непрерываемого ресурса может служить принтер. При вместном использовании принтера задания, посылаемые на печать каждым процес- м» хранятся в очереди. Каждое задание печатается до конца, и только потом начи-
94 Глава 3. Разбиение С++-программ на множество задач нает выполняться следующее задание. Принтер не прерывается ни одним ждущим заданием, если не отменяется текущее задание. Информационные ресурсы— к ним относятся данные (например, объекты), системные данные (например, переменные среды, файлы и дескрипторы) и такие глобально определенные переменные, как семафоры и мьютексы, — являются разделяемыми ресурсами, которые могут быть модифицированы процессами. Обычные файлы и файлы, связанные с физическими устройствами (например, принтером), могут открываться с учетом ограничивающего типа доступа со стороны процессов. Другими словами, процессы могут обладать правом доступа только для чтения, или только для записи, или для чтения и записи. Сыновний процесс наследует ресурсы родительского процесса и права доступа к ним, существующие на момент создания процесса- потомка. Сыновний процесс может переместить файловый указатель, закрыть, модифицировать или перезаписать содержимое файла, открытого родителем. Доступ к совместно используемым файлам и памяти с разрешением записи должен быть синхронизирован. Для синхронизации доступа к разделяемым ресурсам данных можно использовать такие разделяемые данные, как семафоры и мьютексы. Разделяемые библиотеки могут служить примером программных ресурсов. Разделяемые библиотеки предоставляют общий набор функций для процессов. Процессы могут также совместно использовать приложения, программы и утилиты. В этом случае в памяти находится только одна копия программного кода, например, приложения (приложений). При этом должны существовать отдельные копии данных, по одной для каждого пользователя (процесса). К неизменяемому программному коду (который также именуется реентерабельным, или повторно используемым) могут получать доступ несколько процессов одновременно. 3.8.2, POSIX-функции для установки ограничений доступа к ресурсам В библиотеке POSIX определены функции, которые ограничивают возможности процесса по использованию определенных ресурсов. Так, операционная система устанавливает ограничения на возможности процесса по использованию системных ресурсов, а именно: • размер стека процесса; • размер создаваемого файла и файла ядра; • объем времени ЦП, выделенный процессу (размер кванта времени); • объем памяти, используемый процессом; • количество дескрипторов открытых файлов. Операционная система устанавливает жесткие ограничения на использование ресурсов процессом. Процесс может установить или изменить мягкие ограничения ресурсов, но это значение не должно превысить жесткий предел, установленный операционной системой. Процесс может понизить свой жесткий предел, но его значение не должно быть меньше мягкого предела. Операция по понижению процессом своего жесткого предела необратима. Его могут повысить только процессы, обладающие специальными привилегиями.
3.8. Ресурсы процессов 95 Синопсис #include <sys/resource.h> int setrlimit(int resource, const struct rlimit *rlp); int getrlimit(int resource, struct rlimit *rlp); int getrusage(int who, struct rusage *r__usage) ; функция setrlimit () используется для установки ограничений на потребление заданных ресурсов. Эта функция позволяет установить как жесткий, так и мягкий пределы. Параметр resource представляет тип ресурса. Значения типов ресурсов (и их краткое описание) приведено в табл. 3.6. Жесткие и мягкие пределы заданного ресурса представляются параметром rip, который указывает на структуру rlimit, содержащую два объекта типа rlim_t. struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; }; Тип rlim_t — это целочисленный тип без знака. Член rlim_cur содержит значение текущего, или мягкого предела, а член rlim_max— значение максимума, или жесткого предела. Членам rlim_cur и rlim_max можно присвоить любые значения, а также символические константы, определенные в заголовке <sys /resource. h>. RLIM__INFINITY Отсутствие ограничения. RLIM_SAVED_MAX Непредставимый хранимый жесткий предел. RLIM_SAVED_CUR Непредставимый хранимый мягкий предел. Как жесткий, так и мягкий пределы можно установить равными значению RLIM__INFINITY, которое подразумевает, что ресурс неограничен. [Таблица 3.6. Значения параметра resource Определение ресурса Описание RLIMIT__CORE Максимальный размер файла ядра в байтах, который может быть создан процессом IMIT__CPU Максимальный объем времени ЦП в секундах, которое может быть использовано процессом IMIT__DATA Максимальный размер раздела данных процесса в байтах MIT__FSIZE Максимальный размер файла в байтах, который может быть создан процессом MIT__NOFILE Увеличенное на единицу максимальное значение, которое система может назначить вновь созданному дескриптору файла IT__STACK Максимальный размер стека процесса в байтах — -ET__AS Максимальный размер доступной памяти процесса в байтах
95 Глава 3. Разбиение С++-программ на множество задач Функция getrlimit () возвращает значения мягкого и жесткого пределов заданного ресурса в объекте rip. Обе функции возвращают значение 0 при успешном завершении и число -1 в противном случае. Пример установки процессом мягкого предела для размера файлов в байтах приведен в листинге 3.4. // Листинг 3.4. Использование функции setrlimitO для // установки мягкого предела для размера // файлов #include <sys/resource.h> //. . . struct rlimit R_limit; struct rlimit R_limit_values; //. . . R_limit.rlim_cur = 2 000; R_limit.rlim_max = RLIM_SAVED_MAX; setrlimit (RLIMIT_FSIZE, &R__limit) ; getrlimit(RLIMIT_FSIZE,&R_limit_values); cout « "мягкий предел для размера файлов: " « R_limit_values .rlim_cur « endl; //. . . В листинге 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.9. Асинхронные и синхронные процессы 97 Таблица 3.7. Члены структуры rusage Член структуры Описание struct timeval ru_utime struct timeval ru__sutime long ru_maxrss long long long long long long long long long long long long long ru_maxixrss ru_maxidrss ru_maxisrss ru_minfIt ru_majfIt ru_nswap ru_inblock ru_oublock ru_msgsnd ru_msgrcv ru_nsignals ru_nvcsw ru_nivcsw Время, потраченное пользователем Время, использованное системой Максимальный размер, установленный для резидентной программы Размер разделяемой памяти Размер неразделяемой области данных Размер неразделяемой области стеков Количество запросов на страницы Количество ошибок из-за отсутствия страниц Количество перекачек страниц Блочные операции по вводу данных Блочные операции операций по выводу данных Количество отправленных сообщений Количество полученных сообщений Количество полученных сигналов Количество преднамеренных переключений контекста Количество принудительных переключений контекста 3.9. Асинхронные и синхронные процессы Асинхронные процессы выполняются независимо один от другого. Это означает, что процесс А будет выполняться до конца безотносительно к процессу В. Между асинхронными процессами могут быть прямые родственные ("родитель-сын") отношения, а могут и не быть. Если процесс А создает процесс В, они оба могут выполняться независимо, но в некоторый момент родитель должен получить статус завершения сыновнего процесса. Если между процессами нет прямых родственных отношений, у них может быть общий родитель. Асинхронные процессы могут выполняться последовательно, параллельно или с перекрытием. Эти сценарии изображены на рис. 3.12. В ситуации 1 до самого конца вы- олняется процесс А, затем процесс В и процесс С выполняются до самого конца. Это есть последовательное выполнение процессов. В ситуации 2 процессы выполняются ДНовременно. Процессы А и В — активные процессы. Во время выполнения процесса А роцесс В находится в состоянии ожидания. В течение некоторого интервала времени а пР°Цесса пребывают в ждущем режиме. Затем процесс В "просыпается", причем Р ныне процесса А, а через некоторое время "просыпается" и процесс А, и теперь оба роцесса выполняются одновременно. Эта ситуация показывает, что асинхронные роцессы могут выполняться одновременно только в течение определенных интерва- в времени. В ситуации 3 выполнение процессов А и В перекрывается.
дя Глава 3. Разбиение С++-программ на множество задач АСИНХРОННЫЕ ПРОЦЕССЫ выполняется [Ситуация 1jJ- ПРОЦЕССА i ПРОЦЕСС В выполняется ^ ПРОЦЕСС С выполняется. [Ситуация 2^J- _ЛЛ . DDK IUJ 1ПМС1^М иЖИДЦЦ I DDII IUJ 1ПЛСIU71 л ПРОЦЕСС А шшшшшшшшшшшшшш -■ -** , ■ > * ммннЦ ожидает выполняется , ПРОЦЕСС В — — -.— — — — — — шшшшшшшшшшшшшшшшшшш\ выполняется ожидает выполняется [Ситуация 3jJ- ПРОЦЕСС А ПРОЦЕСС В выполняется ожидает выполняется СИНХРОННЫЕ ПРОЦЕССЫ [Ситуация 4м выполняется ожидает выполняется t ПРОЦЕСС А нншц ~, ,,..._•... - шшшшшшшшшшшш^ fork() ; ; 1 выполняется ' возвращает код выхода ПРОЦЕСС В ншн^ Рис. 3.12. Возможные сценарии асинхронных и синхронных процессов Асинхронные процессы могут совместно использовать такие ресурсы, как файлы или память. Это может потребовать (или не потребовать) синхронизации или взаимодействия при разделении ресурсов. Если процессы выполняются последовательно (ситуация 1), то они не потребуют никакой синхронизации. Например, все три процесса, А, В и С, могут разделять некоторую глобальную переменную. Процесс А (перед тем как завершиться) записывает значение в эту переменную, затем процесс В во время своего выполнения считывает данные, хранимые в этой переменной и (перед тем как завершиться) записывает в нее "свое" значение. Затем во время своего выполнения процесс С считывает данные из этой переменной. Но в ситуациях 2 и 3 процессы могут попытаться одновременно модифицировать эту переменную, поэтому здесь не обойтись без синхронизации доступа к ней. Мы определяем синхронные процессы как процессы с перемежающимся выполнением, когда один процесс приостанавливает свое выполнение до тех пор, пока не
3.9. Асинхронные и синхронные процессы 99 ершится другой. Например, процесс А, родительский, при выполнении создает оцесс В, сыновний. Процесс А приостанавливает свое выполнение до тех пор, ка не завершится процесс В. После завершения процесса В его выходной код по- ешается в таблицу процессов. Тем самым процесс А уведомляется о завершении ооцесса В. Процесс А может продолжить выполнение, а затем завершиться или за- еошиться немедленно. В этом случае выполнение процессов А и В является синхронизированным. Сценарий синхронного выполнения процессов А и В (для сравнения с асинхронным) также показан на рис. 3.12. 3.9.1- Создание синхронных и асинхронных процессов с помощью функций fork (), exec (), system () и posix_spawn() Функции fork (), fork-exec и posix_spawn () позволяют создавать асинхронные процессы. При использовании функции fork () дублируется образ родительского процесса. После создания сыновнего процесса эта функция возвращает родителю (через параметр) идентификатор (PID) процесса-потомка и (обычным путем) число О, означающее, что создание процесса прошло успешно. При этом родительский процесс не приостанавливается; оба процесса продолжают выполняться независимо от инструкции, следующей непосредственно за вызовом функции fork (). При создании сыновнего процесса посредством f ork-exec-комбинации его образ инициализируется с помощью образа нового процесса. Если функция exec () выполнилась успешно (т.е. успешно прошла инициализация), она не возвращает родительскому процессу никакого значения. Функции posix_spawn() создают образы сыновних процессов и инициализируют их. Помимо идентификатора (PID), возвращаемого (через параметр) функцией posix_spawn() родительскому процессу, обычным путем возвращается значение, служащее индикатором успешного порождения процесса. После выполнения функции posix_spawn() оба процесса выполняются одновременно. Функция system() позволяет создавать синхронные процессы. При этом создается оболочка, которая выполняет системную команду или запускает выполняемый файл. В этом случае родительский процесс приостанавливается до тех пор, пока не завершится сыновний процесс и функция system () не возвратит значение. 3.9.2. Функция wait () Асинхронный процесс, вызвав функцию wait (), может приостановить выполнение до тех пор, пока не завершится сыновний процесс. После завершения сыновнего роцесса ожидающий родительский процесс считывает статус завершения своего по- омка, чтобы не допустить создания процесса-"зомби". Функция wait () получает ста- ,с завершения из таблицы процессов. Параметр status указывает на ту область, ко- рая содержит статус завершения сыновнего процесса. Если родительский процесс * еет не один, а несколько сыновних процессов и некоторые из них уже завершись, функция wait () считывает из таблицы процессов статус завершения только для ого сыновнего процесса. Если информация о статусе окажется доступной еще до полнения функции wait (), эта функция завершится немедленно. Если родитель- и процесс не имеет ни одного потомка, эта функция возвратит код ошибки.
1.00 Глава 3. Разбиение С++-программ на множество задач Функцию wait () можно использовать также в том случае, когда вызывающий процесс должен ожидать до тех пор, пока не получит сигнал, чтобы затем выполнить определенные действия по его обработке. I Синопсис I #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); Функция waitpidO аналогична функции wait () за исключением того, что она принимает дополнительные параметры pid и options. Параметр pid задает множество сыновних процессов, для которых считывается статус завершения. Другими словами, значение параметра pid определяет, какие процессы попадают в это множество. pid > 0 Единственный сыновний процесс. pid = 0 Любой сыновний процесс, групповой идентификатор которого совпадает с идентификатором вызывающего процесса. pid < -1 Любые сыновние процессы, групповой идентификатор которых равен абсолютному значению pid. pid = -1 Любые сыновние процессы. Параметр options определяет, как должно происходить ожидание процесса, и может принимать одно из значений следующих констант, определенных в заголовке <sys/wait .h>: WCONTINUED Сообщает статус завершения любого продолженного сыновнего процесса (заданного параметром pid), о статусе которого не было доложено с момента продолжения его выполнения. WUNTRACED Сообщает статус завершения любого остановленного сыновнего процесса (заданного параметром pid), о статусе которого не было доложено с момента его останова. WN0HANG Вызывающий процесс не приостанавливается, если статус завершения заданного сыновнего процесса недоступен. Эти константы могут быть объединены с помощью логической операции ИЛИ и переданы в качестве параметра options (например, WCONTINUED | | WUNTRACED). Обе эти функции возвращают идентификатор (PID) сыновнего процесса, для которого получен статус завершения. Если значение, содержащееся в параметре status, равно числу 0, это означает, что сыновний процесс завершился при таких условиях: • процесс вернул значение 0 из функции main (); • процесс вызвал некоторую версию функции exit () с аргументом 0; • процесс был завершен, поскольку завершился последний поток процесса. В табл. 3.8 перечислены макросы, которые позволяют вычислить значение статуса завершения.
3.10. Разбиение программы на задачи 101 Таблица 3.8. Макросы, которые позволяют вычислить значение статуса завершения Макрос Описание WIFEXITED Приводится к ненулевому значению, если статус был возвращен нормально завершенным сыновним процессом WEXITSTATUS Если значение WIFEXITED оказывается ненулевым, то оцениваются младшие 8 бит аргумента status, переданного завершенным сыновним процессом функции _exit () или exit (), либо значения, возвращенного функцией main () WIFSIGNALED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который завершился, поскольку ему был послан сигнал, но этот сигнал не был перехвачен WTERMSIG Если значение WIFSIGNALED оказывается ненулевым, то оценивается номер сигнала, который послужил причиной завершения сыновнего процесса WIFSTOPPED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который в данный момент остановлен WSTOPSIG Если значение WIFSTOPPED оказывается ненулевым, то оценивается номер сигнала, который послужил причиной останова сыновнего процесса WIFCONTINUED Приводится к ненулевому значению, если статус был возвращен от сыновнего процесса, который продолжил выполнение после сигнала останова, принятого от блока управления заданиями 3.10. Разбиение программы на задачи Рассматривая разбиение программы на несколько задач, вы делаете первый шаг к внесению параллелизма в свою программу. В однопроцессорной среде параллелизм реализуется посредством многозадачности. Это достигается путем переключения процессов. Каждый процесс выполняется в течение некоторого короткого интервала времени, после чего процессор "передается" другому процессу. Это происходит настолько быстро, что создается иллюзия одновременного выполнения процессов, многопроцессорной среде процессы, принадлежащие одной программе, могут быть азначены одному или различным процессорам. Процессы, назначенные различным роцессорам, выполняются параллельно. Различают два уровня параллельной обработки в приложении или системе: ,ровень процессов и уровень потоков. Параллельная обработка на уровне пото- носит название многопоточности (она рассматривается в следующей главе), ооы разумно разделить программу на параллельные задачи, необходимо опре- лить, где "гнездится" параллелизм и где можно воспользоваться преимуществами от ° реализации. Иногда в параллелизме нет насущной необходимости. Программа т интерпретироваться с учетом параллелизма, но и при последовательном
102 Глава 3. Разбиение С++-программ на множество задач выполнении действий она прекрасно работает. Безусловно, внесение параллелизма может повысить ее быстродействие и понизить уровень сложности. Одни программы обладают естественным параллелизмом, а другим больше подходит последовательное выполнение действий. Программы также могут иметь двойственную интерпретацию. При декомпозиции программы на функции обычно используется нисходящий принцип, а при разделении на объекты — восходящий. При этом необходимо определить, какие функции или объекты лучше реализовать в виде отдельных программ или подпрограмм, а какие — в виде потоков. Подпрограммы должны выполняться операционной системой как процессы. Отдельные подпрограммы, или процессы, выполняют задачи, порученные проектировщиком ПО. Задачи, на которые будет разделена программа, могут выполняться параллельно, причем здесь можно выделить следующие три способа реализации параллелизма. 1. Выделение в программе одной основной задачи, которая создает некоторое количество подзадач. 2. Разделение программы на множество отдельных выполняемых файлов. 3. Разделение программы на несколько задач разного типа, отвечающих за создание других подзадач только определенного типа. Эти способы реализации параллелизма отображены на рис. 3.13. Например, эти методы реализации параллелизма можно применить к программе визуализации. Под визуализацией будем понимать процесс перехода от представления трехмерного объекта в форме записей базы данных в двухмерную теневую графическую проекцию на поверхность отображения (экран дисплея). Изображение представляется в виде теневых многоугольников, повторяющих форму объекта. Этапы визуализации показаны на рис. 3.14. Визуализацию можно разбить на ряд отдельных задач. 1. Установить структуру данных для сеточных моделей многоугольников. 2. Применить линейные преобразования. 3. Отбраковать многоугольники, относящиеся к невидимой поверхности. 4. Выполнить растеризацию. 5. Применить алгоритм удаления скрытых поверхностей. 6. Затушевать отдельные пиксели. Первая задача состоит в представлении объекта в виде массива многоугольников, в котором каждая вершина многоугольника описывается в трехмерной мировой системе координат. Вторая задача — применить линейные преобразования к сеточной модели многоугольников. Эти преобразования используются для позиционирования объектов на сцене и создания точки обзора или поверхности отображения (области, которая видима наблюдателю с его точки обзора). Третья задача — отбраковать невидимые поверхности объектов на сцене. Это означает удаление линий, принадлежащих тем частям объектов, которые невидимы с точки обзора. Четвертая задача— преобразовать модель вершин в набор координат пикселей. Пятая задача — удалить любые скрытые поверхности. Если сцена содержит
3.10. Разбиение программы на задачи 103 имодействующие объекты, например, когда одни объекты заслоняют другие, то В ытые (передними объектами) поверхности должны быть удалены. Шестая зада- ~ наложить на поверхности изображения тень. ПРОГРАММА СПОСОБ 1 Представление программы в виде родительского процесса, который создает некоторое количество сыновних процессов РОДИТЕЛЬСКИЙ ПРОЦЕСС г— Задача А Задача В Задача С Задача D СПОСОБ 2 Разделение программы на множество отдельных выполняемых файлов ПРОГРАММА ОБОЛОЧКА I ►] Задача А I I Г I — Задача В J Задача С Задача D СПОСОБ 3 Разделение программы на несколько процессов, отвечающих за создание других процессов только определенного типа ПРОГРАММА ЗАДАЧА А ЗАДАЧА В Задача А1 Задача А2 Задача В1 Задача В2 рис. 3.13. Способы разбиения программы на отдельные задачи
104 Глава 3. Разбиение С++-программ на множество задач ЭТАПЫ ВИЗУАЛИЗАЦИИ № вершины 1 2 3 4 5 6 7 8 9 10 1,40000 1,40000 0,78400 0,00000 1,33750 1,33750 0,74900 0,00000 1,43750 1,43750 0,00000 2,30000 -0,78400 2,30000 -1,40000 2,30000 -1,40000 2,30000 0,00000 2,53125 -0,75000 2,53125 -1,33700 2,53125 -1,33700 2,53125 0,00000 2,53125 -0,90500 2,53125 <Ф* 306 1,42500 -0,79800 0,00000 1. Представление трехмерного объекта в виде записей базы данных. Рис. 3.14. Этапы визуализации 2. Построение многоугольной сеточной модели трехмерного объекта. 3. Наложение тени на трехмерный объект. Решение каждой задачи представляется в виде отдельного выполняемого файла. Первые три задачи (Taskl, Task2 и Task3) выполняются последовательно, а остальные три (Task4, Task5 и Task6) — параллельно. Реализация первого способа создания программы визуализации приведена в листинге 3.5. // Листинг 3.5. Использование способа 1 // для создания процессов #include <spawn.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h> #include <errno.h> #include <unistd.h> int main(void) { posix__spawnattr__t Attr; posix_spawn__f ile_actions_t FileActions ; char *const argv4[] = {"Task4",.../NULL}; char *const argv5[] = {"TaskS", .../NULL}; char *const argv6[] = {"Task6n,...,NULL}; pid_t Pid; int stat; //. .. // Выполняем первые три задачи синхронно, system("Taskl ...") system("Task2 ...") system("Task3 ..."),
3.10. Разбиение программы на задачи 105 ,/ инициализируем структуры. 11 ix_spawnattr_init(&Attr) ; Р° . spawn_f ile__actions__init (&FileActions) ; Выполняем последние три задачи асинхронно. ix__spawn(&Pid,"Task4",&FileActions,&Attr,argv4,NULL) p0^ix^spawn(&Pid/ "Task5", &FileAct:ions, &Attr, argv5,NULL) po •x~~Spawn(&Picl, "Тазкб", &FileAct ions, &Attr,argv6, NULL) // Подобно хорошему родителю, ожидаем возвращения // своих "детей". wait (&stat); wait (&stat); wait (fcstat); return(0); } В листинге 3.5 из функции main () с помощью функции system () вызываются на выполнение задачи Taskl, Task2 и Task3. Каждая из них выполняется синхронно с родительским процессом. Задачи Task4, Task5 и Task6 выполняются асинхронно родительскому процессу благодаря использованию функций posix_spawn(). Многоточие (...) используется для обозначения файлов, требуемых задачам. Родительский процесс вызывает три функции wait (), и каждая из них ожидает завершения одной из задач (Task4, Task5 или Task6). Используя второй способ, программу визуализации можно запустить из сценария командной оболочки. Преимущество этого сценария состоит в том, что он позволяет использовать все команды и операторы оболочки. В нашей программе визуализации для управления выполнением задач используются метасимволы & и &&. Taskl . . . && Task2 ... && Task3 Task4 ... & Task5 ... & Task6 Здесь благодаря использованию метасимвола && задачи Taskl, Task2 и Task3 выполняются последовательно при условии успешного выполнения предыдущей задачи. Задачи же Task4, Task5 и Task6 выполняются одновременно, поскольку использован метасимвол &. Приведем некоторые метасимволы, применяемые при разделении команд в средах UNIX/Linux, и способы выполнения этих команд. Каждая следующая команда будет выполняться только в случае успешного выполнения предыдущей команды. ' • Каждая следующая команда будет выполняться только в случае неудачного выполнения предыдущей команды. Команды должны выполняться последовательно. Все команды должны выполняться одновременно. При использовании третьего способа задачи делятся по категориям. При декомпо- и пР°граммы следует разобраться, можно ли в ней выделить различные катего- г задач. Например, одни задачи могут "отвечать" за интерфейс пользователя, т.е. создание, ввод данных, вывод данных и пр. Другим задачам поручаются вычисле- » Управление данными и пр. Такой подход весьма полезен не только при проекти- г ании программы, но и при ее реализации. В нашей программе визуализации мы Жем Разделить задачи по следующим категориям:
106 Глава 3. Разбиение С++-программ на множество задач • задачи, которые выполняют линейные преобразованиях преобразования изображения на экране при изменении точки обзора; преобразования сцены; • задачи, которые выполняют растеризацию: вычерчивание линий; заливка участков сплошного фона; растеризация многоугольников; • задачи, которые выполняют удаление поверхностей: удаление скрытых поверхностей; удаление невидимых поверхностей; • задачи, которые выполняют наложение теней: затенение отдельных пикселей; затенение изображения в целом. Разбиение задач по категориям позволяет нашей программе приобрести более общий характер. Процессы при необходимости создают другие процессы, предназначенные для выполнения действий только определенной категории. Например, если нашей программе предстоит визуализировать лишь один объект, а не всю сцену, то нет никакой необходимости порождать процесс, который выполняет удаление скрытых поверхностей; вполне достаточно будет удаления невидимых поверхностей (одного объекта). Если объект не нужно затенять, то нет необходимости порождать задачу, выполняющую наложение тени; обязательным остается лишь линейное преобразование при решении задачи растеризации. Для запуска программы с использованием третьего способа можно использовать родительский процесс или сценарий оболочки. Родительский процесс может определить, какой нужен тип визуализации, и передать соответствующую информацию каждому из специализированных процессов, чтобы они "знали", какие процессы им следует порождать. Эта информация может быть также перенаправлена каждому из специализированных процессов из сценария оболочки. Реализация третьего способа представлена в листинге 3.6. // Листинг 3.6. Использование третьего метода для // создания процессов. Задачи запускаются из // родительского процесса #include <spawn.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h> #include <errno.h> #include <unistd.h> int main(void) { posix_spawnattr_t Attr; posix_spawn__f ile__actions_t FileActions ; pid_t Pid; int stat;
3.10. Разбиение программы на задачи 107 m("Taskl _.»); // Выполняется безотносительно к s^s // типу используемой визуализации. Определяем, какой нужен тип визуализации. Это можно // сделать, получив информацию от пользователя или // выполнив специальный анализ. // затем сообщаем о результате другим задачам с помощью // аргументов. char *const argv4[] = {"TaskType4",.../NULL} char *const argv5[] = { MTaskType5", . . . ,N171,1,} char *const argv6[] = {"ТавкТуреб", .../NULL} system("TaskType2 ..."); system(nTaskType3 ..."); // Инициализируем структуры. posix_spawnattr_init (&Attr) ; posix_spawn_f ile_actions_init (&FileActions) ; posix_spawn(&Pid, "TaskType4", &FileActions, &Attr,argv4, NULL); posix_spawn(&Pid, "TaskTypeS", &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[l] == X){ // Инициализируем структуры. //. . . posix_spawn(&Pid,"TaskTypeX",&FileActions,&Attr NULL); else{ // Инициализируем структуры.
108 Глава 3. Разбиение С++-программ на множество задач //. • • posix_spawn(&Pid,"TaskTypeY", &FileActions,&Attr, ...,NULL); } wait(&stat); exit(0); В листинге 3.6 тип каждой задачи (а следовательно, и тип порождаемого процесса) определяется на основе информации, передаваемой от родительского процесса или сценария оболочки. 3.10.1. Линии видимого контура Порождение процессов, как показано в листинге 3.7, возможно с помощью функций, вызываемых из функции main (). // Листинг 3.7. Стержневая ветвь программы, из которой // вызывается функция, порождающая процесс int main(int argc, char *argv[]) { //. . - Rt = funcMX, Y, Z) ; } // Определение функции. int fund (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 функция fund () вызывается с тремя аргументами. Эти аргументы передаются порожденному процессу. Процессы также могут порождаться из методов, принадлежащих объектам. Как показано в листинге 3.8, объекты можно объявить в любом процессе.
3.11. Резюме 109 // Листинг 3.8. Объявление объекта в процессе my_pbject MyObject; //••• // объявление и определение класса. class my_object { public: int spawnProcess(int X) ; }; int my_object: : spawnProcess (int X) { // posix__spawn() или system() } Как показано в листинге 3.8, объект может создавать любое количество процессов из любого метода. 3.11. Резюме Параллелизм в С++-программе достигается за счет ее разложения на несколько процессов или несколько потоков. Процесс— это "единица работы", создаваемая операционной системой. Если программа— это артефакт (продукт деятельности) разработчика, то процесс — это артефакт операционной системы. Приложение может состоять из нескольких процессов, которые могут быть не связаны с какой-то конкретной программой. Операционные системы способны управлять сотнями и даже тысячами параллельно загруженных процессов. Некоторые данные и атрибуты процесса хранятся в блоке управления процессами iprocess control block— PCB), или БУП, используемом операционной системой для Дентификации процесса. С помощью этой информации операционная система Управляет процессами. Многозадачность (выполнение одновременно нескольких Роцессов) реализуется путем переключения контекста. Текущее состояние выпол- еМого процесса и его контекст сохраняются в БУП-блоке, что позволяет успешно обновить этот процесс в следующий раз, когда он будет назначен центральному Роцессору. Занимая процессор, процесс пребывает в состоянии выполнения, а когда °жидает использования ЦП,— то в состоянии готовности (ожидания). Получить формацию о процессах, выполняющихся в системе, можно с помощью утилиты ps. процессы, которые создают другие процессы, вступают с ними в "родственные" Цььи-дети) отношения. Создатель процесса называется родительским, а создан-
110 Глава 3. Разбиение С++-программ на множество задач ный процесс — сыновним. Сыновние процессы наследуют от родительских множество атрибутов. "Святая обязанность" родительского процесса— подождать, пока сыновний не покинет систему. Для создания процессов предусмотрены различные системные функции: fork (), fork-exec (), system () и posit_spawn (). Функции forkO, fork-exec () и posix__spawn() создают процессы, которые являются асинхронными, в то время как функция system () создает сыновний процесс, который является синхронным по отношению к родительскому. Асинхронные родительские процессы могут вызвать функцию wait (), после чего "синхронно" ожидать, пока сыновние процессы не завершатся или пока не будут считаны коды завершения для уже завершившихся сыновних процессов. Программу можно разбить на несколько процессов. Эти процессы может породить родительский процесс, либо они могут быть запущены из сценария оболочки как отдельные выполняемые программы. Специализированные процессы могут при необходимости порождать другие процессы, предназначенные для выполнения действий только определенного типа. Порождение процессов может быть осуществлено как из функций, так и из методов.
РАЗБИЕНИЕ С++- ПРОГРАММ НА МНОЖЕСТВО ПОТОКОВ В этой главе... 4.1. Определение потока 4.2. Анатомия потока 4.3. Планирование потоков 4.4. Ресурсы потоков 4.5. Модели создания и функционирования потоков 4.6. Введение в библиотеку Pthread 4.7. Анатомия простой многопоточной программы 4.8. Создание потоков 4.9. Управление потоками 4.10. Безопасность использования потоков и библиотек 4.11. Разбиение программы на несколько потоков 4.12. Резюме
Jls\ZJSJZJ A 11 Непрерывное усложнение компьютерных систем вселяет в нас надежду, что мы и в дальнейшем сможем успешно управлять этим видом абстракции. — Эндрю Кёниг и Барбара My (Andrew Koening and Barbara Moo), Ruminations on C++ Работу любой последовательной программы можно разделить между несколькими подпрограммами. Каждой подпрограмме назначается конкретная задача, и все эти задачи выполняются одна за другой. Вторая задача не может начаться до тех пор, пока не завершится первая, а третья — пока не закончится вторая и т.д. Описанная схема прекрасно работает до тех пор, пока не будут достигнуты границы производительности и сложности. В одних случаях единственное решение проблемы производительности — найти возможность выполнять одновременно более одной задачи. В других ситуациях работа подпрограмм в программе настолько сложна, что имеет смысл представить эти подпрограммы в виде мини-программ, которые выполняются параллельно внутри основной программы. В главе 3 были представлены методы разбиения одной программы на несколько процессов, каждый из которых выполняет отдельную задачу. Такие методы позволяют приложению в каждый момент времени выполнять сразу несколько действий. Однако в этом случае каждый процесс имеет собственные адресное пространство и ресурсы. Поскольку каждый процесс занимает отдельное адресное пространство, то взаимодействие между процессами превращается в настоящую проблему. Для обеспечения связи между раздельно выполняемыми частями общей программы нужно реализовать такие средства межпроцессного взаимодействия, как каналы, FIFO-очереди (с дисциплиной обслуживания по принципу "первым пришел — первым обслужен") и переменные среды. Иногда нужно иметь одну программу (которая выполняет несколько задач одновременно), не разбивая ее на множество мини-программ. В таких обстоятельствах можно использовать
4.1. Определение потока 113 токи. Потоки позволяют одной программе состоять из параллельно выполняемых и, причем все части имеют доступ к одним и тем же переменным, константам и аденому пространству в целом. Потоки можно рассматривать как мини-программы в ос- вной программе. Если программа разделена на несколько процессов, как было пока- ано в главе 3, то с выполнением каждого отдельного процесса связаны определенные траты системных ресурсов. Для потоков требуется меньший объем затрат системных ecvpcoB. Поэтому потоки можно рассматривать как облегченные процессы, т.е. они позволяют воспользоваться многими преимуществами процессов без больших затрат на организацию взаимодействия между ними. Потоки обеспечивают средства разделения основного "русла" программы на несколько параллельно выполняемых "ручейков". 4.1. Определение потока Под потоком подразумевается часть выполняемого кода в UNIX- или Linux-процессе, которая может быть регламентирована определенным образом. Затраты вычислительных ресурсов, связанные с созданием потока, его поддержкой и управлением, у операционной системы значительно ниже по сравнению с аналогичными затратами для процессов, поскольку объем информации отдельного потока гораздо меньше, чем у процесса. Каждый процесс имеет основной, или первичный, поток. Под основным потоком процесса понимается программный поток управления или поток выполнения. Процесс может иметь несколько потоков выполнения и, соответственно, столько же потоков управления. Каждый поток, имея собственную последовательность инструкций, выполняется независимо от других, а все они — параллельно друг другу. Процесс с несколькими потоками, называется многопоточным. Многопоточный процесс, состоящий из нескольких потоков, показан на рис. 4.1. ПОТОКИ ВЫПОЛНЕНИЯ ПРОЦЕССА Поток А JL ' 1 Основной поток ± « создать » « создать » г Поток В 1 «♦—Потоки выполнения —► Т 1 V | Рис. 4.1. Потоки выполнения многопоточного процесса
114 Глава 4. Разбиение С++-программ на множество потоков 4.1.1. Контекстные требования потока Все потоки одного процесса существуют в одном и том же адресном пространстве. Все ресурсы, принадлежащие процессу, разделяются между потоками. Потоки не владеют никакими ресурсами. Ресурсы, которыми владеет процесс, совместно используются всеми потоками этого процесса. Потоки разделяют дескрипторы файлов и файловые указатели, но каждый поток имеет собственные программный указатель, набор регистров, состояние и стек. Все стеки потоков находятся в стековом разделе своего процесса. Раздел данных процесса совместно используется потоками процесса. Поток может считывать (и записывать) информацию из области памяти своего процесса. Когда основной поток записывает данные в память, то любые сыновние потоки могут получить к ним доступ. Потоки могут создавать другие потоки в пределах того же процесса. Все потоки в одном процессе считаются равноправными. Потоки также могут приостановить, возобновить или завершить другие потоки в своем процессе. Потоки — это выполняемые части программы, которые соревнуются за использование процессора с потоками того же самого или других процессов. В многопроцессорной системе потоки одного процесса могут выполняться одновременно на различных процессорах. Однако потоки конкретного процесса выполняются только на процессоре, который назначен этому процессу. Если, например, процессоры 1, 2 и 3 назначены процессу А, а процесс А имеет три потока, то любой из них может быть назначен любому процессору. В среде с одним процессором потоки конкурируют за его использование. Параллельность же достигается за счет переключения контекста. Контекст переключается, если операционная система поддерживает многозадачность при наличии единственного процессора. Многозадачность позволяет на одном процессоре одновременно выполнять несколько задач. Каждая задача выполняется в течение выделенного интервала времени. По истечении заданного интервала или после наступления некоторого события текущая задача снимается с процессора, а ему назначается другая задача. Когда потоки выполняются параллельно в одном процессе, то о таком процессе говорят, что он — многопоточный. Каждый поток выполняет свою подзадачу таким образом, что подзадачи процесса могут выполняться независимо от основного потока управления процесса. При многозадачности потоки могут конкурировать за использование одного процессора или назначаться другим процессорам. Но в любом случае переключение контекста между7 потоками одного и того же процесса требует меньше ресурсов, чем переключение контекста между потоками различных процессов. Процесс использует много системных ресурсов для отслеживания соответствующей информации, а на управление этой информацией при переключении контекста между процессами требуется значительное время. Большая часть информации, содержащейся в контексте процесса, описывает адресное пространство процесса и ресурсы, которыми он владеет. Переключаясь между потоками, определенными в различных адресных пространствах, контекст переключается и между процессами. Поскольку потоки в рамках одного процесса не имеют собственного адресного пространства (или ресурсов), то операционном системе приходится отслеживать меньший объем информации. Контекст потока состоит только из идентификационного номера (id), стека, набора регистров и приоритета. В регистрах содержится программный указатель и указатель стека. Текст (программный код) потока содержится в текстовом разделе соответствующего процесса. Поэтому переключение контекста между потоками одного процесса займет меньше времени и потребует меньшего объема системных ресурсов.
4.1. Определение потока 115 4.1.2. Сравнение потоков и процессов У потоков и процессов есть много общего. Они имеют идентификационный но- (id), состояние, набор регистров, приоритет и привязку к определенной страниц плаНирования. Подобно процессам, потоки имеют атрибуты, которые описывают их для операционной системы. Эта информация содержится в информационном блоке потока, подобном информационному блоку процесса. Потоки и сыновние процессы разделяют ресурсы родительского процесса. Ресурсы, открытые родительским процессом (в его основном потоке), немедленно становятся доступными всем потокам и сыновним процессам. При этом никакой дополнительной инициализации или подготовки не требуется. Потоки и сыновние процессы независимы от родителя (создателя) и конкурируют за использование процессора. Создатель процесса или потока управляет своим потомком, т.е. он может отменить, приостановить или возобновить его выполнение либо изменить его приоритет. Поток или процесс может изменить свои атрибуты и создать новые ресурсы, но не может получить доступ к ресурсам, принадлежащим другим процессам. Однако между потоками и процессами есть множество различий. 4.1.2.1. Различия между потоками и процессами Основное различие между потоками и процессами состоит в том, что каждый процесс имеет собственное адресное пространство, а потоки — нет. Если процесс создает множество потоков, то все они будут содержаться в его адресном пространстве. Вот почему они так легко разделяют общие ресурсы, и так просто обеспечивается взаимодействие между ними. Сыновние процессы имеют собственные адресные пространства и копии разделов данных. Следовательно, когда процесс-потомок изменяет свои переменные или данные, это не влияет на данные родительского процесса. Если необходимо, чтобы родительский и сыновний процессы совместно использовали данные, нужно создать общую область памяти. Для передачи данных между родителем и потомком используются такие механизмы межпроцессного взаимодействия, как каналы и FIFO-очереди. Потоки одного процесса могут передавать информацию и связываться друг с другом путем непосредственного считывания и записи общих данных, которые доступны родительскому процессу. 4.1.2.2. Потоки, управляющие другими потоками В то время как процессы могут управлять другими процессами, если между ними установлены отношения типа "родитель-потомок", потоки одного процесса считаются равноправными и находятся на одном уровне, независимо от того, кто кого создал. Любой поток, имеющий доступ к идентификационном)' номеру (id) некоторого другого потока, может отменить, приостановить, возобновить выполнение этого потока либо изменить его приоритет. Отмена основного потока приведет к завершению всех потоков процесса, т.е. к ликвидации процесса. Любые изменена, внесенные в основной поток, могут повлиять на все потоки процесса. При из- * еНении приоритета процесса все его потоки, которые унаследовали этот приори- ет» должны также изменить свои приоритеты. Сходства и различия между потока- Ми и процессами сведены в табл. 4.1.
116 Глава 4. Разбиение С++-программ на множество потоков Таблица 4.1. Сходства и различия между потоками и процессами Сходства Различия Оба имеют идентификационный номер (id), состояние, набор регистров, приоритет и привязку к определенной стратегии планирования И поток, и процесс имеют атрибуты, которые описывают их особенности для операционной системы Как поток, так и процесс имеют информационные блоки Оба разделяют ресурсы с родительским процессом Оба функционируют независимо от родительского процесса Их создатель может управлять потоком или процессом И поток, и процесс могут изменять свои атрибуты Оба могут создавать новые ресурсы Как поток, так и процесс не имеют доступа к ресурсам другого процесса Потоки разделяют адресное пространство процесса, который их создал; процессы имеют собственное адресное пространство Потоки имеют прямой доступ к разделу данных своего процесса; процессы имеют собственную копию раздела данных родительского процесса Потоки могут напрямую взаимодействовать с другими потоками своего процесса; процессы должны использовать специальный механизм межпроцессного взаимодействия для связи с "братскими" процессами Потоки почти не требуют системных затратна поддержку процессов требуются значительные затраты системных ресурсов Новые потоки создаются легко; новые процессы требуют дублирования родительского процесса Потоки могут в значительной степени управлять потоками того же процесса; процессы управляют только сыновними процессами Изменения, вносимые в основной поток (отмена, изменение приоритета и т.д.), могут влиять на поведение других потоков процесса; изменения, вносимые в родительский процесс, не влияют на сыновние процессы 4.1.3. Преимущества использования потоков При управлении подзадачами приложения использование потоков имеет ряд преимуществ. • Для переключения контекста требуется меньше системных ресурсов. • Достигается более высокая производительность приложения. • Для обеспечения взаимодействия между задачами не требуется никакого специального механизма. • Программа имеет более простую структуру. 4.1.3.1. Переключение контекста при низкой (ограниченной) доступности процессора При организации процесса для выполнения возложенной на него функции может оказаться вполне достаточно одного основного потока. Если же процесс имеет множество параллельных подзадач, то их асинхронное выполнение можно обеспечить с помощью нескольких потоков, на переключение контекста которых потребуются незначительные затраты системных ресурсов. При ограниченной доступности процессора
4.1. Определение потока 117 ли при наличии в системе только одного процессора параллельное выполнение про- ессов потребует существенных затрат системных ресурсов в связи с необходимостью беспечить переключение контекста. В некоторых ситуациях контекст процессов переключается только тогда, когда процессору последовательно назначаются потоки из разных процессов. Под системными затратами подразумеваются не только системные ресурсы, но время, требуемое на переключение контекста. Но если система содержит достаточное количество процессоров, то переключение контекста не является проблемой. 4.1.3.2. Возможности повышения производительности приложения Создание нескольких потоков повышает производительность приложения. При использовании одного потока запрос к устройствам ввода-вывода может остановить весь процесс. Если же в приложении организовано несколько потоков, то пока один из них будет ожидать удовлетворения запроса ввода-вывода, другие потоки, которые не зависят от заблокированного, смогут продолжать выполнение. Тот факт, что не все потоки ожидают завершения операции ввода-вывода, означает, что приложение в целом не заблокировано ожиданием, а продолжает работать. 4.1.3.3. Простая схема взаимодействия между параллельно выполняющимися потоками Потоки не требуют специального механизма взаимодействия между подзадачами. Потоки могут напрямую передавать данные другим потокам и получать данные от них, что также способствует экономии системных ресурсов, которые при использовании нескольких процессов пришлось бы направлять на настройку и поддержку специальных механизмов взаимодействия. Потоки же используют общую память, выделяемую в адресном пространстве процесса. Процессы также могут взаимодействовать через общую память, но они имеют раздельные адресные пространства, и поэтому такая общая память должна быть вне адресных пространств обоих взаимодействующих процессов. Этот подход увеличит времениые и пространственные расходы системы на поддержку и доступ к общей памяти. Схема взаимодействия между потоками и процессами показана на рис. 4.2. 4.1.3.4. Упрощение структуры программы Потоки можно использовать, чтобы упростить структуру приложения. Каждому потоку назначается подзадача или подпрограмма, за выполнение которой он отвечает. Поток должен независимо управлять выполнением своей подзадачи. Каждому потоку можно присвоить приоритет, отражающий важность выполняемой им задачи Для приложения. Такой подход позволяет упростить поддержку программного кода. 4.1.4. Недостатки использования потоков Простота доступности потоков к памяти процесса имеет свои недостатки. • Потоки могут легко разрушить адресное пространство процесса. • Потоки необходимо синхронизировать при параллельном доступе (для чтения или записи) к памяти. • Один поток может ликвидировать целый процесс или программ}7. • Потоки существуют только в рамках единого процесса и, следовательно, не являются многократно используемыми.
118 Глава 4. Разбиение С++-программ на множество потоков АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А РАЗДЕЛ СТЕКОВ Локальные переменные СВОБОДНАЯ ПАМЯТЬ | Локальные переменные I Глобальные j переменные РАЗДЕЛ ДАННЫХ РАЗДЕЛ КОДА Глобальные I структуры данных Глобальные переменные J Константы I Статические переменные [ Разделяемая память АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В РАЗДЕЛ СТЕКОВ Локальные переменные Стек потока А Стек потока В СВОБОДНАЯ ПАМЯТЬ^ Локальные переменные Глобальные переменные РАЗДЕЛ ДАННЫХ РАЗДЕЛ КОДА ! Глобальные j структуры данных i Глобальные переменные ] Константы \ (Статические переменные | « деревья | • графы | '• очереди Кол потеши А Рис. 4.2. Взаимодействие между потоками одного процесса и взаимодействие между несколькими процессами 4.1.4.1. Потоки могут легко разрушить адресное пространство процесса Потоки могут легко разрушить информацию процесса во время "гонки" данных, если сразу несколько потоков получат доступ для записи одних и тех же данных. При использовании процессов это невозможно. Каждый процесс имеет собственные данные, и другие процессы не в состоянии получить к ним доступ, если не сделать это специально. Защита информации обусловлена наличием у процессов отдельных адресных пространств. Тот факт, что потоки совместно используют одно и то же адресное пространство, делает данные незащищенными от искажения. Например, процесс имеет три потока — А, В и С. Потоки А и В записывают информацию в некоторую область памяти, а поток С считывает из нее значение и использует его для вычислений. Потоки А и В могут попытаться одновременно записать информацию в эту область памяти. Поток В может перезаписать данные, записанные потоком А, еще до того, как поток С получит возможность считать их. Поведение этих потоков должно быть синхронизировано таким образом, чтобы поток С мог считать данные, записанные потоком А, до того, как поток В их перезапишет. Синхронизация защищает данные от перезаписи до их использования. Тема синхронизации потоков рассматривается в главе 5.
4.2. Анатомия потока 119 4.1.4.2. Один поток может ликвидировать целую программу Поскольку потоки не имеют собственного адресного пространства, они не изолированы. Если поток стал причиной фатального нарушения доступа, это может привести к завершению всего процесса. Процессы изолированы друг от друга. Если процесс разрушит свое адресное пространство, проблемы ограничатся этим процессом. Процесс может допустить нарушение доступа, которое приведет к его завершению, но все остальные процессы будут продолжать выполнение. Это нарушение не окажется фатальным для всего приложения. Ошибки, связанные с некорректностью данных, могут не выйти за рамки одного процесса. Но ошибки, вызванные некорректньш поведением потока, как правило, гораздо серьезнее ошибок, допущенных процессом. Потоки могут стать причиной ошибок, которые повлияют на все адресное пространство всех потоков. Процессы защищают свои ресурсы от беспорядочного доступа со стороны других процессов. Потоки же совместно используют ресурсы со всеми остальными потоками процесса. Поэтому поток, разрушающий ресурсы, оказывает негативное влияние на процесс или программу в целом. 4.1.4.3. Потоки не могут многократно использоваться другими программами Потоки зависят от процесса, в котором они существуют, и их невозможно от него отделить. Процессы отличаются большей степенью независимости, чем потоки. Приложение можно так разделить на задачи, порученные процессам, что эти процессы можно оформить в виде модулей, которые возможно использовать в других приложениях. Потоки не могут существовать вне процессов, в которых они были созданы и, следовательно, они не являются повторно используемыми. Преимущества и недостатки потоков сведены в табл. 4.2. 4.2. Анатомия потока Образ потока встраивается в образ процесса. Как было описано в главе 3, процесс имеет разделы программного кода, данных и стеков. Поток разделяет разделы кода и данных с остальными потоками процесса. Каждый поток имеет собственный стек, выделенный ему в стековом разделе адресного пространства процесса. Размер потокового стека устанавливается при создании потока. Если создатель потока не определяет размер его стека, то система назначает размер по умолчанию. Размер, устанавливаемый по умолчанию, зависит от конкретной системы, максимально возможного количества потоков в процессе, размера адресного пространства, выделяемого процессу, и пространства, используемого системными ресурсами. Размер потокового стека должен быть достаточно большим для любых функций, вызываемых потоком, Юоого кода, который является внешним по отношению к процессу (например, это • Южет быть библиотечный код), и хранения локальных переменных. Процесс с не- °лькими потоками должен иметь стековый раздел, который будет вмещать все сте- его потоков. Адресное пространство, выделенное для процесса, ограничивает раз- Р стека, ограничивая тем самым размер, который может иметь каждый поток. На \- показана схема процесса, который содержит два потока. Клк показано на рис. 4.3, процесс содержит два потока А и В, и их стеки располо- ы в стековом разделе процесса. Потоки выполняют различные функции: поток А Ь1Полняет функцию fund (), а поток В - функцию func2 ().
120 Глава 4. Разбиение С++-программ на множество потоков scope = process stack size = 1000 priority = 2 joinable II... funcIO Count'= 10 АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА РАЗДЕЛ СТЕКОВ "^1 Ътек потока Threads "^ ~~ I func20 Count'-100 Стек потока ThreadA main() -.РАЗДЁД..ДАННЫХ ThreadA ThreadB X Y AttrObj .РАЗДЕЛ-КОДА.. void fune1(...) Count- 10; void tunc2(.,.: { Counts 10C pthreaoM ThreadA; pthreadj ThreadB; int X; int Y: pthreadjattrj AttrObj; main() it //... pthread_attrjnit(&AttrObj); pthread_create(&ThreadAf&AttrObj.func1, pthread_create{&ThreadB.&AttrObj!func2....};t| Рис. 4.3. Схема процесса, содержащего два потока (SP — указатель стека, PC — счетчик команд)
4.2. Анатомия потока 121 Таблица 4.2. Преимущества и недостатки потоков Преимущества потоков Недостатки потоков Для переключения контекста требуется меньше системных ресурсов Потоки способны повысить производительность приложения Для обеспечения взаимодействия между потоками никакого специального механизма не требуется Благодаря потокам структуру программы можно упростить Для параллельного доступа к памяти (чтения или записи данных) требуется синхронизация Потоки могут разрушить адресное пространство своего процесса Потоки существуют в рамках только одного процесса, поэтому их нельзя повторно использовать 4.2.1. Атрибуты потока Атрибуты процесса содержат информацию, которая описывает процесс для операционной системы. Операционная система использует эту информацию для управления процессами, а также для того, чтобы отличать один процесс от другого. Процесс совместно использует со своими потоками практически все, включая ресурсы и переменные среды. Разделы данных, раздел программного кода и все ресурсы связаны с процессом, а не с потоками. Все, что нужно для функционирования потока, определяется и предоставляется процессом. Потоки же отличаются один от другого идентификационным номером (id), набором регистров, определяющих состояние потока, его приоритетом и стеком. Именно эти атрибуты формируют уникальность каждого потока. Как и при использовании процессов, информация о потоках хранится в структурах данных и возвращается функциями, поддерживаемыми операционной системой. Например, часть информации о потоке содержится в структуре, именуемой информационным блоком потока, который создается вместе с потоком. Идентификационный номер (id) потока— это уникальное значение, которое идентифицирует каждый поток во время его существования в процессе. Приоритет потока определяет, каким потокам предоставлен привилегированный доступ к процессору в выделенное время. Под состоянием потока понимаются условия, в которых он пребывает в любой момент времени. Набор регистров для потока включает программный счетчик и указатель стека. Программный счетчик содержит адрес инструкции, которую поток должен выполнить, а указатель стека ссылается на вершину стека потока. Библиотека потоков POSIX определяет объект атрибутов потока, инкапсулирующий свойства потока, к которым его создатель может получить доступ и модифицировать их. Объект атрибутов потока определяет следующие компоненты: область видимости; размер стека; адрес стека; приоритет; состояние; стратегия планирования и параметры.
122 Глава 4. Разбиение С++-программ на множество потоков Объект атрибутов потока может быть связан с одним или несколькими потоками. При использовании этого объекта поведение потока или группы потоков определяется профилем. Все потоки, которые используют объект атрибутов, приобретают все свойства, определенные этим объектом. На рис. 4.3 показаны атрибуты, связанные с каждым потоком. Как видите, оба потока (А и В) разделяют объект атрибутов, но они поддерживают свои отдельные идентификационные номера и наборы регистров. После того как объект атрибутов создан и инициализирован, его можно использовать в любых обращениях к функциям создания потоков. Следовательно, можно создать группу потоков, которые будут иметь "малый стек и низкий приоритет" или "большой стек, высокий приоритет и состояние открепления". Открепленный (detached) поток— это поток, который не синхронизирован с другими потоками в процессе. Иначе говоря, не существует потоков, которые бы ожидали до тех пор, пока завершит выполнение открепленный поток. Следовательно, если уж такой поток существует, то его ресурсы (а именно id потока) немедленно принимаются на повторное использование. Для установки и считывания значений этих атрибутов предусмотрены специальные методы. После создания потока его атрибуты нельзя изменить до тех пор, пока он существует. Атрибут области видимости описывает, с какими потоками конкретный поток конкурирует за обладание системными ресурсами. Потоки соперничают за ресурсы в рамках двух областей видимости: процесса (потоки одного процесса) и системы (все потоки в системе). Конкуренция потоков в пределах одного и того же процесса происходит за дескрипторы файлов, а конкуренция потоков в масштабе всей системы — за ресурсы, которые выделяются системой (например, реальная память). Потоки соперничают с потоками, которые имеют область видимости процесса, и потоками из других процессов за использование процессора в зависимости от состязательного режима и областей выделения ресурсов (набора процессоров). Поток, обладающий системной областью видимости, будет обслуживаться с учетом его приоритета и стратегии планирования, которая действует для всех потоков в масштабе всей системы. Члены POSIX-объекта атрибутов потока перечислены в табл. 4.3. Таблица 4.3. Члены объекта атрибутов потока Атрибуты Функции Описание detachstate guardsize inheritsched int pthread_attr_ setdetachstate (pthread_attr_t *attr, int detachstate); int pthread_attr_ setguardsize (pthread_attr_t *attr, size_t guardsize) int pthread_attr_ setinheritsched (pthread_attr_t *attr, int inheritsched) Атрибут detachstate определяет, является ли новый поток открепленным. Если это соответствует истине, то его нельзя объединить ни с каким другим потоком Атрибут guardsize позволяет управлять размером защитной области стека нового потока. Он создает буферную зону размером guardsize на переполненяемом конце стека Атрибут inheritsched определяет, как будут установлены атрибуты планирования для нового потока, т.е. будут ли они унаследованы от потока- создателя или установлены атрибутным объектом
4.3. Планирование потоков 123 Окончание табл. 4.3 Атрибуты param Функць Описание schedpolicy contentionscope stackaddr stacksize stackaddr stacksize int pthread_attr_ setschedparam (pthread_attr_t *restrict attr, const struct sched_param *restrict param); int pthread_attr_ setschedpolicy (pthread_attr_t *attr, int policy); int pthread_attr_ setscope (pthread_attr_t *attr, int contentionscope); int pthread_attr_ setstack (pthread_attr_t *attr, void *stackaddr, size_t stacksize), int pthread_attr_ setstackaddr (pthread_attr_t *attr, void *stackaddr); int pthread_attr_ setstacksize (pthread_attr_t *attr, size_t stacksize) Атрибут param— это структура, которую можно использовать для установки приоритета нового потока Атрибут schedpolicy определяет стратегию планирования создаваемого потока Атрибут contentionscope определяет, с каким множеством потоков будет соперничать создаваемый поток за использование процессорного времени. Область видимости процесса означает, что поток будет состязаться со множеством потоков одного процесса, а область видимости системы означает, что поток будет состязаться с потоками в масштабе всей системы (т.е. сюда входят потоки других процессов) Атрибуты stackaddr и stacksize определяют базовый адрес и минимальный размер (в байтах) стека, выделяемого для создаваемого потока, соответственно Атрибут stackaddr определяет базовый адрес стека, выделяемого для создаваемого потока Атрибут stacksize определяет минимальный размер стека в байтах, выделяемого для создаваемого потока 4.3. Планирование потоков огда подходит время для выполнения процесса, процессор занимает один из его оков. Если процесс имеет только один поток, то именно он (т.е. основной поток) на- ается процессору. Если процесс содержит несколько потоков и в системе есть доста- °е количество процессоров, то процессорам назначаются все потоки. Потоки сопер- ак>т за процессор либо со всеми потоками из активного процесса системы, либо только
124 Глава 4. Разбиение С++-программ на множество потоков с потоками из одного процесса. Потоки помещаются в очереди готовых потоков, отсортированные по значению их приоритета. Потоки с одинаковым приоритетом назначаются процессорам в соответствии с некоторой стратегией планирования. Если система не содержит достаточного количества процессоров, поток с более высоким приоритетом может выгрузить поток, выполняющийся в данный момент. Если новый активный поток принадлежит тому же процессу, что и выгруженный, возникает переключение контекста потоков. Если же новый активный поток "родом" из другого процесса, то сначала происходит переключение контекста процессов, а затем — контекста потоков. 4.3.1. Состояния потоков Потоки имеют такие же состояния и переходы между ними (см. главу 3), как и процессы. Диаграмма состояний, показанная на рис. 4.4, — это копия диаграммы, изображенной на рис. 3.4 из главы 3. (Вспомним, что процесс может пребывать в одном из четырех состояний: готовности, выполнения, останова и ожидания, или блокирования.) Состояние потока — это режим или условия, в которых поток существует в данный момент. Поток находится в состоянии готовности (работоспособности), когда он готов к выполнению. Все готовые к работе потоки помещаются в очереди готовности, причем в каждой такой очереди содержатся потоки с одинаковым приоритетом. Когда поток выбирается из очереди готовности и назначается процессору, он (поток) переходит в состояние выполнения. Поток снимается с процессора, если его квант времени истек, или если перешел в состояние готовности поток с более высоким приоритетом. Выгруженный поток снова помещается в очередь готовых потоков. Поток пребывает в состоянии ожидания, если он ожидает наступления некоторого события или завершения операции ввода-вывода. Поток прекращает выполнение, получив сигнал останова, и остается в этом состоянии до тех пор, пока не получит сигнал продолжить работу. ОСТАНОВЛЕН Выдан сигнал/ Выдан сигнал Выгрузка Вх°Д ^( ГОТОВ (работоспособен) Произошло I событие или\ завершена операция ввода-вывода Загрузка ВЫПОЛНЯЕТСЯ Выход J<OHeu кванта времени Ожидание события или } завершения операции ввода-вывода ОЖИДАЕТ Прекращение Выход "ЗОМБИ" Рис. 4.4. Состояния потоков и переходы между ними
4.3. Планирование потоков 125 При получении этого сигнала поток переходит из состояния останова в состояние готовности. Переход потока из одного состояния в другое является своего рода сигналом о наступлении некоторого события. Переход конкретного потока из состояния готовности состояние выполнения происходит потому, что система выбрала именно его для выполнения, т.е. поток отправляется (dispatched) на процессор. Поток снимается, или выгружается (preempted), с процессора, если он делает запрос на ввод-вывод данных (или какой- либо иной запрос к ядру), или если существуют какие-то причины внешнего характера. Один поток может определить состояние всего процесса. Состояние процесса с одним потоком синонимично состоянию его основного потока. Если его основной поток находится в состоянии ожидания, значит, и весь процесс находится в состоянии ожидания. Если основной поток выполняется, значит, и процесс выполняется. Что касается процесса с несколькими потоками, то для того, чтобы утверждать, что весь процесс находится в состоянии ожидания или останова, необходимо, чтобы все его потоки пребывали в состоянии ожидания или останова. Но если хотя бы один из его потоков активен (т.е. готов к выполнению или выполняется), процесс считается активным. 4.3.2. Планирование потоков и область конкуренции Область конкуренции потоков определяет, с каким множеством потоков будет соперничать рассматриваемый поток за использование процессорного времени. Если поток имеет область конкуренции уровня процесса, он будет соперничать за ресурсы с потоками того же самого процесса. Если же поток имеет системную область конкуренции, он будет соперничать за процессорный ресурс с равными ему по правам потоками (из одного с ним процесса) и с потоками других процессов. Пусть, например, как показано на рис. 4.5, существуют два процесса в мультипроцессорной среде, которая включает три процессора. Процесс А имеет четыре потока, а процесс В — три. Для процесса А "расстановка сил" такова: три (из четырех его потоков) имеют область конкуренции уровня процесса, а один — уровня системы. Для процесса В такая "картина": два (из трех его потоков) имеют область конкуренции уровня процесса, а один— уровня системы. Потоки процесса А с процессной областью конкуренции соперничают за процессор А, а потоки процесса В с такой же (процессной) областью конкуренции соперничают за процессор С. Потоки процессов А и В с системной областью конкуренции соперничают за процессор В. ПРИМЕЧАНИЕ: потоки при моделировании их реального поведения в приложении Должны иметь системную область конкуренции. 4.3.3. Стратегия планирования и приоритет Стратегия планирования и приоритет процесса принадлежат основному потоку, 'каждый поток (независимо от основного) может иметь собственную стратегию планирования и приоритет. Потокам присваиваются целочисленные значения приоритета, °торые лежат в диапазоне между заданными минимальным и максимальным значения- и. Схема приоритетов используется при определении, какой поток следует назначить роцессору: поток с более высоким приоритетом выполняется раньше потока с более изким приоритетом. После назначения потокам приоритетов задачам, которые тре- У^т немедленного выполнения или ответа от системы, предоставляется необходимое
126 Глава 4. Разбиение С++-программ на множество потоков процессорное время. В операционной системе с приоритетами выполняющийся поток снимается с процессора, если в состояние готовности переходит поток с более высоким приоритетом, обладающий при этом тем же уровнем области конкуренции. Например как показано на рис. 4.5, потоки с процессной областью конкуренции соревнуются за процессор с потоками того же процесса, имеющими такой же уровень области конкуренции. Процесс А имеет два потока с приоритетом 3, и один из них назначен процессору. Как только поток с приоритетом 2 заявит о своей готовности, активный поток будет вытеснен, а процессор займет поток с более высоким приоритетом. Кроме того, в процессе В есть два потока (процессной области конкуренции) с приоритетом 1 (приоритет 1 выше приоритета 2). Один из этих потоков назначается процессору. И хотя другой поток с приоритетом 1 готов к выполнению, он не вытеснит поток с приоритетом 2 из процесса А, поскольку эти потоки соперничают за процессор в рамках своих процессов. Потоки с системной областью конкуренции и более низким приоритетом не вытесняются ни одним из потоков из процессов А или В. Они соперничают за процессорное время только с потоками, имеющими системную область конкуренции. ПРОЦЕСС А ОБЛАСТЬ ВИДИМОСТИ ПРОЦЕССА Т1 Т2 ТЗ ПРИОР ПРИОР ОБЛАСТЬ ВИДИМОСТИ СИСТЕМЫ ОБЛАСТЬ ВИДИМОСТИ ПРОЦЕССА Т1 Т2 | ПР¥\ОР РА РВ PC Рис. 4.5. Планирование потоков (с процессной и системной областями конкуренции) в мультипроцессорной среде Как упоминалось в главе 3, очереди готовности организованы в виде отсортированных списков, в которых каждый элемент представляет собой уровень приоритета. Под уровнем приоритета понимается очередь потоков с одинаковым значением приоритета. Все потоки одного уровня приоритета назначаются процессору с использованием стратегии планирования: FIFO (сокр. от First In First Out, т.е. первым прибыл, первым обслужен), RR (сокр. от round-robin, т.е. циклическая) или какой-либо другой. При использовании
4.3. Планирование потоков 127 и планирования FIFO поток, квант процессорного времени которого истек, по- СТР гется в головную часть очереди соответствующего приоритетного уровня, а процес- МС назначается следующему потоку из очереди. Следовательно, поток будет выполняться тех пор, пока он не завершит выполнение, не перейдет в состояние ожидания Г снет") или не получит сигнал остановиться. Когда "спящий" поток "просыпается", он метается в конец очереди соответствующего приоритетного уровня. Стратегия плани- вания RR аналогична FIFO-стратегии, за исключением того, что по истечении кванта оиессорного времени поток помещается не в начало, а в конец "своей" очереди. Циклическая стратегия планирования (RR) считает все потоки обладающими одинаковыми приоритетами и каждому потоку предоставляет процессор только в течение некоторого кванта времени. Поэтому выполнение задач получается попеременным. Например, программа, которая выполняет поиск файлов по заданным ключевым словам, разбивается на два потока. Один поток (1) находит все файлы с заданным расширением и помещает их пути в контейнер. Второй поток (2) выбирает имена файлов из контейнера, просматривает каждый файл на предмет наличия в нем заданных ключевых слов, а затем записывает имена файлов, которые содержат такие слова. Если к этим потокам применить циклическую стратегию планирования с единственным процессором, то поток 1 использовал бы свой квант времени для поиска файлов и вставки их путей в контейнер. Поток 2 использовал бы свой квант времени для выделения имен файлов и поиска заданных ключевых слов. В идеальном мире потоки 1 и 2 должны выполняться попеременно. Но в действительности все может быть иначе. Например, поток 2 может выполниться до потока 1, когда в контейнере еще нет ни одного файла, или поток 1 может так долго искать файл, что до истечения кванта времени не успеет записать его путь в контейнер. Такая ситуация требует синхронизации, краткое рассмотрение которой приводится ниже в этой главе и в главе 5. Стратегия планирования FIFO позволяет каждому потоку выполняться до завершения. Если рассмотреть тот же пример с использованием FIFO-стратегии, то поток 1 будет иметь достаточно времени, чтобы отыскать все нужные файлы и вставить их пути в контейнер. Поток 2 затем выделит имена файлов и выполнит поиск заданных ключевых слов. В идеальном мире завершение выполнения потока 2 будет означать завершение программы в целом. Но в реальном мире поток 2 может быть назначен процессору до потока 1, когда контейнер еще не будет содержать файлов для поиска в них ключевых слов. После "холостого" выполнения потока 2 процессору будет назначен поток 1, который может успешно отыскать нужные файлы и поместить в контейнер их пути. Однако поиск ключевых слов выполнять уже будет некому. Поэтому программа в целом потерпит фиаско. При использовании FIFO-стратегии не предусматривается перемешивания задач. Поток, назначенный процессору, занимает его До полного выполнения своей задачи. Такую стратегию планирования можно исполь- овать для приложений, в которых потоки необходимо выполнить как можно скорее. °Д Другими" стратегиями планирования подразумеваются уже рассмотренные, но небольшими вариациями. Например, FIFO-стратегия может быть изменена таким °разом, чтобы позволить разблокировать потоки, выбранные случайно. 4-3.3.1. Изменение приоритета потоков приоритеты потоков следует менять, чтобы ускорить выполнение потоков, от кото- г зависит выполнение других потоков. И, наоборот, этого не следует делать ради того, bi какой-то конкретный поток получил больше процессорного времени. Это может
128 Глава 4. Разбиение С++-программ на множество потоков изменить обшую производительность системы. Потоки с более высоким классом приоритета получают больше процессорного времени, чем потоки с более низким классом приоритета, поскольку они выполняются чаще. Потоки с более высоким приоритетом практически монополизируют процессор, не выделяя потокам с более низким приоритетом такое ценное процессорное время. Эта ситуация получила название информационного голода (starvation). Системы, в которых используются механизмы динамического назначения приоритетов, реагируют на подобную ситуацию путем назначения приоритетов, которые бы действовали в течение коротких периодов времени. Система регулирует приоритет потоков таким образом, чтобы потоки с более низким приоритетом увеличили время выполнения. Такой подход должен повысить общую производительность системы. Гарантировать, что конкретный процесс или поток будет выполняться до его полного завершения, — все равно что присвоить ему самый высокий приоритет. Однако реализация такой стратегии может повлиять на общую производительность системы. Такие привилегированные потоки могут нарушить взаимодействие программных компонентов через сетевые средства коммуникации, вызвав потерю данных. На потоки, которые управляют интерфейсом пользователя, может быть оказано чрезмерно большое влияние, выраженное в замедлении реакции на использование клавиатуры, мыши или экрана. В некоторых системах пользовательским процессам или потокам не назначается более высокий приоритет, чем системным процессам. В противном случае системные процессы или потоки не смогли бы реагировать на критические изменения в системе. Поэтому большинство пользовательских процессов и потоков попадают в категорию программных компонентов с нормальным (средним) приоритетом. 4.4. Ресурсы потоков Потоки используют большую часть своих ресурсов вместе с другими потоками из того же процесса. Собственные ресурсы потока определяют его контекст. Так, в контекст потока входят его идентификационный номер, набор регистров (включающих указатель стека и программный счетчик) и стек. Остальные ресурсы (процессор, память и файловые дескрипторы), необходимые потоку для выполнения его задачи, он должен разделять с другими потоками. Дескрипторы файлов выделяются каждому процессу в отдельности, и потоки одного процесса соревнуются за доступ к этим дескрипторам. Что касается памяти, процессора и других глобально распределяемых ресурсов, то за доступ к ним потоки конкурируют с другими потоками своего процесса, а также с потоками других процессов. Поток при выполнении может запрашивать дополнительные ресурсы, например, файлы или мьютексы, но они становятся доступными для всех потоков процесса. Существуют ограничения на ресурсы, которые может использовать один процесс. Таким образом, все потоки в общей сложности не должны превышать предельный объем ресурсов, выделяемых процессу. Если поток попытается расходовать больше ресурсов, чем предусмотрено предельным объемом, формируется сигнал о том, что достигнут предельный объем ресурсов для данного процесса. Потоки, которые используют ресурсы, должны следить за тем, чтобы эти ресурсы не оставались в нестабильном состоянии после их аннулирования. Поток, который открыл файл или создал мьютекс, может завершиться, оставив этот файл открытым или мьютекс заблокированным. Если приложение завершилось, а файл не был закрыт надлежащим
4.5. Модели создания и функционирования потоков 129 м это может привести к его разрушению или потере данных. Завершение по- после блокировки мьютекса надежно запирает доступ к критическому разделу, ооый находится под контролем этого мьютекса. Перед завершением поток должен полнить некоторые действия очистительно-восстановительного характера, чтобы допустить возникновения нежелательных ситуаций. 4.5. Модели создания и функционирования потоков Цель потока— выполнить некоторую работу от имени процесса. Если процесс содержит несколько потоков, каждый поток выполняет некоторые подзадачи как части общей задачи, выполняемой процессом. Потокам делегируется работа в соответствии с конкретной стратегией, которая определяет, каким образом реализуется делегирование работы. Если приложение моделирует некоторую процедуру или объект, то выбранная стратегия должна отражать эту модель. Используются следующие распространенные модели: • делегирование ("управляющий-рабочий"); • сеть с равноправными узлами; • конвейер; • "изготовитель-потребитель". Каждая модель характеризуется собственной декомпозицией работ (Work Breakdown Structure — WBS), которая определяет, кто отвечает за создание потоков и при каких условиях они создаются. Например, существует централизованный подход, при котором один поток создает другие потоки и каждому из них делегирует некоторую работу. Существует также конвейерный (assembly-line) подход, при котором на различных этапах потоки выполняют различную работу. Созданные потоки могут выполнять одну и ту же задачу на различных наборах данных, различные задачи на одном и том же наборе данных или различные задачи на различных наборах данных. Потоки подразделяются на категории по выполнению задач только определенного типа. Например, можно создать группы потоков, которые будут выполнять только вычисления, только ввод или только вывод данных. Возможны задачи, для успешного решения которых следует комбинировать перечисленные выше модели. В главе 3 мы рассматривали процесс визуализации. Задачи 1, 2 и 3 выполнялись последовательно, а задачи 4, 5 и 6 могли выполняться параллельно. Все задачи можно выполнить различными потоками. Если необходимо визуализировать несколько изображений, потоки 1, 2 и 3 могут сформировать кон- Веиер. По завершении потока 1 изображение передается потоку 2, в то время ак поток 1 может выполнять свою работу над следующим изображением. После }феризации изображений потоки 4, 5 и 6 могут реализовать параллельную обра- тку. Модель функционирования потоков представляет собой часть структуриро- ания параллелизма в приложении, в котором каждый поток может выполняться на Дельном процессоре. Модели функционирования потоков (и их краткое описа- Ние) приведены в табл. 4.4.
130 Глава 4. Разбиение С++-программ на множество потоков Таблица 4.4. Модели функционирования потоков Модель Описание Модель Центральный поток ("управляющий") создает потоки ("рабочие"), на- делегирования значая каждому из них задачу. Управляющий поток может ожидать до тех пор, пока все потоки не завершат выполнение своих задач Модель с равно- Все потоки имеют одинаковый рабочий статус. Такие потоки называют- правными узлами ся равноправными. Поток создает все потоки, необходимые для выполнения задач, но не осуществляет никакого делегирования ответственности. Равноправные потоки могут обрабатывать запросы от одного входного потока данных, разделяемого всеми потоками, или каждый поток может иметь собственный входной поток данных Конвейер Конвейерный подход применяется для поэтапной обработки потока входных данных. Каждый этап — это поток, который выполняет работу на некоторой совокупности входных данных. Когда эта совокупность пройдет все этапы, обработка всего потока данных будет завершена Модель Поток-"изготовитель" готовит данные, потребляемые потоком- "изготовителъ- "потребителем". Данные сохраняются в блоке памяти, разделяемом потребитель" потоками — "изготовителем" и "потребителем" 4.5.1. Модель делегирования В модели делегирования один поток ("управляющий") создает потоки ("рабочие") и назначает каждому из них задачу. Управляющему потоку нужно ожидать до тех пор, пока все потоки не завершат выполнение своих задач. Управляющий поток делегирует задачу, которую каждый рабочий поток должен выполнить, путем задания некоторой функции. Вместе с задачей на рабочий поток возлагается и ответственность за ее выполнение и получение результатов. Кроме того, на этапе получения результатов возможна синхронизация действий с управляющим (или другим) потоком. Управляющий поток может создавать рабочие потоки в результате запросов, обращенных к системе. При этом обработка запроса каждого типа может быть делегирована рабочему потоку. В этом случае управляющий поток выполняет некоторый цикл событий. По мере возникновения событий рабочие потоки создаются и на них тут же возлагаются определенные обязанности. Для каждого нового запроса, обращенного к системе, создается новый поток. При использовании такого подхода процесс может превысить предельный объем выделенных ему ресурсов или предельное количество потоков. В качестве альтернативного варианта управляющий поток может создать пул потоков, которым будут переназначаться новые запросы. Управляющий поток создает во время инициализации некоторое количество потоков, а затем каждый поток приостанавливается до тех пор, пока не будет добавлен запрос в их очередь. По мере размещения запросов в очереди управляющий поток сигнализирует рабочему о необходимости обработки запроса. Как только поток справится со своей задачей, он извлекает из очереди следующий запрос. Если в очереди больше нет доступных запросов, поток приостанавливается до тех пор. пока управляющий поток не просигналит ему о появлении очередного задания в очереди. Если все рабочие потоки должны разделять одну очередь, то их можно
4.5. Модели создания и функционирования потоков 131 ограммировать на обработку запросов только определенного типа. Если тип 3 оса в очереди не совпадает с типом запросов, на обработку которых ориенти- 33 ан данный поток, то он может снова приостановиться. Главная цель управляю- ^.—^ потока— создать все потоки, поместить задания в очередь и "разбудить" рабо шего потоки, когда эти задания станут доступными. Рабочие потоки справляются наличии запроса в очереди, выполняют назначенную задачу и приостанавливают- ми, еСЛи для них больше нет работы. Все рабочие и управляющий потоки вы- лняются параллельно. Описанные два подхода к построению модели делегирования представлены для сравнения на рис. 4.6. Мде?ь делегирования 1 Управляющий поток создает новый поток для каждого нового запроса. ПРОГРАММА А поток. Запрос 1 -—- | Поток данных -»; Запрос 2 - Запрос 3 - Запрос Н- создает Поток 1 обрабатывает ь Поток 2 обрабатывает Поток 3 обрабатывает Поток N обрабатывает WMtfBI I Модель делегирования 2 Управляющий поток создает пул потоков, которые обрабатывают все запросы. ПРОГРАММА В рЧоток Данных Управляющий поток Пул потоков - Запрос типа А ■ Запрос типа А ■ Запрос типа С- Запрос типа В ■ создает ► Поток 1 обрабатывает запрос типа А Поток 2 обрабатывает запрос типа В Поток 3 обрабатывает запрос типа С 1 • Два подхода к реализации модели делегирования
132 Глава 4. Разбиение С++-программ на множество потоков 4.5.2. Модель с равноправными узлами Если в модели делегирования есть управляющий поток, который делегирует задачи рабочим потокам, то в модели с равноправными узлами все потоки имеют одинаковый рабочий статус. Несмотря на существование одного потока, который изначально создает все потоки, необходимые для выполнения всех задач, этот поток считается рабочим потоком, но он не выполняет никаких функций по делегированию задач. В этой модели нет никакого централизованного потока, но на рабочие потоки возлагается большая ответственность. Все равноправные потоки могут обрабатывать запросы из одного входного потока данных, либо каждый рабочий поток может иметь собственный входной поток данных, за который он отвечает. Входной поток данных может также храниться в файле или базе данных. Рабочие потоки могут нуждаться во взаимодействии и разделении ресурсов. Модель равноправных потоков представлена на рис. 4.7. ПРОГРАММА А - .Узловой поток Узел 1 ———, Узел 2 УэелЗ создает L Поток 1 Поток 2 Поток 3 <— Поток входных данных, " разделяемый потоками выполнения Узловой поток Узел 1 Узел 2 Узел 3 создает ПРОГРАММА В Поток 1 обрабатывает входной поток А У каждого потока есть собственный входной поток Ц Поток 2 обрабатывает входной поток В Поток 3 обрабатывает | входной поток С Рис. 4.7. Модель равноправных потоков (или модель с равноправными узлами) 4.5.3. Модель конвейера Модель конвейера подобна ленте сборочного конвейера в том, что она предполагает наличие потока элементов, которые обрабатываются поэтапно. На каждом этапе отдельный поток выполняет некоторые операции над определенной совокупностью входных данных. Когда эта совокупность данных пройдет все этапы, обработка всего входного
4.5. Модели создания и функционирования потоков 133 а данных будет завершена. Этот подход позволяет обрабатывать несколько вход- П° потоков одновременно. Каждый поток отвечает за получение промежуточных ре- нь*х и . * татов, делая их доступными для следующего этапа (или следующего потока) конвей- Последний этап (или поток) генерирует результаты работы конвейера в целом. По мере того как входные данные проходят по конвейеру, не исключено, что неко- е иХ порции придется буферизировать на определенных этапах, пока потоки еще нимаются обработкой предыдущих порций. Это может вызвать торможение конвейе- если окажется, что обработка данных на каком-то этапе происходит медленнее, чем на'других. При этом образуется отставание в работе. Чтобы предотвратить отставание, можно для "слабого*' этапа создать дополнительные потоки. Все этапы конвейера должны быть уравновешены по времени, чтобы ни один этап не занимал больше времени, чем другие. Для этого необходимо всю работу распределить по конвейеру равномерно. Чем больше этапов в конвейере, тем больше должно быть создано потоков обработки. Увеличение количества потоков также может способствовать предотвращению отставаний в работе. Модель конвейера представлена на рис. 4.8. ПРОГРАММА Поток -* данных Поток 1 обрабатывает входные данные 6 ЭТАП1 Поток 1 обрабатывает входные данные 5 ЭТАП 2 БУФЕР Ш 3 4 Ш-с- ш Поток 3 II обрабатывает L входные данные 2 | ЭТАП 3 Обработка входных данных 1 завершена Рис. 4.8. Модель конвейера 4.5.4. Модель "изготовитель-потребитель" В модели "изготовитель-потребитель" существует поток-"изготовитель", который готовит данные, потребляемые потоком-"потребителем". Данные сохраняются в блоке памяти, разделяемом между потоками "изготовителем" и "потребителем". Поток- изготовитель" должен сначала приготовить данные, которые затем поток- ^потребитель" получит. Такому процессу необходима синхронизация. Если поток- изготовитель" будет поставлять данные гораздо быстрее, чем поток-"потребитель" сможет их потреблять, поток-"изготовитель" несколько раз перезапишет результаты, °лученные им ранее, прежде чем поток-"потребитель" успеет их обработать. Но если оток- потребитель" будет принимать данные гораздо быстрее, чем поток- зготовитель" сможет их поставлять, поток-"потребитель" будет либо снова обраба- шать уже обработанные им данные, либо попытается принять еще не подготовлен- е Данные. Модель "изготовитель-потребитель" представлена на рис. 4.9. 4-5.5. Модели SPMD и MPMD для потоков каждой из описанных выше моделей потоки вновь и вновь выполняют одну и ту задачу на различных наборах данных или им назначаются различные задачи для SPK/m Нения на различных наборах данных. Эти потоковые модели используют схемы и (Single-Program, Multiple-Data— одна программа, несколько потоков данных)
134 Глава 4. Разбиение С++-программ на множество потоков и MPMD (Multiple-Programs, Multiple-Data — множество программ, множество потоков данных). Эти схемы представляют собой модели параллелизма, которые делят программы на потоки инструкций и данных. Их можно использовать для описания типа работы, которую реализуют потоковые модели с использованием параллелизма. В контексте нашего изложения материала модель MPMD лучше представить как модель M7MD (Multiples-Threads, Multiple-Data— множество потоков выполнения, множество потоков данных). Эта модель описывает систему с различными потоками выполнения (thread), которые обрабатывают различные наборы данных, или потоки данных (stream). Аналогично модель SPMD нам лучше рассматривать как модель S7MD (Single- Thread, Multiple- Data— один поток выполнения, несколько потоков данных). Эта модель описывает систему с одним потоком выполнения, который обрабатывает различные наборы, или потоки, данных. Это означает, что различные наборы данных обрабатываются несколькими идентичными потоками выполнения (вызывающими одну и ту же подпрограмму). ПРОГРАММА Потребитель использует д Рис. 4.9. Модель конвейера Как модель делегирования, так и модель равноправных потоков могут использовать модели параллелизма STMD и MTMD. Как было описано выше, пул потоков может выполнять различные подпрограммы для обработки различных наборов данных. Такое поведение соответствует модели MTMD. Пул потоков может быть также настроен на выполнение одной и той же подпрограммы. Запросы (или задания), отсылаемые системе, могут представлять собой различные наборы данных, а не различные задачи. И в этом случае поведение множества потоков, реализующих одни и те же инструкции, но на различных наборах данных, соответствует модели STMD. Модель равноправных потоков может быть реализована в виде потоков, выполняющих одинаковые или различные задачи. Каждый поток выполнения может иметь собственный поток данных или несколько файлов сданными, предназначенных для обработки каждым потоком. В модели конвейера используется MTMD-модель параллелизма. На разных этапах выполняются различные виды обработки, поэтому в любой момент времени различные совокупности входных данных будут находиться на различных этапах выполнения. Модельное представление конвейера было бы бесполезным, если бы на каждом этапе выполнялась одна и та же обработка. Модели параллелизма STMD и MTMD представлены на рис. 4.10. 4.6. Введение в библиотеку Pthread Библиотека Pthread предоставляет API-интерфейс для создания и управления потоками в приложении. Библиотека Pthread основана на стандартизированном интерфейсе программирования, который был определен комитетом по выпуску стандартов IEEE в стандарте POSIX 1003.1с. Сторонние фирмы-изготовители придерживаются стандарта POSIX в реализациях, которые именуются библиотеками потоков Pthread или POSIX. Изготовитель создает и 1 БУФЕР Данны %■
4.6. Введение в библиотеку Pthread 135 Набор данных 1 | | Набор данных 2 Поток 1 выполняет .подпрограмму А j Поток 2 jвыполняет | подпрограмму А | Набор данных 1 | | Набор данных 2 Поток 1 | выполняет i подпрограмму А | I .Поток;2, j выполняет" I подпрограмму Э Рис. 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 она должна быть согласована со стандартизированным интерфей-
136 Глава 4. Разбиение С++-программ на множество потоков сом. Библиотека Pthread— не единственная реализация потокового API-интерфейса Существуют другие реализации, созданные сторонними фирмами-производителями аппаратных и программных средств. Например, среда Sun поддерживает библиотеку Pthread и собственный вариант библиотеки потоков Solaris. В этой главе мы рассмотри некоторые функции библиотеки Pthread, которые реализуют управление потоками. 4.7. Анатомия простой многопоточной программы Любая простая многопоточная программа должна состоять из основного потока и функций, которые будут выполнять другие потоки. Выбранная для реализации модель создания и функционирования потоков определяет, каким образом в программе будут создаваться потоки и как будет осуществляться управление ими. Потоки создаются по принципу "все и сразу" или при определенных условиях. Пример простой многопоточной программы, в которой реализована модель делегирования, представлен в листинге 4.1. // Листинг 4.1. Использование модели делегирования в // простой многопоточной программе #include <iostream> #include <pthread.h> void *taskl(void *X) // Определяем задачу для выполнения // потоком ThreadA. { //. .. cout « "Поток А завершен." « endl; } void *task2(void *X) // Определяем задачу для выполнения // потоком ThreadB. { //. . . cout « "Поток В завершен." « endl; } int main(int argc, char *argv[]) { pthread_t ThreadA,ThreadB; // Объявляем потоки. pthread_create(&ThreadA,NULL,taskl,NULL); // Создаем // потоки. pthread__create(&ThreadB,NULL,task2/NULL); // Дополнительная обработка. pthreadyjoin(ThreadA,NULL); // Ожидание завершения pthread_join(ThreadB,NULL); // потоков, return (0) ; } В листинге 4.1 делается акцент на определении набора инструкций для основного потока. Основным в данном случае является управляющий поток, который объявляет два рабочих потока ThreadA и ThreadB. С помощью функции pthread_create () эти два потока связываются с задачами, которые они должны выполнить (taskl и task2). Здесь (ради простоты примера) эти задачи всего лишь
4.7. Анатомия простой многопоточной программы 137 вляют сообщение в стандартный выходной поток, но понятно, что они могли °Т 6 ггь запрограммированы на нечто более полезное. При вызове функции d create () потоки немедленно приступают к выполнению назначенных Р задач. Работа функции pthread_join() аналогична работе функции wait () процессов. Основной поток ожидает до тех пор, пока не завершатся оба рабо- потока. Диаграмма последовательностей, соответствующая листингу 4.1, пока- на рис. 4.11. Обратите внимание на то, что происходит с потоками выполне- при вызове функций pthread_create () и pthread_j oin (). На рис. 4.11 показано, что вызов функции pthread_create () является причи- ой разветвления, или образования "вилки" в основном потоке выполнения, в ре- ьтате чего образуются два дополнительных "ручейка" (по одному для каждой задачи), которые выполняются параллельно. Функция pthread_create () завершается сразу же после создания потоков. Эта функция предназначена для создания асинхронных потоков. Это означает, что, как рабочие, так и основной поток, выполняют свои инструкции независимо друг от друга. Функция pthread_join () заставляет основной поток ожидать до тех пор, пока все рабочие потоки завершатся и "присоединятся" к основному. Основной поток Поток — выполнения « создать » ThreadA :создать: Ожидание присоединения Т : присоединиться: ThreadB )\««— Выход « присоединиться » Рис. 4.11. Диаграмма последовательностей, соответствующая листингу 4.1 4-7.1. Компиляция и компоновка многопоточных программ се многопоточные программы, использующие библиотеку потоков POSIX, долж- Ы Включать заголовок: ^thread. h>
138 Глава 4. Разбиение С++-программ на множество потоков Для компиляции многопоточного приложения в средах UNIX или Linux с помощью компиляторов командной строки д++ или дсс необходимо скомпоновать его с библиотекой Pthreads. Для задания библиотеки используйте опцию -1. Так, команда -lpthread обеспечит компоновку вашего приложения с библиотекой, которая согласуется с многопоточным интерфейсом, определенным стандартом POSIX 1003.1с. Библиотеку Pthread, libpthread. so, следует поместить в каталог, в котором хранится системная стандартная библиотека, обычно это /usr/lib. Если она будет находиться не в стандартном каталоге, то для того, чтобы обеспечить поиск компилятора в заданном каталоге до поиска в стандартных, используйте опцию -L. По команде д++ -о blackboard -L /src/local/lib blackboard.cpp -lpthread компилятор выполнит поиск библиотеки Pthread сначала в каталоге / src / local /1 ib, а затем в стандартных каталогах. Законченные программы, представленные в этой книге, сопровождаются профилем. Профиль программы содержит такие специальные сведения по ее реализации, как необходимые заголовки и библиотеки, а также инструкции по компиляции и компоновке. Профиль программы также включает раздел примечаний, содержащий специальную информацию, которую необходимо учитывать при выполнении программы. 4.8. Создание потоков Библиотека Pthreads используется для создания, поддержки и управления потоками многопоточных программ и приложений. При создании многопоточной программы потоки могут создаваться на любом этапе выполнения процесса, поскольку это — динамические образования. Функция pthread_create () создает новый поток в адресном пространстве процесса. Параметр thread указывает на дескриптор, или идентификатор (id), создаваемого потока. Новый поток будет иметь атрибуты, заданные объектом attr. Созданный поток немедленно приступит к выполнению инструкций, заданных параметром start_routine с использованием аргументов, заданных параметром arg. При успешном создании потока функция возвращает его идентификатор (id), значение которого сохраняется в параметре thread. Синопсис #include <pthread.h> int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), I void ^restrict arg) ; | Если параметр attr содержит значение NULL, новый поток будет использовать атрибуты, действующие по умолчанию. В противном случае новый поток использует атрибуты, заданные параметром attr при его создании. Если же значение параметра attr изменится после того, как поток был создан, это никак не отразится на его атрибутах. При завершении параметра-функции start_routine завершается и поток, причем так, как будто была вызвана функция pthread_exit () с использованием в качестве статуса завершения значения, возвращаемого функцией start_routine.
4.8. Создание потоков 139 При успешном завершении функция возвращает число 0. В противном случае по- не создается, и функция возвращает код ошибки. Если в системе отсутствуют ре- сы для создания потока или в процессе достигнут предел по количеству возмож- ых потоков, выполнение функции считается неудачным. Неудачным оно также будет в случае, если атрибут потока задан некорректно или если инициатор вызова потока имеет разрешения на установку необходимых атрибутов потока. Приведем примеры создания двух потоков с заданными по умолчанию атрибутами: thread_create(&threadA/NULL/ taskl,NULL) ; pthread_create (&threadB, NULL, task2, NULL) ; Это — два вызова функции pthread_create () из листинга 4.1. Оба потока создаются с атрибутами, действующими по умолчанию. В программе 4.1 отображен основной поток, который передает аргумент из командной строки в функции, выполняемые потоками. // Программа 4.1 #include <iostream> #include <pthread.h> #include <stdlib.h> int main(int argcf 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 представлена каждая из потоковых функций. 11 Программа 4.2 ^°id *taskl(void *X) int *Temp; Temp = static_cast<int *>(X); f°r(int Count = l;Count < *Temp;Count++){ cout « "В потоке A: " « Count « " * 2 = "
140 Глава 4. Разбиение С++-программ на множество потоков << Count * 2 « endl; } cout « "Поток А завершен." « endl; } void *task2(void *X) { int *Temp; Temp = static_cast<int *>(X); for(int Count = l;Count < *Temp;Count++){ cout « "В потоке В: " « Count « " + 2 = " << Count + 2 « endl; } cout << "Поток В завершен." « endl; } В программе 4.2 функции taskl и task2 выполняют цикл, количество итераций которого равно числу, переданному каждой функции в качестве параметра. Одна функция увеличивает переменную цикла на два, вторая — умножает ее на два, а затем каждая из них отправляет результат в стандартный поток вывода данных. По выходу из цикла каждая функция выводит сообщение о завершении выполнения потока. Инструкции по компиляции и выполнению программ 4.1 и 4.2 содержатся в профиле программы 4.1. [ Профиль программы 4.1 Имя программы ,program4-12.ее * Описание Принимает целочисленное значение из командной строки и передает функциям: потоков. Каждая функция выполняет цикл, в котором переменная цикла ' увеличивается (в одной функции на два, а в другой в два раза), а затем результат отсылается в стан-дартный поток вывода данных. Код основного потока выполнения приведен в программе 4.1, а код функций — в программе 4.2. Требуемая библиотека libpthread Требуемые заголовки <pthread.h> <iostream> <stdlib.h> Инструкции по компиляции и компоновке программ C++ -о program4-12 program4-12.ee -Ipthread Среда для тестирования SuSE Linux 7.1, gec 2.95.2, Инструкции по выполнению ./program4-12 34 Примечания Эта программа требует задания аргумента командной строки.
4.8. Создание потоков 141 r этом разделе был приведен пример передачи функции потока лишь одного аргу- нта. Если необходимо передать функции потока несколько аргументов, создайте тоуктуру (struct) или контейнер, содержащий все требуемые аргументы, и пере- айте функции потока указатель на эту структуру. 4.8.1. Получение идентификатора потока Как упоминалось выше, процесс разделяет все свои ресурсы с потоками, используя лишь собственное адресное пространство. Потокам в собственное пользование выделяются весьма небольшие их объемы. Идентификатор потока (id) — это один из ресурсов, уникальных для каждого потока. Чтобы узнать свой идентификатор, потоку необходимо вызвать функцию pthread_self (). Синопсис #include <pthread.h> pthread_t pthread_self(void); Эта функция аналогична функции getpid () для процессов. При создании потока его идентификатор возвращается его создателю или вызывающему потоку. Однако идентификатор потока не становится известным созданному потоку автоматически. Но если уж поток обладает собственным идентификатором, он может передать его (предварительно узнав его сам) другим потокам процесса. Функция pthread__self () возвращает идентификатор потока, не определяя никаких кодов ошибок. Вот пример вызова этой функции: //... pthread_t Threadld; Threadld = pthread_self(); Поток вызывает функцию pthread_self (), а значение, возвращаемое ею (идентификатор потока), сохраняет в переменной Threadld типа pthread_t. 4.8.2. Присоединение потоков Функция pthread_join () используется для присоединения или воссоединения потоков выполнения в одном процессе. Эта функция обеспечивает приостановку выпол- ения вызывающего потока до тех пор, пока не завершится заданный поток. По своему Действию эта функция аналогична функции wait (), используемой процессами. Эту функцию может вызвать создатель потока, после чего он будет ожидать до тех пор, пока завершится новый (созданный им) поток, что, образно говоря, можно назвать воссо- инением потоков выполнения. Функцию pthread_j oin () могут также вызывать ноправные потоки, если потоковый дескриптор является глобальным. Это позволя- любому потоку соединиться с любым другим потоком выполнения в процессе. Если шающий поток аннулируется до завершения заданного (для присоединения) потопа! Т°Т Заданный поток не станет открепленным (detached) потоком (см. следующий t, л'* Если различные равноправные потоки одновременно вызовут функцию а^Ooin () для одного и того же потока, его дальнейшее поведение не определено.
142 Глава 4. Разбиение С++-программ на множество потоков Синопсис 1 Uinclude <pthread.h> int pthread_join(pthread_t thread, void **value_ptr); Параметр thread представляет поток, завершения которого ожидает вызывающий поток. При успешном выполнении этой функции в параметре value_ptr будет записан статус завершения потока. Статус завершения — это аргумент, передаваемый при вызове функции pthread_exit () завершаемым потоком. При неудачном выполнении эта функция возвратит код ошибки. Функция не будет выполнена успешно, если заданный поток не является присоединяемым, т.е. создан как открепленный. Об успешном выполнении этой функции не может быть и речи, если заданный поток попросту не существует. Функцию pthread_join () необходимо вызывать для всех присоединяемых потоков. После присоединения потока операционная система сможет снова использовать память, которую он занимал. Если присоединяемый поток не был присоединен ни к одному потоку или если поток, который вызывает функцию присоединения, аннулируется, то заданный поток будет продолжать использовать свою память. Это состояние аналогично зомбированному процессу, в которое переходит сыновний процесс, когда его родитель не принимает статус завершения потомка, и этот "беспризорный" сыновний процесс продолжает занимать структуру в таблице процессов. 4.8.3. Создание открепленных потоков Открепленным называется завершаемый поток, который не присоединился или завершения которого не дождался другой поток. При завершении потока ограниченные ресурсы, которые он использовал, включая его идентификатор, освобождаются и возвращаются системе. Потокам нет необходимости получать статус завершения. Попытка со стороны любого потока вызвать функцию pthread_join () для открепленного потока обречена на неудачу. Существует функция pthread_detach (), которая открепляет поток, заданный параметром thread. По умолчанию все потоки создаются как присоединяемые, если атрибутным объектом не обусловлено иное. Эта функция позволяет открепить уже существующие присоединяемые потоки. Если поток не завершен, обращение к этой функции не обеспечит его завершения. Синопсис #include <pthread.h> I int pthread_detach(pthread_t thread thread); | При успешном выполнении эта функция возвращает число 0, в противном случае— код ошибки. Функция pthread_detach () не будет успешной, если заданный поток уже откреплен или поток, заданный параметром thread, не был обнаружен. Вот пример открепления уже существующего присоединяемого потока: //. . . pthread_create (ScthreadA, NULL, taskl, NULL) ; pthread_detach(threadA); //. . .
4.8. Создание потоков 143 Пои выполнении этих строк кода поток threadA станет открепленным. Чтобы соз- ть открепленный поток (в противоположность динамическому откреплению пото- а) необходимо установить атрибут detachstate в объекте атрибутов потока и ис- ользовать этот объект при создании потока. 4.8.4. Использование объекта атрибутов Объект атрибутов инкапсулирует атрибуты потока или группы потоков. Он используется для установки атрибутов потоков при их создании. Атрибутный объект потока имеет тип pthread_attr_t. Он представляет собой структуру, позволяющую хранить следующие атрибуты: • размер стека потока; • местоположение стека потока; • стратегия планирования, наследование и параметры; • тип потока: открепленный или присоединяемый; • область конкуренции потока. Для типа pthread_attr_t предусмотрен ряд методов, которые могут быть вызваны для установки или считывания каждого из перечисленных выше атрибутов (см. табл. 4.3). Для инициализации и разрушения атрибутного объекта потока используются функции pthread_attr_init () и pthread_at trades troy () соответственно. Синопсис #include <pthread.h> 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 (). При успешном выполнении эта функция возвращает число О, впротивном случае— код ошибки. Функция pthread_attr_init () завершится не- ' Пещно, если для создания объекта в системе недостаточно памяти. Функцию pthread_attr_destroy () можно использовать для разрушения объ- а типа pthread_attr_t, заданного параметром attr. При обращении к этой Ч\ нкции будут удалены любые скрытые данные, связанные с этим атрибутным объ- ом потока. При успешном выполнении эта функция возвращает число 0, в пробном случае - код ошибки.
144 Глава 4. Разбиение С++-программ на множество потоков 4.8.4.1. Создание открепленных потоков с помощью объекта атрибутов После инициализации объекта потока его атрибуты можно модифицировать. Для установки атрибута detachstate атрибутного объекта используется функция pthread__attr_setdetachstate(). Параметр detachstate описывает поток как открепленный или присоединяемый. I Синопсис I #include <pthread.h> 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_JOINABLE — в присоединяемые. Значение PTHREAD__CREATE_JOINABLE атрибут detachstate принимает по умолчанию. При успешном выполнении функция pthread_attr__setdetachstate () возвращает число 0, в противном случае— код ошибки. Эта функция выполнится неуспешно, если значение параметра detachstate окажется недействительным. Функция pthread__attr__getdetachstate () возвращает значение атрибута detachstate атрибутного объекта потока. При успешном выполнении эта функция возвращает значение атрибута detachstate в параметре detachstate и число О обычным способом. При неудаче функция возвращает код ошибки. В листинге 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[l]) ; pthread_attr_init(&DetachedAttr);
4.9. Управление потоками 145 pthread_attr_setdetachstate(&DetachedAttr, | PTHREAD_CREATE_DETACHED); pthread_create(&ThreadA,NULL,taskl,&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() не используется, поскольку открепленные потоки присоединить невозможно. 4.9. Управление потоками Создавая приложение с несколькими потоками, можно по-разному организовать их выполнение, использование ими ресурсов и состязание за ресурсы. Управление потоками по большей части осуществляется путем установки стратегий планирования и значений приоритета. Эти факторы влияют на эффективность потока. Кроме них, эффективность потока также определяется тем, как потоки состязаются за ресурсы: в рамках одного процесса либо в масштабе всей системы. Стратегию планирования, приоритет и область конкуренции потока можно установить с помощью объекта атрибутов потока. Поскольку потоки совместно используют ресурсы, доступ к ним необходимо синхронизировать. Эту тему мы кратко затронем в этой главе и более подробно— в главе5. К вопросам синхронизации также относятся и такие: где и как завершаются и аннулируются потоки. 4.9.1. Завершение потоков Выполнение потока может быть прервано по разным причинам: • в результате выхода из процесса с возвращаемым им статусом завершения (или без него); • в результате собственного завершения и предоставления статуса завершения; • в результате аннулирования другим потоком в том же адресном пространстве. Завершаясь, функция присоединения потока pthread_join() возвращает вызы- а*ощему потоку статус завершения, передаваемый функции pthread_exit (), которая была вызвана завершающимся потоком. Если завершающийся поток не обращал- к Функции pthread_exit (), то в качестве статуса завершения будет использовано чение, возвращаемое этой функцией, если оно существует; в противном случае атус завершения равен значению NULL. озможна ситуация, когда одному потоку необходимо завершить другой поток м же процессе. Например, приложение может иметь поток, который контролиру- ра&оту других потоков. Если окажется, что некоторый поток "плохо себя ведет"
146 Глава 4. Разбиение С++-программ на множество потоков или больше не нужен, то ради экономии системных ресурсов, возможно, его нулсно завершить. Завершающийся поток может окончиться немедленно или отложить завершение до тех пор, пока не достигнет в своем выполнении некоторой логической точки. При этом вполне вероятно, что такой поток (прежде чем завершиться) должен выполнить некоторые действия очистительно-восстановительного характера. Поток имеет также возможность отказаться от завершения. Для завершения вызывающего потока используется функция pthread_exit () Значение value_.pt г передается потоку, который вызывает функцию pthread_Join() для этого потока. Еще не выполненные процедуры, связанные с "уборкой", будут выполнены вместе с деструкторами, предусмотренными для потоковых данных. Никакие ресурсы, используемые потоками, при этом не освобождаются Синопсис | #include <pthread.h> int pthread_exit(void *value_ptr); При завершении последнего потока в процессе завершается сам процесс со статусом завершения, равным 0. Эта функция не может вернуться к вызывающему потоку и не определяет никаких кодов ошибок. Для отмены выполнения некоторого потока по инициативе потока из того же адресного пространства используется функция pthread__cancel (). Отменяемый поток задается параметром thread. I Синопсис #include <pthread.h> int pthread_cancel(pthread. _t thread thread); Обращение к функции pthread_cancel () представляет собой запрос аннулировать поток. Этот запрос может быть удовлетворен немедленно, с отсрочкой или проигнорирован. Когда произойдет аннулирование (и произойдет ли оно вообще), зависит от типа аннулирования и состояния потока, подлежащего этой кардинальной операции. Для удовлетворения запроса на отмену потока предусмотрен процесс аннулирования, который происходит асинхронно (т.е. не совпадает по времени) по отношению к выходу из функции pthread_cancel () и ее возврату в вызывающий поток. Если потоку нужно выполнить "уборочные" задачи, они обязательно выполняются. После выполнения последней такой задачи-обработчика вызываются деструкторы потоковых объектов, если таковые предусмотрены, и только после этого поток завершается. В этом и состоит процесс аннулирования потока. При успешном выполнении функция pthread_cancel () возвращает число 0, в противном случае— код ошибки. Эта функция не выполнится успешно, если параметр thread не соответствует ни одному из существующих потоков. Некоторые потоки могут потребовать принять меры безопасности против преждевременного их аннулирования. Внесение в потоковую функцию средств безопасности может предотвратить возникновение некоторых нежелательных ситуаций. Потоки разделяют общие данные, и (в зависимости от используемой потоковой модели; один поток может обрабатывать данные, которые должны быть переданы другому потоку для последующей обработки. Пока поток обрабатывает данные, он является HN
4.9. Управление потоками 147 инственным обладателем благодаря блокированию мьютекса, связанного с этими иными. Если поток, имеющий заблокированный мьютекс, аннулируется до его ос- обождения, возникает взаимоблокировка. Для того чтобы снова использовать дан- ые их следует привести в определенное состояние. Если поток отменяется до освобождения мьютекса, могут возникнуть нежелательные условия. Другими словами, в зависимости от типа обработки, которую выполняет поток, его аннулирование должно происходить тогда, когда это безопасно. Об опасных и безопасных периодах "знает" только сам поток, и поэтому только он может предотвратить свое аннулирование в опасные периоды. Следовательно, круг потоков, которые можно аннулировать, должен быть ограничен потоками, которые не относятся к числу "жизненно важных" или которые не имеют блокировок ресурсов. Кроме того, аннулирование может быть отсрочено до тех пор, пока не будут выполнены "жизненно важные" действия. Состояние готовности к аннулированию (cancelability state) описывает условия, при которых поток может (или не может) быть аннулирован. Тип аннулирования (cancelabilty type) потока определяет способность потока продолжать выполнение после получения запросов на аннулирование. Поток может отреагировать на аннулирующий запрос немедленно или отложить аннулирование до определенной (более поздней) точки в его выполнении. Состояние готовности к аннулированию и тип аннулирования устанавливаются динамически самим потоком. Для определения состояния готовности к аннулированию и типа аннулирования вызывающего потока используются функции pthread_setcancelstate () Hpthread_setcanceltype(). Функция pthread_set cancel state () устанавливает вызывающий поток в состояние, заданное параметром state, и возвращает предыдущее состояние в параметре olds tat е. Синопсис ♦include <pthread.h> int pthread_setcancelstate(int state, int *oldstate) [int pthread_setcanceltype(int type, int *oldtype); Параметры state и oldstate могут принимать такие значения: PTHREAD_CANCEL_DI SABLE PTHREAD_CANCEL_ENABLE Значение PTHREAD_CANCEL_DISABLE определяет состояние, в котором поток будет игнорировать запрос на аннулирование, а значение PTHREAD_CANCEL_ENABLE — со- °яние, в котором поток "согласится" выполнить соответствующий запрос (это со- °яние по умолчанию устанавливается для каждого нового потока). При успешном олнении функция возвращает число 0, в противном случае— код ошибки. Функ- Ptnread_set cancel state () не может выполниться успешно, если переданное ачение параметра state окажется недействительным. ункция pthread_setcanceltype () устанавливает для вызывающего потока тип '" иР°вания, заданный параметром type, и возвращает предыдущее значение типа раметре oldtype. Параметры type и oldtype могут принимать такие значения: ^THRpAD-CANCEL-DEFFERED HEAD^CANCEL_ASYNCHRONOUS
148 Глава 4. Разбиение С++-программ на множество потоков Значение PTHREAD_CANCEL_DEFFERED определяет тип аннулирования, при котором поток откладывает завершение до тех пор, пока он не достигнет точки, в котором его аннулирование возможно (этот тип по умолчанию устанавливается для каждого нового потока). Значение PTHREAD_CANCEL_ASYNCHRONOUS определяет тип аннулирования, при котором поток завершается немедленно. При успешном выполнении функция возвращает число 0, в противном случае— код ошибки. Функция р t hread_set cane el type () не может выполниться успешно, если переданное ей значение параметра type окажется недействительным. Функции pthread_setcancelstate() и pthread_setcanceltype() используются вместе для установки отношения вызывающего потока к потенциальному запросу на аннулирование. Возможные комбинации значений состояния и типа аннулирования перечислены и описаны в табл. 4.5. Таблица 4.5. Комбинации значений состояния и типа аннулирования Состояние Тип Описание Отсроченное аннулирование. Эти состояние и тип аннулирования потока устанавливаются по умолчанию. Аннулирование потока происходит, когда он достигает соответствующей точки в своем выполнении или когда программист определяет точку аннулирования с помощью функции pthread__testcancel () Асинхронное аннулирование. Аннулирование потока происходит немедленно Аннулирование запрещено. Оно вообще не выполняется 4.9.1.1. Точки аннулирования потоков Если удовлетворение запроса на аннулирование потока откладывается, значит, оно произойдет позже, когда это делать "безопасно", т.е. когда оно не попадает на период выполнения некоторого критического кода, блокирования мьютекса или пребывания данных в некотором "промежуточном" состоянии. Вне этих "опасных" раз* делов кода потоков вполне можно устанавливать точки аннулирования. Точка аннулирования — это контрольная точка, в которой поток проверяет факт существования каких-либо ждущих (отложенных) запросов на аннулирование и, если таковые имеются, разрешает завершение. Точки аннулирования можно пометить с помощью функции pthread_t est cancel () • Эта функция проверяет наличие необработанных запросов на аннулирование. Если они есть, она активизирует процесс аннулирования в точке своего вызова. В противном случае функция продолжает выполнение потока без каких-либо последствий. Вызов этой функции можно разместить в любом месте кода потока, которое считается безопасным для его завершения. PTHREAD_CANCEL_ PTHREAD_CANCEL_ ENABLE DEFERRED PTHREAD_CANCEL_ PTHREAD_CANCEL_ ENABLE ASYNCHRONOUS PTHREAD_CANCEL_ Игнорируется DISABLE
4.9. Управление потоками 149 синопсис ^include <pthread.h> void pthread_testcancel(void) Программа 4.3 содержит функции, которые вызывают функции othread_set cancel state (), pthread_setcanceltype () и pthread_testcancel (), связанные с установкой типа аннулирования потока и состояния готовности к аннулированию. // Программа 4.3 #include <iostream> #include <pthread.h> void *taskl(void *X) { int OldState; // Запрет на аннулирование. pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &OldState); for(int Count = 1;Count < 100;Count++) { cout << "В потоке A: " << Count « endl; } } void *task2 (void *X) { int OldState,OldType; // Разрешено аннулирование асинхронного типа. pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &OldState); pthread_setcancel type (PTHREAD_CANCEL_ASYNCHRONOUS, &OldType); for(int Count = 1;Count < 100;Count++) { cout « "В потоке В: " « Count « endl; } } void *task3(void *X) int OldState,OldType; ' Разрешено аннулирование отложенного типа. Pthread_setcancelstate (PTHREAD_CANCEL_ENABLE, &01dState); Pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, f &01dType); j°r(lnt Count = l;Count < 1000;Count++) cout « »b потоке С: " « Count « endl; lf((Count%100) == 0){ Pthread_testcancel();
150 Глава 4. Разбиение С++-программ на множество потоков } } } В программе 4.3 каждая задача устанавливает свое условие аннулирования. В задаче taskl аннулирование потока запрещено, поскольку вся она состоит из критического кода, который должен быть выполнен до конца. В задаче task2 аннулирование потока разрешено. Обращение к функции pthread_setcancelstate () является необязательным, поскольку все новые потоки имеют статус разрешения аннулирования. Тип аннулирования здесь устанавливается равным значению PTHREAD_CANCEL_ASYNCHRONOUS. Это означает, что после поступления запроса на аннулирование поток немедленно запустит соответствующую процедуру, независимо от того, на какой этап его выполнения придется этот запрос. А поскольку этот поток установил для себя именно такой тип аннулирования, значит, он не выполняет никакого "жизненно важного" кода. Например, вызовы системных функций должны попадать под категорию опасных для аннулирования, но в задаче task2 таких нет. Там выполняется лишь цикл, который будет работать до тех пор, пока не поступит запрос на аннулирование. В задаче task3 аннулирование потока также разрешено, а тип аннулирования устанавливается равным значению PTHREAD__CANCEL_DEFFERED. Эти состояние и тип аннулирования действуют по умолчанию для новых потоков, следовательно, обращения к функциям pthread_setcancelstate () и р thread_set cancel type () здесь необязательны. Критический код этого потока здесь может спокойно выполняться после установки состояния и типа аннулирования, поскольку процедура завершения не стартует до вызова функции 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[1]), NULL,task2,NULL); pthread_create(&(Threads[2]),NULL,task3,NULL); pthread_cancel(Threads[0]); pthread_cancel(Threads[1]); pthread_cancel(Threads[2] ) ; for(int Count = 0;Count < 3;Count++) { pthread_join(Threads[Count],&Status); if(Status == PTHREAD_CANCELED){ cout « "Поток" << Count « " аннулирован." << endl; }
4.9. Управление потоками 151 else{ cout « "Поток" « Count « " продолжает выполнение." « endl; } } return (0) ; } Управляющий поток в программе 4.4 сначала создает три рабочих потока, затем делает запрос на аннулирование каждого из них. Управляющий поток для каждого рабочего потока вызывает функцию pthread_join(). Эта функция завершается успешно даже тогда, когда она пытается присоединить поток, который уже завершен, функция присоединения в этом случае просто считывает статус завершения завершенного потока. И такое поведение весьма кстати, поскольку поток, который сделал запрос на аннулирование, и поток, вызвавший функцию pthread_join (), могут оказаться совсем разными потоками. Мониторинг функционирования всех рабочих потоков может оказаться единственной задачей того потока, который "по совместительству" и аннулирует потоки. Опрашивать же статус завершения потоков с помощью функции pthread_join() может совершенно другой поток. Этот тип информации используется для получения статистической оценки того, какой из потоков наиболее эффективен. В рассматриваемой нами программе все это делает один управляющий поток: в цикле он и присоединяет рабочие потоки, и проверяет их статус завершения. Поток Thread [0] не аннулирован, поскольку он имеет запрет на аннулирование, в то время как два остальных потока были аннулированы. Статус завершения аннулируемого потока может иметь, например, значение PTHREAD_CANCELED. Профили программ 4.3 и 4.4 представлены в разделе "Профиль программы 4.2". IПрофиль программы 4.2 Имя программы jProgram4-34.cc ; Описание Демонстрирует аннулирование потоков. Три потока имеют различные типы «и состояния аннулирования. Каждый поток выполняет цикл. Состояние и тип аннулирования определяет количество итераций цикла и то, будет ли цикл выполняться вообще. Основной поток определяет статус завершения каждого , рабочего потока. Требуемая библиотека Hbpthread Требуемые заголовки ^thread.h> <iostream> ^рукции по компиляции и компоновке программ + -о program4-34 program4-34.cc -lpthread РЗДа для тестирования j5uSE Linux 7.1, gec 2.95.2. :.И»струкции по выполнению --:/Program4-34
152 Глава 4. Разбиение С++-программ на множество потоков В функциях, определенных пользователем, используются точки аннулирования отмеченные обращением к функции pthread_testcancel (). Библиотека Pthread определяет в качестве точек аннулирования выполнение других функций. Эти функ- ции блокируют вызывающий поток, а заблокированному потоку аннулирование не грозит. Вот эти функции библиотеки Pthread: pthread_testcancel() pthread_cond_wait() pthread_timedwait pthread_join() Если поток, пребывающий в состоянии отсроченного аннулирования, имеет ждущий запрос на аннулирование, то при вызове одной из перечисленных выше функций библиотеки Pthread будет инициирована процедура аннулирования. Некоторые из системных функций, претендующих на роль точек аннулирования, перечислены в табл. 4.6. Таблица 4.6. Системные POSIX-функции, претендующие на роль точек аннулирования accept() aio__suspend() clock_nanosleep() close() connect() creat() fcntl() fsync() getmsg() lockf() mq_receive() mq_send() mq_timedreceive() mq_timedsend () msgrcv() msgsnd() msync() nanosleep() open() pause() poll() pread() pthread_cond_timedwait() pthread__cond_wait () pthread_j oin() putmsg() putpmsg() pwrite() read() readv() recvfrom() recvmsg() select() sem_timedwait() sem_wait() send() sendmsg() sendto() sigpause() sigsuspend() sigtimedwait() sigwait() sigwaitinfo() sleep() system() usleep() wait() waitpid() write() writev() Несмотря на то что эти функции безопасны для отсроченного аннулирования потоков, они могут не быть таковыми для асинхронного аннулирования. Асинхронное аннулирование во время вызова библиотечной функции, которая не является асинхронно-безопасной, может привести к тому, что библиотечные данные останутся не в надлежащем состоянии. Библиотека выделит память от имени потока, и, когда поток будет аннулирован, продолжит удерживать "за собой" эту память. Для других библиотечных и системных функций, которые не являются безопасными для аннулир0' вания (асинхронного или отсроченного), возможно, имеет смысл написать код, пре"
4.9. Управление потоками 153 ятствующий завершению потока путем установки категорического запрета на анну- ирование или использования отсроченного аннулирования до тех пор, пока эти функции не будут выполнены. 4.9.1-2. Очистка перед завершением Поток, "позволивший" себя аннулировать, прежде чем завершиться, обычно должен выполнить некоторые заключительные действия. Так, нужно закрыть файлы, привести разделяемые данные в надлежащее состояние, снять блокировки или освободить занимаемые ресурсы. Библиотека Pthread определяет механизм поведения каждого потока "в последние минуты своей жизни". С каждым потоком связывается стек очистительно-восстановительных операций (cleanup stack), который содержит указатели на процедуры (или функции), предназначенные для выполнения во время аннулирования потока. Для того чтобы поместить в этот стек указатель на процедуру, предусмотрена функция pthread_cleanup_push (). Синопсис #include <pthread.h> 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-функции. Необходимо позаботиться о том, чтобы для каждой функции занесения в стек vpush) существовала функция извлечения из стека (pop) в пределах одной и той же ексической области видимости. Например, для функции f uncA () обязательно вы- °лнение cleanup-обработчика при ее нормальном завершении или аннулировании: void *funcA(void *Х) int *Tid; Tid = new int; / Выполнение некоторых действий. ^h^ead~cleanup_push(cleanup_funcA/Tid); Выполнение некоторых действий. } Pthrea3-Cleanup__pop (0 ) ;
154 Глава 4. Разбиение С++-программ на множество потоков Здесь функция funcA() помещает указатель на обработчик cleanup_funcA() в стек завершающих процедур путем вызова функции pthread_cleanup_push (). Каждому обращению к этой функции должно соответствовать обращение к функции pthread_cleanup_pop(). Если функции извлечения указателя из стека (pop- функции) передается значение 0, то извлечение из стека состоится, но без выполнения обработчика. Обработчик будет выполнен лишь при аннулировании потока, выполняющего функцию f uncA (). Для функции f uncB () также требуется cleanup-обработчик: void *funcB(void *X) { int *Tid; Tid = new int; // Выполнение некоторых действий. //. . . pthread__cleanup_push(cleanup_funcB,Tid); // Выполнение некоторых действий. //. . . pthread_cleanup_pop(1); } Здесь функция f uncB () помещает указатель на обработчик cleanup_f uncB () в стек завершающих процедур. Отличие этого примера от предыдущего состоит в том, что функции pthread_cleanup_pop () передается параметр со значением 1, т.е. после извлечения из стека указателя на обработчик этот обработчик будет тут же выполнен. Необходимо отметить, что выполнение обработчика в данном случае состоится "при любой погоде", т.е. и при аннулировании потока, который обеспечивает выполнение функции funcB(), и при обычном его завершении. Обработчики-"уборщики", cleanup_funcA() и cleanup_f uncB (), — это обычные функции, которые можно использовать для закрытия файлов, освобождения ресурсов, разблокирования мьютексов и пр. 4.9.2, Управление стеком потока Адресное пространство процесса делится на раздел кода, раздел статических данных, свободную память и раздел стеков. Стекам потоков выделяется область из стекового раздела процесса. Стек потока предназначен для хранения стекового фрейма, связанного с каждой процедурой (функцией), которая была вызвана, но еще не завершена. Стековый фрейм содержит временные переменные, локальные переменные, адреса точек возврата и любую другую дополнительную информацию, которая необходима потоку, чтобы найти "обратную дорогу" к ранее вызванным процедурам. При выходе из процедуры (функции) ее стековый фрейм извлекается из стека. Расположение фреймов в стеке схематично показано на рис. 4.12. Предположим, что поток А (см. рис. 4.12) выполняет функцию taskl () , которая создает некоторые локальные переменные, выполняет заданную обработку, а затем вызывает функцию taskX (). При этом для функции taskl () создается стековый фрейм, который помещается в стек потока. Функция taskX () выполняет "свои" действия, создает локальные переменные, а затем вызывает функцию taskC (). Нетрудно догадаться, что стековый фрейм, созданный для функции taskX () , также помещается в стек. Функция taskC () вызывает функцию taskY () и т.д. Каждый стек должен иметь достаточно большой размер, чтобы поместить всю
4.9. Управление потоками 155 . рмацию, необходимую для выполнения всех функций потока, а также цепочки И гих подпрограмм, которые будут вызваны потоковыми функциями. Размером естоположением стека потока управляет операционная система, но для установ- и считывания этой информации предусмотрены методы, которые определены в объекте атрибутов потока. АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА [ВАЗДЕЛ. СТЕКОВ, ... 1 —• ■ : г- ■ ■ ■ . taskCO _ „_*, __■ г_ _ __ _ —.«. — *_ ^ taskX{) А= 1 В = 3 ^._*!*^.-__ — ~_.^.._-_ч. _ task1() Х= 10' ,r Г taskCO { II... } taskX() A = 1 B = 3 taskC(); II... } task1() X = 10; Y = 5; taskX(); II... ) Рис. 4.12. Стековые фреймы, сгенерированные потоками Функция pthread_attr__get stacksize () возвращает минимальный размер стека, устанавливаемый по умолчанию. Параметр attr определяет объект атрибутов потока, из которого считывается стандартный размер стека. При успешном выполнении функция возвращает значение 0, а стандартный размер стека, выраженный в байтах, сохра- яется в параметре stacksize. В случае неудачи функция возвращает код ошибки. Функция pthread__attr_setstacksize() устанавливает минимальный размер стека По *• параметр attr определяет объект атрибутов потока, для которого устанав- ается размер стека. Параметр stacksize содержит минимальный размер стека, раженный в байтах. При успешном выполнении функция возвращает значение 0, ротивном случае — код ошибки. Функция завершается неудачно, если значение раметра stacksize оказывается меньше значения PTHREAD_MIN_STACK или МенВЫШаеТ СИстемный минимум. Вероятно, значение PTHREAD_STACK_MIN будет Pth минимального размера стека, возвращаемого функцией СТе — attr_getstacksize (). Прежде чем увеличивать минимальный размер pt^ от°ка, следует поинтересоваться значением, возвращаемым функцией цИе — attr_getstacksize (). Размер стека фиксируется, чтобы его расшире- пРост Ремя выполнения программы ограничивалось рамками фиксированного СТВа стека, установленного во время компиляции.
156 Глава 4. Разбиение С++-программ на множество потоков Синопсис I #include <pthread.h> 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__set stackaddr() и pthread_attr_getstackaddr(). Функция pthread_attr_setstackaddr () для потока, создаваемого с помощью атрибутного объекта, заданного параметром attr, устанавливает базовый адрес стека равным адресу, заданному параметром stackaddr. Этот адрес stackaddr должен находиться в пределах виртуального адресного пространства процесса. Размер стека должен быть не меньше минимального размера стека, задаваемого значением PTHREAD_STACK_MIN. При успешном выполнении функция возвращает значение О, в противном случае — код ошибки. Функция pthread_attr_getstackaddr () считывает базовый адрес стека для потока, создаваемого с помощью атрибутного объекта потока, заданного параметром attr. Считанный адрес сохраняется в параметре stackaddr. При успешном выполнении функция возвращает значение 0, в противном случае — код ошибки. Синопсис #include <pthread.h> void pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr); void pthread_attr_getstackaddr( const pthread_attr__t *restrict attr, I void **restrict stackaddr); J Атрибуты стека (размер и местоположение) можно установить с помощью одной функции. Функция pthread_attr_setstack() устанавливает как размер, так и адрес стека для потока, создаваемого с помощью атрибутного объекта, заданного параметром attr. Базовый адрес стека устанавливается равным адресу, заданному параметром stackaddr, а размер стека— равным значению параметра stacksize. Функция pthread_attr_getstack() считывает размер и адрес стека для потока, создаваемого с помощью атрибутного объекта, заданного параметром attr. Пр*1 успешном выполнении функция возвращает значение 0, в противном случае— коД ошибки. Если считывание атрибутов стека прошло успешно, адрес будет записан в параметр stackaddr, а размер— в параметр stacksize. функция pthread_setstack () выполнится неудачно, если значение параметра stacksize окажется меньше значения PTHREAD_STACK_MIN или превысит некоторый преДеЛ' определяемый реализацией.
4.9. Управление потоками 157 Синопсис #include <pthread.h> -A Dthread__attr_setstack(pthread_attr_t *attr/ vo1 V 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_JReq - DefaultSize; NewSize = DefaultSize + SizeOffset; pthread_attr_setstacksize(&Attrl,(size_t)NewSize); } В листинге 4.3 сначала из атрибутного объекта потока считывается размер стека, действующий по умолчанию. Затем, если окажется, что этот размер меньше желаемого минимального размера стека, вычисляется разность между сравниваемыми размерами, после чего значение этой разности (смещение) суммируется с размером стека, используемым по умолчанию. Результат суммирования становится новым минимальным размером стека для этого потока. ПРИМЕЧАНИЕ: установка размера и местоположения стека может сделать вашу программу непереносимой. Размер и местоположение стека, устанавливаемые на одной платформе, могут оказаться неподходящими для использования в качестве размера и местоположения стека для другой платформы. 4-9.3. Установка атрибутов планирования и свойств потоков Добно процессам, потоки выполняются независимо один от другого. Каждый по- начается процессору для выполнения его задачи. Для каждого потока определя- OH - ^атегия планирования и приоритет, которые предписывают, как и когда именно Пы п Назначен процессору. Стратегия планирования и приоритет потока (или груп- Dt-ъ К )станавливаются с помощью объекта атрибутов и следующих функций: Pthrea^attr-Setinheritsched() Pthre^""attr-Setschedpolicy(} a-attr-setschedparam()
158 Глава 4. Разбиение С++-программ на множество потоков Для получения информации о характере выполнения потока используются еле дующие функции: pthread_attr_getinheritsched() pthread_attr_getschedpolicy() pthread_attr_getschedparam() I Синопсис | #include <pthread.h> Uinclude <sched.h> 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() позволяет определить, как будут устанавливаться атрибуты планирования потока: путем наследования от потока-создателя или в соответствии с содержимым объекта атрибутов. Параметр inherit sched может принимать одно из следующих значений. PTHREAD_INHERIT_SCHED Атрибуты планирования потока должны быть унаследованы от потока-создателя, при этом любые атрибуты планирования, определяемые параметром attr, будут игнорироваться. PTHREAD_EXPLICIT_SCHED Атрибуты планирования потока должны быть установлены в соответствии с атрибутами планирования, хранимыми в объекте, заданном параметром attr. Если параметр inheritsched получает значение PTHREAD_EXPLICIT_SCHED, то функция pthread_attr_setschedpolicy () используется для установки стратегии планирования, а функция pthread_attr_setschedparam () — установки приоритета. Функция pthread_attr_setschedpolicy () устанавливает член объекта атрибутов потока (заданного параметром attr), "отвечающий" за стратегию планирования потока. Параметр policy может принимать одно из следующих значений, определенных в заголовке <sched. h>. SCHED_FIFO Стратегия планирования типа FIFO (первым прибыл, первым обслужен), при которой поток выполняется до конца. SCHED_RR Стратегия циклического планирования, при которой каждый поток назначается процессору только в течение некоторого кванта времени- SCHED_OTHER Стратегия планирования другого типа (определяемая реализацией)- Для любого нового потока эта стратегия планирования принимается по умолчанию.
4.9. Управление потоками 159 кция pthread_attr_setschedparam() используется для установки членов бтного объекта (заданного параметром attr), связанных со стратегией плани- аТР ' Параметр param представляет собой структуру, которая содержит эти чле- Р° с DVKTypa 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 <sched.h> int sched_get_priority_max(int policy); int sched_get_priority_min(int policy); Обеим функциям в качестве параметра policy передается значение, определяющее выбранную стратегию планирования, для которой нужно установить значения приоритета, и обе функции возвращают соответствующее значение приоритета (минимальное и максимальное) для заданной стратегии планирования. Как установить стратегию планирования и приоритет потока с помощью атрибутного объекта, показано в листинге 4.4. // Листинг 4.4. Использование атрибутного объекта потока ' I для установки стратегии планирования и '' приоритета потока define Min_Stack_Req 3000000 Pthread_t ThreadA; Pthread_attr_t SchedAttr; int^ DefaultSize,SizeOffsetfNewSize; srb ^lnPriority»MaxPriority#MidPriority; cne^aram SchedParam; \nt main(int argC/ char *argv[]) Инициализируем объект атрибутов. a -att^init(&SchedAttr) ; // при ЫВаем минимальное и максимальное значения toinpri .Тета Для стратегии планирования. Махр^^^ = sched-get-Priority-max(SCHED-RR) ; ritV = sched_get_priority_min(SCHED_RR);
160 Глава 4. Разбиение С++-программ на множество потоков // Вычисляем значение приоритета. MidPriority = (MaxPriority + MinPriority)/2; // Записываем значение приоритета в структуру sched__param. SchedParam.sched_priority = MidPriority; // Устанавливаем объект атрибутов. pthread_attr_setschedparam(&Attrl,&SchedParam); // Обеспечиваем установку атрибутов планирования //с помощью объекта атрибутов. pthread_attr_setinheritsched(&Attrl, PTHREAD_EXPLICIT_SCHED); // Устанавливаем стратегию планирования. pthread__attr_setschedpolicy (&Attrl, SCHEDJRR) ; // Создаем поток с помошью объекта атрибутов. pthread_create(&ThreadA&Attrl,task2,Value); } В листинге 4.4 стратегия планирования и приоритет потока ThreadA устанавливаются с использованием атрибутного объекта SchedAttr. Выполним следующие действия. 1. Инициализируем атрибутный объект. 2. Считаем минимальное и максимальное значения приоритета для стратегии планирования. 3. Вычислим значение приоритета. 4. Запишем значение приоритета в структуру sched_param. 5. Установим атрибутный объект. 6. Обеспечим установку атрибутов планирования с помощью атрибутного объекта. 7. Установим стратегию планирования. 8. Создадим поток с помощью атрибутного объекта. Последовательное выполнение этих действий позволяет установить стратегию планирования и приоритет потока до его создания. Для динамического изменения стратегии планирования и приоритета используйте функции pthread_setschedparam() и pthread_setschedprio(). Синопсис #include <pthread.h> 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) ; ^^^
4.9. Управление потоками 161 . кцИЯ pthread_setschedparam() устанавливает как стратегию планирова- к и приоритет потока без использования атрибутного объекта. Параметр нИ ' d содержит идентификатор потока, параметр policy— новую стратегию пла- ^ вания и параметр param — значения, связанные с приоритетом. Функция Н ь ead__getschedparam() сохраняет значения стратегии планирования и приори- Р в параметрах policy и par am соответственно. При успешном выполнении обе киии возвращают число 0, в противном случае — код ошибки. Условия, при кото- v эти функции могут завершиться неудачно, перечислены в табл. 4.7. Таблица 4.7. Условия потенциального неудачного завершения функций установки стратегии планирования и приоритета Функции Условия отказа int pthread_getschedparam (pthread_t thread, int *restrict policy, struct sched_param *restrict par am) ; int pthread_setschedparam (pthread_t thread, int *policy, const struct sched__param *param); lr*t pthread_setschedprio (pthread_t thread, int prio); Параметр thread не ссылается на существующий поток Некорректен параметр policy или один из членов структуры, на которую указывает параметр param Параметр policy или один из членов структуры, на которую указывает параметр param, содержит значение, которое не поддерживается в данной среде Вызывающий поток не имеет соответствующего разрешения на установку значений приоритета или стратегии планирования для заданного потока Параметр thread не ссылается на существующий поток Данная реализация не позволяет приложению заменить один из параметров планирования заданным значением Параметр prio не подходит к стратегии планирования заданного потока Параметр prio имеет значение, которое не поддерживается в данной среде Вызывающий поток не имеет соответствующего разрешения на установку приоритета для заданного потока Параметр thread не ссылается на существующий поток Данная реализация не позволяет приложению заменить значение приоритета заданным
162 Глава 4. Разбиение С++-программ на множество потоков Функция pthread_setschedprio () используется для установки значения прц оритета выполняемого потока, идентификатор которого задан параметром thread В результате выполнения этой функции текущее значение приоритета будет заменено значением параметра prio. При успешном выполнении функция возвращает число о в противном случае — код ошибки. При неуспешном выполнении функции приоритет потока изменен не будет. Условия, при которых эта функция может завершиться неуспешно, также перечислены в табл. 4.7. ПРИМЕЧАНИЕ: к изменению стратегии планирования или приоритета выполняемого потока необходимо отнестись очень осторожно. Это может непредсказуемым образом повлиять на общую эффективность приложения. Потоки с более высоким приоритетом будут вытеснять потоки с более низким, что приведет к зависанию либо к тому, что поток будет постоянно выгружаться с процессора и поэтому не сможет завершить выполнение. 4.9.3.1. Установка области конкуренции потока Область конкуренции потока определяет, какое множество потоков с одинаковыми стратегиями планирования и приоритетами будут состязаться за использование процессора. Область конкуренции потока устанавливается его атрибутным объектом. Синопсис #include <pthread.h> 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, в противном случае — код ошибки- 4.9.4. Использование функции sysconf () Знание пределов, устанавливаемых системой на использование ресурсов, позволит в» шему приложению эффективно управлять ресурсами. Например, максимальное количество потоков, приходящихся на один процесс, составляет верхнюю границу числа pav° чих потоков, которое может быть создано процессом. Функция sysconf () использу^ ся для получения текущего значения конфигурируемых системных пределов или опЦИ*1'
4.9. Управление потоками 163 С^яопсис . „тпЛе <unistd.n> int «ygeonf <int паше) П оаметр name — это запрашиваемая системная переменная. Функция возвращает ения, соответствующие стандарту POSIX IEEE Std. 1003.1-2001 для заданных сис- 3 ых переменных. Эти значения можно сравнить с константами, определенными ТС й реализацией стандарта, чтобы узнать, насколько они согласуются между собой. Я я ояда системных переменных существуют константы-аналоги, относящиеся к потокам, процессам и семафорам (см. табл. 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 -SC_THREAD_ATTR__ STACKADDR -SCJTHREAD_ATTR STACKSIZE rSC_THREAD_STACK MIN - ~SCLTHREAD_THREADS MAX — -SC_THREAD_KEYS_MAX -SC-THREAD_PRi0 _POSIX_THREADS _POS IX_THREAD_ATTR_ STACKADDR _POSIX_THREAD_ATTR_ STACKSIZE PTHREAD_STACK_MIN PTHREAD THREADS MAX PTHREAD_KEYS_MAX __POSIX_THREAD_PRIO_ INHERIT _POSIX_THREAD_PRIO_ Поддерживает потоки Поддерживает атрибут адреса стека потока Поддерживает атрибут размера стека потока Минимальный размер стека потока в байтах Максимальное количество потоков на процесс Максимальное количество ключей на процесс Поддерживает опцию наследования приоритета Поддерживает опцию приоритета потока
164 Глава 4. Разбиение С++-программ на множество потоков Окончание табл. 4. s Переменная Значение Описание _SC_THREAD_PRIORITY_ SCHEDULING _SC_THREAD_PROCESS_ SHARED _SC_THREAD_SAFE_ FUNCTIONS _SC__THREAD__ DESTRUCTOR_ ITERATIONS _SC_CHILD_MAX _SC_PRIORITY_ SCHEDULING _SC_REALTIME_ SIGNALS _SC_XOPEN_REALTIME_ THREADS _SC_STREAM__MAX _SC_SEMAPHORES _SC_SEM_NSEMS_MAX _POSIX_THREAD__PRIORITY_ SCHEDULING _POSIX_THREAD_PROCESS_ SHARED __POSIX_THREAD_SAFE_ FUNCTIONS _PTHREAD_THREAD_ DESTRUCTOR_ITERATIONS CHILD_MAX _POSIX_PRIORITY_ SCHEDULING _POSIX_REALTIME_ SIGNALS _XOPENJREALTIME_ THREADS STREAMLMAX _POSIX_SEMAPHORES SEM NSEMS MAX _SC_SEM_VALUE_MAX SEM__VALUE_MAX _SC_SHARED_MEMORY_ OBJECTS _POSIX_SHARED_MEMORY_ OBJECTS Поддерживает опцию планирования приоритета потока Поддерживает синхронизацию на уровне процесса Поддерживает функции безопасности потока Определяет количество попыток, направленных на разрушение потоковых данных при завершении потока Максимальное количество процессов, разрешенных для UID Поддерживает планирование процессов Поддерживает сигналы реального времени Поддерживает группу потоковых средств реального времени X/Open POSIX Определяет количество потоков данных, которые один процесс может открыть одновременно Поддерживает семафоры Определяет максимальное количество семафоров, которое может иметь процесс Определяет максимальное значение, которое может иметь семафор Поддерживает объекты общей 4.9.5. Управление критическими разделами Параллельно выполняемые процессы (или потоки в одном процессе) могут совместно использовать структуры данных, переменные или отдельные данные. Разделение глобальной памяти позволяет процессам или потокам взаимодействовать ДрУг с другом и получать доступ к общим данным. При использовании нескольких процессов разделяемая глобальная память является внешней по отношению к ним. Внешнюю структуру данных можно использовать для передачи данных или команд межДУ процессами. Если же необходимо организовать взаимодействие потоков, то они мог)'1'
4.9. Управление потоками 165 ь доступ к структурам данных или переменным, являющимся частью одного получ ^ прОЦесса, которому они принадлежат. иГ° существуют процессы или потоки, которые получают доступ к разделяемым , ЦИруемым данным, структурам данных или переменным, то все эти данные М ятся в критической области (или разделе) кода процессов или потоков. Крити- Н •* тчлел кода — это та его часть, в которой обеспечивается доступ потока или ческии р^/А ^ сса к разделяемому блоку модифицируемой памяти и обработка соответствую- данных. Отнесение раздела кода к критическому можно использовать для управ- состоянием "гонок". Например, создаваемые в программе два потока, поток А поток В, используются для поиска нескольких ключевых слов во всех файлах системы. Поток А просматривает текстовые файлы в каждом каталоге и записывает нужные пути в списочную структуру данных TextFiles, а затем инкрементирует переменную FileCount. Поток В выделяет имена файлов из списка TextFiles, декрементирует переменную FileCount, после чего просматривает файл на предмет поиска в нем заданных ключевых слов. Файл, который их содержит, переписывается в другой файл, и инкрементируется еще одна переменная FoundCount. К переменной FoundCount поток А доступа не имеет. Потоки А и В могут выполняться одновременно на отдельных процессорах. Поток А выполняется до тех пор, пока не будут просмотрены все каталоги, в то время как поток В просматривает каждый файл, путь к которому выделен из переменной TextFiles. Упомянутый список поддерживается в отсортированном порядке, и в любой момент его содержимое можно отобразить на экране. Здесь возможна масса проблем. Например, поток В может попытаться выделить имя файла из списка TextFiles до того, как поток А его туда поместит. Поток В может попытаться декрементировать переменную SearchCount до того, как поток А ее инкрементирует, или же оба потока могут попытаться модифицировать эту переменную одновременно. Кроме того, во время сортировки элементов списка TextFiles поток А может попытаться записать в него имя файла, или поток В будет в это время пытаться выделить из него имя файла для выполнения своей задачи. Описанные проблемы— это примеры условий "гонок", при которых несколько потоков (или процессов) пытаются одновременно модифицировать один и тот же блок общей памяти. Если потоки или процессы одновременно лишь читают один и тот же блок памяти, условия "гонок" не возникают. Они возникают в случае, когда несколько процессов или потоков одновременно получают доступ к одному и тому же блоку памяти, и по раинеи мере один из этих процессов или потоков делает попытку модифицировать иные. Раздел кода становится критическим, когда он делает возможными одновре- * енные попытки изменить один и тот же блок памяти. Один из способов защитить Р тический раздел — разрешить только монопольный доступ к блоку памяти. Моно- НЫи °осЩп означает, что к разделяемому блоку памяти будет иметь доступ один Р есс или поток в течение короткого промежутка времени, при этом всем осталь- Р°Цессам или потокам запрещено (путем блокировки) входить в свои критиче- разделы, которые обеспечивают доступ к тому же самому блоку памяти. Ров ^71Равления условиями "гонок" можно использовать такой механизм блоки- "wu/n 1 ^ взаимно исключающий семафор, или мьютекс (mutex— сокращение от кРит slon"~ взаимное исключение). Мьютекс используется для блокирования Него — СКОГО РазДела: он блокируется до входа в критический раздел, а при выходе из Деблокируется:
166 Глава 4. Разбиение С++-программ на множество потоков Блокирование мьютекса // Вход в критический раздел. // Доступ к разделяемой модифицируемой памяти. // Выход из критического раздела. Деблокирование мьютекса Класс 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 Istat(FileName.c_str(),fcStatBuffer); 6 if((StatBuffer.st_mode & S_IFDIR) == -1) 7 { 8 cout « "Невозможно получить статистику по файлу." « endl; 9 return (0); Ю } 11 else{ 12 if(StatBuffer.st_mode & S__IFDIR) { 13 return (1); 14 } 15 } 16 return (0) ; 17 } 18 19 2 0 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 « "Невозможно получить статистику по файлу." « endl; 28 return (0); 29 } 30 else{ 31 if(StatBuffer.st_mode & S_IFREG){ 32 return (1); 33 }
4.9. Управление потоками 167 49 50 \\ return (0); 36 ) 37 38 id depthFirstTraversal(const char *CurrentDir) g ( MR *DirP; до string Temp; и3 string FileName; 44 struct dirent *EntryP; 45 chdir(CurrentDir); 46 cout « "Просмотр каталога: " « CurrentDir « endl; 47 DirP = opendir(CurrentDir); 48 if(DirP == NULL){ cout « "He удается открыть файл." « 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.appendd, '/') ; 62 F i 1 eName. append (En tryP - >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; n\ Flag = FileName. f ind (". cpp") ; It if(Flag > 0){ pthread_mutex_lock(&CountMutex); * FileCount++; 7fi pthread_mutex_unlock(&CountMutex); 77 pthread_mutex_lock(&QueueMutex); 78 Text Files, push (Fil eName) ; 7Q pthread_mutex_unlock(&QueueMutex); } } } 80 81 82 83 84 85 86 87 88 89 90 91 } EntryP = readdir(DirP) closedir(DirP); Voi<a * task (void *X)
168 Глава 4. Разбиение С++-программ на множество потоков 92 { 93 char *Directory; 94 Directory = static_cast<char *>(X); 95 depthFirstTraversal(Directory); 96 re turn(NULL); 97 98 } Программа 4.6 содержит поток-"потребитель", который выполняс ных ключевых слов. // Программа 4.6 1 void *keySearch(void *X) 2 { 3 string Tempf Filename; 4 less<string> Comp; 5 6 while(!Keyfile.eof() && Keyfile.good()) 7 { 8 Keyfile >> Temp; 9 if(IKeyfile.eof()){ 10 Keywords.insert(Temp); 11 } 12 } 13 Keyfile.close(); 14 15 while(TextFiles.empty() ) 16 { } 17 18 while(!TextFiles.empty()) 19 { 2 0 pthread__mutex_lock(&QueueMutex) ; 21 Filename = TextFiles.front(); 22 TextFiles.popO ; 23 pthread_mutex__unlock(&QueueMutex) ; 24 Infile. open (Filename. c__str() ) ; 2 5 SearchWords.erase(SearchWords.begin(), 2 6 SearchWords.end()); 27 while(Unfile.eof() && Infile.good()) 28 { 29 Infile » Temp; 3 0 SearchWords.insert(Temp); 31 } 32 33 Infile.close(); 34 i f(includes(SearchWords.begin(),SearchWords.end(), Keywords.begin(),Keywords.end(),Comp)){ 3 5 Outfile « Filename « endl; 3 6 pthread_mutex_lock(&CountMutex); 37 FileCount—; 3 8 pthread__mutex__unlock(&CountMutex) ; 39 FoundCount++; 40 } 41 ) 42 return(NULL); 43 44 }
4.9. Управление потоками 169 П ограмма 4.7 содержит основной поток для потоков модели "изготовитель- 4.7 битель", реализованных в программах 4.5 и 4.6. // программа 1 #include <sys/stat.h> 2 #include <fstream> 3 #include <queue> 4 #include <algorithm> 5 #include <pthread.h> 6 #include <iostream> 7 #include <set> 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<string> TextFiles; 17 set <string,less<string> >KeyWords; 18 set <string,less<string> >SearchWords; 19 ifstream Infile; 20 of stream Outfile; 21 ifstream Keyfile; 22 string KeywordFile; 23 string OutFilename; 24 pthread_t Threadl; 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) { cerr << "Нужна дополнительная информация." « endl; exit (1); 34 35 36 } 37 38 1q Outfile.open(argv[3],ios::app||ios:rate); ^ Keyfile.open(argv[2]); Pthread_create(&Threadl,NULLftask,argv[1]); Pthread_create(&Thread2,NULL,keySearch,argv[1]); Pthread_join(Threadl,NULL); 44 Pthread^join(Thread2,NULL); 45 Pth^ead_mutex_destroy(&CountMutex) ; 46 pthread-mutex_destroy(&QueueMutex) ; 47 cout « argv[l] « " содержит " « FoundCount 40 <<: " файлов со всеми ключевыми словами." << endl; 49 return(0);
170 Глава 4. Разбиение С++-программ на множество потоков С помощью мьютексов доступ к разделяемой памяти для чтения или записи дац. ных разрешается получить только одному потоку. Для гарантии безопасности работы функций, определенных пользователем, можно использовать и другие механизмы и методы, которые реализуют одну из моделей PRAM: EREW (монопольное чтение и монопольная запись) CREW (параллельное чтение и монопольная запись) ERCW (монопольное чтение и параллельная запись) CRCW (параллельное чтение и параллельная запись) Мьютексы используются для реализации EREW-алгоритмов, которые рассматриваются в главе 5. 4.10. Безопасность использования потоков и библиотек Климан (Klieman), Шах (Shah) и Смаалдерс (Smaalders) утверждали: "Функция или набор функций могут сделать поток безопасным или реентерабельным (повторно входимым), если эти функции могут вызываться не одним, а несколькими потоками без предъявления каких бы то пи было требований к вызывающей части выполнить определенные действия" (1996). При разработке многопоточного приложения программист должен обеспечить безопасность параллельно выполняемых функций. Мы уже обсуждали безопасность функций, определенных пользователем, но без учета того, что приложение часто вызывает функции из системных библиотек или библиотек, созданных сторонними производителями. Одни такие функции и/или библиотеки безопасны для потоков, а другие — нет. Если функция небезопасна, это означает, что в ней используется хотя бы одна статическая переменная, осуществляется доступ к глобальным данным и/или она не является реентерабельной. Известно, что статические переменные поддерживают свои значения между вызовами функции. Если некоторая функция содержит статические переменные, то для ее корректного функционирования требуется считывать (и/или изменять) их значения. Если же к такой функции будут обращаться несколько параллельно выполняемых потоков, возникнут условия "гонок". Если функция модифицирует глобальную переменную, то каждый из нескольких потоков, вызывающих функцию, может попытаться модифицировать эту глобальную переменную. Возникновения условий "гонок" также не миновать, если не синхронизировать множество параллельных доступов к глобальной переменной. Например, несколько параллельных потоков могут выполнять функции, которые устанавливают переменную errno. Для некоторых потоков, предположим, эта функция не может выполниться успешно, и переменная errno устанавливается равной сообщению об ошибке, в то время как другие потоки выполняются успешно. Если реализация компилятора не обеспечивает потоковую безопасность поддержки переменной errno, то какое сообщение получит поток при проверке состояния переменной errno- Блок кода считается реентерабельным, если его невозможно изменить при выполнении. Реентерабельный код исключает возникновение условий "гонок" благодаря отсутствию ссылок на глобальные переменные и модифицируемые статические данные. Следовательно, такой код могут совместно использовать несколько параллельных потоков или процессов без риска создать условия "гонок". Стандарт POSIX опр^ деляет ряд реентерабельных функций. Их легко узнать по наличию "суффикса" _г' присоединяемого к имени функции. Перечислим некоторые из них:
4.10. Безопасность использования потоков и библиотек 171 sterror^r О strtok^r( readdir^rO rand^r О ttyname^f v) V ли функция получает доступ к незащищенным глобальным переменным, содер- атические модифицируемые переменные или нереентерабельна, то такая кыия считается небезопасной для потока. Системные библиотеки или библиоте- созданные сторонними производителями, могут иметь различные версии своих андартных библиотек. Одна версия предназначена для однопоточных приложений, угая _ для многопоточных. Если предполагается разрабатывать многопоточное ппиложение, программист должен использовать многопоточные версии нужной ему библиотеки. Некоторые среды требуют не компоновки многопоточных приложений с многопоточной версией библиотеки, а лишь определения макросов, что позволяет объявить реентерабельные версии функций. Такое приложение будет затем компилироваться как безопасное для выполнения потоков. Во всех ситуациях использовать многопоточные версии функций попросту невозможно. В отдельных случаях многопоточные версии конкретных функций недоступны для данного компилятора или среды. Иногда один интерфейс функции не в состоянии сделать ее безопасной. Кроме того, программист может столкнуться с увеличением числа потоков в среде, которая изначально использовала функции, предназначенные для функционирования в однопоточной среде. В таких условиях обычно используются мьютексы. Например, программа имеет три параллельно выполняемых потока. Два из них, threadl и thread2, параллельно выполняют функцию f uncA (), которая не является безопасной для одновременной работы потоков. Третий поток, thread3, выполняет функцию funcB (). Для решения проблемы, связанной с функцией f uncA (), возможно, достаточно заключить в защитную оболочку мьютекса доступ к ней со стороны потоков threadl и thread2: threadl thread2 thread3 { n { { loc*() lockO funcBO funcA() funcAO } unlock() unlock() } } При реализации таких защитных мер к функции f uncA () в любой момент времени * ет получить доступ только один поток. Но проблемы на этом не исчерпываются. Если ^Нкции ^uncA () и f uncB () небезопасны для выполнения потоками, они могут обе th ИСРПЦиРовать глобальные или статические переменные. И хотя потоки threadl и ead2 используют мьютексы для функции f uncA (), поток thread3 может выполнять Цию f uncB () одновременно с любым из остальных потоков. В такой ситуации впол- rvr Р°ЯТНо возникновение условий "гонок", поскольку функции f uncA () и f uncB () мо- е модифицировать одну и ту же глобальную или статическую переменную. Нии Р°ИллюстРиРУем eu*c один тип условий "гонок", возникающих при использова- в и6лиотеки iostream. Предположим, у нас есть два потока, А и В, которые вы- об-к ^анные в стандартный выходной поток, с out, который представляет собой ттипа ostream. При использовании операторов "»" и "<<" вызываются мето-
172 Глава 4. Разбиение С++-программ на множество потоков ды объекта cout. Вопрос: являются ли эти методы безопасными? Если поток А о правляет сообщение "Мы существа разумные" объекту stdout, а поток В отправляе сообщение "Люди алогичные существа", то не произойдет ли "перемешивание" Вь ходных данных, в результате которого может получиться сообщение вроде такого- " Мы Люди существа алогичные разумные существа"? В некоторых случаях безопасны для потоков функции реализуются как атомные. Атомные функции — это функции, которые, если их выполнение началось, не могут быть прерваны. Если операция "»" для объекта cout реализована как атомная, то подобное "перемешивание" не произойдет. Если есть несколько обращений к оператору "»", то они будут выполнены последовательно. Сначала отобразится сообщение потока А, а затем сообщение потока В или наоборот, хотя они вызвали функцию вывода одновременно. Это — пример преобразования параллельных действий в последовательные, которое обеспечит безопасность выполнения потоков. Но это не единственный способ обезопасить функцию. Если функция не оказывает неблагоприятного эффекта, она может смешивать свои операции. Например, если метод добавляет или удаляет элементы из структуры, которая не отсортирована, и этот метод вызывают два различных потока, то перемешивание их операций не даст неблагоприятного эффекта. Если неизвестно, какие функции из библиотеки являются безопасными, а какие - нет, программист может воспользоваться одним из следующих вариантов действий. • Ограничить использование всех опасных функций одним потоком. • Не использовать безопасные функции вообще. • Собрать все потенциально опасные функции в один набор механизмов синхронизации. Еще один вариант — создать интерфейсные классы для всех опасных функций, которые должны использоваться в многопоточном приложении, т.е. опасные функции инкапсулируются в одном интерфейсном классе. Такой интерфейсный класс может быть скомбинирован с соответствующими объектами синхронизации с помощью наследования или композиции и использован специализированным классом. Такой подход устраняет возможность возникновения условий "гонок". 4.11. Разбиение программы на несколько потоков Выше в этой главе мы рассматривали делегирование работы в соответствии с конкретной стратегией или потоковой моделью. Итак, используются следующие распространенные модели: • делегирование ("управляющий-рабочий"); • сеть с равноправными узлами; • конвейер; • "изготовитель-потребитель". Каждая модель характеризуется собственной декомпозиг^ией работ (Work Breakdow Structure — WBS), которая определяет, кто отвечает за создание потоков и при каки условиях они создаются. В этом разделе мы рассмотрим пример программы для ка* дой модели, использующей функции библиотеки Pthread.
4.11. Разбиение программы на несколько потоков 173 Алл 1 Использование модели делегирования ассмотрели два подхода к реализации модели делегирования при разделении ^ь " на потоки. Вспомним: в модели делегирования один поток (управляющий) соз- програ. потоки (рабочие) и назначает каждому из них задачу. Управляющий поток дает ДР* каЖДому рабочему потоку задачу, которую он должен выполнить, путем зада- ДеЛСГекоторой функции. При одном подходе управляющий поток создает рабочие по- результат запросов, обращенных к системе. Управляющий поток обрабатывает екоторой функции. При одном подходе управляющий поток создает рабочие по- Т° с каждого типа в цикле событий. Как только событие произойдет, будет создан part* ий поток и ему будет назначена задача. Функционирование цикла событий в управ- ющем потоке и создание рабочих потоков продемонстрировано в листинге 4.5. // Листинг 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) ; // Управляющий поток. { //.. . if (sysconf (_SC_THREAD_THREADS_MAX) > 0) { AvailableThreads = sysconf(_SC_THREAD_THREADS_MAX) ) else{ AvailableThreads = Default } int Count = 1; Цикл while(Очередь запросов не пуста) if (threadAvailability () ) { Count++ decrementThreadAvailability() Классификация запроса switch(Тип запроса) { case X : pthread_create(&(Thread[Count])...taskX...) case Y : pthread_create(&(Thread[Count])...taskY...) case Z : pthread_create(&(Thread[Count])...taskZ...) //.. . } } else{ // Освобождаем ресурсы потоков. к°нец цикла
174 Глава 4. Разбиение С++-программ на множество потоков void *taskX(void *X) // Обрабатываем запрос типа X. incrementThreadAvailability() return(NULL) void *taskY(void *Y) // Обрабатываем запрос типа Y. incrementThreadAvailability() return(NULL) void *taskZ(void *Z) // Обрабатываем запрос типа Z. 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) {
4.11. Разбиение программы на несколько потоков 175 tjireacL.mutex_lock (&Mutex) ?f (AvailableThreads > 1) return 1 else return 0 Dthread_mutex__unlock (&Mutex) } функция threadAvailability () возвратит число 1, если максимально допусти- количество потоков для процесса еще не достигнуто. Эта функция опрашивает бальную переменную ThreadAvai lability, в которой хранится число потоков, те доступных для процесса. Управляющий поток вызывает функцию decrementThreadAvailability (), которая декрементирует эту глобальную переменную до создания им рабочего потока. Каждый рабочий поток вызывает функцию incrementThreadAvailability (), которая инкрементирует глобальную переменную ThreadAvai lability до начала его выполнения. Обе функции содержат обращение к функции pthread_mutex_lock () до получения доступа к этой глобальной переменной и обращение к функции pthread__mutex__unlock () после него. Если максимально допустимое количество потоков превышено, управляющий поток может отменить создание потока, если это возможно, или породить другой процесс, если это необходимо. Функции taskX (), taskY () и taskZ () выполняют код, предназначенный для обработки запроса соответствующего типа. Другой подход к реализации модели делегирования состоит в создании управляющим потоком пула потоков, которым (вместо создания под каждый новый запрос нового потока) переназначаются новые запросы. Управляющий поток во время инициализации создает некоторое количество потоков, а затем каждый созданный поток приостанавливается до тех пор, пока в очередь не будет добавлен новый запрос. Управляющий поток для выделения запросов из очереди по-прежнему использует цикл событий. Но вместо создания нового потока для обслуживания очередного запроса, управляющий поток уведомляет уже существующий поток о необходимости обработки запроса. Этот подход к реализации модели делегирования представлен в листинге 4.7. у Листинг 4.7. Подход 2: скелет программы реализации .' модели управляющего и рабочих потоков (псевдокод) PthreadJ: Thread[N] / { t УпРавляющий поток. **hread__create(& (Thread [1] . . .taskX. . . ) ^£read-_create(& (Thread [2] . . .taskY. . .) P^nread__create (& (Thread [ 3 ] . . . taskZ . . .) K£ while(Очередь запросов не пуста) Получение запроса Классификация запроса switch(Тип запроса) case X : Ставим запрос в очередь XQueue
176 Глава 4. Разбиение С++-программ на множество потоков Посылаем сигнал потоку Thread[l] case Y : Ставим запрос в очередь YQueue Посылаем сигнал потоку Thread[2] case Z : Ставим запрос в очередь ZQueue Посылаем сигнал потоку Thread[3] //. .. } Конец цикла } void *taskX(void *X) { Цикл Приостанавливаем поток до получения сигнала Цикл while (Очередь XQueue не пуста) Извлекаем запрос из очереди Обрабатываем запрос Конец цикла while Конец цикла } void *taskY(void *Y) { Цикл Приостанавливаем поток до получения сигнала Цикл while (Очередь YQueue не пуста) Извлекаем запрос из очереди Обрабатываем запрос Конец цикла while Конец цикла } void *taskZ(void *Z) { Цикл Приостанавливаем поток до получения сигнала Цикл while (Очередь ZQueue не пуста) Извлекаем запрос из очереди Обрабатываем запрос Конец цикла while Конец цикла } //... В листинге 4.7 управляющий поток создает N рабочих потоков (по одному для к дого типа задачи). Каждая задача связана с обработкой запросов некоторого тй В цикле событий управляющий поток извлекает запрос из очереди запросов, опре^ ляет его тип, ставит его в очередь запросов, соответствующую типу, а затем оправ-71 сигнал потоку, который обрабатывает запросы из этой очереди. Функции пото также содержат циклы событий. Поток приостанавливается до тех пор, пока не п •>. чит сигнал от управляющего потока о существовании запроса в его очереди. П° J "пробуждения" (уже во внутреннем цикле) поток обрабатывает все запросы Д° пор, пока его очередь не опустеет.
4.11. Разбиение программы на несколько потоков 177 л 11 2. Использование модели сети с равноправными узлами «яде/иг равноправных узлов один поток сначала создает все потоки, необходимые тполнения всех задач. Каждый из равноправных потоков обрабатывает запро- ^ оступающие из собственного входного потока данных. В листинге 4.8 представ- СЪ скелет программы, реализующий при разделении программы на потоки метод равноправных узлов. Листинг 4.8. Скелет программы реализации модели . равноправных потоков pthread_t Thread [N] // initial thread pthread_create(&(Thread[l] . . .taskX. . . ) pthread_create(&(Thread[2]...taskY...) pthread_create(& (Thread [3] . . . taskZ . . . ) //... } void *taskX(void *X) { Цикл while (Доступны запросы типа XRequests) Выделяем запрос Обрабатываем запрос Конец цикла return (NULL) м°Дели равноправных потоков каждый поток отвечает за собственный входной °к данных. Входные данные могут быть выделены из базы данных, файла и т.п. 4-11.3. Использование модели конвейера эта °дели к°нвейера поток входных данных обрабатывается поэтапно. На каждом ним еКоторая порция работы (часть входного потока данных) обрабатывается од- nODir ТОКОМ выполнения, а затем передается для обработки следующему. Каждая буДе Вх°Дных данных переходит на очередной этап обработки до тех пор, пока не вхОДн еРШена вся обработка. Такой подход позволяет обрабатывать несколько ^нсен Потоков данных одновременно. Каждый поток выполнения отвечает за дос- (т-е, ел пРОМежуточного результата, делая его доступным для следующего этапа РаПпе ^1°1Цего потока конвейера). Скелет программы реализации модели конвейе- Дставлен в листинге 4.9.
178 Глава 4. Разбиение С++-программ на множество потоков // Листинг 4.9. Скелет программы реализации модели // конвейера //- . . pthread_t Thread[N] Queues[N] // Начальный поток. { Помещаем все входные потоки данных pthread_create(&(Thread[1] pthread_create(&(Thread[2] pthread_create(&(Thread[3] //. . . } анных в stagel. stage2. stage3. очередь этапа stagel. .); .); .); void *stageX(void *X) { Цикл Приостанавливаем выполнение до появления в очереди порции входных данных. Цикл while (Очередь XQueue не пуста) Извлекаем порцию входных данных. Обрабатываем порцию входных данных. Помещаем результат в очередь следующего этапа. Конец цикла Конец цикла return(NULL) } //. . . В листинге 4.9 объявляется N очередей для N этапов. Начальный поток помещает все порции входных потоков в очередь первого этапа, а затем создает все потоки, необходимые для выполнения всех этапов. Каждый этап содержит свой цикл событий. Поток выполнения находится в состоянии ожидания до тех пор, пока в его очереди не появится порция входных данных. Внутренний цикл продолжается № опустения соответствующей очереди. Порция входных данных извлекается из очереди, обрабатывается, а затем помещается в очередь следующего этапа обработки (следующего потока выполнения). 4.11.4. Использование модели "изготовитель- потребитель" В модели "изготовитель-потребитель" поток-"изготовитель" готовит даннь "потребляемые" потоком-"потребителем" (причем таких потоков-"потребителеи * жет быть несколько). Данные хранятся в блоке памяти, разделяемом всеми потока* как изготовителем, так и потребителями. В листинге 4.10 представлен скелет пр граммы реализации модели "изготовитель-потребитель" (эта модель также исполь валась в программах 4.5, 4.6 и 4.7).
4.11. Разбиение программы на несколько потоков 179 тинг 4.10. Скелет программы реализации модели // Я*1 "изготовитель-потребитель" // d mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER jgjSCt Thread[2] Queue // начальный поток. * Pthread_create(&(Thread[1] . . .producer. . .) ; pthread_create(&(Thread[2] . . .consumer. . .) ; //... } oid *producer (void *X) // Поток-"изготовитель" { Цикл Выполняем действия pthread_mutex_lock(&Mutex) Помещаем данные в очередь pthread_mutex_unlock(&Mutex) Уведомляем потребителя Конец цикла } void * consumer(void *X) // Поток-"потребитель" { Цикл Приостанавливаем до получения сигнала Цикл while(Очередь данных не пуста) pthread_mutex_lock(&Mutex) Извлекаем данные из очереди pthread__mutex_unlock (&Mutex) Выполняем действия Конец цикла Конец цикла Ь листинге 4.9 начальный поток создает оба потока: "изготовителя" и "потребителя", ^^изготовитель" содержит цикл, в котором после выполнения некоторых дейст- олокируется мьютекс для совместно используемой очереди, чтобы поместить в нее дготовленные для потребителя данные. После этого "изготовитель" деблокирует ТСКС И Посылает сигнал потоку-"потребителю" о том, что ожидаемые им данные уже нfi Дятся в очереди. Поток-"изготовитель" выполняет итерации цикла до тех пор, пока он n T ВЬ1Полнена вся работа. Поток-"потребитель" также выполняет цикл, в котором "потп °СТанавливается Д° тех пор, пока не получит сигнал. Во внутреннем цикле поток- *Hdv ИТель' обрабатывает все данные до тех пор, пока не опустеет очередь. Он бло- р,.^ Мы°текс для разделяемой очереди перед извлечением из нее данных и деблоки- грамм Ютекс после этого. Затем он выполняет обработку извлеченных данных. В пробыть п°ток-"потребитель" помещает свои результаты в файл. Вместо файла может роли. ользована другая структура данных. Зачастую потоки-"потребители" играют две Tp^' ПотРебителя, так и изготовителя. Сначала возможно "исполнение" роли по- аоток Я Не°бработанных данных, подготовленных потоком-"изготовителем", а затем 1Ч>ЛС П^ает роль "изготовителя", когда он обрабатывает данные, сохраняемые в дру- естн° используемой очереди, "потребляемой" другим потоком.
180 Глава 4. Разбиение С++-программ на множество потоков 4.11.5. Создание многопоточных объектов Модели делегирования, равноправных потоков, конвейера и типа "изготовит потребитель" предлагают деление программы на несколько потоков с помощ **" функций. При использовании объектов функции-члены могут создавать потоки п выполнения нескольких задач. Потоки используются для выполнения кода от име объекта: посредством отдельных функций и функций-членов. В любом случае потоки объявляются в рамках объекта и создаются одной из фун ций-членов (например, конструктором). Потоки могут затем выполнять некотопь независимые функции (функции, определенные вне объекта), которые вызывают функции-члены глобальных объектов. Это — один из способов создания многопоточного объекта. Пример многопоточного объекта представлен в листинге 4.10. // Листинг 4.10. Объявление и определение многопоточного // объекта #include <pthread.h> #include <iostream> #include <unistd.h> 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_j oin(Threadl,NULL); pthread_j oin(Thread2,NULL); //. . . } int multithreaded_object::cl(void) { // Выполнение действий. return(1); } int multithreaded_object::c2(void) { // Выполнение действий. return(1);
Резюме 181 } •threaded_object MObj ; •я *taskl(void *) return(NULL); } void *task2(void *) { MObj.c2(); return (NULL) ; } В листинге 4.10 в классе multithread_object объявляются два потока. Они создаются и присоединяются к основному потоку в конструкторе этого класса. Поток Threadl выполняет функцию taskl (), а поток Thread2 — функцию task2 (). Функции taskl () и task2 () затем вызывают функции-члены глобального объекта MOb j. 4.12. Резюме В последовательной программе всю нагрузку можно разделить между отдельными подпрограммами таким образом, чтобы выполнение очередной подпрограммы было возможно только после завершения предыдущей. Существует и другая организация программ, когда, например, вся работа выполняется в виде мини-программ в рамках основной программы, причем эти мини-программы выполняются параллельно основной. Такие мини-программы могут быть реализованы как процессы или потоки. Если в реализации используются процессы, то каждый процесс должен иметь собственное адресное пространство, а если процессы должны взаимодействовать между собой, то такая реализация требует обеспечения механизма межпроцессного взаимодействия. Для потоков, разделяющих адресное пространство одного процесса, не нужны специальные методы взаимодействия. Но для защиты совместно используемой амяти (чтобы не допустить возникновения условий "гонок") необходимо использование механизмы синхронизации, как мьютексы. Существует ряд моделей, которые можно использовать для делегирования работы окам и управления их созданием и аннулированием. В модели делегирования один по- ^(Управляющий) создает другие потоки (рабочие) и назначает им задачи. Управляю- Поток ожидает до тех пор, пока каждый рабочий поток не завершит свою задачу. Даец ^ ^iwk ожидает до тех пор, пока каждый раоочии поток не завершит свою задачу. г* спользовании модели равноправных узлов есть один поток, который изначально соз- р - Се п°токи, необходимые для выполнения всех задач, причем этот поток считается Им потоком, поскольку он не осуществляет никакого делегирования. Все потоки 8 этой °Ха Модели имеют одинаковый статус. Применяя модель конвейера, программу можно рактеризовать как сборочную линию, в которой входной поток (поток входных дан- Чйт Рабатывается поэтапно. На каждом этапе поток обрабатывает некоторую пор- а^ ХоДНых элементов. Порция входных элементов перемещается от одного потока **ения к следующему до тех пор, пока не завершится вся предусмотренная обра-
182 Глава 4. Разбиение С++-программ на множество потоков ботка. На последнем этапе работы конвейера формируются его результаты, т.е. поел ний поток отвечает за формирование конечных результатов программы. В мол ^ "изготовитель-потребитель" поток-"изготовитель" готовит данные, "потребляемые" И током-"потребителем". Данные хранятся в блоке памяти, разделяемом всеми пото ми: как изготовителем, так и потребителями. При использовании объектов функци члены могут создавать потоки для выполнения нескольких задач. Объекты мож создавать с многопоточной направленностью. В этом случае потоки объявляют в самом объекте. Функция-член может создать поток, который выполняет независ мую функцию, а она (в свою очередь) вызывает одну из функций-членов объекта. Для создания и управления потоками многопоточного приложения можно и пользовать библиотеку Pthread. Библиотека Pthread опирается на стандартизирован- ный программный интерфейс, предназначенный для создания и поддержки потоков Этот интерфейс определен комитетом стандартов IEEE в стандарте POSIX 1003.1с Сторонние производители при создании своих продуктов должны придерживаться этого стандарта POSIX.
СИНХРОНИЗАЦИЯ ПАРАЛЛЕЛЬНО ВЫПОЛНЯЕМЫХ ЗАДАЧ В этой главе... 5.1. Координирование порядка выполнения потоков 5.2. Синхронизация доступа к данным 5.3. Что такое семафоры 5.4. Объектно-ориентированный подход к синхронизации 5.5. Резюме
J~y\3J^J3J Отношение этих механизмов ко времени требует тщательного изучения. <...> Нас почти не интересовала производительность вычислительной машины для одного входного сигнала. Чтобы адекватно функционировать, она должна показывать удовлетворительную производительность для целого класса входных сигналов, а это будет означать удовлетворительную производительность для класса входных сигналов, получение которого ожидается статистически... - Ноберт Винер (Norbert Wiener), Кибернетика Во всех компьютерных системах ресурсы ограничены. Ведь любой объем памяти конечен, как и количество устройств ввода-вывода, портов, аппаратных прерываний и процессоров. Если в среде ограниченных аппаратных ресурсов приложение состоит из нескольких процессов и потоков, то эти составляющие должны конкурировать за память, периферийные устройства и процессорное время. КогД и как долго процесс или поток будет использовать системные ресурсы, определяе операционная система. При использовании приоритетного планирования операЦ онная система может прерывать выполняющийся процесс или поток, чтобы удовл творить все остальные процессы и потоки, соревнующиеся за системные ресурс Процессам и потокам приходится также соперничать за программные ресурсы и р сурсы данных. Примерами программных ресурсов служат разделяемые библиоте (которые предоставляют в общее пользование набор процедур или функций для пр цессов и потоков), а также приложения, программы и утилиты. При совместном пользовании программных ресурсов в памяти содержится только одна копия пр граммного кода. Под ресурсами данных подразумеваются объекты, системные данН
5.1. Координация порядка выполнения потоков 185 мер, переменные среды), файлы, глобально определенные переменные и струк- ^НаП анных. Что касается ресурсов данных, то процессы и потоки могут иметь собст- Г^Ь е копии данных. В других случаях желательно и, возможно, даже необходимо, ВСН^ данные были разделяемыми. Одни процессы и потоки, работая вместе, исполь- ЧТ ограниченные системные ресурсы в определенном порядке, в то время как другие 3 - твуют независимо и асинхронно, соревнуясь за использование разделяемых ресур- Для управления процессами и потоками, конкурирующими за использование дан- программист может задействовать ряд специальных методов и механизмов. Синхронизация также необходима для координации порядка выполнения парал- ных задач. Примером может служить модель "изготовитель-потребитель", кото- рассмотрена в главе 4. "Изготовитель" обязательно начинает выполняться до "потребителя", но не обязательно завершается до него. Подобные задачи нуждаются я синхронизации. Синхронизация данных {синхронизация доступа к данным) и задач (синхронизация последовательностей инструкций) — два типа синхронизации, которые необходимо обеспечить при выполнении нескольких параллельных задач. 5.1. Координация порядка выполнения потоков Предположим, у нас есть три параллельно выполняющихся потока — А, В и С. Все они участвуют в обработке списка. Список необходимо отсортировать, выполнить в нем операции поиска и вывода результатов. Каждому потоку назначается отдельная задача. Так, поток А должен отобразить результаты поиска, В — отсортировать список, а С — провести поиск. Сначала список необходимо отсортировать, затем выполнить несколько параллельных операций поиска, а уж потом отобразить результаты. Если задачи, выполняемые потоками, не синхронизировать надлежащим образом, то поток А может попытаться отобразить еще не сгенерированные результаты, что нарушит постусловие, или выходное условие (postcondition), процесса. Предусловием, или входным условием (precondition), здесь является необходимость получения отсортированного списка до выполнения в нем поиска. Поиск в неотсортированном списке может дать неверные результаты. Поэтому для этих трех потоков необходимо обеспечить синхронизацию задач, которая приводит в исполнение постусловия и предусловия логических процессов. UML-диаграмма идов деятельности для этого процесса представлена на рис. 5.1. ^начала поток В должен отсортировать список, затем эстафета управления переда- ся МНогоканальному" поиску, порождаемому потоком С. И только после завершения сковых работ по всем направлениям поток А отображает результаты поиска. &-'-1. Взаимоотношения между синхронизируемыми задачами *-> Двум ^ествУет четыре основных типа отношений синхронизации между любыми Ло * отоками в одном процессе или между любыми двумя процессами в одном при- (ффх ^И: старт-старт (СС), финиш-старт (ФС), старт-финиш (СФ) и финиш-финиш Haim Пом°Щью этих основных типов отношений можно охарактеризовать коорди- Каокп За^ач между потоками и процессами. UML-диаграмма видов деятельности для ° типа отношений синхронизации показана на рис. 5.2.
186 Глава 5. Синхронизация параллельно выполняемых задач Поток А Отображение результатов поиска Поток В ? [Отсортированный список] Поток С [Неотсортированный список] Поиск значения 1 Поиск значения 2 Поиск значения п Рис. 5.1. Диаграмма видов деятельности для задач сортировки списка, поиска и отображен результатов поиска 5.1.2. Отношения типа старт-старт (СС) В отношениях синхронизации типа старт-старт одна задача не может начатьс тех пор, пока не начнется другая. Одна задача может начаться раньше другой, н позже. Предположим, у нас есть программа, которая реализует инкарна (воплощение). Инкарнация "материализуется" в виде говорящей головы, создан! разумеется, компьютерной программой. Инкарнация обеспечивает своего Р "одушевление" программ. Программа, которая реализует "одушевление", имее сколько потоков. Здесь нас в первую очередь интересует поток А, который "отвечае
5.1. Координация порядка выполнения потоков 187 результата, и поток В, который управляет звуком, или голосом, говорящей аНИМ Мы хотим создать иллюзию синхронизации звука и движений рта. В идеале они г0710 происходить абсолютно одновременно. При наличии нескольких процессоров Д°Л тока могут начинаться одновременно. Эти потоки связаны отношением типа тарт. В соответствии с условиями временной синхронизации допускается, чтобы стаР * наЧинался немного раньше потока В (именно немного — иначе будет нарушена П мя одновременности), но поток В не может начаться раньше потока А. Голос иллюзии ^а г ен ожидать анимацию, а не наоборот. Совершенно нежелательно услышать голос А того как зашевелятся губы (если это не синхронное озвучивание). гтАРТ-СТАРТ ФИНИШ-ФИНИШ Поток В —г « сигнал » Поток В заблокирован Поток А JL Поток В . ПотокА ' заблокирован I « сигнал » Р4" ФИНИШ-СТАРТ Поток А СТАРТ-ФИНИШ Поток В Поток А Поток В « сигнал » Поток В заблокирован 1 Поток А I заблокирован I « сигнал » Рйс- 5.2. Возможн! ые отношения синхронизации между задачами А и В ■1 -3. Отношения типа финиш-старт (ФС) До тех °Шениях синхронизации типа финиш-старт задача А не может завершиться СьШовн пока не начнется задача В. Этот тип отношений типичен для родительско- ^Pbix о пРоц^сс°в* Родительский процесс не может завершить выполнение неко- Не бУДет ^аЦИ^ до тех поР' пока не будет сгенерирован сыновний процесс или пока Ние. СынП°ЛУ^еНа обРатная связь от сыновнего процесса, который начал выполне- ^АИмукч НИИ пР°Цесс, "просигналивший" родителю или предоставивший ему необ- 141 Может <Р°РМаЦию, продолжает выполняться, а родительский процесс после это- Завершиться.
188 Глава 5. Синхронизация параллельно выполняемых задач 5.1.4. Отношения типа старт-финиш (СФ) Отношения типа старт-финиш можно считать обратным вариантом отношен * типа финиш-старт. В отношениях синхронизации типа старт-финиш одна задача может начаться до тех пор, пока не завершится другая. Задача А не может начать в полнение до тех пор, пока задача В не финиширует или не завершит выполнение о ределенной операции. Если процесс А считывает данные из канала, связанног с процессом В, то процесс В должен записать данные в канал, прежде чем процесс А начнет считывать из него данные. Процесс В должен завершить по крайней мере одну операцию, записав в канал один элемент, прежде чем начнет действовать процесс А Потоки, действующие по принципу "производитель-потребитель", — это еще один пример взаимоотношений типа финиш-старт. Потоки, обслуживающие сортировку и поиск элементов в списке (см. рис. 5.1), также демонстрируют этот тип отношений Прежде чем начнут действовать потоки, реализующие поиск, должен завершить свою работу поток сортировки. Во всех этих случаях один поток или процесс должен завершить свою операцию, прежде чем другой попытается выполнить свою задачу. Если работа потоков не будет скоординирована, глобальная цель потока, процесса или приложения достигнута не будет или же будут получены ошибочные результаты. Отношения типа старт-финиш обычно предполагают существование информационной зависимости между задачами. При информационной зависимости для корректной работы потоков или процессов необходимо обеспечить межпоточное или межпроцессное взаимодействие. Например, поток поиска данных в списке сгенерирует некорректные результаты, если не будет выполнена сортировка списка. И поток- "потребитель" не получит файлы для обработки, если поток-"производитель" не подготовит их для потребителя. 5.1.5. Отношения типа финиш-финиш (ФФ) В отношениях синхронизации типа финиш-финиш одна задача не может завершиться до тех пор, пока не завершится другая, т.е. задача А не может финишировать до задачи В. Этот тип отношений можно применить к описанию отношений между родительскими и сыновними процессами, которые рассматривались в главе 3. Родительский процесс должен ожидать до тех пор, пока не завершатся все сыновние процессы, и только потом сможет завершиться сам. Если описанная последовательность нарушится, и родительский процесс финиширует раньше своих потомков, то эти за вершенные сыновние процессы перейдут в зомбированное состояние. Родительски процессы не должны завершаться (выходить из системы в данном случае) до тех пор» пока не выполнятся до конца их сыновние процессы. Для родительских процесс это достигается за счет вызова функции wait () для каждого из своих сыновних пр цессов либо ожидания деблокировки (освобождения) мьютекса или условной пер менной, что может быть осуществлено сыновними потоками. Еще одним пример * отношений типа финиш-финиш может служить модель "управляющий-рабочий • дача управляющего потока — делегировать работу рабочим потокам. Для управляю го крайне нежелательно завершить работу раньше рабочих. В этом случае не был обработаны новые запросы к системе, не имели работы существующие потоки создавались новые. Если управляющий поток является основным потоком проН и он завершается, то процесс должен завершиться вместе со всеми рабочими по
5.2. Синхронизация доступа к данным 189 модели равноправных потоков поток А динамически создает объект, пере- ми. Ьсл ^ тоКуБ, и потокА затем завершается, то вместе с ним разрушается и создавав ^ объект. Если это произойдет до того, как поток В получит возможность даняы * этот объект, возникнет ошибка сегментации или нарушится доступ исП° Чтобы предотвратить возникновение этого типа ошибок, завершение по- кданн ин оНИЗИруется с помощью функции pthread_join(). Обращение к этой ТОК° и заставляет вызывающий поток ожидать до тех пор, пока не финиширует за- " поток. Таким образом и создается синхронизация типа финиш-финиш. 5.2. Синхронизация доступа к данным Существует разница между данными, разделяемыми между процессами, и данными разделяемыми между потоками. Потоки совместно используют одно и то же адресное пространство, в то время как процессы имеют отдельные адресные пространства. Если существуют два процесса А и В, то данные, объявленные в процессе А, недоступны процессу В, и наоборот. Следовательно, один из методов, используемых процессами для разделения данных, состоит в создании блока памяти, отображаемого затем на адресное пространство процессов, которые должны разделять память. Другой подход предполагает создание блока памяти, существующего вне адресного пространства обоих процессов. К типам механизмов межпроцессного взаимодействия (МПВ) относятся каналы, файлы и передача сообщений. Именно блок памяти, разделяемый между потоками внутри одного и того же адресного пространства, и блок памяти, разделяемый между процессами вне обоих адресных пространств, требует синхронизации данных. Память, разделяемая между потоками и процессами, показана на рис. 5.3. Синхронизация данных необходима для управления состоянием "гонок", а также для того, чтобы позволить параллельным потокам или процессам безопасно получить доступ к блоку памяти. Синхронизация данных позволяет управлять считыванием и модификацией данных в блоке памяти. В многопоточной среде параллельный доступ к общей памяти, глобальным переменным и файлам обязательно должен быть синхронизирован. Что касается программного кода задачи, то синхронизация данных еобходима в тех его блоках, где делается попытка получить доступ к блоку памяти, ооальным переменным или файлам, разделяемым с другими параллельно выпол- мыми процессами или потоками. Такие блоки кода называются критическими раз- w. ь качестве критического раздела может выступать любой блок кода, который яет позицию файлового указателя, записывает данные в файл, закрывает файл дел 1вает или устанавливает глобальные переменные либо структуры данных. Вы- Изэ С Таких заДач- которые выполняют чтение или запись данных, является одним в управления параллельным доступом к совместно используемой памяти. 5-2-1. Модель PRAM в°льнь Ь ^^'^ (Parallel Random-Access Machine — машина с параллельным произ- ^*> Р £°ступом) — это упрощенная модель с N процессорами, обозначаемыми Р,, ^Нно "' котоРые разделяют одну глобальную память. Все процессоры одновре- •^чают доступ для чтения и записи к совместно используемой глобальной
190 Глава 5. Синхронизация параллельно выполняемых задач памяти. Каждый из этих теоретических процессоров может получить доступ к п ляемой глобальной памяти в течение одного непрерываемого интервала времени и дель PRAM включает алгоритмы параллельного, а также исключающего чтения и ° писи. Алгоритмы параллельного чтения позволяют нескольким процессорам ол * временно использовать одну и ту же область памяти без какого бы то ни бь искажения данных. Алгоритмы параллельной записи позволяют нескольким проп ° сорам записывать данные в разделяемую область памяти. Алгоритмы исключают чтения используются для получения гарантии того, что никакие два процессора ник гда не будут считывать информацию из одной и той же области памяти одновреме но. Алгоритмы исключающей записи гарантируют, что никакие два процессора ник гда не будут записывать данные в одну и ту же область памяти одновременно. Модель PRAM можно использовать для определения характера параллельного доступа к общей памяти со стороны нескольких задач. АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А РАЗДЕЛ СТЕКОВ СВОБОДНАЯ ПАМЯТЬ Локальные переменные Локальные переменные Глобальные переменные РАЗДЕЛ ДАННЫХ | Глобальные , структуры данных. j Глобальные I переменные ! Константы j ' Статические , переменные РАЗДЕЛ КОДА ■. >■.";■ " .-.■.-;■.:■■"*,, 71 ■■ 1 ! } ! i i Разделяемая память [ Файлы 1 | FIFO-очереди/каналы | МЕХАНИЗМЫ МПВ АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В РАЗДЕЛ СТЕКОВ ^ГЩ^ТЬ^ РАЗДЕЛ ДАННЫХ РАЗДЕЛ КОДА Локальные переменные Стек потока А Стек потока В _ | Глобапьные п^е^нные \ *WWi Данных j Глобальные 1 переменные Глобальные ! Константы переменные j Статические J переманные •Деревья * Графы * Очереди * Стеки :.-■ ■•■■'•"'-■■■■■■'- ■■-*'» ■■::'- '" ■:;[ 1 ,<1 1 -1 '■■■ ■■■! ? ■ .-.:..• - ., .1 ::' ': 1 •;Кед;дето1Ш.Д :\. 1 ,,.. ...... ....... .... , ••• j ■ | -Мод:§1отдаа.8 ■ ' j Рис. 5.3. Память, разделяемая между потоками и процессами
5.2. Синхронизация доступа к данным 191 1 Параллельный и исключающий доступ к памяти ^' ' ы параллельного и исключающего чтения и записи можно скомбиниро- ^ЛГ° лучить следующие типы объединенных алгоритмов, которые можно реали- ваТЬ^ МЯ организации доступа к данным: ключающее чтение и исключающая запись (exclusive read and exclusive # Site-EREW); оаллельное чтение и исключающая запись (concurrent read and exclusive Site-CREW); исключающее чтение и параллельная запись (exclusive read and concurrent urite-ERCW); • параллельное чтение и параллельная запись (concurrent read and concurrent „rite-CRCW). Эти алгоритмы можно рассматривать как стратегии доступа, реализуемые задачами, которые совместно используют данные (рис. 5.4). Алгоритм EREW подразумевает последовательный доступ к разделяемой памяти, т.е. к общей памяти в любой момент времени может получить доступ только одна задача. Примером стратегии доступа EREW может служить вариант реализации модели потоков "производитель- потребитель", рассмотренный в главе 4. Доступ к очереди, содержащей имена файлов, может быть ограничен исключающей записью "изготовителя" и исключающим чтением "потребителя". Б любой момент времени доступ к очереди может быть разрешен только для одной задачи. Стратегия CREW позволяет множественный доступ для чтения общей памяти и исключающий доступ для записи в нее данных. Это означает отсутствие ограничений на количество задач, которые могут одновременно читать разделяемую память, но записывать в нее данные может только одна задача. При этом параллельное чтение может происходить одновременно с записью данных в общую память. При использовании этой стратегии доступа все читающие задачи могут прочитать различные значения, поскольку во время чтения значения из общей памяти записывающая задача может его модифицировать. Стратегия доступа ERCW — это прямая противоположность стратегии CREW. При использовании стратегии ERCW Р решены параллельные записи в общую память, но лишь одна задача может читать в любой момент времени. Стратегия доступа CRCW позволяет множеству задач вы- олнять параллельное чтение и запись, низ Я ЭТИХ четыРех типов алгоритмов требуются различные уровни и типы синхро- треб Диапазон довольно широк: от стратегии доступа, реализация которой требу Т МИнимальнои синхронизации, до стратегии доступа, реализация которой п°ДДе максимальн°й синхронизации. Наша задача— реализовать эти стратегии, мы. ЕЯЕ\уВаЯ Целостность данных и удовлетворительную производительность систе- сути, т ~" самая простая для реализации стратегия, поскольку она предполагает, по Сказать К° ПоследовательнУю обработку. На первый взгляд самой простой может 1сажется тРатегия CRCW, но она таит в себе массу трудностей. А ведь это только Не Идет ° С^ЛИ К памяти можно получить доступ без ограничений, то в ней и речь Ная Для d аК°И бЫ Т° НИ было стратегии. Все как раз наоборот: CRCW — самая трудодни стратегия, которая требует максимальной синхронизации.
Глава 5. Синхронизация параллельно выполняемых задач EREW (исключающее чтение и исключающая запись) ^блокируется Поток А Запись Разделяемая |Чтение^ память Поток В Поток А Запись ■ Разделяемая I память Чтение Поток В ERCW (исключающее чтение и параллельная запись) Поток А Поток С Запись ► Запись ► Разделяемая память |Чтение 1 Чтение Поток В Поток D CREW (параллельное чтение и исключающая запись) Поток А Поток С Запись w Запись! '■ Разделяемая память Чтение Чтение Поток В Поток D CRCW (параллельное чтение и параллельная запись) Поток А Поток С Запись ► Запись р Разделяемая память Чтение Чтение Поток В Поток D Рис. 5.4. Стратегии доступа EREW, CREW, ERCW и CRCW
5.3. Что такое семафоры 193 5.3. Что такое семафоры Семафор— это механизм синхронизации, который можно использовать для управ- ения отношениями между параллельно выполняющимися программными компонентами и реализации стратегий доступа к данным. Семафор — это переменная спе- шального вида, которая может быть доступна только для выполнения узкого диапазона операций. Семафор используется для синхронизации доступа процессов и потоков к разделяемой модифицируемой памяти или для управления доступом к устройствам или другим ресурсам. Семафор можно рассматривать как ключ к ресурсам. Этим ключом может владеть в любой момент времени только один процесс или поток. Какая бы задача ни владела этим ключом, он надежно запирает (блокирует) нужные ей ресурсы для ее монопольного использования. Блокирование ресурсов заставляет другие задачи, которые желают воспользоваться этими ресурсами, ожидать до тех пор, пока они не будут разблокированы и снова станут доступными. После разблокирования ресурсов следующая задача, ожидающая семафор, получает его и доступ к ресурсам. Какая задача будет следующей, определяется стратегией планирования, действующей для данного потока или процесса. 5.3.1. Операции по управлению семафором Как упоминалось выше, к семафору можно получить доступ только с помощью специальных операций, подобных тем, которые выполняются с объектами. Это операции декремента, Р (), и инкремента, V (). Если объект Mutex представляет собой семафор, то логика реализации операций Р (Mutex) и V (Mutex) может выглядеть таким образом: Р (Mutex) if (Mutex > 0) { Mutex--; } else { Блокирование по объекту Mutex; У (Mutex) 1f (Очередь доступа к объекту Mutex не пуста){ Передача объекта Мьютекс следующей задаче; ®lse{ Mutex++; Реализация зависит от конкретной системы. Эти операции неделимы, т.е. их не- Можно прервать. Если операцию Р () попытаются выполнить сразу несколько за- » то лишь одна из них получит разрешение продолжить работу. Если объект Mutex уже декрементирован, то задача будет заблокирована и займет место в очереди, рация V () вызывается задачей, которая имеет доступ к объекту Mutex. Если полу- Ия доступа к объекту Мьютекс ожидают другие задачи, он "передается" следующей ^е из очереди. Если очередь задач пуста, объект Mutex инкрементируется. Перации с семафором могут иметь другие имена: х ПеРация Р (): Операция V (): °Ck() unlock()
194 Глава 5. Синхронизация параллельно выполняемых задач Значение семафора зависит от его типа. Двоичный семафор будет иметь значение о 1. Вычислительный семафор (определяющий лимиты ресурсов для процессов, получаюц И доступ к ним) может иметь некоторое неотрицательное целочисленное значение Стандарт POSIX определяет несколько типов семафоров. Эти семафоры испо зуются процессами или потоками. Типы семафоров (а также их некоторые основы операции) перечислены в табл. 5.1. Таблица 5.1. Типы семафоров, определенные стандартом POSIX Тип семафора Пользователь Описание Мьютексный семафор Блокировка для обеспечения чтения и записи Условная переменная Несколько условных переменных Процессы или потоки Процессы или потоки Процессы или потоки Процессы или потоки Механизм, используемый для реализации взаимного исключения в критическом разделе кода Механизм, используемый для реализации стратегии доступа для чтения и записи среди потоков Механизм, используемый для уведомления потоков о том, что произошло событие. Событийный мьютекс остается заблокированным потоком до тех пор, пока не будет получен соответствующий сигнал Аналогичен событийному мьютексу, но включает несколько событий или условий Операционные системы, которые не противоречат спецификации Single UNIX Specification или стандарту POSIX Standard, поддерживают реализацию семафоров, которые являются частью библиотеки libpthread (соответствующие функции объявлены в заголовке рthread. h). 5.3.2. Мьютексные семафоры Стандарт POSIX определяет мьютексный семафор, используемый потоками и процессами, как объект типа pthread_mutex_t. Этот мьютекс обеспечивает базовые операции, необходимые для функционирования практического механизма синхронизации. • инициализация; • запрос на монопольное использование; • отказ от монопольного использования; тестирование монопольного использования; разрушение. Функции класса pthread_mutex_t, которые используются для выполнения этих па- сема- базовых операций, перечислены в табл. 5.2. Во время инигшализации выделяется мять, необходимая для функционирования мьютексного семафора, и объекту сС фора присваивается некоторое начальное значение. Для двоичного семафора чальным может быть значение 0 или 1 Начальным значением вычислительного мафора может быть неотрицательное число, которое представляет количе
5.3. Что такое семафоры 195 ix ресурсных единиц. Семафорное значение можно использовать для представ- досту! льНОГО количества запросов, которое способна обработать программа в одном ЛСН В отличие от обычных переменных, в инициализации которых сомневаться не С ится факт инициализации мьютекса с помощью вызова соответствующей функции ПР ир0вать невозможно. Чтобы убедиться в том, что мьютекс проинициализирован, ^Р лИмо после вызова операции инициализации принять некоторые меры предос- НС ности (например, проверить значение, возвращаемое соответствующей мьютекс- Т " Дикцией, или значение переменной ептю). Системе не удастся создать мьютекс, если ется, что занята память, предусмотренная ранее для мьютексов, или превышено тимое КОЛИчество семафоров, или семафор с данным именем уже существует, е имеет место какая-то другая проблема, связанная с выделением памяти. Таблица 5.2. Функции класса pthread_mutex_t Мьютекспые операции Прототипы функций (макросы) ttinclude <pthread.h> int pthread_mutex_init( pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) ; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; <time.h> int pthread_mutex_lock( pthread_mutex_t *mutex); int pthread_mutex_timedlock( pthread_mutex_t *restrict mutex, const struct tiemspec *restrict abs_timeout); int pthread_mutex_unlock( pthread_mutex_t *mutex); int pthread_mutex_trylock( pthread_mutex_t *mutex); int pthread_mutex_destroy( pthread_mutex_t *mutex); Инициализация Запрос на монопольное использование Отказ от монопольного использования Тестирование монопольного использования Разрушение Аодобно потокам, мьютекс библиотеки Pthread имеет атрибутный объект рассматривается ниже), который инкапсулирует все атрибуты мьютекса. Этот Р утный объект можно передать функции инициализации, в результате чего бу- ин С°ЗДан мьютекс с атрибутами, заданными с помощью этого объекта. Если при ализации атрибутный объект не используется, мьютекс будет инициализирован лизи НИЯМИ' деиствующими по умолчанию. Объект типа pthread_mutex_t инициа- По ^* Тся как Деблокированный и закрытый. Закрытый мьютекс разделяется между Неск И ОДНОГО процесса. Разделяемый мьютекс совместно используется потоками мьют ИХ пРоцессов- При использовании атрибутов, действующих по умолчанию, °бъек Может быть инициализирован статически для статических мьютексных pt, B c п°мощью следующего макроса: а -Inutext Mutex = PTHREAD MUTEX INITIALIZER;
196 Глава 5. Синхронизация параллельно выполняемых задач Этот метод менее затратный, но в нем не предусмотрено проверки ошибок. Мьютекс может иметь или не иметь владельца. Операция запроса па монопольное пользование предоставляет право владения мьютексом вызывающему потоку или пп цессу. После того как мьютекс обрел владельца, поток (или процесс) получает мон польный доступ к запрашиваемому ресурсу. При попытке завладеть "уже занятым" мьютексом (путем вызова этой операции), совершенной любыми другими потокам или процессами, они будут заблокированы до тех пор, пока мьютекс не станет доступ ным. При освобождении мьютекса следующий (по очереди) поток или процесс (который был заблокирован) деблокируется и получает право собственности на этот мьютекс. И освободить его может только поток, получивший данный мьютекс во владение с помощью функции pthread_mutex_lock(). Можно также использовать синхронизированную версию этой функции. В этом случае, если мьютекс несвободен то процесс или поток будет ожидать в течение заданного промежутка времени. Если мьютекс за это время не освободится, то процесс или поток продолжит выполнение. Операция тестирования монопольного использования предназначена для проверки доступности мьютекса. Если он занят, функция возвращает соответствующее значение. Достоинство этой операции состоит в том, что поток или процесс (в случае недоступности мьютекса) не блокируется и может продолжать выполнение. Если же мьютекс свободен, вызывающему потоку или процессу предоставляется право владения запрашиваемым мьютексом. При выполнении операции разрушения освобождается память, связанная с мьютексом. Память не освобождается, если у мьютекса есть владелец или если поток (или процесс) ожидают права на владение им. 5.3.2.1. Использование мьютексного атрибутного объекта Мьютексный объект типа pthread_mutex_t можно использовать вместе с атрибутным объектом подобно атрибутному объекту потока. Мьютексный атрибутный объект инкапсулирует все атрибуты объекта мьютекса. После инициализации его могут использовать несколько мьютексных объектов, передавая в качестве параметра функции pthread_mutex_init (). Мьютексный атрибутный объект определяет ряд функций, используемых для установки таких атрибутов, как предельный приоритет, протокол и тип. Эти и другие функции мьютексного атрибутного объекта перечислены в табл. 5.3. Таблица 5.3. Функции доступа к мьютексному атрибутному объекту Прототипы функций Описание #include <pthread.h> int pthread_mutexattr_init Инициализирует мьютексный атрибутный объ- (pthread_mutexattr_t * attr) ; ект, заданный параметром attr, значениями, действующими по умолчанию для всех атрибутов, определяемых реализацией int pthread_mutexattr_destroy Разрушает мьютексный атрибутный объект, за- (pthread_mutexattr_t * attr); данный параметром attr, в результате чего он становится неинициализированным. Его можно инициализировать повторно с помощью функции pthread_mutexattr_init () ^
5.3. Что такое семафоры 197 Продолжение табл. 5.3 Прототипы функций Описание 1^hread_mucexattr_setprioceiling (pthread-_mutexattr_t: * attr, int prioceiling); Dthread„mutexattr_getprioceiling (const pthread_mutexattr_t * restrict attr, int *restrict prioceiling); int pthread_mutexattr_setprotocol (pthread_mutexattr_t * attr, protocol int protocol); int pthread_mutexattr__getprotocol (const pthread_mutexattr_t * restrict attr, int *restrict protocol); int P^hr®ad-"iutexattr_setpshared (pthread_mutexattr_t * attr, lr*t pshared) ; int Pthread_mutexattr_getpshared *const Pthread_mutexattr_t * j_. ^ restrict attr, nt ^strict pshared) ; Устанавливает и возвращает атрибут предельного приоритета мьютекса, заданного параметром attr. Параметр prioceiling содержит значение предельного приоритета мьютекса. Атрибут prioceiling определяет минимальный уровень приоритета, при котором еще выполняется критический раздел, защищаемый мьютексом. Значения, которые попадают в этот диапазон приоритетов, определяются стратегией планирования SCHED_FIFO Устанавливает и возвращает атрибут протокола мьютекса, заданного параметром attr. Параметр protocol может содержать следующие значения: PTHREAD_PRI0_NONE (на приоритет и стратегию планирования потока владение мьютексом не оказывает влияния); PTHREAD_PRIO_INHERIT (при таком протоколе поток, блокирующий другие потоки с более высокими приоритетами, благодаря владению таким мьютексом будет выполняться с самым высоким приоритетом из приоритетов потоков, ожидающих освобождения любого из мьютексов, которыми владеет данный поток); PTHREAD_PRIO_PROTECT (при таком протоколе потоки, владеющие таким мьютексом, будут выполняться при наивысших предельных значениях приоритетов всех мьютексов, которыми владеют эти потоки, независимо оттого, заблокированы ли другие потоки по каким-то из этих мьютексов) Устанавливает и возвращает атрибут process- shared мьютексного атрибутного объекта, заданного параметром attr. Параметр pshared может содержать следующие значения: PTHREAD_PROCESS_SHARED (разрешает разделять мьютекс с любыми потоками, которые имеют доступ к выделенной для этого мьютекса памяти, даже если эти потоки принадлежат различным процессам); PTHREAD_PROCESS_PRIVATE (мьютекс разделяется между потоками одного и того же процесса)
198 Глава 5. Синхронизация параллельно выполняемых задач ^^_ Окои^ниепдбк 5з Прототипы функций Описание int Устанавливает и возвращает атрибут мыотекс^ pthread_mutexattr_settype type мьютексного атрибутного объекта за- (pthread_mutexattr_t * attr, данного параметром attr. Атрибут мьютекса int type , type позволяет определить, будет ли мьютекс int распознавать взаимоблокировку, проверять pthread_mutexattr_gettype ошибки и т.д. Параметр type может содео- (const Pthread_mutexattr_t * жаТЬ такие значения: F restrict attr, int *restrict type); PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_RECURS IVE PTHREAD_MUTEX_ERRORCHECK PTHREAD_MUTEX_NORMAL Самый большой интерес представляет установка атрибута, связанного с тем, каким должен быть мьютекс: закрытым или разделяемым. Закрытые мьютексы разделяются между потоками одного процесса. Можно либо объявить мьютекс глобальным, либо организовать передачу дескриптора между потоками. Разделяемые мьютексы используются потоками, имеющими доступ к памяти, в которой размещен данный мьютекс. Такой мьютекс могут разделять потоки различных процессов. Принцип действия закрытого и разделяемого мьютексов показан на рис. 5.5. Если разделять мьютекс приходится потокам различных процессов, его необходимо разместить в памяти, которая является общей для этих процессов. В библиотеке POSIX определено несколько функций, предназначенных для распределения памяти между объектами с помощью отображаемых в памяти файлов и объектов разделяемой памяти. В процессах мьютексы можно использовать для защиты критических разделов, которые получают доступ к файлам, каналам, общей памяти и внешним устройствам. 5.3.2.2. Использование мьютексных семафоров для управления критическими разделами Мьютексы используются для управления критическими разделами процессов и потоков, чтобы предотвратить возникновение условий "гонок". Мьютексы позволяют избежать условий "гонок", реализуя последовательный доступ к критическому разделу. Рассмотрим код листинга 5.1. В нем демонстрируется выполнение двух потоков. Для защиты их критических разделов и используются мьютексы. // Листинг 5.1. Использование мьютексов для защиты // критических разделов потоков // . . . pthread_t ThreadA,ThreadB; pthread_mutex_t Mutex,- pthread_mutexattr_t MutexAttr; void *taskl(void *X) { pthread_mutex_lock(&Mutex); // Критический раздел кода. pthread_mutex_unlock(&Mutex);
5.3. Что такое семафоры 199 return(0); } void *task2(void *Х) { thread_mutex_lock (&Mutex) ; // критический раздел кода. pthread_mutex_unlock(&Mutex) ; 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 (&MutexAt tr , PTHREAD_PROCESS_SHARED); Pthread_mutex_init (ScMutexl, &MutexAttr) ; M(Rt = fork()) == 0){ // Сыновний процесс. Pthread_mutex_lock(&Mutexl) ; // Критический раздел. } pthread-mutex_unlock (&Mutexl) ; e^ // Родительский процесс. Pthread_mutex_lock(&Mutexl) ; //Критический раздел. } Pthread-mutex_unlock (&Mutexl) ; //... } Ге^игп(0);
200 Глава 5. Синхронизация параллельно выполняемых задач АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА Поток. А Поток С Запись Запись 1 i Закрытый' мьютекс А 1 АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В Поток А Ротоне Запись Запись 1 Разделяемый мьютекс АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В Поток А Поток С Запись Запись | 1 Разделяемый мьютекс Чтение |>1 Чтение ПотхжА | Поток С Рис. 5.5. Закрытые и разделяемые мьютексы Важно отметить, что в листинге 5.2 при вызове следующей функции мьютекс и циализируется как разделяемый: pthread_mutexattr_setpshared(&MutexAttr, PTHREAD_PROCESS__SHARED) ;
5.3. Что такое семафоры 201 вка этого атрибута равным значению PTHREAD_PROCESS_SHARED позволяет tv Mutex стать разделяемым между потоками различных процессов. После вы- Ъ Жункции fork () сыновний и родительский процессы могут защищать свои кри- 3°В кие разделы с помощью объекта Mutex. Критические разделы этих процессов ТИ содержать некоторые ресурсы, разделяемые обоими процессами. 5 3.3. Блокировки для чтения и записи Мьютексные семафоры позволяют управлять критическими разделами, обеспе- последовательный вход в эти разделы. В любой момент времени вход в кри- ический раздел разрешается только одному потоку или процессу. Реализуя блоки- овки для чтения и записи, можно разрешить вход в критический раздел сразу нескольким потокам, если они намерены лишь считывать данные из разделяемой памяти. Следовательно, блокировкой для чтения может владеть любое количество потоков. Но если сразу несколько потоков должны записывать или модифицировать данные общей памяти, то доступ для этого будет предоставлен только одному потоку. Другими словами, никаким другим потокам не будет разрешено входить в критический раздел, если одному потоку предоставлен монопольный доступ для записи в разделяемую память. Такой подход может оказаться полезным, если приложения чаще считывают данные, чем записывают их. Если в приложении создается множество потоков, организация взаимно исключающего доступа может оказаться излишней предосторожностью. Производительность такого приложения может значительно увеличиться, если в нем разрешить одновременное считывание данных несколькими потоками. Стандарт POSIX определяет механизм блокировки для чтения и записи посредством типа pthread_rwlock_t. Блокировки для чтения и записи имеют такие же операции, как и мьютексные семафоры. Они перечислены в табл. 5.4. Различие между обычными мьютексами и мьютексами, обеспечивающими чтение и запись, заключается в операциях запроса на блокирование. Вместо одной операции блокирования здесь предусмотрено две: Pthread_rwlock_rdlock () Pthread_rwlock_wrlock () ункция pthread_rwlock_rdlock() предоставляет вызывающему потоку блокировку чтения, а функция pthread_rwlock_wrlock () — блокировку записи. Запросив ровку чтения, поток получит ее в том случае, если нет потоков, удерживающих ровку записи. Если же таковые имеются, вызывающий поток блокируется. Если Уде апРосит блокировку записи, он ее получит в том случае, если нет потоков, й. t_ Ва1°Щих блокировку чтения или блокировку записи. Если же таковые имеются, ызьтающий поток блокируется. pthr ровка чтения-записи реализуется с помощью объектов типа РУетат "ffW — *-* Этот же тип имеет атрибутный объект, который инкапсули- числень *ТЫ объекта блокировки. Функции установки и чтения атрибутов пере- v36*bf*vr п°т°ками ИПа pthread— rwlock_t может быть закрытым (для разделения между Личныхп ° пР°Цесса) или разделяемым (для разделения между потоками раз-
202 Глава 5. Синхронизация параллельно выполняемых задач Ё Таблица 5.4. Операции, используемые для блокировки чтения-записи Операции Прототипы функций Инициализация Запрос на блокировку #include <pthread.h> int pthread_rwlock_init( pthread_rwlock_t *restrict rwlock, const pthread__rwlockattr_t *restrict attr) #include <time.h> 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 Прототипы функций Описание #include <pthread.h> int pthread_rwlockattr_init (pthread_rwlockattr_t * attr); int pthread_rwlockattr_destroy (pthread_rwlockattr_t * attr); Инициализирует атрибутный объект блокировки чтения-записи, заданный параметром attr, значениями, действующими по умолчанию для всех атрибутов, определенных реализацией Разрушает атрибутный объект блокировки чтения-записи, заданный параметром attr. Его можно инициализировать повторно, вызвав функцию pthread_rwlockattr_init () ^^^^.
5.3. Что такое семафоры 203 Окончание табл. 5.5 Прототипы функций Описание int n\,read rwlockattr_setpshared P5pthread_rwlockattr_t * attr, int pshared); int othread_rwlockattr_getpshared (const pthread_rwlockattr_t * restrict attr, int *restrict pshared); Устанавливает или возвращает атрибут process-shared атрибутного объекта блокировки чтения-записи, заданного параметром attr. Параметр pshared может содержать следующие значения: PTHREAD_PROCESS_SHARED (разрешает блокировку чтения-записи, разделяемую любыми потоками, которые имеют доступ к памяти, выделенной для этого объекта блокировки, даже если потоки принадлежат различным процессам); PTHREAD_PROCESS_PRIVATE (блокировка чтения-записи разделяется между потоками одного процесса) 5.3.3.1. Использование блокировок чтения-записи для реализации стратегии доступа Блокировки чтения-записи можно использовать для реализации стратегии доступа 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); {°id *Producer2 (void *X) //Ь^аСа-^1оск^г1ос^(&^Ьоск) ; Р^гГГЧеСКИЙ Раздел. } ead-rwlock_unlock(&RWLock);
204 Глава 5. Синхронизация параллельно выполняемых задач void *consumerl(void *X) { pthread_rwlосk_rdlоск(&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,producer1,NULL); pthread_create(&ThreadB,NULL, consumer1,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. Частичная таблица решений для листинга 5.3 Поток А Поток В Поток С Поток D (выполняет запись) (выполняет чтение) (выполняет запись) (выполняет чгп£Н№)^ Нет Нет Нет Нет Нет Да Нет Да Нет Нет Да Нет Да Нет Нет Да Нет Нет Да Нет
5.3. Что такое семафоры 205 с 3 4. Условные переменные Уловная переменная представляет собой семафор, используемый для сигнализации я тии которое произошло. Сигнала о том, что произошло некоторое событие, ° С ожидать один или несколько процессов (или потоков) от других процессов или М° в Следует понимать различие между условными переменными и рассмотрен- П выше мьютексными семафорами. Назначение мьютексного семафора и блоки- Н к чтения-записи — синхронизировать доступ к данным, в то время как условные менные обычно используются для синхронизации последовательности опера- По этому поводу в своей книге UNIX Network Programming прекрасно высказался Р чард Стивене (W. Richard Stevens): "Мьютексы нужно использовать для блокирования, а не для ожидания". В листинге 4.6 поток-"потребитель" содержал цикл: 15 while (TextFiles. empty () ) 16 {} Поток-"потребитель" выполнял итерации цикла до тех пор, пока в очереди TextFiles были элементы. Этот цикл можно заменить условной переменной. Поток- изготовитель" сигналом уведомляет потребителя о том, что в очередь помещены элементы. Поток-"потребитель" может ожидать до тех пор, пока не получит сигнал, а затем перейдет к обработке очереди. Условная переменная имеет тип pthread_cond_t. Ниже перечислены типы операций, которые может она выполнять: • инициализация; • разрушение; • ожидание; • ожидание с ограничением по времени; • адресная сигнализация; • всеобщая сигнализация; Операции инициализации и разрушения выполняются условными переменными °Добно аналогичным операциям других мьютексов. Функции класса rea — cond_t, которые реализуют эти операции, перечислены в табл. 5.7. . аблица5.7. Функции класса pthread_cond_t, которые реализуют операции условных переменных рации Прототипы функций (макросы) #include <pthread.h> "ициализация int pthread_cond_init( pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
206 Глава 5. Синхронизация параллельно выполняемых задач Окончание тпбщ. 5 у Операции Прототипы функций (макросы) Ожидание 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) ; Условные переменные используются совместно с мьютексами. При попытке заблокировать мьютекс поток или процесс будет заблокирован до тех пор, пока мьютекс не освободится. После разблокирования поток или процесс получит мьютекс и продолжит свою работу. При использовании условной переменной ее необходимо связать с мьютексом. //- . . pthread_mutex_lock(&Mutex) ; pthread_cond_wait(&EventMutex,&Mutex); //. . . pthread_mutex_unlock(&Mutex) ; Итак, некоторая задача делает попытку заблокировать мьютекс. Если мьютекс уже заблокирован, то эта задача блокируется. После разблокирования задача освободит мьютекс Мьютекс и при этом будет ожидать сигнала для условной переменной EventMutex. Если мьютекс не заблокирован, задача будет ожидать сигнала неограниченно долго. При ожидании с ограничением по времени задача будет ожидать сигнала в течение заданного интервала времени. Если это время истечет до получения задачей сигнала, функция возвратит код ошибки. Затем задача вновь затребует мьютекс. Выполняя адресную сигнализацию, задача уведомляет другой поток или процесс о том, что произошло некоторое событие. Если задача ожидает сигнала для заданной условной переменной, эта задача будет разблокирована и получит мьютекс. Если сраз) несколько задач ожидают сигнала для заданной условной переменной, то разблок рована будет только одна из них. Остальные задачи будут ожидать в очереди, и их разблокирование будет происходить в соответствии с используемой стратегией пл* нирования. При выполнении операции всеобщей сигнализации уведомление полу4 все задачи, ожидающие сигнала для заданной условной переменной. При разблокир вании нескольких задач они будут состязаться за право владения мьютексом в соо ветствии с используемой стратегией планирования. В отличие от операции ожид ния, задача, выполняющая операцию сигнализации, не предъявляет прав на владе мьютексом, хотя это и следовало бы сделать. Условная переменная также имеет атрибутный объект, функции которого пер числены в табл. 5.8.
5.3. Что такое семафоры 207 "^5щца 5.8. Функции доступа к атрибутному объекту для условной переменной типа pthread_cond_t Прототипы функций Описание -j^liiae <Pthread-h> •nt pthread_condattr_init ( pthread_condattr_t * attr) ; int pthread_condattr_destroy( pthread_condattr_t * attr) ; int pthread_condattr_setpshared ( pthread_condattr_t * attr, int pshared) ; int pthread__condattr_getpshared ( const pthread_condattr__t * restrict attr, int *restrict pshared); int pthread_condattr_setclock( pthread_condattr_t * attr, clockid_t clock_id); mt pthread__condattr_getclock( const pthread_condattr_t * restrict attr, clockid__t * , , restrict clocked) Инициализирует атрибутный объект условной переменной, заданный параметром attr, значениями, действующими по умолчанию для всех атрибутов, определенных реализацией Разрушает атрибутный объект условной переменной, заданный параметром attr. Этот объект можно инициализировать повторно, вызвав функцию pthread_condattr_init () Устанавливает или возвращает атрибут process -shared атрибутного объекта условной переменной, заданного параметром attr. Параметр pshared может содержать следующие значения: PTHREAD_PROCESS_SHARED (разрешает блокировку чтения-записи, разделяемую любыми потоками, которые имеют доступ к памяти, выделенной для этой условной переменной, даже если потоки принадлежат различным процессам); PTHREAD_PROCESS_PRIVATE (условная переменная разделяется между потоками одного процесса) Устанавливает или возвращает атрибут clock атрибутного объекта условной переменной, заданного параметром attr. Атрибут clock представляет собой идентификатор часов, используемых для измерения лимита времени в функции pthread_cond_timedwait (). По умолчанию для атрибута clock используется идентификатор системных часов •4.1. Использование условных переменных для управления у отношениями синхронизации зацИи ^Ю ПеРеменную можно использовать для реализации отношений синхрони- Финиш ^глТ°^ЫХ упоминалось выше: старт-старт (СС), финиш-старт (ФС), старт- Ками ол И ФИНИШ'ФИНИШ (ФФ). Эти отношения могут существовать между пото- Реализа1 ° ИЛИ Различных процессов. В листингах 5.4 и 5.5 представлены примеры МЫотекга п ФФ-отношений синхронизации. В каждом примере определено два а Другой -1 ДИН МЬЮтекс используется для синхронизации доступа к общим данным, ДЛя синхронизации выполнения кода.
208 Глава 5. Синхронизация параллельно выполняемых задач // Листинг 5.4. ФС-отношения синхронизации между // двумя потоками //. . . float Number; pthread_t ThreadA,ThreadB; pthread__mutex_t Mutex, EventMutex; pthread_cond_t Event; void *workerl(void *X) { for(int Count = l;Count < 100;Count++){ pthread_mutex_lock(&Mutex); Number++; pthread_mutex_unlock(&Mutex); cout « "workerl: число равно " « Number « endl; if(Number == 50){ pthread_cond_signal(&Event); } } cout « "Выполнение функции workerl завершено." « endl; return(0); } void *worker2(void *X) { pthread_mutex_lock(&EventMutex); pthread_cond__wait(&Event,&EventMutex); pthread_mutex_unlock(&EventMutex); for(int Count = l;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.3. Что такое семафоры 209 еализации ФФ-отношений синхронизации показан в листинге r 5- ФФ-отношения синхронизации между // Лист^п* - • дВуМя потоками // ^Sa^T^eadA^ThreadB; P^read~mutest Mutex, EventMutex; ^hreadlcond^ Event; void *workerl(void *X) { for(int Count = l;Count < 10;Count++){ pthread_mutex_lock(&Mutex) ; Number++; pthread_mutex_unlock(&Mutex) ; cout « "worker 1: число равно " << Number « endl; pthread__mutex_lock(&EventMutex) ; cout « "Функция worker1 в состоянии ожидания. " « endl; pthread_cond_wait (&Event, &EventMutex) ; pthread_mutex_unlock (&EventMutex) ; return(0); } void *worker2 (void *X) { for(int Count = 1;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); 5.5 int maindnt argC/ char *argv[]) Ptnread_mutex_init(&Mutex,NULL) ; Pthread__mutex_init (&EventMutex,NULL) ; pt£read_cond_init (&Event, NULL) ; D^bad~"Creat:e <&Тпгеа^А/ NULL, worker 1, NULL) ; Pthread_create (&ThreadB, NULL, worker2, NULL) ; return (0); Ши J стинге5.5 поток ThreadA не может завершиться до тех пор, пока не завер- 100 ГТ °ТОК T^reaclB. Поток ThreadA должен выполнить цикл 10 раз, a ThreadB — °Жил °К геас^ завершит выполнение своих итераций раньше ThreadB, но будет СС- г^ ТеХ ПОРу пока поток ThreadB не просигналит о своем завершении. Эти м ф'отношения синхронизации невозможно реализовать подобным образом. ДЬ1 используются для синхронизации порядка выполнения процессов.
210 Глава 5. Синхронизация параллельно выполняемых задач 5.4. Объектно-ориентированный подход к синхронизации Одно из преимуществ объектно-ориентированного программирования сост в защите, которую обеспечивает инкапсуляция компонентов данных объекта. Инк суляция может обеспечить для пользователя объектов "стратегии доступа к объект и принципы их применения" [24]. В примерах, представленных в этой главе, за пп меняемые стратегии доступа вся ответственность возлагалась на пользователя да ных. С помощью объектов и инкапсуляции ответственность можно переложит с пользователя данных на сами данные. При таком подходе создаются данные, которые, в отличие от функций, являются безопасными для потоков. Для реализации такого подхода данные многопоточного приложения (по возможности) необходимо инкапсулировать с помощью С++-конструкций class или struct Затем инкапсулируйте такие механизмы синхронизации, как семафоры, блокировки для обеспечения чтения-записи и мьютексы событий. Если данные или механизмы синхронизации представляют собой объекты, создайте для них интерфейсный класс. Наконец, объедините объект данных с объектами синхронизации посредством наследования или композиции, чтобы создать объекты данных, которые будут безопасны для потоков. Этот подход подробно рассматривается в главе 11. 5.5. Резюме Для координации порядка выполнения процессов и потоков (синхронизация задач), 2l также доступа к разделяемым данным (синхронизация данных) можно использовать различные механизмы синхронизации. Существует четыре основных вида отношений синхронизации задач. Отношение вида "старт-старт" (СС) означает, что задача А не может начаться до тех пор, пока не начнется задача В. Отношение вида "финиш- старт" (ФС) означает, что задача А не может завершиться до тех пор, пока не начнется задача В. Отношение вида "старт-финиш" (СФ) означает, что задача А не может начаться до тех пор, пока не завершится задача В. Отношение вида "финиш-финиш (ФФ) означает, что задача А не может завершиться до тех пор, пока не завершится задача В. Для реализации этих отношений синхронизации задач можно использовать условную переменную THnapthread_cond_t, которая определена стандартом POS1A- Для описания синхронизации данных используются некоторые типы алгоритмов модели PRAM. Стратегию доступа EREW (исключающее чтение и исключающая за пись) можно реализовать с помощью мьютексного семафора. Мьютексный семаф°г защищает критический раздел, обеспечивая последовательный вход в него. Эта стр тегия разрешает либо доступ для чтения, либо доступ для записи. Стандарт POSIX ° ределяет мьютексный семафор типа pthread__mutex_t, который можно исполъ вать для реализации стратегии доступа EREW. Чтобы реализовать стратегию Д°сТ^ CREW (параллельное чтение и исключающая запись), можно использовать блокир ки чтения-записи. Стратегия доступа CREW описывает возможность удовлетворен множества запросов на чтение, но при монопольной записи данных. Стандарт Уи ' определяет объект блокировки для обеспечения чтения-записи т ^ pthread_rwlock_t, а объектно-ориентированный подход к синхронизации ДаН позволяет встроить механизм синхронизации в объект данных.
ОБЪЕДИНЕНИЕ ВОЗМОЖНОСТЕЙ ПАРАЛЛЕЛЬНОГО ПРОГРАММИРОВАНИЯ И С++-СРЕДСТВ НА ОСНОВЕ PVM В этой главе... 6.1. Классические модели параллелизма, поддерживаемые системой PVM 6.2. Библиотека PVM для языка C++ 6.3. Базовые механизмы PVM 6.4. Доступ к стандартному входному потоку (stdin) и стандартному выходному потоку (stdout) со стороны PVM-задач 6.5. Резюме
J~1/\±JSJ±J Мы разделили нашу проблему на две части: сгенерированную программу и процесс обучения. Эти две части остаются тесно связанными. Нельзя ожидать, что сгенерированная машина окажется удачной с первой же попытки. Необходимо поэкспериментировать с обучением одной такой машины и посмотреть, как пойдет этот процесс обучения... — Алан Тьюринг (Alan Turing), Может ли машина думать? Система программного обеспечения PVM (Parallel Virtual Machine — параллельная виртуальная машина) предоставляет разработчику ПО средства для написания и выполнения программ, использующих параллелизм. Система PVM позволяет разработчику представить коллекцию сетевых компьютеров в виде единой логической машины с возможностями параллелизма. Компьютеры этой коллекции могут иметь одинаковые или различные архитектуры. В PVM-систему связываютс даже компьютеры, которые попадают в категорию МРР (Massively Parallel Processor - процессор с массовым параллелизмом). Несмотря на то что PVM-программы мог) разрабатываться для одного компьютера, реальные преимущества этой системы пр являются при связывании двух и более компьютеров. Система PVM в качестве средства связи между параллельно выполняющимися дачами поддерживает модель передачи сообщений. Приложение взаимодейству с PVM посредством библиотеки, которая состоит из API-интерфейсов, предназнач ных для управления процессами, отправки и получения сообщений, сигнализаН процессов и т.д. С++-программа взаимодействует с PVM-библиотекой точно так • как с любыми другими библиотеками функций. С++-программе для получения досту к функциям PVM-библиотеки не нужно создавать специальную форму или архитектур?'
Классические модели параллелизма, поддерживаемые системой PVM 213 еМЯ как программам, написанным на других языках, необходимо вызывать оп- В **° енные функции для инициализации среды. Это означает, что С++-програм-мист ^ т сочетать PVM-возможности с другими стилями С++-программирования М° имер, объектно-ориентированным, параметризованным, агентно-ориенти- иным и структурированным программированием). Благодаря использованию та- Р° 5йблиотек, как PVM, MPI или Linda, С++-разработчик может реализовать различ- К модели параллелизма, тогда как другие языки ограничены примитивами парал- изма, которые встроены в сами языки. Библиотека PVM предлагает, пожалуй, са- й простой способ расширения средств языка C++ за счет возможностей параллельного программирования. 6,1. Классические модели параллелизма, поддерживаемые системой PVM Система PVM поддерживает модели MIMD (Multiple-Instruction, Multiple- Data— множество потоков команд, множество потоков данных) и SPMD (Single- Program, Multiple-Data— одна программа, множество потоков данных) параллелизма. В действительности SPMD — это вариант модели SIMD (Single-Instruction, Multiple-Data— один поток команд, множество потоков данных). Эти модели разбивают программы на потоки команд и данных. В модели MIMD программа состоит из нескольких параллельно выполняющихся потоков команд, причем каждому из них соответствует собственный локальный поток данных. По сути, каждый процессор здесь имеет собственную память. В PVM-среде модель MIMD считается моделью с распределенной памятью (в отличие от модели с общей памятью). В моделях с общей памятью все процессоры "видят" одни и те же ячейки памяти. В модели с распределенной памятью связь между хранимыми в ней значениями обеспечивается посредством механизма передачи сообщений. Однако модель SPMD подразумевает наличие одной программы (одного набора команд), которая параллельно выполняется на нескольких компьютерах, причем эти одинаковые на всех машинах программы обрабатывают различные потоки данных. VM-среда поддерживает как MIMD-, так и SIMD-модели или их сочетание. Четыре классические модели параллелизма показаны на рис. 6.1. Обратите внимание на то, что модели SISD и MISD (см. рис. 6.1) неприменимы МКгТСМе *^^' Модель SISD описывает однопроцессорную машину, а для модели вообще трудно найти практическое применение. Две остальные модели, кото- * ОЖно использовать с системой PVM, определяют, как С++-программа взаимо- ный СТ С компьютеРами- Разработчик ПО представляет один логический виртуаль- Дач лпьютер как среду для выполнения нескольких различных параллельных за- вЫпо ДаЯ "? котоРых получает доступ к собственным данным, либо одной задачи, Нь1м ofi Щеися в виде набора параллельных клонов, получающих доступ к различали п ЯМ Данных- Таким образом, с PVM-задачами мы будет связывать только мо- F Д олагающие наличие множества потоков команд и одной программы.
214 Глава 6 Объединение возможностей параллельного программирования MPMD (MIMD SPMD (SIMD) Набор данных 1 Ж" _^цЭадача 1 выполняет процедуру А Набор данных 2 связь ' \4 щ Задача 2 выполняет процедуру В Набор данных 1 Ж" Набор данныхТ] Задача 1 выполняет процедуру А СВЯЗЬ *———н Задача 2 выполняет процедуру а MPSD (MISD) Набор данных 1 Задача 1 выполняет процедуру А СВЯЗЬ И—— v Задача 2 выполняет процедуру В- Набор данных 3 Набор данных 2 Набор данных 1 ^здача1 выполняет процедуры А и В, последовательно с эрабатывая наборы данных Рис. 6.1. Четыре классические модели параллелизма 6.2. Библиотека PVM для языка C++ К функциональным возможностям PVM из С++-программы можно получить доступ с помощью коллекции библиотечных процедур, предоставляемых средой PVM. Эти функции и процедуры PVM обычно делят на семь категорий. • Управление процессами. • Упаковка сообщений и их отправка. • Распаковка сообщений и их прием. • Обмен задач сигналами. • Управление буфером сообщений. • Функции обработки информации и служебные процедуры. • Групповые операции. Эти библиотечные функции легко интегрировать в С++-среду. Префикс pvm__ в име ни каждой функции позволяет не забыть о ее принадлежности соответствующему пр странству имен. Для использования PVM-функций необходимо включить в программ, заголовочный файл pvm3 . h и скомпоновать ее с библиотекой libpvm. В программах и 6.2 демонстрируется, как работает простая PVM-программа. Инструкции по компил ции и выполнению программы 6.1 приведены в разделе "Профиль программы 6.1 •
6.2. Библиотека PVM для языка C++ 215 // пр°гРамма б-1 include »pvm3.h» linclude <iostream> linclude <string.h> int main(int argcchar *argv[]) int RetCode,Messageld; int PTid, Tid; char Message[100] ; float Result[1] ; PTid = pvm__mytid() ; RetCode = pvnuspawn("program6-2" #NULL, 0, "", 1, &Tid) ; if(RetCode == 1) { Messageld = 1; strcpy(Message,"22") ; pvm__initsend(PvmDataDefault) ; pvm_pkstr(Message); pvm__send(Tid,Messageld) ; pvm_recv(Tid,Messageld) ; pvm_upkfloat(Result,1,1); cout « Result[0] « endl; pvm_exit(); return(0) ; } else{ cerr << "Задачу породить невозможно. " << endl; pvm_exit(); return(1); } ГПррфиль программы 6.1 4мяпрограммы ЙЙодгатб-l.cc [Описание t л * использует функцию pvm_send<) для пересылки числа в другую PVM-задачу, которая выполняется параллельно с данной (программа 6.2), и функцию pvm_recv() УШЯ получения числа от этой аяламм 1 получения числа от этой задачи. ^Требуемая библиотека j^ibpvm3 |ТРв6уемые заголовки !<PVm3-h> <iostream> <string.h> L++ рум*ии по компиляции и компоновке программ r- $pVM ^с9Гат6_1 -I $PVM_ROOT/include -L $PVM_ROOT/lib/ [^^АЛЯ тестирования ^^S&W.SAS. SuSE Linux 7.1, gcc 2.95.2.
216 Глава 6. Объединение возможностей параллельного программирования Инструкции по выполнению . /ргодгатб-1 Примечания Необходимо запустить на выполнение программу pvmd. В программе 6.1 использовано восемь самых распространенных PVM-функций. pvm_mytid(), pvm_spawn(), pvm_initsend(), pvm_pkstr(), pvm__send() pvm_recv(), pvm_upkf loat () и pvm_exit(). Функция pvm_mytid() возвращает идентификатор вызывающего процесса (задачи). PVM-система связывает идентификатор задачи с каждым процессом, который ее создает. Идентификатор задачи используется для отправки сообщений задачам, получения сообщений от других задач сигнализации, прерывания задач и т.п. Любая PVM-задача может связываться с любой другой PVM-задачей до тех пор, пока не получит доступ к ее идентификатору. Функция pvm_spawn() предназначена для запуска нового PVM-процесса. В программе 6.1 функция pvm_spawn () используется для запуска на выполнение программы 6.2. Идентификатор новой задачи возвращается в параметре &Tid вызова функции pvm_spawn (). В PVM-среде для передачи данных между задачами используются буферы сообщений. Каждая задача может иметь один или несколько таких буферов. При этом только один из них считается активным. Перед отправкой каждого сообщения вызывается функция pvm_initsend(), которая позволяет подготовить или инициализировать активный буфер сообщений. Функция pvm_pkstr () используется для упаковки строки, содержащейся в параметре Message. При упаковке строка шифруется для передачи другой задаче (в другой процесс), выполняемой, возможно, на другом компьютере с другой архитектурой. PVM-среда обрабатывает элементы, связанные с преобразованием из одной архитектуры в другую. Среда PVM требует применять процедуру упаковки сообщения до его отправки и процедуру распаковки при его получении, чтобы сделать сообщение читабельным для получателя. Однако из этого правила существует исключение, которое мы обсудим ниже. Функции pvm_send() и pvm_recv() используются для отправки и приема сообщений соответственно. Параметр Messageld просто определяет, с каким сообщением работает отправитель. Обратите внимание на то, что в программе 6.1 функции pvm_send() и pvm_recv() содержат идентификатор задачи, получающей данные, и идентификатор задачи, отправляющей данные, соответственно. Функция pvm_upkf loat () извлекает полученное сообщение из активного буфера сообщений и распаковывает его, сохраняя в массиве типа float. Программа 6.1 порождает РVM-задачу для выполнения программы 6.2. Обратите внимание на то, что обе программы 6.1 и 6.2 содержат обращение к функции pvm_exit (). Эту функцию необходимо вызывать при завершении PVM-обработки задачи. Несмотря на то что функция pvm_exit () не разрушает процесс и не прекраШ ет его выполнение, она позволяет PVM-среде освободиться от задачи и отсоединить за дачу от PVM-среды. Обратите внимание на то, что программы 6.1 и 6.2 — вполне авт номные и независимые программные модули, которые содержат функцию main () • А тали реализации программы 6.2 приведены в разделе "Профиль программы 6.2". // Программа 6.2 #include "pvm3.h" #include "stdlib.h" int main(int argc, char *argv[])
6.2. Библиотека PVM для языка C++ 217 int Messageld, Ptid; char Message[100]; float Num,Result; Ptid = pvm_parent(); Messageld = 1; pvirurecvfPtid, Messageld); pvm_upkstr(Message); Num = atof(Message); Result = Num / 7.0001; pVni_ini tsend (PvmDataDe fault) pvitLPkfloat<&Resultfl'1> '" pvm__send(Ptid,Messageld) ; pvm_exit(); return(0); Шрофиль программы 6,2 ГИмя программы §§>rdgram6-2. ее [Описание |Эта профамма принимает число от родительского процесса и делит его на 7. Затем |эна отправляет результат своему родительскому процессу [Требуемая библиотека [Требуемые заголовки fepvm3.h> <stdlib.h> | Инструкции по компиляции и компоновке программы Ь++;^о;,ргодгатб-2 -I $PVM_ROOT/include program6-2.ee -L > ^PVM^ROOT/lib/PVM_ARCH -lpvm3 £Среда для тестирования рдЗЕ Linux 7.1 gnu C++ 2.95.2, Solaris 8 Workshop 6, PVM 3.4.3. И^грукции по выполнению pto профамма порождается программой 6.1. (Примечания ^ еобходимо запустить на выполнение программу pvmd. •1- Компиляция и компоновка C++/PVM-nporpaMM бы ском Я Х ^^'cPe^bl представлена в виде единой библиотеки libpvm3 . а. Что- *a^Pvm3 Р°ВаТЬ РуМ-программу, необходимо включить в ее код заголовочный $ с+ и ск°мпоновать ее вместе с библиотекой libpvm3 . a: Чрруо~° "^^-P^ogram -I $PVM_ROOT/include °ffram.cc «I$PVM_ROOT/lib -lpvm3
218 Глава 6. Объединение возможностей параллельного программирования Переменная среды $PVM_ROOT указывает на каталог, в котором инсталлирована F> лиотека PVM. При выполнении этой команды создается двоичный ф - mypvm_program. Для выполнения программ 6.1 и 6.2 сначала необходимо инсталлировать PVM-cn Выполнить PVM-программу можно одним из трех основных способов: запустить авт номный выполняемый (двоичный) файл, использовать PVM-консоль или среду XPVM 6.2.2. Выполнение PVM-программы в виде двоичного файла Во-первых, необходимо запустить программу pvmd; во-вторых, на каждом компьютере, включенном в PVM-среду, корректно скомпилированные программы-участницы должны находиться в соответствующих каталогах. По умолчанию для скомпилированных программ (выполняемых файлов) используется такой каталог: $H0ME/pvm3 /bin/ $PVM_ARCH Здесь PVM_ARCH содержит имя архитектуры компьютера (см. табл. 6.1 и параграфы 1 и 2 из раздела 6.2.5). Для выполняемых программ должны быть установлены соответствующие разрешения на доступ и использование. Программу pvmd можно запустить так: pvmd & или так: pvmd hostfile & Здесь hostfile — это файл конфигурации, содержащий специальные параметры для передачи программе pvmd (см. табл. 6.2 и параграфы 1, 2 из раздела 6.2.3). После запуска программы pvmd на одном из компьютеров, включенных в среду PVM, можно запустить PVM-программу, используя следующую простую команду: $MyPvmProgram Если эта программа порождает другие задачи, то они запустятся автоматически. 6.2.2.1. Запуск PVM-программ с помощью PVM-консоли Для выполнения программ с помощью PVM-консоли необходимо сначала запустить PVM-консоль, введя следующую команду: $pvm Получив приглашение на ввод команд pvm>, введите имя программы, которую нуэкн выполнить: pvm> spawn -> MyPvmProgram 6.2.2.2. Запуск PVM-программ с помощью XPVM Кроме PVM-консоли, можно использовать графический интерфейс XPVM Д-71 Windows. На рис. 6.2 показано диалоговое окно сеанса работы с XPVM-интерфейсоМ. Библиотека PVM не требует, чтобы С++-программа придерживалась какои-л конкретной структуры. Первая PVM-функция, вызываемая программой, "помеШ ее в PVM-среду. Для каждой программы, которая является частью PVM-среды, еле?., всегда вызывать функцию pvm_exit (). Если этого не сделать, система завис
6.2. Библиотека PVM для языка C++ 219 ■тика показывает, что функции pvm_mytid() и pvm_parent () необходимо вы- 1*/***ть в начале обработки задачи. Наиболее популярные категории функций PVM 1 —Meлены в табл. 6.1. ПраК 31 пере числены ] ТЁ2£ XPVM 1.2.S (PVM3.43) [Т10«0х400а4) IStatus: Welcome to ХВ/М |ЙЙрГ-Raise / lower Tasks "ытш for Spawning & ConWtlmQ t; FUe...j Hosts...; Tasks,,.] views... Options...; Reset...} HeJp...j ж. Add AB ■ SPAWM Other H ON-THE-FLYi Ш horns KILL P.j£ SIGNAL i.0.**!? SVS TASKS B_tmhoi Doner Done i LINUX ka % LINUX imhotep Active System ■ No Tasks Met Кву: Ш 44j ■! M ff Ш: Time: 0.000000 TrBte Fto: \nn^hq^m.br9CBi [^"я'ауВаск i^Toveirtriirite' Рис. 6.2. Диалоговое окно графического интерфейса XPVM ^Таблица 6.1. Семь категорий функций библиотеки PVM Категории PVM-функций Описание Управление процессами Упаковка сообщений и их отправка Распаковка сообщений 4 их прием Обмен задач сигналами У»Р«ыение буфером сообщений ^^обрабошкиинформг, Ч"ии«У*ебные процедуры 'Ыповые операции Используются для управления PVM-процессами Применяются для упаковки сообщений в пересылочном буфере и отправки их от одного PVM-процесса другому Используются для получения сообщений и распаковки данных из активного буфера Применяются для сигнализации и уведомления PVM- процессов о возникновении события Используются для инициализации, очистки и размещения буферов, предназначенных для приема и отправки сообщений, которыми обмениваются PVM-процессы Применяются для получения информации о PVM- процессах и выполнения других важных задач Используются для объединения процессов в группы и выполнения других групповых операций
220 Глава 6. Объединение возможностей параллельного программирован^ 'я... 6.2.3. Требования к PVM-программам Если PVM-среда реализуется в виде сети компьютеров, то, прежде чем ваша С программа начнет взаимодействовать с ней, необходимо обработать следующие элементк Параграф 1 Следует установить переменные среды PVM_ROOT и PVM_ARCH. Переменная сред PVM_ROOT должна указывать на каталог, в котором инсталлирована PVM-библиотека Использование оболочки Bourne (BASH) Использование С-оболочки $ PVM_ROOT=/usr/lib/pvm3 $ export PVM_ROOT setenv PVM_ROOT /usr/lib/pvm3 Переменная среды PVM_ARCH идентифицирует архитектуру компьютера. Каждый компьютер, включенный в среду PVM, должен быть идентифицирован архитектурой. Например, Ultrasparcs-компьютеры имеют обозначение SUN4SOL2, а Linux- компьютеры — обозначение LINUX. В табл. 6.2 перечислены самые распространенные архитектуры для PVM-среды. Эта таблица содержит имя и тип компьютера, соответствующий этому имени. Установите свою переменную среды PVM_ARCH равной одному из имен, приведенных в табл. 6.2. Например: Использование оболочки Bourne (BASH) $PVM_ARCH= -LINUX $export PVM_ARCH Использование С-оболочки setenv PVM_ARCH LINUX Таблица 6.2. Самые распространенные архитектуры для PVM-среды J PVM_ARCH AFX8 ALPHA BAL BFLY BSD386 CM2 CM5 CNVX CNVXN CRAY CRAY2 CRAYSIMP Компьютер Alliance DEC Alpha Sequent Balance BBN Butterfly TC2000 80386/486 PC (UNIX) "Мыслящая машина" СМ2 "Мыслящая машина" СМ5 Convex С-серии Convex С-серии С-90, YMP,T3D (доступный Сгау-2 Cray S-MP PVM_ARCH LINUX MASPAR MIPS NEXT PGON PMAX RS6K RT SGI порт) SGI5 SGIMP SUN3 Компьютер 80386/486 PC (UNIX) Maspar MIPS 4680 NeXT Intel Paragon DECstation 3100,5100 IBM/RS6000 IBMRT Silicon Graphics IRIS Silicon Graphics IRIS SGI Multiprocessor Sun3 __^^~
6.2. Библиотека PVM для языка C++ 221 Окончание табл. 6.2 \СН Компьютер Data General Aviion Encore 88000 HP-9000 Model 300 HP-9000 PA-RISC Intel iPSC/860 Intel iPSC/2 386 Host Kendall Square KSR-1 PVM_ARCH SUN4 SUN2SOL2 SUNMP SYMM TITN U370 UVAX Компьютер Sun 4, SPARCstation Sun 4, SPARCstation SPARC Multiprocessor Sequent Symmetry Stardent Titan IBM 370 DECLicroVAX Параграф 2 Выполняемые файлы любых программ, участвующих в среде PVM, должны быть размещены на всех компьютерах, включенных в среду PVM, или доступны всем компьютерам, включенным в среду PVM. При этом каждая программа должна быть скомпилирована для работы с учетом конкретной архитектуры. Это означает, что, если вереду PVM включены процессоры UltraSparcs, PowerPCs и Intel, то мы должны иметь версию программы, скомпилированную для каждой архитектуры. Эту версию программы следует разместить в известном для PVM месте. Таким местом часто служит каталог $HOME/pvm3/bin. Этот каталог может быть также задан в файле конфигурации PVM, который обычно имеет имя host file или .xpvm_hosts (если используется среда XPVM). Файл host file должен содержать такую запись: ep=/usr/local/pvm3/bin Эта запись означает, что любые пользовательские выполняемые файлы, необходимые для среды PVM, можно найти в каталоге /usr/ local /pvm3 /bin. Параграф 3 Пользователь, запускающий PVM-программу, должен иметь сетевой доступ (rsh ли ssh) к каждому компьютеру, включенному в среду PVM. По умолчанию PVM подует Доступ к каждому компьютеру, используя зарегистрированное имя пользователя, пускающего PVM-программу, или учетную запись компьютера, на котором она за- ается. Если потребуется другая учетная запись (помимо зарегистрированного ^ени пользователя-инициатора), то в файл конфигурации PVM hostfile или _nosts необходимо добавить соответствующую запись, например: io=flashgordon ПаРаграф 4 Пьютеп ТС На каждом компьютере файл .rhosts, в котором перечислите все ком- Можност ОДЛежахДие использованию. Эти компьютеры имеют потенциальную воз- •*Pvm ь Я включения в среду PVM. В зависимости от содержимого файла HbI B PVM S ИЛИ Фа**ла Pvm_hosts, эти компьютеры автоматически будут добавле- Файлах та ^^ П^И запУске программы pvmd. Компьютеры, перечисленные в этих е могут динамически включаться в PVM-среду во время работы.
222 Глава 6. Объединение возможностей параллельного программировани Параграф 5 Создайте файл $НОМЕ/ .xpvm_hosts и/или файл $HOME/pvm_hosts, в кото перечислите все подлежащие использованию компьютеры с приставкой "&и. Нали ^ приставки "&" означает неавтоматическое включение компьютера. Без этой приг е ки компьютер будет включен в PVM-среду автоматически. Файл pvm_hosts создае В пользователем и может иметь произвольное имя. Но в среде XPVM необходимо и* пользовать только имя .xpvm_hosts. Пример такого файла показан на рис. 6.3. А логичный формат следует использовать для pvmjiosts- или . xpvm_host s-файла Главное внимание необходимо уделить сетевому доступу пользователя, запускаю щего PVM-программу. Владелец PVM-программы должен иметь доступ к каждому компьютеру, включенному в пул процессоров. Этот доступ будет использовать либо команду rsh, либо г login, либо ssh. Выполняемая программа должна быть доступна на каждом компьютере, а PVM-среда должна быть "в курсе" того, какие компьютеры имеются в наличии и где будут инсталлированы выполняемые файлы. # Строки комментариев начинаются с символа "#" # (пустые строки игнорируются). # Строки, начинающиеся с символа "&", позволяют # включить компьютеры в среду PVM позднее. Если # имя компьютера не предваряется символом "&", # этот компьютер включается в среду PVM # автоматически. flavius marcus &cambius lo^romulus &karsius # Символ "*" означает стандартные опции для # следующих компьютеров # dx=/export/home/fred/pvm3/lib/pvmd &octavius # Если компьютеры являются частью типичного # linux-кластера, то их имена можно использовать # для включения узлов кластера в среду PVM # вместе с другими узлами. Рис. 6.3. Примерpvmjiosts-файла 6.2.4. Объединение динамической С++-библиотеки с библиотекой PVM Поскольку доступ к PVM-средствам обеспечивается через коллекцию библио ных функций, С++-программа использует PVM как любую другую библиотеку- След)
6.2. Библиотека PVM для языка C++ 223 виду, что каждая PVM-программа представляет собой автономную С++- ИМС aMMV с собственной функцией main (). Это означает, что все PVM-программы ПР° V, с0бственное адресное пространство. При порождении каждой PVM-задачи И ется ее собственный процесс с новым адресным пространством и, соответст- С° о идентификационный номер процесса. PVM-процессы видимы для утилиты ps. ff мотря на то что несколько PVM-задач могут выполняться вместе для решения йеной проблемы, они будут иметь собственные копии динамической С++- блиотеки. Каждая программа имеет собственный поток iostream, библиотеку блонов, алгоритмы и пр. В область видимости глобальных С++-переменных адрес- пространство не попадает. Это означает, что глобальные переменные одной PVM-задачи невидимы для других PVM-задач. Для взаимодействия отдельных задач используется механизм передачи сообщений. Этим они отличаются от многопоточных программ, в которых потоки разделяют одно адресное пространство и могут взаимодействовать посредством глобальных переменных и передачи параметров. Если PVM-программы выполняются на одном компьютере с несколькими процессорами, то как дополнительные средства коммуникации программы могут совместно использовать файловую систему, каналы, FIFO-очереди и общую память. Несмотря на то что передача сообщений — основной метод взаимодействия между PVM-задачами, ничто не мешает им в качестве дополнительных средств использовать файловую систему, буфер обмена или даже аргументы командной строки. PVM-библиотека не ограничивает, а расширяет возможности динамической С++-библиотеки. 6.2.5. Методы использования PVM-задач Работу, которую выполняет С++-программа, можно распределить между функциями, объектами или их сочетаниями. Действия, выполняемые программой, обычно делятся на такие логические категории: операции ввода-вывода, интерфейс пользователя, обработка базы данных, обработка сигналов и ошибок, числовые вычисления и т.д. Отделяя код интерфейса пользователя от кода обработки файлов, а также код процедур печати °т кода числовых вычислений, мы не только распределяем работу программы между функциями или объектами, но и стараемся выделять категории действий в соответствии их характером. Логические группы организуются в библиотеки, модули, объектные а лоны, компоненты и оболочки. Такой тип организации мы поддерживаем и при сении PVM-задач в С++-программу. Мы можем подойти к декомпозиции работ breakdown structure), используя метод либо восходящего, либо нисходящего г ирования. В любом случае параллелизм должен естественно вписываться в ра- j. оторая намечена для выполнения функцией, модулем или объектом, од амая удачная идея — попытаться директивно навязать параллелизм программе. Кол' енНо насаждаемый параллелизм является причиной формирования громозд- слож ектУрь1, которая, как правило, трудна для понимания и поддержки и создает Пользу р °^И опРеделении корректности программы. Поэтому, если программа ис- 'Раммь к- задачи» они должны быть результатом естественного разбиения про- ^^прим Д^Ю ^^"заДачУ следует отнести к одной из функциональных категорий. На естес если мы разрабатываем приложение, которое содержит обработку данных пРоизвеп ОМ языке (Natural Language Processing- NLP), механизм речевого вос- ^^теля л? Текста (text-to-speech engine — TTS-engine) как часть интерфейса поль- формирование логических выводов как часть выборки данных, то парал-
224 Глава 6. Объединение возможностей параллельного программирована лелизм (естественный для NLP-компонента) должен быть представлен в виде внутри NLP-модуля или объекта, который отвечает за NLP-обработку. Аналогичн Ч раллелизм внутри компонента формирования логических выводов следует пое * вить в виде задач, составляющих модуль (объект или оболочку) выборки данных ^ вечающий за выборку данных. Другими словами, мы идентифицируем PVM-3an T там, где они логически вписываются в работу, выполняемую программой, а не пп И разбиваем работу программы на набор некоторых общих PVM-задач. ° Соблюдение первичности логики и вторичности параллелизма имеет несколк последствий для С++-программ. Это означает, что мы могли бы порождать Р\ли задачи из функции main () или из функций, вызываемых из функции main () (и даж из других функций). Мы могли бы порождать PVM-задачи из методов, принадлежащих объектам. Место порождения задач зависит от требований к параллельности, выдвигаемых соответствующей функцией, модулем или объектом. В общем случае PVM- задачи можно разделить на две категории: SPMD (производная от SIMD) и MPMD (производная от MIMD). В модели SPMD все задачи будут выполнять одинаковый набор инструкций, но на различных наборах данных. В модели MPMD все задачи будут выполнять различные наборы инструкций на различных наборах данных. Но какую бы модель мы не использовали (SPMD или MPMD), создание задач должно происходить в соответствующих областях программы. Некоторые возможные конфигурации для порождения PVM-задач показаны на рис. 6.4. 6.2.5.1. Реализация модели SPMD (SIMD) с помощью PVM- и С++-средств Вариант 1 на рис. 6.4 представляет ситуацию, при которой функция main () порождает от 1 до N задач, причем каждая задача выполняет один и тот же набор инструкций, но на различных наборах данных. Существует несколько вариантов реализации этого сценария. В листинге 6.1 показана функция main (), которая вызывает функцию pvm_spawn(). // Листинг 6.1. Вызов функции pvm_spawn() из // функции main() int main(int argc, char *argv[]) { int TaskldflO]; int Taskld2[5]; // 1-е порождение: pvm_spawn( "set^ombination" .NULL, 0, "" # 10,TaskId) ; // 2-е порождение: pvm_spawn( "se^combination", argv, 0, "", 5, Taskld2) ; //. . . } В листинге 6.1 при первом порождении создается 10 задач. Каждая задача бУ^е^е полнять один и тот же набор инструкций, содержащихся в прогр • set_combination. При успешном выполнении функции pvm_spawn () массив Та будет содержать идентификаторы PVM-задач. Если программа в листинге 6.1 lL идентификатор Tasklds, то она может использовать функции pvm_send() ДЛЯ оТ * ки данных, подготовленных для обработки каждой программой. Это возможно ол ря тому, что функция pvm_send () содержит идентификатор задачи-получателя.
6.2. Библиотека PVM для языка C++ 225 (вариант 1: main() - - * * pvm Задача 1 * pvm Задача 2 * pvm Задача п с main() funcA() funcB() pvm Задача 1 pvm Задача п (Вариант 3:J- main() 1 funcA() pvm Задача 1 pvm Задача n pvm Задача 1 pvm Задача n my_object constructor() destructor() getValue() pvm_spawn(1..n) • 6.4. Некоторые возможные конфигурации для порождения PVM-задач
226 Глава 6. Объединение возможностей параллельного программировани При втором порождении (см. листинг 6.1) создается пять задач, но в этом ел > каждой задаче с помощью параметра argv передается необходимая информат ^ ЭТо — дополнительный способ передачи информации задачам при их запуске. Тем *' мым сыновние задачи получают еще одну возможность уникальным образом идеи ^ фицировать себя с помощью значений, получаемых в параметре argv. В листинге fi 9 чтобы создать N задач, функция main() несколько раз (вместо одного) обращает к функции pvm_spawn (). // Листинг 6.2. Использование нескольких вызовов // функции pvm_spawn() из функции main() int main(int argc, char *argv[]) { int Taskl; int Task2; int Task3; //. . . pvm_spawn("set_combination", NULL,!,"hostl",1,&Taskl); pvm_spawn("set_combination",argv,1,"host2",1,&Task2); pvm_spawn ("se^combination", argv++, 1, "host3", 1, &Task3) ; //. . . } Подход к созданию задач, продемонстрированный в листинге 6.2, можно использовать в том случае, когда нужно, чтобы задачи выполнялись на конкретных компьютерах. В этом состоит одно из достоинств PVM-среды. Ведь программе иногда стоит воспользоваться преимуществами некоторых конкретных ресурсов конкретного компьютера, например, специальным математическим спецпроцессором, процессором графического устройства вывода или какими-то другими возможностями. В листинге 6.2 обратите внимание на то, что каждый компьютер выполняет один и тот же набор инструкций, но все они получили при этом разные аргументы командной строки. Вариант 2 (см. рис. 6.4) представляет сценарий, в котором функция main() не порождает PVM-задачи. В этом сценарии PVM-задачи логически связаны с функцией f uncB (), и поэтому здесь именно функция f uncB () порождает PVM-задачи. Функциям main() и funcA() нет необходимости знать что-либо о PVM-задачах, поэтом)' им и не нужно иметь соответствующий PVM-код. Вариант 3 (см. рис. 6.4) представляет сценарий, в котором функции main () и другим функциям в программе присущ естественный параллелизм. В этом случае роль "других" функций играет функция funcA ( PVM-задачи, порождаемые функциями main () и f uncA (), выполняют различный код. Несмотря на то что задачи, порожденные функцией main (), выполняют идентичнь код, и задачи, порожденные функцией f uncA (), выполняют идентичный код, эти Д набора задач совершенно различны. Этот вариант иллюстрирует возможность программы использовать коллекции задач для одновременного решения различ проблем. Ведь не существует причины, по которой на программу бы налагалось огр чение решать в любой момент времени только одну проблему. Вариант 4 (см. рис- ■ представляет случай, когда параллелизм заключен внутри объекта, поэтому порожД PVM-задач реализует один из методов этого объекта. Этот вариант показывает, что необходимости параллелизм может исходить из класса, а не из "свободной" функци • Как и в других вариантах, все PVM-задачи, порожденные в варианте 4, выпол одинаковый набор инструкций, но с различными данными. Этот SPMD-метод (^ °
6.2. Библиотека PVM для языка C++ 227 Multiple-Data — одна программа, множество потоков данных) часто исполь- progra » оеализации параллельного решения проблем некоторого типа. И то, что зуется jv ^ладает поддержкой объектов и средств обобщенного программирования ЯЗЬ1К шаблонов, делает его основным инструментом при решении подобных за- На0<лб екты и шаблоны позволяют С++-программисту представлять обобщенные ДаЧ* решения для различных проблем с помощью одной-единственной про- И ГИ й единицы. Наличие единой программной единицы прекрасно вписывается ^аМ ль параллелизма SPMD. Понятие класса расширяет модель SPMD, позволяя ре- В елый класс проблем. Шаблоны дают возможность решать определенный класс Ша блем для практически любого типа данных. Поэтому, хотя все задачи в модели SPMD лняют один и тот же код (программную единицу), он может быть предназначен для бого объекта или любого из его потомков и рассчитан на различные типы данных ( значит, и на различные объекты!). Например, в листинге 6.3 используется четыре PVM-задачи для генерирования четырех множеств, в каждом из которых имеется С(п,г) элементов: С(24,9), С(24Д2), С(7,4) и С(7,3). В частности, в листинге 6.3 перечисляются возможные сочетания из 24 цветов, взятые по 9 и по 12. Здесь также перечисляются возможные сочетания из 7 чисел с плавающей точкой, взятые по 4 и по 3. Пояснения по обозначению С(п,г) приведены в разделе $ 6.1 ("Обозначение сочетаний"). // Листинг 6.3. Создание сочетаний из заданных множеств int main(int argcchar *argv[]) { int RetCode,Taskld[4]; RetCode = pvm_spawn ("pvm_generic_combination", NULL, 0, "", 4,TaskId); if(RetCode == 4) { colorCombinations (Taskld[0] , 9) ; colorCombinations(TaskId[l] , 12) ; numericCombinations(TaskId[2] , 4) ; пшпегicCombinations (Taskld[3], 3) ; saveResult(Taskld[0]) saveResult(TaskId[l]) saveResult(Taskld[2]), saveResult(Taskld[3]) , pvm__exit (); else{ cerr « "Ошибка при порождении сыновнего процесса." « endl; pvm_exit(); } return(0); инге 6.3 обратите внимание на порождение четырех PVM-задач: Pvin-»spavm i ««r-v, м Pvm_generic_combinationH,NULL,О,и■Л,Taskld); Каждая п Pvni ge °РожДенная задача должна выполнять программу с именем Чае*\ что 1C_-COinkinati-0ri- Аргумент NULL в вызове функции pvm_spawn() озна- Функции ^С3 ПаРаметР argv [ ] не передаются никакие опции Значение 0 в вызове —Spawn () свидетельствует, что нас не беспокоит, на каком компьютере
228 Глава 6. Объединение возможностей параллельного программирования будет выполняться наша задача. Аргумент Taskld представляет массив, предна ченный для хранения четырех целочисленных значений, который при условии yen a ного выполнения функции pvm_spawn () будет содержать идентификаторы каждой рожденной PVM-задачи. В листинге 6.3 обратите также внимание на вызов функц - colorCombinations () и numeri cCombinations (). Они "дают работу" PVM-заддча Определение функции colorCombinations () представлено в листинге 6.4. // Листинг 6.4. Определение функции colorCombinations() void colorCombinations(int Taskld,int Choices) { int Messageld = 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(Taskld,Messageld); N = pvm_initsend(PvmDataDefault); pvnupkbyte(Buffer,strlen(Buffer),1); pvm_send(Taskld,Messageld); delete Buffer; } В листинге 6.3 отметьте два обращения к функции colorCombinations (). Каждое из них велит PVM-задаче перечислить различное количество сочетаний цветов: С(24,9) и С(24,12). Первая PVM-задача должна сгенерировать 1 307 504 цветовых сочетаний, а вторая — 2 704 156. Эту работу выполняет программа, заданная в вызове функции pvm_spawn (). Каждый цвет представляется строкой. Следовательно, программа pvm_generic_combination (с помощью функции colorCombinations О) генерирует сочетания цветов, используя в качестве входных данных набор строк. Но когда она орудует "руками" функции numeri cCombinations (), показанной в листин ге 6.5, в качестве входных данных используется набор чисел с плавающей точкой. К.°Д листинга 6.3 также содержит два вызова функции numeri cCombinations (). Первый генерирует С(7,4) сочетаний, а второй — С(7,3). // Листинг 6.5. Использование PVM-задач для генерирования // сочетаний чисел void numericCombinations(int Taskld,int Choices) { int Messageld = 2,- int redouble ImportantNumbers[7] = {3.00e+8,6.67e-ll,1.99e+30,
6.2. Библиотека PVM для языка C++ 229 1.676-27,6.0236+23,6.636-34, 3.14159265359}; - pVm_initsend(PvmDataDefault) ; N ' nkint(&Choices,l,l) ; P^n send(TaskId,MfessageId) ; P - pvin initsend(PvmDataDefault); N ~ nkdouble (ImportantNumbers ,5,1); ^Isend(TaskId,MessageId) ; } В (Ьункции numericCombinations () из листинга 6.4 PVM-задача использует масел с пЛавающей точкой, а не массив байтов, представляющих строки. Поэтому , ия coiorCombinations() отправляет свои данные PVM-задачам с помощью вызовов таких функций: pvnupkbyte (Buffer, strlen (Buffer) ,1) ; pvm_send(TaskId,MessageId) ; А функция numericCombination() отправляет свои данные PVM-задачам таким образом: pvm_pkdouble (ImportantNumbers ,5,1); pvm_send(TaskId,MessageId) ; Функция colorCombinations () в листинге 6.4 создает строку названий цветов, а затем копирует ее в массив Buffer типа char. Этот массив затем упаковывается и отправляется PVM-задаче с помощью функций pvm__pkby te () и pvm_send (). Функция numericCombinations () в листинге 6.5 создает массив типа double и отсылает его PVM-задаче с помощью функций pvm__pkdouble () и pvm_send(). Одна функция отправляет символьный массив, а другая — массив типа double. В обоих случаях PVM-задачи выполняют одну и туже программу pvm_generic_combi nation. Именно здесь нас выручает преимущество использования С++-шаблонов. Одинаковые задачи благодаря этому могут работать не только с различными данными, но и с различными типами Данных без изменения самого кода. Использование шаблонов в C++ позволяет сделать мо- .дель SPMD более гибкой и эффективной. Программе pvm_generic_combination практически безразлично, с какими типами данных ей придется работать. Использо- ание контейнерных С++-классов позволяет генерировать любые комбинации векторов (vector<T>) объектов. Программа pvm_generic_combination "не знает", что УДет работать с двумя типами данных. В листинге 6.6 представлен раздел кода из программы pvm_generic_combination. // стинг 6.6. Использование тега Messageld для распознания типов данных if (M^!lnf ° (N' &Nun^Bytes, ScMessageld, &Ptid) ; messageld == 1W vector<string> Source; Pvm unv!W char£NumBytes]; £byte<Buf•NumBytes . 1 > ; ^stream Buffer; Й5^^£ « «de, { elBuffer.good()) Buffer » Col
230 Глава 6 Объединение возможностей параллельного программировани if(iBuffer.eofО){ Source.push_back(Color); } } generateCombinations<string>(Source,Ptid,Value); delete Buf; } if(Messageld == 2){ vector<double> Source; double *ImportantNumber; NumBytes = NumBytes / sizeof(double); ImportantNumber = new double[NumBytes]; pvm_upkdouble(ImportantNumber,NumBytes, 1) ; copy(ImportantNumber,ImportantNumber +(NumBytes + 1), inserter(Source,Source.begin())); generateCombinations<double>(Source,Ptid,Value); delete ImportantNumber; } Здесь используется тег Messageld, позволяющий распознать, с каким типом данных мы работаем. Но в C++ возможно более удачное решение. Если тег Messageld содержит число 1, значит, мы работаем со строками. Следовательно, можно сделать следующее объявление: vector<string> Source; Если тег Messageld содержит число 2, то мы знаем, что должны работать с числами с плавающей точкой, и поэтому делаем такое объявление: vector<double> Source; Объявив, какого типа данные будет содержать вектор Source, остальную часть функции в программе рvm_generic_combination можно легко обобщить. В листинге 6.6 обратите внимание на то, что каждая инструкция if О вызывает функцию generateCombinations (), которая является шаблонной. Эта шаблонная архитектура позволяет достичь такой степени универсальности, которая распространяет сценарии SPMD и MPMD на наши PVM-программы. Мы вернемся к обсуждению нашей программы pvm_generic_combination после рассмотрения базовых механизмов PVM-среды. Важно отметить, что контейнерные С++-классы, потоковые классы и шаблонные алгоритмы значительно усиливают гибкость PVM-программирования, которую невозможно было бы так просто реализовать в других PVM-средах. Именно такая гибкость создает возможности для построения высокоорганизованных и эле гантных параллельных архитектур. 6.2.5.2. Реализация модели MPMD (MIMD) с помощью PVM- и С++-средств В то время как модель SPMD использует функцию pvm_spawn () для создания которого числа задач, выполняющих одну и ту же программу, но на потенции различных наборах данных или ресурсов, модель MPMD использует функ pvm_spawn () для создания задач, которые выполняют различные программы на у ^ личных наборах данных. Как с помощью одной С++-программы реализовать м° MPMD (на основе PVM-функций), показано в листинге 6.7.
6.2. Библиотека PVM для языка C++ 231 6.7. Использование PVM для реализации II Листи MPMD-модели вычисления // int maindnt argc char *argv[]) { int Taskl[20]; £j Task2[50]; •nt Task3[30]; *sDawn("pvm_generic_combination", NULL, 1, p "hostl",20,Taskl); spawn ("generate_plans",argv,0, "",50,Task2); pvmlspawn ("agent_f ilters■, argv++, 1 * "host 3 ", 30, &Task3) ; } При выполнении кода, представленного в листинге 6.7, создается 100 задач. Первые 20 задач генерируют сочетания. Следующие 50 по мере создания сочетаний генерируют планы на их основе. Последние 30 задач отфильтровывают самые удачные планы из набора планов, сгенерированного предыдущими 50 задачами. Уже только это краткое описание позволяет ощутить отличие модели MPMD от модели SPMD, в которой все программы, порожденные функцией pvm_spawn (), были одинаковы. Здесь же за работу, назначаемую PVM-задачам, "отвечают" программы pvm_generic_combination, generate__plans и agent_filters. Все эти задачи выполняются параллельно и работают с собственными наборами данных, несмотря на то что одни наборы являются результатом преобразования других. Программа Pvm_generic_combination преобразует свой входной набор данных в набор, который затем может использовать программа generate_plans. Программа generate_plans, в свою очередь, преобразует входной набор данных в набор, который может затем использовать программа agent_f ilters. Очевидно, что эти задачи должны обмениваться сообщениями. Эти сообщения представляют собой входную и управляющую информацию, которая передается между процессами. Необходимо также отметить, что в листинге 6.7 функция pvm_spawn () используется для размещения Д) задач pvm_generic_combination на компьютере с именем hostl. Задача generate_plans была размещена на 50 безымянных процессорах, но каждая из этих задач получила при этом один и тот же аргумент командной строки с помощью па- Р етра argv. Задачи agent_f ilters также были направлены на конкретный ком- ^ ер (с именем host 3), и каждая задача получила один и тот же аргумент команд- Роки Посредством параметра argv. Этот пример — лишь еще одно подтвержде- МРМп К°СТИ и мощи библиотеки PVM. Некоторые варианты реализации модели U с использованием среды PVM показаны на рис. 6.5. конк Желании мы можем воспользоваться преимуществами конкретных ресурсов Hbix б НЫХ КомпьютеРов или же "положиться на судьбу" в виде "заказа" произволь- различ лянных к°мпьютеров. Мы можем также назначить различные виды работ Пьюте 1М Задачам °Дн°временно. На рис. 6.5 компьютер А представляет собой ком- РЬ1м ко ассовЬ1м параллелизмом (МП-компьютер), а компьютер В оснащен некото- Те* что Р\л^СТВ°М спеЧиализиРованных математических процессоров. Также отметь- Sparcs г- Х СРеда в Данном случае состоит из таких компьютеров, как PowerPCs, СГях ком И Т Д* ^ ОДних случаях можно не беспокоиться о конкретных возможно- теров в PVM-среде, а в других требуется иной подход. Использование
232 Глава 6 Объединение возможностей параллельного программирования функции pvm_spawn () позволяет С++-программисту не указывать конкретный пьютер для решения задачи, когда это не важно. Но если вам известно, что комп ^ тер оснащен специализированными средствами, то их можно эффективно испол вать, определив соответствующий параметр при вызове функции pvm_spawn () ° PVM-ПР ОЖЕ ОДНА ЛОГИЧЕСКАЯ ПАРАЛЛЕЛЬНАЯ ВИРТУАЛЬНАЯ МАШИНА (КЛАСТЕР)И Программа 1 Задача С Программа 2 Задача В Программа 3 Задача А Задача D . КОМПЬЮТЕРА J I КС CRAYT3D * Р1 ] J———-—т. P32 1 P48 Р2 l~г~^: _™«»-_™»«»j )МПЬЮТЕР С G4P0\ Pl| Р2 I PH КОМПЬЮТЕР В \ PI | КОМПЬЮТЕР D VER PC % ULTRA SPARC 3 j P1 P2 [ Рис. 6.5. Некоторые варианты модели MPMD доступны для реализации благодаря использованию среды PVM § 6.1. Обозначение сочетаний Предположим, мы хотели бы набрать команду программистов (в количестве вось> человек) из 24 кандидатов. Сколько различных команд из восьми программис кого- можно было бы составить из этого числа кандидатов? Один из результатов, рый следует из основного закона комбинаторики, говорит о том, что сущест ) 735 471 различных команд, состоящих из восьми программистов, которые м быть выбраны из 24 кандидатов. Обозначение С(п,г) читается как сочетание J элементов по г (и означает количество комбинаций из п элементов по г). Сочета С(п,г) вычисляется по формуле:
6.3. Базовые механизмы PVM 233 г\(п-г)\ ас есть множество, которое представляет сочетания, например {а,Ь,с}, то счи- ^СЛИ что оно совпадает с множеством {Ь,а,с} или {С.Ь.а}. Другими словами, нас ин- ^CT ет не порядок членов в этом множестве, а сами члены. Многие параллельные теРе ' ммь1, а именно программы, использующие алгоритмы поиска, эвристические ПР J и средства искусственного интеллекта, обрабатывают огромные множества ^четаний и их близких родственников перестановок. 6.3. Базовые механизмы PVM Среда PVM состоит из двух компонентов: PVM-демона (pvmd) и библиотеки pvmd. Один PVM-демон pvmd выполняется на каждом компьютере в виртуальной машине. Этот демон служит в качестве маршрутизатора сообщений и контроллера. Каждый демон pvmd управляет списком PVM-задач на своем компьютере. Демон управляет процессами, выполняет минимальную аутентификацию и отвечает за отказоустойчивость. Обычно первый демон запускается вручную. Затем он запускает другие демоны. Только исходный демон может запускать дополнительные демоны. И только исходный демон может безусловно остановить другой демон. Библиотека pvmd состоит из функций, которые позволяют одной PVM-задаче взаимодействовать с другими. Эта библиотека также включает функции, которые позволяют PVM-задаче связываться со своим демоном pvmd. Базовая архитектура PVM- среды показана на рис. 6.6. PVM-среда состоит из нескольких PVM-задач. Каждая задача должна содержать один или несколько буферов отправки сообщений, но в каждый момент времени активным может быть только один буфер (он называется активным буфером отправки сообщений). Каждая задача имеет активный буфер приема сообщений. Обратите внимание (см. рис. 6.6) на то, что взаимодействие между PVM-задачами реально выполняется с использованием ТСР-сокетов. Функции pvm_send () делают доступ к сокетам прозрачным, рограммист не получает доступа к функциям ТСР-сокетов напрямую. На рис. 6.6 также оказано взаимодействие PVM-задач со своими демонами pvmd с помощью ТСР- етов и взаимодействие между самими демонами с помощью UDP-сокетов. И снова- дол ° Ращения к с°кетам выполняются посредством PVM-функций. Программист не торьСН Заниматься программированием сокетов на низком уровне. PVM-функций, ко- *спользуются в этой книге, делятся на четыре следующие категории: Управление процессами; Упаковка сообщений и их отправка; Распаковка сообщений и их получение; Управление буфером сообщений. °нные и с существование других категорий PVM-функций (например, информаци- Внимание н гКСНЫе ФУ1**™11 или функции групповой обработки), рекомендуем обратить 3X6 Функции к, 11Кции °бработки сообщений и функции управления процессами. Другие , ДУТ рассмотрены в контексте программ, в которых они используются.
234 Глава 6. Объединение возможностей параллельного программирование Упрощенная архитектура PVM-программы PVM-ЗАДАЧА Г ДАННЫЕ Р\М-функции|. упаковки | Буфер отправки PVM-функции отправки ДАННЫЕ х "PVM-функции}. I распаковки j Буфер приема ТСР-СОКЕТЫ PVMD Список управляемых задач pvm-задача (id 1) pvm-задача (id 2) pvm-задача (id 3) PVM-ЗАДАЧА Г ТСР-СОКЕТЫ ДАННЫЕ !РУМ~фуйкцййТ_ [ упаковки Г Буфер отправки '(РУЬА'футит ! отправки ДАННЫЕ РУМ*функции*# распаковки * Буфер приема | PVM-фумкци^ ТСР-СОКЕТЫ UDP-СОКЕТЫ PVMD Список управляемых задач pvm-задача (id 101) pvm-задача (id 102) pvm-задача (id 103) Среда UNIX/Linux Рис. 6.6. Базовая архитектура PVM-среды 6.3.1. Функции управления процессами Библиотека PVM содержит шесть часто используемых функций. Функция pvm_spawn () используется для создания новых PVM-задач. При выз этой функции можно указать количество создаваемых задач, место их создания и <■ Y гументы, передаваемые каждой задаче, например: pvm_spawn( "agent_f ilters", argv++, 1, "host 3 ", 30, &Task3) ;
6.3. Базовые механизмы PVM 235 с**опсИС , ь- include "Pvm3.h ^дп spawn(char *task, char **argv, int flag, int P - char *location,int ntask,int *taskids); int pvmJtilKint taskid) ; nt pvm exit (void); • nvm addhosts(char **hosts,int nhosts,int *status); fn pvm^delhosts(char **hosts,int nhosts,int *status); ffi nvitrhalt(void); Параметр task содержит имя программы, которую должна выполнить функция spawnO- Поскольку программа, которая запускается посредством функции vm'spawnO, является автономной, ей могут потребоваться аргументы командной строки. Поэтому для их передачи используется параметр argv. Параметр location позволяет указать, на каком компьютере должна быть выполнена задача. Параметр taskids содержит либо идентификаторы порождаемых задач, либо коды состояния, представляющие любые ситуации сбоя, которые могут возникнуть во время порождения процесса. Параметр ntasks определяет, сколько экземпляров задачи требуется создать. Функция pvm_kill() используется для аннулирования задачи, указанной с помощью параметра taskid. С помощью этой функции можно аннулировать любую задачу, определенную пользователем в среде PVM, за исключением вызывающей. Эта функция отправляет сигнал SIGTERM PVM-задаче, подлежащей уничтожению. Функция pvm_exit () используется для выхода вызывающей задачи из среды PVM. Несмотря на возможность вывода задачи из среды PVM, процесс, которому принадлежит эта задача, может продолжать выполнение. Следует иметь в виду, что задача, выполняющая вызовы PVM-функций, может выполнять и другую работу, которая не связана со средой PVM. Функцию pvm_exit () должна вызывать любая задача, которая больше не имеет отношения к специфике PVM-обработки. Функция pvm_addhosts () позволяет динамически вносить дополнительные компьютеры в среду PVM. Обычно при вызове функции pvm_addhosts () передается список имен добавляемых компьютеров, например: int Status[3]; char *Hosts[] = {"porthos", "dartagnan","athos"}; Pvm^addhosts ("porthose", 1, &Status) ; Pvm^addhosts (Hosts, 3, Status) ; араметр Hosts обычно содержит имена компьютеров (одно или несколько), пере- ко Нных в файле .rhosts или .xpvm_hosts. Параметр nhost содержит количество Ное ТеРов' подлежащих добавлению в сред)' PVM, а параметр status — значение, рав- Если Чепию параметра nhosts при успешном выполнении функции pvm_addhosts (). функ *~ее ВЫзове не удалось добавить ни одного компьютера, значение, возвращаемое Vcnei СИ ^дет меньше числа 1. Если выполнение этой функции было лишь частично Леннь 1М ЗНачение> возвращаемое функцией, будет равно количеству реально добав- ^едь рл1$?МпьютеРов- Функция pvm_delhosts () позволяет динамически извлечь из списо °ДИН или несколько заданных компьютеров. Параметр hosts содержит их р^ ' ПаРаметр nhosts — количество выводимых компьютеров, например: ^elhosts ("dartagnan", 1) ;
236 Глава 6. Объединение возможностей параллельного программирования При выполнении этой функции компьютер с именем dartagnan будет извлечен среды PVM. Функции pvm_addhosts () и pvm_delhosts () можно вызывать во вп 3 выполнения приложения. Это позволяет программисту динамически изменять * меры среды PVM. Любая PVM-задача, выполняемая на компьютере, который удал ся из PVM-среды, будет аннулирована. Демоны, выполняющиеся на удаляемых к пьютерах (pvmd), будут остановлены. В случае возникновения аварийной ситуации каком-либо компьютере PVM-среда автоматически удалит его. Значения, возврата мые функцией pvm_delhosts, совпадают со значениями, возвращаемыми функцие" pvm_addhosts (). Функция pvm_halt () прекращает работу всей системы PVM. При этом все задачи и демоны (pvmd) останавливаются. 6.3.2. Упаковка и отправка сообщений Гейст Бигулин (Geist Beguelin) и его коллеги так описывают модель сообщений PVM-среды: PVM-демоны и задачи могут формировать и отправлять произвольной длины сообщения, содержащие типизированные данные. Если содержащиеся в сообщениях данные имеют несовместимые форматы, то при передаче между компьютерами их можно преобразовать, используя стандарт XDR1. Сообщения помечаются во время отправки с помощью определенного пользователем целочисленного кода и могут быть отобраны для приема посредством адреса источника, или тега. Отправитель сообщения не ожидает от получателя подтверждения приема (квитирования), а продолжает работу сразу после отправки сообщения в сеть. Затем буфер сообщений может быть очищен или вновь использован по назначению. Сообщения буфери- зируются до тех пор, пока не будут приняты получателем. PVM-система надежно доставляет сообщения адресатам, если таковые существуют. При оправке сообщений от каждого отправителя каждому получателю их порядок сохраняется. Это означает, что если отправителем было послано несколько сообщений, они будут получены адресатом в том же порядке, в котором были отправлены. Библиотека PVM содержит семейство функций, используемых для упаковки данных различных типов в буфер оправки. В это семейство входят функции упаковки, предназначенные для символьных массивов, значений типа double, float, int, long, byte и т.д. Список рллп_рк-функций представлен в табл. 6.3. Таблица 6.3. Функции упаковки Байты: int pvm_pkbyte(char *ср, int count, int std) ; Комплексные числа (комплексные числа типа double): int pvm_pkcplx(float *xp, int count, int std) ; int pvm_pkdcplx(double *zp, int count, int std) ; Значения типа double: int pvm_pkdouble(double *dp, int count, int std) ; ' XDR (eXternal Data Representation) - стандарт для аппаратно-независимых структур данны , И работанный фирмой Sun Microsystems.
6.3. Базовые механизмы PVM 237 Окончание табл. 6.3 ^Значения типа float: vin__pkfloat(float *fp, int count, int std); Значения типа int: • t pvm_pkint(int *np, int count, int std) ; Значения типа long. •nt pvm_pkl°ng(long *np, int count, int std); Значения типа short: int pvm__pkshort(short *np, int count, int std) ; Строки: int pvm_j?kstr(char *cp) ; Все функции упаковки, перечисленные в табл. 6.3, используются для сохранения массива данных в буфере отправки. Обратите внимание на то, что каждая PVM-задача (см. рис. 6.6) должна иметь по крайней мере один буфер отправки и один буфер приема. Каждая функция упаковки принимает указатель на массив соответствующего типа данных. Все функции упаковки, за исключением функции pvm_pkstr (), принимают общее количество элементов, подлежащих сохранению в массиве (а не количество байтов!). Для функции pvm_pkstr() предполагается, что символьный массив, с которым она работает, завершается значением NULL. Каждая функция упаковки, за исключением функции pvm_pkstr (), в качестве последнего параметра принимает значение, которое представляет способ обхода элементов исходного массива при их упаковке в буфер отправки. Этот параметр часто называют шагом по индексу (stride). Например, если этот шаг равен четырем, то в буфер упаковки будет помещен каждый четвертый элемент исходного массива. Важно отметить, что до отправки каждого сообщения необходимо использовать функцию pvm_initsend (), которая очищает буфер и готовит его к пересылке следующего сообщения. Функция pvm_initsend() готовит 6Уфер к пересылке сообщения в одном из трех форматов: XDR, Raw или In Place. Формат XDR (External Data .Representation) — это стандарт, используемый для описания и шифрования данных. Следует иметь в виду, что компьютеры, включенные РеДу PVM, могут быть совершенно разными, т.е. среда PVM, например, может со- оять из Sun-, Macintosh-, Crays- и AMD-компьютеров. Эти компьютеры могут отли- ся размерами машинных слов и по-разному сохранять различные типы данных. Да угл°^ЫХ слУчаях компьютеры могут различаться и битовой организацией. Стан- а £ позволяет компьютерам обмениваться данными вне зависимости от типа их ком еКТ^>ы' Ф°рмат Raw используется для отправки данных в собственном формате ется лТеРа*отпРавителя- При этом никакое специальное кодирование не применя- Пр* рмат In Place в действительности не требует упаковки данных в буфере от- C;iV4a ' И адРесатУ отправляются лишь указатели на данные и размер данных. В этом кодип Ча"ПолУчатель напрямую копирует данные. В библиотеке PVM эти три типа ния данных представляются соответствующими тремя константами: ^taDefauIt XDR Pv^t. Ьез специального кодирования ^^«nPiace п*л, та^е В буфер отправки копируются лишь указатели и размер данных
238 Глава 6. Объединение возможностей параллельного программирована Вот пример: int Bufferld; Bufferld = pvm_initsend(PvmDataRaw); //... Здесь константа PvmDataRaw, переданная функции pvm_initsend() в качестве раметра, означает, что данные упаковываются в буфер как есть, т.е. без специально кодирования. При успешном выполнении функция возвращает номер буфера отпп ки (в данном случае он будет записан в переменную Bufferld). Важно помнить чт хотя в каждый момент времени активным может быть только один буфер отправки любая PVM-задача может иметь несколько таких буферов, и с каждым из них связывается некоторый идентификационный номер. В библиотеке 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-задачи, которая принимает сообщение. При вызове функции pvm_mcast () параметр taskid означает коллекцию задач, представляемых идентификаторами, которые передаются в массиве * taskid. Параметр messageid указывает идентификатор посылаемого сообщения. Идентификаторы сообщений представляют собой целочисленные значения, определенные пользователем. Они используются отправителем и получателем для идентификации сообщения, например: pvm_bufinfо(N,&NumBytes,&MessageId,&Ptid); //. . . switch(Messageid) { case 1 : // Некоторые действия. break; case 2 : // Другие действия. break //. . . } В данном случае функция pvm_buf inf о () используется для получения инфор>* ции о последнем сообщении, принятом в буфер приема N. Мы можем получить ко чество байтов, идентификатор сообщения (messageid) и узнать, кто его отправь Зная значение messageid, мы можем выполнить соответствующие логические леи вия. Функция pvm_send () посылает заданной задаче команду псевдоблокирован • после приема которой задача блокируется до тех пор, пока отправитель не убеди в том, что сообщение было послано правильно. Задача-отправитель не ожидает у ального получения сообщения. Функция pvm_psend () отправляет сообше
6.3. Базовые механизмы PVM 239 лственно указанной задаче. Обратите внимание на то, что функция НСП° end () имеет параметр buffer, используемый в качестве буфера для хранения pVtfUP моГО сообщения. Функция pvm_mcast () используется для отправки сообще- П°СЬ неСКОльким задачам одновременно. Аргументы, передаваемые функции НИ cast (), включают массив идентификаторов задач-получателей сообщения Р "k^d), количество задач— участников "широковещания" (ntask) и идентифика- сообщения (messageid) для идентификации отправляемого сообщения. На Т 6 6 показано, что у каждой PVM-задачи есть собственный буфер отправки, кото- " существует в течение промежутка времени, длительности которого было бы дос- точно, чтобы сообщение гарантированно дошло до адресата. За исключением управляющих сообщений, значение сообщений, которыми обмениваются любые две PVM-задачи, заранее определено логикой конкретного приложения, те назначение каждого сообщения должно быть заранее известно для задачи- отправителя и задачи-получателя. Эти сообщения передаются асинхронно, могут иметь любой тип данных и произвольную длину. Тем самым для приложения обеспечивается максимальная гибкость. Аналогами отправляемых PVM-сообщений являются принимаемые PVM-сообщения. Так, за прием сообщений "отвечают" пять основных функций. Синопсис #include "pvm3.h" int int int int [int pvm_recv(int taskid, pvm_nrecv(int pvm__precv (int int int pvm_trecv(int taskid, taskid, int int int messageid); messageid); messageid, char size, int type, int sender, messagetag, taskid, int struct timeval pvm_probe(int taskid *buffer, int messagelength); messageid, *timeout); , int messageid); Функция pvm_recv () используется одними PVM-задачами для получения сообщении от других. Эта функция создает новый активный буфер, предназначенный для хранения полученного сообщения. Параметр taskid определяет идентификатор задачи-отправителя. Параметр messageid идентифицирует сообщение, которое по- ано отправителем. Следует иметь в виду, что задача может отправить несколько ° Щений, имеющих различные или одинаковые идентификаторы (messageid). taskid = -1, то функция pvm_recv () примет сообщение от любой задачи. Ес- essageid = -1, то функция примет любое сообщение. При успешном выполне- функция pvm_recv () возвращает идентификатор нового активного буфера, зал Тивн°м случае — отрицательное значение. После вызова функции pvm_recv () луч ^ДСТ за^локиРована и станет ожидать до тех пор, пока сообщение не будет по- од 0 ' 11осле получения сообщение считывается из активного буфера с помощью °и из функций распаковки, например: fcValue^0]; pv£-recv(400002,2) ; cOUtU<<k^°at (400002, Value,!);
240 Глава 6. Объединение возможностей параллельного программирована Здесь функция pvm_recv() обеспечивает ожидание сообщения от задачи, идент rk катор которой равен 400002. Идентификатор сообщения (messageid), полученн от задачи с номером 400002, должен быть равен значению 2. Затем исполь-™ ° У ется функция распаковки для считывания массива чисел с плавающей точкой типа f i Тогда как функция pvm_recv () вынуждает задачу ожидать до тех пор, пока она не п лучит сообщение, функция pvm_nrecv () обеспечивает прием сообщений без бло рования. Если соответствующее сообщение не поступает адресату, функи pvm_nrecv () немедленно завершается. По прибытии сообщения по месту назнач ния функция pvm_nrecv () сразу же завершается, а активный буфер будет содержат полученное сообщение. Если произойдет сбой, функция pvm_nrecv() возвратит отрицательное значение. Если сообщение не поступит адресату, функция возвратит число 0. Если сообщение благополучно прибудет по месту назначения, функция возвратит номер нового активного буфера. Параметр taskid содержит идентификатор задачи-отправителя. Параметр messageid содержит идентификатор сообщения, определенный пользователем. Если taskid = -1, функция pvm__nrecv () примет сообщение от любой задачи. Если messageid = -1, эта функция примет любое сообщение. При приеме сообщений с помощью функций pvm_recv () или pvm_nrecv () создается новый активный буфер, а текущий буфер приема очищается. Тогда как функции pvm_recv (), pvm_nrecv () и pvm_trecv () принимают сообщения в новый активный буфер, функция pvm_precv () принимает сообщение непосредственно в буфер, определенный пользователем. Параметр taskid содержит идентификатор задачи-отправителя. Параметр messageid идентифицирует получаемые сообщения. Параметр buffer должен содержать реально принятое сообщение. Поэтому вместо получения сообщения из активного буфера с помощью одной из функций распаковки, сообщение считывается напрямую из параметра buffer. Параметр size содержит длину сообщения в байтах. Параметр type определяет тип данных, содержащихся в сообщении. Параметр type может иметь следующие значения: 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 () позволяет программисту организовать процедуру по" лучения сообщений с ограничением по времени. Эта функция заставляет ВЪ13Ъ вающую задачу перейти в заблокированное состояние и ожидать прихода соо щения, но лишь в течение промежутка времени, заданного параметром timeou Этот параметр представляет собой структуру типа timeval, определенную в головке time.h, например: #include "pvm3.h" //. . . struct timeval TimeOut; TimeOut.tv_sec = 1000; int Taskid; int Messageid;
6.3. Базовые механизмы PVM 241 v-гЛ = pviruparent () ; fas*1" _ 2; Hessage ^TaskId^ Mes sageld, ScTimeOut) ; pvnucr переменная TimeOut содержит член tv_sec, установленный равным 1000 с. vktvpv timeval можно использовать для установки времениых значений в секун- и микросекундах. Структура timeval имеет следующий вид: struct timeval { long tv__sec; // секунды long tv_usec; // микросекунды }; Этот пример означает, что функция pvm_trecv () заблокирует вызывающую задачу максимум на 1000 с. Если сообщение будет получено до истечения заданных 1000 с, функция сразу завершится. Функцию pvm_trecv () можно использовать для предотвращения бесконечных задержек и взаимоблокировок. При успешном выполнении функция pvm_trecv() возвращает номер нового активного буфера, в противном случае (при возникновении ошибки)— отрицательное значение. Если taskid = -1, функция примет сообщение от любого отправителя. Если mess age id = -1, функция примет любое сообщение. Функция pvm_probe () определяет, поступило ли сообщение, заданное параметром messageid, от отправителя, заданного параметром taskid. Если функция pvm_probe () "видит" указанное сообщение, она возвращает номер нового активного буфера. Если заданное сообщение не прибыло, функция возвращает число 0. При возникновении сбоя функция возвращает отрицательное значение. 1 Синопсис #include "pvm3.h" int int int int int [int pvm_getsbuf (voi< pvirugetrbuf (voi< Pvm_setsbuf(int pvm_setrbuf(int i); i); bufferid); bufferid); Pvnurikbuf (int Code) ; pvm,freebuf(int bufferid); ^1 библиотеке PVM предусмотрено шесть полезных функций управления буфе- и, которые можно использовать для установки, идентификации и динамическо- °здания буферов отправки и приема. Функция pvm_getsbuf () используется олучения номера активного буфера отправки. Если текущего буфера отправки .Ществует, функция возвращает число 0. Функция pvm_getrbuf () использует- Име ПолУчения идентификационного номера активного буфера приема. Следует феп ВидУ» что при каждом получении сообщения создается новый активный бу- Функ ТеК'1Ци^ буфер очищается. Если текущего буфера приема не существует, bujf . Возвращает число 0. Функция pvm_setsbuf () устанавливает параметр т°лыс РавнЬ1м номеру активного буфера отправки. Обычно PVM-задача имеет КиХ 6vA "УФеР отправки. Но иногда возникает необходимость в нескольких та- Н* pax. Хотя в любой момент времени активным может быть только один
242 Глава 6. Объединение возможностей параллельного программирована буфер отправки, PVM-задача может создавать дополнительные буфера отпп с помощью функции pvm_mkbuf (). Функцию pvm_setsbuf () можно использо И для установки в качестве активного буфера одного из буферов отправки, кото Ь были созданы во время работы приложения. Эта функция возвращает идентисЬ С тор предыдущего активного буфера отправки. Функция pvm_setrbuf () устанав вает активный буфер приема равным значению buffer id. Помните, что PVM функции распаковки работают с активным буфером приема. Если существует сколько буферов, функция pvm_setrbuf () позволит применить текущий буфер л использования функциями распаковки. При успешном выполнении функци pvm_setrbuf () возвращает идентификатор предыдущего активного буфера. Если идентификатора буфера, переданного функции pmv_setrbuf (), не существует или он оказался недействительным, функция возвратит одно из следующих сообщений об ошибке: PvmBadParam или PvmNoSuchbuf. Функция pvm_mkbuf () используется для создания нового буфера сообщений. Параметр Code определяет формат данных, которые будут содержаться в этом буфере: XDR, собственный формат компьютера или формат, использующий указатели и размеры. Поэтому параметр Code может содержать одно из трех значений: PvmDataDefault XDR PvmDataRaw В зависимости от марки компьютера (без кодирования) PvmDatalnPlace Используются только указатели на данные и их размер При успешном выполнении функция pvm_mkbuf () возвращает идентификатор нового активного буфера, в противном случае — отрицательное значение. Для каждого обращения к функции pvm__mkbuf () , если буфер отправки больше не будет нужен, необходимо вызвать функцию pvm_f reebuf () , которая освободит память, выделенную функцией pvm_mkbuf (). Функцию pvm_f reebuf () следует использовать только в случае, когда сообщение уже отправлено и в буфере нет никакой необходимости. 6.4. Доступ к стандартному входному потоку (stdin) и стандартному выходному потоку (stdout) со стороны PVM-задач Среда PVM связывает воедино коллекцию компьютеров и представляет их ПЛ программы в виде одной логической машины с несколькими процессорами. При эт возникают следующие вопросы. Какой компьютер в PVM-среде должен действова как консоль? Где будут отображаться данные, выводимые PVM-задачей в объект со типа ostream? Если PVM-задача попытается принять данные с клавиатуры, то с ка именно клавиатуры она должна их считывать? Выходной поток stdout для кажД сыновнего процесса перехватывается и отправляется назначенной PVM-задаче в в PVM-сообщения. Каждый сыновний процесс наследует информацию, которая о у деляет, какая задача должна принять данные, записанные в поток stdout, и как данные должны быть идентифицированы. Входной поток каждого сыновнего У цесса связан с устройством /dev/null. Все, что записано в устройство /dev/n
6.5. Резюме 243 Если устройство /dev/null открыто для чтения, возвращается эквивалент теряет • ^^нца файла. Это означает, что код сыновних процессов не должен созда- ПРИЗ оасчете на считывание входных данных из стандартного потока stdin (сin) ВаТЬС апись выходных данных в стандартный поток stdout (cout). При этом потаили на ки -. и stdout для родительской задачи ведут себя вполне ожидаемым образом. К\я тип для взаимодействия между собой должны использовать сообщения. Это что входные данные можно принимать из сообщении, каналов, общей 3 ляемой) памяти, переменных среды, аргументов командной строки или фай- И точно так же выходные данные можно записывать в сообщения, каналы, общую память и файлы. 6.4.1. Получение доступа к стандартному выходному потоку (cout) из сыновней задачи Поведение выходных данных, записанных в выходной поток stdout или помещенных в объект cout, отличается для различных порожденных PVM-задач. Именно родительский процесс решает, что в конце концов с ними должно произойти. Когда выходные данные из порожденного потомка помещаются в объект cout или сегг, они перехватываются демоном pvmd и упаковываются в стандартные PVM- сообщения, которые отправляются задаче с идентификатором Taskld, заданным родителем. Родительский процесс может связать пару (Taskld, Code) с объектами cout и сегг своего сыновнего процесса. Это реализуется с помощью функции pvm_setopt (), которая вызывается перед порождением потомка. Если значение Taskld равно 0, сообщения попадут ведущему демону pvmd и будут записаны в его журнал регистрации ошибок. Порожденный процесс может установить значение переменной Taskld равным 0 или значению, унаследованному от его родителя, или собственному значению идентификатора Taskld. Это означает, что именно родительский процесс управляет тем, куда будет записано содержимое объектов cout или сегг. Порожденная PVM-задача может назначить другие PVM-задачи для получения Данных, помещенных в объекты cout или сегг. Обычно записью любых важных Данных в потоки stdout или stdin управляет порождающая задача, а всем остальным ведает ведущий демон pvmd. 6.5. Резюме лиотека PVM, отличающаяся большой гибкостью средств, поддерживает инство моделей параллельного программирования. К достоинствам PVM- теп ОТНосится ее способность работать с гетерогенными коллекциями компью- быст °3°Рые м°гут состоять из процессоров, отличающихся характеристиками бибч твия» размерами и архитектурой. Помимо аппаратной совместимости, библ* СКа « прекрасно работает со стандартной С++-библиотекой и системной шабло К°И ^^/Linux. В результате объединения с возможностями С++- алгоп! сРеДств объектно-ориентированного программирования и коллекций ^Ются В Мои*ь PVM-среды значительно возрастает. Шаблоны прекрасно вписы- MD-программирование. А для расширения возможностей PVM-среды
244 Глава 6. Объединение возможностей параллельного программирован при использовании моделей MIMD (MPMD) можно успешно использовать контейнеры и алгоритмы. В главе 13 мы подробнее познакомимся со средс ++* PVM-библиотеки и покажем, как ее можно использовать для С++-реализации ^ тегии "классной доски". Эта стратегия — один из основных способов решения ^ блем параллельного программирования. Р°*
ОБРАБОТКА ОШИБОК, ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ И НАДЕЖНОСТЬ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ В этой главе... 7.1. Надежность программного обеспечения 7.2. Отказы в программных и аппаратных компонентах 7.3. Определение дефектов в зависимости от спецификаций ПО 7.4. Обработка ошибок или обработка исключительных ситуаций? 7.5. Надежность ПО: простой план 7.6. Использование объектов отображения для обработки ошибок 7.7. Механизмы обработки исключительных ситуаций в C++ 7.8. Диаграммы событий, логические выражения и логические схемы 7.9. Резюме
J~!/\3J^J3J Всегда можно изобрести суперсложные модели, чтобы объяснить множество исследуемых фактов, но ученый, если он не философ, скорее примет самую простую теорию, которая согласуется со всеми имеющимися у него данными. — Алястер Pn(AlastairRae), Quantum Physics Illusion or Reality Одна из главных целей разработки и проектирования программного обеспечения— создать программу, которая бы отвечала требованиям пользователя и работала корректно и надежно. Пользователи требуют от ПО корректности и надежности, независимо от его конкретного назначения. Использование ненадежных программ в любой сфере — финансовой, промышленной, медицинской, научной или военной— может иметь разрушительные последствия. Зависимость люд& и механизмов от ПО на всех уровнях нашего общества вынуждает его создателей еде лать все возможное, чтобы их детище было надежным, робастным и отказоустойчивы. • Эти требования налагают дополнительную ответственность на разработчиков и пр° тировщиков ПО, которые создают системы, содержащие параллелизм. Программ с параллелизмом или компоненты, которые выполняются в распределенных ср^3*' держат больше (по сравнению с ПО без параллелизма) программных уровней, больше уровней, тем сложнее управлять таким ПО. Чем выше сложность системы, • больше изъянов может остаться в ней невыявленными. А чем больше изъянов в П » выше вероятность того, что оно откажет, причем в самый неподходящий момент. Для программ, разбиваемых на параллельно выполняемые или распределе задачи, характерны дополнительные сложности, которые проявляются в ПР° п поиска правильного решения, связанного с декомпозицией работ (work brea structure — WBS). Кроме того, здесь необходимо учитывать проблемы, которые явл
7.1. Надежность программного обеспечения 247 млеМой частью именно сетевых коммуникаций. Помимо проблем коммуника- НС° декомпозиции, не следует забывать о таких "прелестях" синхронизации, как цИ „ ланных и взаимоблокировка. Параллельное программирование "по определе- " практически всегда сложнее последовательного, а следовательно, обработка 6 к и исключительных ситуаций для параллельных программ требует больше ° ий (и умственных, и физических, и временных), т.е. "больше" программирова- У Интересно отметить, что разработка ПО развивается в направлении приложе- о которые требуют параллельного и распределенного программирования. В про- ировании современного ПО распространены Internet- и Intranet-модели. Нынче вятся НОрМОй (а не исключением) многопроцессорные компьютеры общего на- начения. Встроенные и промышленные вычислительные устройства становятся все более высокоорганизованными и мощными. Для серверного развертывания "де- факто" становится стандартом понятие кластера. Мы считаем, что нынешним разработчикам и проектировщикам ПО не остается ничего другого, как разрабатывать и проектировать надежные приложения для многопроцессорных и распределенных сред. И, безусловно, излишне повторять, что требования, предъявляемы к ПО такого рода, постоянно возрастают как по сложности, так и организации. Во многих примерах программ этой книги мы не приводим кода обработки ошибок и исключительных ситуаций, чтобы не отвлекать внимание читателя от основной идеи или концепции. Однако важно иметь в виду, что использованные здесь примеры имеют вводный характер. В действительности объем кода, посвященного обработке ошибок и исключительных ситуаций в программах, включающих параллелизм или рассчитанных на распределенную среду, довольно значителен. Обработка ошибок и исключительных ситуаций должна быть составной частью проекта ПО на каждом этапе его разработки. Мы — сторонники моделирования на основе раскрытия параллелизма в области проблемы и ее решения. И именно на этапе моделирования следует заниматься разработкой моделей подсистем обработки ошибок и исключительных ситуаций. В главе 10 показано, как можно использовать язык uML (Unified Modeling Language— унифицированный язык моделирования) для визуализации проектирования систем, требующих параллельных или распределен- ых методов программирования. Разработка подсистем обработки ошибок и искупительных ситуаций лишь выиграет от применения средств UML и самого проса визуализации, который ничем другим заменить нельзя. Следовательно, в ка- Пп С Исходп°й Цели вам необходимо представить надежность разрабатываемого с помощью таких инструментов, как UML, диаграммы событий, событийные им • ения> Диаграммы синхронизации и пр. В этой главе рассматриваются пре- ппо Ва Ряда мет°Дов проектирования, которые способствуют визуализации вкач Подсистемы обработки ошибок и исключительных ситуаций. Кроме того, ся вст С °СНовы для разработки надежного и отказоустойчивого ПО используют- 1НЫе сРеДства языка C++, содержащие иерархию классов исключений. • Надежность программного обеспечения ^Ьания Ъ п^огРпЛ1Л1Пого обеспечения— это вероятность безотказного функциони- ^ Идеале э ЬЮтерной программы в течение заданного времени в заданной среде. "*ему, кото еР°ятн°сть приближается к 100%. Если разработчики хотят создать сис- УДет отличаться безотказной работой, ее ПО должно разрабатываться
248 Глава 7. Обработка ошибок, исключительных ситуаций и надежность с использованием методов отказоустойчивого программирования. Отказоустпой система — это система, которая сохраняет работоспособность в результате устран ^ последствий ошибок ПО. Под ошибкой (fault) понимается программный дефект к Я рый может привести к отказу в работе некоторой части ПО. В понятие "сбоя в си °" ме программного обеспечения" (failure) мы вкладываем выполнение некоторого к понента ПО, который отклоняется от системных спецификаций. Мы согла с трактовкой ошибок и сбоев, которую предложили Муса (Musa), Ианино (Iannin ^ и Окумото (Okumoto) в своей книге Software Reliability: Ошибка — это дефект в программе, который при некоторых условиях приводит к ее отказу. К отказу могут привести различные совокупности условий, причем эти условия могут повторяться. Следовательно, ошибка может быть источником не одного, а нескольких отказов. Ошибка (дефект) — это свойство программы, а не результат (свойство) ее выполнения или поведения. Именно этот смысл мы вкладываем в понятие термина "bug". Ошибка ПО — это следствие оплошности, или недоработки (error), программиста. Ошибки, которые допускает программист или разработчик ПО, могут возникнуть из-за неверной интерпретации требований к ПО или некачественного, некорректного или недостаточно полного перевода этих требований в код. Если программист совершает оплошности такого рода, он вносит в программу ошибки, или дефекты. При выполнении дефектного кода может произойти сбой программы. Ошибки ПО можно обнаружить только при выполнении кода. Очистить программу от ошибок, а следовательно, и не допустить возможность отказа, позволяет процесс тестирования и отладки ПО. Обратите внимание на то, что мы используем термины "дефект" и "ошибка" взаимозаменяемо. Термин "оплошность" мы относим к допускаемым программистом промахам, которые являются причиной дефектов ПО. Отказоустойчивость — это свойство, которое позволяет некоторой части ПО оставаться в исправном состоянии или восстанавливать работоспособность после программных сбоев, вызванных ошибками, внесенными в ПО в результате недоработки программистов. Одни отказы ПО являются результатом наличия дефектов в программах, другие же— результатом исключительных условий (необязательно созданными оплошностью программиста), которые могут создаться в оборудовании или используемых программных продуктах. Например, сетевая карта, поврежденная в результате всплеска напряжения, может привести соответствующую часть ПО к сбою. Вирус может нарушить процесс передачи данных, в результате чего может отказать программа, которая зависит от этого процесса. Пользователь может нечаянно удалить критические компоненты из системы, что неминуемо приведет к ее отказу. Перечисленные выше не приятности вызываются не из-за дефектов в программе, а создаются в результате у ловий, которые мы называем исключительными ситуациями. Исключительная сит) ция, или исключение, — это ненормальные условия, или исключительн обстоятельства, или экстраординарные явления (события), с которыми сталкивае ПО, в результате чего оно (или некоторая его часть) отказывает. И хотя как дефек так и исключения приводят к отказам ПО, важно понимать различие между ними, скольку для "борьбы" с ними применяются, как правило, различные методы. Нес* ря на то что конечным результатом применения этих методов является наде и отказоустойчивое ПО, для обработки исключений и обработки ошибок (дефеК используются различные способы проектирования и программные конструкции.
7.2. Отказы в программных и аппаратных компонентах 249 7 2 Отказы в программных и аппаратных компонентах тт проектировании надежного и отказоустойчивого ПО мы должны поставить здать такое ПО, которое бы продолжало функционировать даже после отказа цеЛ х его компонентов (аппаратных или программных). Если наше ПО претен- неК то ЧТобы называться отказоустойчивым, оно должно обладать средствами, Ш ые могли бы предусматривать последствия аппаратных или программных оши- По крайней мере наши отказоустойчивые проекты должны обеспечивать не венное прекращение работы системы, а постепенное сокращение ее возможно- й Если наше ПО является отказоустойчивым, то в случае отказа отдельного его омпонента (компонентов) оно должно продолжать функционирование, но на более низком уровне. Ошибки, которые наше ПО должно обрабатывать, можно разделить на две категории: программные и аппаратные. На рис. 7.1 показана схема некоторых аппаратных компонентов, а также уровни ПО, которые могут включать ошибки. | ; Уровень ошибок пользователя ''Программные сбои на уровне приложения Щ^^тшщ сбоя на,уровне библиотеки .ТфОДессора : Сбои устройств Специальные случаи -™ ■ „ЩйЬтв памяти ч ввода-вывода.т.е. :.'сбоёй устройств к дисководов, шрт ..ввода-вывода (особенно специального -для-распределенных ■ назначения, приложений), •видёзус?ройств .'сбой других устройств % т * ?.*. i | ч; "'" " ■'■ " -СВЯЗИ ' Щ ~ * "*• Схема аппаратных компонентов, а также уровней ПО, которые могут содержать ошибки тодь * С МЫ отделили аппаратные компоненты от программных, поскольку ме- грамм Р °тки аппаратных сбоев часто отличаются от методов обработки про- нахол ОШибок. Здесь также выделены различные уровни ПО. Некоторые из них toyio) вне Досягаемости" разработчика (т.е. он не может ими управлять напря- бок. НаТребуЮТ специального рассмотрения процесса обработки исключений и оши- пРИним ПаХ пРоектиРования» разработки и тестирования ПО обязательно следует нЫх uQj.n ,f° ВНимание возможность аппаратных сбоев и наличия ошибок в различ- 11РеДелен ^ЛЯ пРогРамм* которым присущ параллелизм или состоящих из рас- компонентов, следует учитывать дополнительные обстоятельства, весьма
250 Глава 7. Обработка ошибок, исключительных ситуаций и надежность "благоприятные" для возникновения аппаратных сбоев. Например, в распределе программах используется взаимодействие аппаратных и программных сре ЫХ Ошибка, "закравшаяся" в компонент, отвечающий за это взаимодействие, может В вести к отказу всей системы. Программы, разработанные для параллельной раб ^И процессоров, могут сбоить, если ожидаемое количество процессоров окажется не Ы тупным. Даже если средства связи и процессоры прекрасно отработали при загру системы, ее отказ возможен в любой момент после начала функционирования И ключительная ситуация может возникнуть в любом из компонентов оборудован и на любом уровне ПО. Кроме того, каждый программный уровень может содержат дефекты, которые необходимо каким-то образом обрабатывать. На этапе проектип вания ПО следует рассматривать возможные исключительные ситуации и ошибки в программах, присущие каждому уровню ПО в отдельности. Ведь варианты восстановления приложения после возникновения исключительных ситуаций и исправления ошибок, которые возможны на уровне 2, отличаются от вариантов, применимых к уровню 3. К сбоям, которые возможны на различных уровнях ПО и в аппаратных компонентах, следует добавить сбои, характеризующиеся архитектурной областью локализации, специфической для каждого приложения. Например, на рис. 7,2 показано, как по мере увеличения дистанции между задачами возрастает уровень сложности обработки ошибок и исключительных ситуаций. о Ошибки и исключения, возникающие при взаимодействии лроц*-);. выполняющихся на различных компьютерах, отнс^ящ^х-:^ к различным сетям (с различными протоколами Ошибки и исключения, возникающие при вза^уодойсшии процессий. выполняющихся на различных компьютерах, но относки *:.:,я к одной сети Ошибки и исключения возникающие при взаимодействии процессов. выполняющихся на одном компьютере Ил Ошибки и "исключения, возникающие.при'взаимодействии функций Ез одного процесса (одного адресного пространства) Щм Ошибки в одной потоке Щ Ошибки в разных потоках Ёж о ¥Ж Ы Рис. 7.2. Зависимость увеличения уровня сложности обработки исключительных ситуации и ошибок от увеличения дистанции между логическим местоположением задач
7.3. Определение дефектов в зависимости от спецификаций ПО 251 больше в программных или аппаратных компонентах дистанция между па- ьно выполняющимися задачами, тем более высокий уровень организации тре- Р3* для проектирования компонентов обработки исключительных ситуаций бок. Изучив рис. 7.1 и 7.2, можно понять: для того, чтобы спроектировать и раз- И б тать надежное ПО, необходимо предусмотреть не только, какие возможны ис- ^ ггельные ситуации и ошибки, но и где они могут возникнуть. 7.3. Определение дефектов в зависимости от спецификаций ПО Спецификация ПО — это своего рода "эталон", позволяющий определить, имеет ли данная часть ПО дефекты. Мы не можем оценить корректность программных компонентов без доступа к программным спецификациям. Спецификация ПО содержит описание и требования, из которых должно быть ясно, что должен делать данный программный компонент и чего он делать не должен. Общеизвестно, что довольно трудно написать полные, исчерпывающие и точные спецификации. Спецификации могут представлять собой формальные документы и требования, составленные конечными пользователями, аналитиками, специалистами по созданию пользовательского интерфейса, специалистами в предметной области и др. Спецификации могут также выглядеть как множество целей и не жестко определенных задач, устно излагаемых пользователями проектировщикам и разработчикам ПО. Любое отклонение компонента ПО от его спецификации является дефектом. Чем выше качество спецификации, тем проще выявить дефекты и понять, где программист сделал ошибки. Если спецификация проекта расплывчата, с плохо определенными элементами и нечетко описанными требованиями, то определение программных дефектов для такого проекта представляет собой движущуюся мишень. Если спецификации неоднозначны, то трудно сказать, что дефектно, а что нет. Точно так же невозможно утверждать, прав ли был разработчик. Туманно определенные спецификации являются причиной так же туманно определяемых ошибок. В таких условиях создание отказоустойчивого и надежного ПО попросту невозможно. 7-4. Обработка ошибок или обработка исключительных ситуаций? Доп fi ЩСМ Сл^чае ошибки ПО (которые являются результатом оплошности или не- dor КИ пРогРаммиста) должны быть обнаружены и исправлены на этапах тести- Р НИЯ' перечисленных в табл. 7.1. L- jj8 7.1. Типы тестирования, используемые в процессе разработки ПО ^^^^анш Описание 11/111 niecnwt Р0впнш> ПО тестируется поэлементно. Под элементом может подра- МенШов (un* HUC ЭЛ€ зумеваться отдельный программный модуль, коллекция мо- Sllng) дулей, функция, процедура, алгоритм, объект, программа или компонент
252 Глава 7. Обработка ошибок, исключительных ситуаций и надежность Ыычаниегпд^ 7 ; Тип тестирования Описание Проверка взаимодействия и функционирования компонентов системы (integration testing) Регрессивное тестирование (regression testing) Испытания в утяжеленном режиме (stress testing) Эксплуатационные испытания (operational testing) Тестирование спецификации (specification testing) Приемочные испытания (acceptance testing) Тестируется некоторая совокупность элементов. Элементы объединяются в логические группы, и каждая группа тестии ся как единый блок (элемент). Эти группы могут подвергатьс одинаковым проверкам. Если группа элементов проходит тес ее присоединяют к тестируемой совокупности, которая в свою очередь должна быть протестирована с новым дополнением Увеличение количества элементов, подлежащих тестированию должно подчиняться формулам комбинаторики Программные модули должны повторно тестироваться, если в них были внесены изменения. Регрессивное тестирование дает гарантию, что изменение любого компонента не приведет к потере функциональности Тестирование, которое проводится для компонента или всей системы при предельных и "запредельных" значениях входных параметров. Использование граничных условий позволяет определить, что может произойти с компонентом или системой в нештатных ситуациях Тестирование системы с полной нагрузкой. Для этого используется реальная среда, создающая реальную нагрузку. Этот тип тестирования также применяется для определения производительности системы в совершенно незнакомой среде Компонент проверяется при сравнении с исходными спецификациями. Именно спецификация устанавливает, какие компоненты включены в систему и какие взаимоотношения должны быть между ними. Этот этап является частью процесса верификации ПО Тестирование этого типа выполняется конечным пользователем модуля, компонента или системы для определения его (ее) производительности. Этот этап является частью процесса аттестации ПО Во время процесса тестирования и отладки программные дефекты должны быть обнаружены и ликвидированы. Однако исключительные ситуации (исключения) обрабатываются во время выполнения программы. Следует различать исключительные и нежелательные условия. Например, если мы спроектировали программу, котор будет добавлять в список числа, вводимые пользователем, а пользователь будет вв дить и числа, и символы, которые не являются числами, то такая ситуация относит к нежелательной, а не к исключительной. Мы должны проектировать программы, к торые были бы робастными, т.е. устойчивыми к ошибкам, предусматривая провер , корректности входных данных. Ввод данных в программу должен быть организов таким образом, чтобы пользователь был вынужден вводить данные, которые требу ся нашей программе для надлежащего выполнения. Если, например, спроектиров ный нами компонент программы сохраняет информацию на внешнем устроис ' и программа попадает в ситуацию отсутствия свободного пространства на эт
7 4. Обработка ошибок или обработка исключительных ситуаций? 253 ° е то такие условия работы программы также можно назвать нежелательными, У01^ ^ючительными, или экстраординарными. Исключительные ситуации мы связы- а ^ необычными условиями, а не с нежелательными. Методы обработки исключи- 836 ix ситуаций предназначены для непредвиденных обстоятельств. Ситуации же, ie являются нежелательными, но вполне возможными и потому предсказуемыми, к° " 0брабатываться с применением обычной программной логики, например: <БХодные данные неприемлемы, то> 1 <повторно запрашиваем входные данные> 6 S <выполняем нужную операцию> end if Такая проверка условий — одна из основополагающих граней искусства программирования. Продемонстрированный стиль программирования позволяет не допустить возникновения многих проблем, но эта модель ситуации не "дотягивает" до определения исключительной. Существуют различия между дефектами и исключительными ситуациями, а также между исключительными ситуациями и нежелательными условиями. С дефектами справляются путем тестирования и отладки. Нежелательные условия обрабатываются в рамках обычной программной логики, а исключительные ситуации — методами обработки исключений. Различия между характеристиками обработки ошибок, исключений и нежелательных условий сведены в табл. 7.2. Таблица 7.2. Различия между характеристиками обработки ошибок, исключений и нежелательных условий ЙК Обработка ошибок Логические ошибки обнаруживаются на этапе тестирования и отладки Корректно работающие программы не содержат ошибок Для предупреждения и исправления ошибок используется программная логика Поддерживается нор. сальный ход выполнения программы Обработка исключительных Обработка нежелательных ситуаций условий Описывает непредвиденные условия во время выполнения Корректно написанные программы могут попадать в исключительные ситуации Для восстановления работоспособности программы после возникновения исключительных ситуаций используются методы обработки исключений Нормальный ход выполнения программы наруша- Описывает нежелательные условия, которые весьма вероятны во время выполнения Корректно написанные программы могут попадать в нежелательные ситуации Для исправления нежелательных условий используется программная логика Делается попытка поддержать нормальный ход выполнения программы Наща На целк **Ий, чт гГ ТаК постРоить компоненты обработки ошибок и обработки исключе- ^иМи п Затем их можно было объединить с другими компонентами, составляю- Р лельные или распределенные приложения. Эти компоненты должны об-
254 Глава 7. Обработка ошибок, исключительных ситуаций и надежность ладать средствами идентификации проблем и уведомления о них, а также возмо^ стями их корректировки или восстановления работоспособности приложения гт ° восстановлением и корректировкой подразумеваются самые различные спос к* достижения поставленной цели: от предложения пользователю еще раз ввести ^ ные (с подсказкой, например, их правильного формата) до перезагрузки подсисте в рамках ПО. Действия по восстановлению и корректировке могут включать обраб * ку файлов, возврат из базы данных, изменение сетевого маршрута, маскирован процессоров, повторную инициализацию устройств, а для некоторых систем даже мену элементов оборудования. Компоненты обработки ошибок и исключительно ситуаций могут быть выполнены в различных формах: от простых предписаний л интеллектуальных агентов, единственное назначение которых состоит в предвидении ситуаций сбоя и их предотвращении. Компонентам обработки ошибок и исключений в ответственных участках ПО уделяется значительное внимание. Архитектура упрощенного компонента обработки ошибок представлена на рис. 7.3. УПРОЩЕННЫЙ КОМПОНЕНТ ОБРАБОТКИ ОШИБОК D и Список номеров ошибок и их описание 1 г Преобразование номера ошибки в программный адрес перехода р Ппйпбпалпнанмй мпмрря \ И ^\ ошибки в иерархическую j "" 5 структуру о гнетов ; г и логику-отчетов ? 1 ' | Преобразование .номера 1 ошибки в логику коррекции) И [_ ситуации сбоя Iшт ^ ^ Поеобоазование номера "1 { ОШИбКИ В ЛОГИКУ ; 1 ... ■ „ восстановления ■.■■,■_] Рис. 7.3. Архитектура упрощенного компонента обработки ошибок Компонент 1 на рис. 7.3— это простой компонент отображения (тар), который содержит список номеров ошибок и их описания. Компонент 2 содержит объект, который преобразует номера ошибок в адреса переходов, функций или подсистем. По номеру ошибки компонент 2 определяет направление перехода. Компонент 3 преобразует номера ошибок в иерархическую структуру отчетов и логику отчетов. Иерар хическая структура отчетов содержит данные о том, кого (или что) необходимо }ве домить об ошибке. Логика отчетов определяет, что должно включать это уведомле ние. Компонент 4 содержит два объекта отображения. Первый преобразует номер ошибок в объекты, назначение которых — скорректировать некоторые ситуации сьо (условия). Второй преобразует номера ошибок в объекты, которые возвращают с\ тем)' в стабильное или хотя бы частично стабильное состояние. Упрощенный ком нент обработки ошибок, показанный на рис. 7.3, можно применить к ПО любого р мера и формы. Характер использования компонентов обработки ошибок и исклю тельных ситуаций определяется требуемой степенью надежности ПО.
7.5. Надежность ПО: простой план 255 7 5 Надежность ПО: простой план иМ что мы различаем ошибочные и неудобные (нежелательные) условия. * ые или нежелательные условия должны обрабатываться обычной программ- «Д икой. Ошибки (дефекты) требуют специального программирования. В книге НОИ - TDvnaifowK программирования C++ (1997) автор приводит четыре основных аль- ^^ ' нЫХ действия, которые может предпринять программа при обнаружении ТС^ 6 -и По мнению Страуструпа, программа, выявив проблему, которую невозможно °* ботать логически, должна реализовать один из следующих вариантов поведения. Вариант 1. Завершить программу. Вариант 2. Возвратить значение, обозначающее "ошибку". Ваоиант 3. Возвратить значение, обозначающее нормальное завершение, и оставить программу в состоянии с необработанной ошибкой. Вариант 4. Вызвать функцию, предназначенную для вызова в случае ошибки. Эти четыре альтернативы можно "примерить" к отношениям типа "изготовитель- потребитель". Изготовитель— это обычно некоторый участок программного кода, который реализует библиотечную функцию, класс, библиотеку классов или оболочку приложения. В качестве потребителя можно представить участок программного кода, который вызывает библиотечную функцию, класс, библиотеку классов или оболочку приложения. Потребитель делает запрос. Изготовитель при попытке выполнить запрос обнаруживает ошибку, и его дальнейшее поведение должно быть направлено на реализацию одного из перечисленных выше четырех альтернативных вариантов. Однако проблема состоит в том, что ни один из них не универсален. Очевидно, что завершать программу при каждом обнаружении ошибки попросту неприемлемо. Здесь мы согласны со Страуструпом. В таких случаях следует поступать более изобретательно. Что касается варианта 2, то примитивный возврат значения ошибки действительно может помочь в некоторых ситуациях, но далеко не во всех. Не каждое возвращаемое значение может интерпретироваться как успешное или неудачное. Например, если значение, возвращаемое некоторой функцией, имеет веще- венный тип, и область определения функции включает как отрицательные, так и ожительные значения, то какое тогда значение функции можно использовать для ^редставления ошибки? Другими словами, это не всегда возможно. С нашей точки V я, вариант 3 также неприемлем. Ведь если "изготовитель" возвращает значение, жив Чающее нормальное завершение, "потребитель" продолжит работу, предполо- тало СГО запРос ^ыл выполнен, а это может вызвать еще большие проблемы. Ос- Дении ЧаСС^ютРеть вариант 4. Он требует более внимательного подхода при обсуж- раоотки как ошибок, так и исключительных ситуаций. • План А: модель возобновления, план Б: модель завершения Нь*х плана ении ошибки или исключительной ситуации существует два основ- Вать Услови РеализаЦии варианта 4. Первый план состоит в попытке скорректиро- ^рой была *-.°тоРЬ1е вызвали сбой, а затем возобновить выполнение с точки, в ко- аружена ошибка или исключительная ситуация. Этот подход называ-
256 Глава 7. Обработка ошибок, исключительных ситуаций и надежность.. ется возобновлением. Второй план состоит в признании (подтверждении) ошибки исключительной ситуации и постепенном выходе из подсистемы или подпрограм И в которой возникла проблема. Постепенный выход реализуется путем закрытия с * ветствующих файлов, разрушения требуемых объектов, регистрации (если это можно) ошибки, освобождения соответствующей памяти и обработки устройг которые этого требуют. Такой подход называется завершением, и его не следует пута с понятием резкого выхода из программы. Оба плана вполне действенны и в разл ных ситуациях оказываются весьма полезными. Прежде чем обсуждать способы п лизации моделей возобновления и завершения, имеет смысл рассмотреть средств обработки ошибок и исключительных ситуаций, которые предусмотрены в языке C++ 7.6. Использование объектов отображения для обработки ошибок Компонент отображения (тар) можно использовать как составную часть стратегии обработки ошибок или обработки исключений. Назначение отображения — связать один элемент с другим. Например, отображение можно использовать для связи номеров ошибок с их описаниями: //... map<int,string> ErrorTable; 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);
7.6. Использование объектов отображения для обработки ошибок 257 //■■• >; Т перь мы можем внести в отображение объекты типа def ect_response: ^'<int,defect_reponse *> ErrorTable; ^a?ect_response * Response; onse = new defect_response; ErrorTable [123] = Response; //■•• Этот код связывает объект отклика (на ошибку) с номером ошибки 123. Благодаря лиморфизму объект отображения может содержать указатели на любой объект типа defect_response или любой объект, который выведен из него. Предположим, что у нас есть следующий класс: class exception_response : public defect_response{ //..- public: int doSomething (void) }; Этот класс exception_response является потомком класса def ect_response, поэтом)' мы можем внести в объект ErrorTable указатели на тип exception_response. //... map<int/defect_reponse *> ErrorTable; defect_response * Response; exception_response *Response2,- Response = new defect_response; Respone2 = new exception_response; ErrorTable[123] = Response; // Хранит объект типа // defect_response. ErrorTable[456] = Response2; // Хранит объект типа . // exception_response. To определение означает, что объект типа ErrorTable может связывать с соот- твуклцим номером ошибки различные объекты (с различными описаниями и ха- Р ^РИстиками)- Следовательно, при вызове метода doSomething () объект ^Solver будет выполнять различные наборы инструкций: defect problo Sponse *Pr°blemSolver; Probl!moOVler = ErrorTable [ 123 ] ; ProblenIo0lver~>doSomethin9 <> ' Pr°bb!cer = ErrorTable [ 456 ] ; //... Sovler->doSomething () ; Несм°тря Ря defect a To что переменная ProblemSolver представляет собой указатель на объект Па ех~~ esPonse, полиморфизм позволяет этой переменной указывать на объект ти- ^efect n—response или любой другой объект, выведенный из класса ^efect^ esP°nse. Поскольку метод doSomething () объявлен виртуальным в классе -"* sPonse, компилятор может выполнить динамическое связывание. Это дает
258 Глава 7. Обработка ошибок, исключительных ситуаций и надежность. гарантию корректного вызова метода doSomething () при выполнении приложен Именно динамическое связывание позволяет каждому потомку ^ Н' def ect_response определить собственный метод doSomething (). Нам нужно, чток* вызов метода doSomething () зависел от того, ссылка на какой именно потомок кла ^ def ect_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 () могут выполниться неудачно. Поэтому ре" альный код будет, конечно же, несколько сложнее, чем тот, что приведен в листинге 7.1. Но все же этот пример ясно показывает, как одним "ударом" обработать мно жество ситуаций сбоя. Мы можем пойти еще дальше. В листинге 7.1 предполагаете , что все возможные ошибки будут охвачены объектами типа ErrorTable. Ь ErrorTable-объекты представляют собой либо объекты типа def ect_response, •> во объекты, выведенные из класса defect_response. А что, если у нас будет сколько семейств классов обработки ошибок? В листинге 7.2 показано, как с помоШ шаблонов сделать функцию importantOperation () более общей. // Листинг 7.2. Использование шаблона в функции // importantOperation() template<class T,class U> int importantOperation(void) { T ErrorTable; //. . .
7.7. Механизмы обработки исключительных ситуаций в C++ 259 v *solver; ^iver - ErrorTable[Result]; ^lver->doSomething () ; //-■■ }; листинге 7.2 тип ErrorTable не ограничен объектами класса t response. Этот метод позволяет упростить код обработки ошибок и повы- го" гибкость. Здесь демонстрируется использование полиморфизма как по вер- так и по горизонтали, что чрезвычайно важно для SPMD- и MPMD-программ. %с упростить программы, реализующие параллелизм с помощью шаблонов и поли- Аизма, описано в главе 9. Использование объектов отображения и объектов об- ботки ошибок— это важная составляющая повышения надежности ПО. Помимо методов обработки ошибок, мы можем также воспользоваться преимуществами механизма обработки исключительных ситуаций и классов исключений, предусмотренных в C++ (этому посвящен следующий раздел). 7.7. Механизмы обработки исключительных ситуаций в C++ В идеале во время тестирования и отладки должны быть ликвидированы все дефекты программы или по крайней мере максимально возможное их количество. Кроме того, следует обработать нежелательные и неудобные условия с использованием обычной программной логики. После устранения всех (или почти всех) дефектов и обработки нежелательных и неудобных условий все остальные "неприятности" попадают в разряд исключительных ситуаций. Обработка исключительных ситуаций в C++ поддерживается с помощью трех ключевых слов: try, throw и catch. Любой код, сталкивающийся с исключительной ситуацией, с которой он не в силах справиться самостоятельно, генерирует исключение "в надежде" на то, что с ней совладает некоторый другой обработчик (расположенный где-то в другом месте программы) (Б. Страуструп, Язык программирования C++, 1997). Для генерирования объекта некоторого специального типа (типа сключения) используется ключевое слово throw. При этом происходит передача Управления обработчику исключения, который предназначен для обработки объектов ного типа. Для идентификации обработчиков, предназначенных для перехвата ектов исключений, используется ключевое слово catch. Рассмотрим пример. ^°ld imP°rtantOperation Возникает исключительная ситуация. } Possible_condition ImpossibleCondition; ^nrow ImpossibleCondition; {atCh <^Possible кие действий, связанный с объектом Е. { vimP°ssible_condition &E) 11 Выполнена
260 Глава 7. Обработка ошибок, исключительных ситуаций и надежность. } Функция importantOperation () пытается выполнить свою работу и сталкив ся с необычными условиями, с которыми она не в состоянии справиться. В нап примере она создает объект типа impossible_condition и использует ключей слово throw для генерирования этого объекта. Блок кода, в котором использует ключевое слово catch, предназначен для перехвата объектов ТИп impossible_condition. Этот блок кода называется обработчиком исключений. Обг> ботчики исключений связаны с блоками кода, помещенными в try-выражения Н значение try-блоков— обозначить область, в которой возможно возникновение исключительной ситуации. Блок catch должен сразу же следовать за соответствующим try-блоком или другим catch-блоком. Вот пример: try{ //... importantOperation() //. . . } catch(impossible_condition &E) { // Выполнение действий, связанных с объектом Е. //. . . } Здесь при выполнении функции importantOperation () возможно возникновение условий, с которыми она не в состоянии справиться. В этом случае функция сгенерирует исключение, в результате чего управление будет передано первому обработчику, который принимает объект исключений типа impossible_cond.ition. Этот обработчик либо сам справится с этой исключительной ситуацией, либо сгенерирует исключение, с которым придется иметь дело другому обработчику исключений. Объекты, генерируемые при исключительных ситуациях, могут быть определены пользователем, причем они могут просто содержать коды ошибок или сообщения об ошибках, которые способны помочь обработчику исключений выполнить его работу. Если бы мы использовали объекты, подобные объектам типа exception_response из листингов /•! и 7.2, то обработчик исключений мог бы применить их для решения проблемы либо для восстановления работоспособного состояния программы. Для создания объектов исключений можно также использовать встроенные С++-классы исключений. 7.7.1. Классы исключений Стандартная библиотека классов C++ содержит девять классов исключений, раЗД ленных на две основные группы (группа динамических ошибок и группа логическ ошибок), которые приведены в табл. 7.3. Группа динамических ошибок представл ошибки, которые трудно предотвратить. В группу логических ошибок входят оши которые "теоретически предотвратимы".
7.7. Механизмы обработки исключительных ситуаций в C++ 261 *—Г*"!^а7^3. Классы динамических и логических ошибок ~~Кассы динамических ошибок Классы логических ошибок error range-' underflow_error overflow-error domain_error invalid_argument length_error out_o f_range 7 7.1.1. Классы runtime_error На рис. 7.4 показана схема отношений между классами для семейства классов time_error. Зто семейство выведено из класса exception. Из класса runtime_error выведено три класса: range_error, overf low_error wunderflow_error, которые сообщают об ошибках промежуточных вычислений (об ошибках выхода за границы диапазона, переполнения и потери значимости). Потомки класса runtime_error наследуют основное поведение от своего предка, класса exception (имеется в виду метод what (), оператор присваивания operator= () и конструкторы класса обработки исключений). runtime error t I range_error -D> exception i underflow _error i overflow _error Рис. 7.4. Схема отношений между классами для семейства классов runtime_error Каждый класс обеспечивает определенный диапазон наследуемых функций, которыми программист может воспользоваться для конкретной программы. Например, ссы defect_response и exception_response, созданные в листингах 7.1 и 7.2, ЖНо вывести как из класса runtime_error, так и из класса logic_error. Но сначала 7?Н° РассмотРеть работу базовых классов исключений без специализации. В листин- показано, как можно сгенерировать объекты классов except ion и logic_error. II тинг 7.3. Генерирование объекта класса exception и объекта класса logic_error try{ ^eption X; } tnrow(X); (atCh(const exception &x) } °ut « X.whatO « endl;
262 Глава 7. Обработка ошибок, исключительных ситуаций и надежность try{ logic_error Logic("Логическая ошибка"); throw(Logic); } catch(const exception &X) { cout << X.whatO « endl; Объекты базового класса exception обладают лишь конструкторами, деструктор ми, средствами присваивания, копирования и простейшего вывода отчетной информ ции. При сбое они не способны его скорректировать. Здесь можно рассчитывать лишь на вывод сообщения об ошибке, возвращаемого методом what () классов исключений. Это сообщение будет определяться строкой, переданной конструктору для объекта класса logic_error. В листинге 7.3 переданная конструктору строка "Логическая ошибка" будет возвращена методом what () в catch-блоке и выведена в виде сообщения. 7.7.1.2. Классы logic_error Семейство классов logic_error выведено из класса exception. И в самом деле, большинство функций классов этого семейства также унаследовано от класса exception. Класс exception содержит метод what (), используемый для уведомления пользователя о возникшей ошибочной ситуации. Каждый класс logic_error- семейства содержит конструктор, используемый для привязки сообщения, специфического для данного конкретного класса. Схема отношений между классами для семейства logic_error показана на рис. 7.5. I domain_error logic_error f i invalid_ argument 4> exception i length_error i out_of_range Рис. 7.5. Схема отношений между классами для семейства классов logic_error Подобно классам семейства runtime_error эти классы также предназначены дл последующей специализации. Если пользователь не расширит их функциональное они не смогут сделать ничего, кроме как уведомить об ошибке и ее типе. Упомянут выше девять классов исключений общего назначения не обеспечивают никаких Д ствий по корректировке ситуации или обработке ошибок. 7.7.1.3. Выведение новых классов исключений Классы исключений можно использовать как есть, т.е. просто для вывода соо ний с описанием происшедших ошибок. Но в качестве метода обработки исключ ^ такой подход практически бесполезен. Просто знать о возникновении исключу - ной ситуации — не слишком большой шаг на пути повышения надежности ПО- ^ ная польза иерархии классов исключений состоит в обеспечении ими архитектур
7.7. Механизмы обработки исключительных ситуаций в C++ 263 nor для проектировщика и разработчика. Классы исключений предусматри- карты вные типы ошибок, которые разработчик может уточнить. Многие исклю- ваК>Т ые ситуации, которые возникают в среде выполнения, можно было бы отне- ЧИТС категориям, "охватываемым" семействами классов logic_error или стИ . error. В качестве примера возьмем класс runtime_error и продемонст- ГиП * м как можно "сузить" его специализацию. Класс runtime_error является по- РИР- ' j^acca exception. Специализацию класса можно определить с помощью механизма наследования. Вот пример: file access_exception : public runtime_error{ class j-j- — protected: int ErrorNumber; string DetailedExplanation; string FileName; //--- public: virtual int takeCorrectiveAction(void) string detailedExplanation(void); }; Здесь класс f ile_access_exception наследует класс runtime_error и получает специализацию путем добавления нескольких членов данных и функций-членов. В частности, добавляется метод takeCorrectiveAction (). Этот метод можно использовать в качестве вспомогательного средства, с помощью которого обработчик исключений мог бы выполнять работу по коррекции ситуации и восстановлению работоспособности программы. Объект класса f ile_access_exception "знает", как идентифицировать взаимоблокировку и как ее прекратить. Кроме того, он содержит специализированную логику, предназначенную для борьбы с вирусами, которые могут разрушить файлы, а также специальные средства на случай неожиданного прерывания процесса передачи файлов. Мы можем использовать объекты класса file_access_exception вместе со средствами генерирования, перехвата и обработки исключений, предусмотренными в языке C++. Рассмотрим пример. try{ f ileProcessingOperation () ; } {* Ch(file-access__exception &E) cerr « E.whatO « endl; E fru<< E-detailedExplanation() « endl; // Об C°rrectiveAction(); // п работчик выполняет дополнительные действия // о корректировке ситуации. Эт ^Ъекта еТ°Д позволяет создать объекты отображения ExceptionTable, подобные ИсКлюч °^°^Ражения ErrorTable из листингов 7.1 и 7.2. При этом код обработчика йогоПо И Можно упростить за счет использования вертикального и горизонталь-
264 Глава 7. Обработка ошибок, исключительных ситуаций и надежность. 7.7.1.4. Защита классов исключений от исключительных ситуаций Объекты исключений генерируются в случае, когда некоторый программный к понент сталкивается с аномалией программного или аппаратного характера. Одн следует отметить, что объекты исключений сами не должны генерировать исклю ний. Ведь если окажется, что обработка одной исключительной ситуации слищк сложна и потенциально может вызвать возникновение другой исключительн ° ситуации, то схему такой обработки необходимо пересмотреть, упростив ее везде только это возможно. Механизм обработки исключительных ситуаций неоправданн усложняется именно тогда, когда код обработчика может генерировать исключения Именно поэтому большинство методов в классах исключений содержат пустые спецификации 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 throwO; }; Обратите внимание на отсутствие аргументов в объявлениях throw () -методов. Пустые аргументы означают, что данный метод не может сгенерировать исключение. Если он попытается это сделать, во время компиляции будет выдано сообщение об ошибке. Если базовый класс не может сгенерировать исключение, то соответствующий метод в любом производном классе также не сделает этого. 7.8. Диаграммы событий, логические выражения и логические схемы Обработку исключительных ситуаций необходимо использовать в качестве "последней линии обороны", поскольку ее механизм в корне меняет естественную передачу управления в программе. Существуют схемы, которые пытаются замаскировать этот факт, но эти схемы обычно не характеризуются гибкостью, достаточной для про грамм, реализующих методы параллелизма или ра пределения. В подавляющем большинстве сит> ций, в которых есть соблазн использовать обрао чики, перехватывающие абсолютно все исклю ния, программную логику можно сделать ^>°J ошибкоустойчивой с помощью ее усовершенст вания или жесткой обработки ошибок. Для о чения идентификации компонентов системы, к рые критичны для приемлемого завершения часто используются диаграммы событий. Диагр^ * ^ _ ^ „ „ событий помогают понять, какие компоненты Рис. 7.6. Простая диаграмма событии А / о D / F /_ S = (АС + FBH S = Успешное С / В + DE) j н _У I завершение программы
7 Я Диаграммы событий, логические выражения и логические схемы 265 но не опасны (и их можно не принимать во внимание), а какие могут привести ТеНЦИ * системы. В некоторых приложениях отказ одного компонента необязательно к °ТК ' -г * отказу всей системы. Для обеспечения безотказной работы системы в тех пг" когда отказ одного компонента таки приводит к отказу системы в целом, мето- сЯ^1 f 5отки исключений можно использовать в сочетании с методами обработки ДЫ °& Пример простой диаграммы событий показан на рис. 7.6. °Ш М используем диаграммы событий для построения схемы действия обработчика ис- течьных ситуаций. На рис. 7.6 схематично изображена система, состоящая из 10 задач, помеченных буквами А, В, С, D, E, F и Н. Обратите внимание на то, что метка (обозначающая задачу) расположена над переключателем. Если пере- ючатели закрыты, компонент функционирует, в противном случае— нет. Крайняя чка слева представляет начало, а крайняя точка справа— конец выполнения. Для успешного завершения программы необходимо найти путь через действующие компоненты. Попробуем продемонстрировать, как применить эту диаграмму к нашему случаю обработки исключений. Предположим, что мы начинаем программу с выполнения задачи А. Чтобы успешно завершить программу, необходимо корректно решить обе задачи А и С. На языке диаграммы это означает, что переключатели А и С должны быть закрыты. На нашей диаграмме событий переключатели А и С находятся на одной ветви, что свидетельствует об их параллельном выполнении. Если произойдет отказ в любой из этих задач (А или С), будет сгенерировано исключение. Обработчик исключений мог бы снова начать выполнение задач А и С. Однако анализ нашей диаграммы событий показывает, что завершение всей программы будет успешным, если успешным будет выполнение либо ветви АС, либо ветви DE, либо ветви FBH. Поэтому мы проектируем наш обработчик исключений таким образом, чтобы он выполнял один из альтернативных наборов компонентов (например, DE или FBH). Наборы компонентов (AC, DE и FBH) связаны между собой отношением ИЛИ. Это значит, что к успешному завершению программы приведет успешное выполнение любого набора параллельно выполняемых компонентов. Таким образом, простая диаграмма событий (см. рис. 7.6) позволяет понять, как следует построить обработчик исключений. Выражение S = (АС + DE + FBH) часто называют логическим выражением, или булевым. Это выражение означает, что Для пребывания системы в устойчивом состоянии (т.е. ее надежной работы) необходимо успешное выполнение одной из следующих групп задач: (А и С) или (D и Е) или и в и Н). По диаграмме событий нетрудно также понять, какие комбинации отка- компонентов могут привести к отказу системы. Например, если откажут только оненты Е и F, то система успешно отработает, если при этом "не подведут" ком- . Ты ^ и С- Но если бы дали сбой компоненты A, D и Н, то систему в этом случае ' его бы не спасло от отказа. Диаграмма событий и логическое выражение — это п олезные средства для описания параллельных зависимых и независимых ком- Tvaii ° tr a Также для построения схемы действия обработчика ^едл^ НапРимер, используя диаграмму событий (см. рис. 7.6), ш подход к обработке исключений для нашего примера: try{ } Start(task A and В) исключительных си- мы можем наметить (mysterious_condition &E) {
266 Глава 7. Обработка ошибок, исключительных ситуаций и надежность try{ if (! (A ScSc B)){ start(F and В and H) } } catch (mysterious__condit ion &E){ start(D and E) } }; Этот вид стратегии призван улучшить надежность системы. Следует также отм тить, что параллельно выполняемые программные компоненты и альтернативны варианты для планирования безотказной работы системы можно отобразить с помощью традиционной логической схемы, показанной на рис. 7.7. A—f С F —| В Н —I D 2=Q Успешное функционирование системы Рис. 7.7. Логическая диаграмма, отображающая три И-схемы, объединяемые с помощью ИЛИ-схемы, для успешного завершения работы системы Итак, на рис. 7.7 показано три И-схемы, объединяемые на основе ИЛИ-отношений для получения результата S (который означает успешное завершение работы системы). Диаграмма событий (см. рис. 7.6) и логическая схема (см. рис. 7.7) — это примеры простых методов, которые можно использовать для визуализации критических путей (ветвей) и критических компонентов в некоторой части ПО. После идентификации критических путей и компонентов разработчик должен предусмотреть ответные действия, которые должна выполнить система в случае, если откажет любой из критических компонентов. Если при этом используется модель завершения, то обработчик исключений не делает попытку возобновить выполнение ПО с точки, в которой возникла исключительная ситуация. Вместо этого осуществляется выход из функции или процедуры, в которой произошло исключение, и предпринимаются действия по переводу системы в стабильное (насколько это возможно) состояние. Но если использ} ется модель возобновления, то корректируются условия, создавшие аномалию, и про грамма возобновляется с точки, в которой возникла исключительная ситуация. Важ отметить, что при реализации модели возобновления возможны определенные тр> ности. Например, предположим, что наш код содержит следующую последовате ность вложенных вызовов процедур: try{ А вызывает В В вызывает С С вызывает D D вызывает Е Е сталкивается с аномалией, с которой не может справиться
8 Диаграммы событий, логические выражения и логические схемы 267 latch (exception Q) { Vcjih в процедуре Е возникла аномалия и было сгенерировано исключение, то воз- проблема со стеком вызовов. Нужно также решить вопрос с разрушением объ- М в и проблему "подвешенных" значений, возвращаемых процедурами. Подумайте, произойдет, если процедуры С и D являются рекурсивными? Даже если мы от- оектируем условие, вызвавшее исключение в процедуре Е, то как вернуть про- а\ДОУ в состояние, в котором она пребывала непосредственно перед выбросом исключения? А ведь мы должны сохранить информацию в стеке, таблицы создания и разрушения объектов, таблицы прерываний и пр. Это потребует больших затрат и обеспечения сложного взаимодействия между вызывающими и вызываемыми сторонами. Все вышесказанное обозначило лишь поверхностный слой трудностей. Из-за сложности реализации модели возобновления и благодаря тому факту, что разработка больших систем может обойтись без нее, для C++ была выбрана модель завершения. В книге [44] Страуструп дает полное обоснование того, почему комитет ANSI в конце концов выбрал для механизма обработки исключений модель завершения. Но если, несмотря на то что модель возобновления действительно сопряжена с большими трудностями, надежность и бесперебойность ПО являются критичными факторами, то для реализации этой модели все же имеет смысл приложить соответствующие усилия. При этом стоит иметь в виду, что С++-средства обработки исключений можно использовать и для реализации модели возобновления. 7.9. Резюме Создание надежного ПО— серьезное занятие. К вопросам обработки исключительных ситуаций и исправления ошибок следует подходить с особой ответственностью. Тщательное тестирование и отладка каждого компонента ПО должны быть основными средствами защиты от программных дефектов. Обработку исключений необходимо внести в систему или подсистему ПО после того, как оно прошло этап строжайшего тестирования. Механизм генерирования исключений не следует ис- ользовать в качестве общего правила для обработки ошибок, поскольку он нарушает 1чныи ход выполнения программы. К средствам генерирования исключений сле- рибегать только после того, как будут исчерпаны все остальные меры. Про- Р < мист, который планирует проектировать более полные и полезные (с его точки я) классы исключений, должен использовать стандартные классы обработки ис- Циа, СНИИ В качестве архитектурных "дорожных карт". Стандартные классы, не спе- ^ Р°ванные с помощью наследования, могут лишь уведомлять об ошибках, ру^ СОЗДать более полезные классы исключений, которые бы обладали корректи- верш ФУнкЦиями и большей информативностью. В общем случае, как модель за- Мь1. Об ТаК И модель возобновления позволяют продолжать выполнение програм- °бнао\ ТИ Модели предлагают альтернативу простому прерыванию программы при Ситуап - ИИ ОШи^ки- Более полное рассмотрение темы обработки исключительных У ЧИи можно найти в работе [44].
РАСПРЕДЕЛЕННОЕ ОБЪЕКТНО- ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В C++ В этой главе... 8.1. Декомпозиция задачи и инкапсуляция ее решения 8.2. Доступ к объектам из других адресных пространств 8.3. Анатомия базовой CORBA-программы потребителя 8.4. Анатомия базовой CORBA-программы изготовителя 8.5. Базовый проект CORBA-приложения 8.6. Служба имен 8.7. Подробнее об объектных адаптерах 8.8. Хранилища реализаций и интерфейсов 8.9. Простые распределенные Web-службы, использующие CORBA-спецификацию 8.10. Маклерская служба 8.11. Парадигма "клиент-сервер" 8.12. Резюме
/*1 Sl/\ZJ^JZJ Итак, основное различие между человеком и андроидом состоит в том, что человек приходит со своим собственным "я", чего нельзя сказать о роботе. — Кэриде Бессонет (Сагу G deBessonet), Towards A Sentential 'Reality' for the Android Распределенные объекты — это объекты, которые являются частью одного приложения, но размещены в различных адресных пространствах. Адресные пространства могут относиться к одному или различным компьютерам, связанным сетью или другими средствами коммуникации. Объекты, включенные в приложение, могли пъ изначально нацелены на совместную работу или разрабатываться различными от- лами, подразделениями, компаниями или организациями в различное время и с раз- и Целями. В категорию распределенных объектно-ориентированных приложений гут попадать приложения из большого диапазона: от одноразовой собранной совме- u * и усилиями коллекции несвязанных объектов до мульти-приложения, F овозрастные" объекты которого разбросаны по сети Internet. Местоположение п ов м°жет быть самым разнообразным: intranet (корпоративная локальная сеть енной надежности с ограниченным доступом, использующая сетевые стандарты (э евые программно-аппаратные средства, аналогичные Internet), extranet ВУК>1 сеть— объединение корпоративных сетей различных компаний, взаимодейст- Пр ДР}Т с Другом через Internet) и Internet. Согласно большинству описаний рас- Иапп Нных объектов, они (объекты) могут быть реализованы в различных языках, Р°Лей u^ ' Java» Eiffel и Smalltalk. Распределенные объекты могут играть множество СТв^ • одних ситуациях такой объект (или коллекция объектов) используется в каче- " ера, который способен обеспечить услуги, например, по доступу к базе данных,
270 Глава 8. Распределенное объектно-ориентированное программирован^ е.. по обработке данных или их передаче. В других ситуациях объекты играют роль кли тов. Распределенные объекты можно использовать в таких объединенных моделях шения проблем, как "классная доска" и мультиагентные системы. Помимо объедин ных моделей, распределенные объекты могут быть полезны для реализации таких па дигм параллельного программирования, как SPMD и MPMD. Объектам одно приложения не требуется специального протокола для связи между собой. Коммуникл ция достигается за счет обычного вызова методов, передачи параметров и использов ния глобальных переменных. Но, поскольку распределенные объекты расположень в различных адресных пространствах, здесь не обойтись без методов межпроцессного взаимодействия и во многих случаях необходимо сетевое программирование. Необходимость создания распределенных приложений обусловлена разными причинами. • Необходимые ресурсы (например, базы данных, специализированные процессоры, модемы, принтеры и т.п.) расположены на различных компьютерах. Клиентские объекты (объекты, формирующие запрос на обслуживание) взаимодействуют с серверными объектами (объектами, реагирующими на запрос обслуживания) для получения доступа к этим ресурсам. • Для выполнения некоторой важной работы или решения насущной проблемы необходимо скооперировать объекты, различающиеся временем разработки, разработчиками и местоположением. • Агенты, реализованные как объекты, отличающиеся узкой специализацией, требуют собственных адресных пространств, поскольку они запускаются как отдельные процессы. • Объекты используются в качестве базовых модулей, которые реализованы как отдельные программы, каждая из которых имеет собственное адресное пространство. • Объекты реализованы в SPMD- или MPMD-архитектуре, рассчитанной на использование параллельного программирования, причем эти объекты расположены в различных процессах и на различных компьютерах. В объектно-ориентированном приложении выполняемая программой работа делится между несколькими объектами. Эти объекты представляют собой модели определенной реальной личности, реального места, предмета или идеи. Выполнение объектно-ориентированной программы вынуждает ее объекты взаимодействовать между собой в соответствии с правилами, заложенными в этой модели. В распределенном объектно-ориентированном приложении некоторые взаимодействующие объекты будут создаваться различными программами, которые, возможно, выполняются на различных компьютерах. В главе 3 упоминалось о том, что каждая выполняемая про грамма включает один или несколько процессов. Каждый процесс обладает собстве ными ресурсами. Например, любой процесс имеет собственную память, дескриптор файлов, стековое пространство, идентификатор и т.п. Задачи, выполняемые в одно* процессе, не имеют прямого доступа к ресурсам, принадлежащим другому процесс, • Если задачам, выполняемым в одном процессе, необходима информация, храним в памяти другого процесса, то эти два процесса должны явно обменяться информаи ей с помощью файлов, каналов, общей памяти, переменных среды или сокетов. Ооъ ты, которые принадлежат различным процессам и нуждаются во взаимодействии * жду собой, также должны использовать один из перечисленных выше способов явно
8.1. Декомпозиция задачи и инкапсуляция ее решения 271 формацией. Как правило, С++-разработчик при разработке распределенно- *; кТНо-ориентированного приложения сталкивается с необходимостью реше- Нйя°с^едук>щих проблем. Декомпозиция задачи и ее решения на множество объектов, причем некоторые из них будут принадлежать различным процессам и размещаться на разных компьютерах. Обеспечение связи между объектами, принадлежащими различным процессам (адресным пространствам). • Синхронизация взаимодействия между локальными и удаленными объектами. • Обработка ошибок и исключений в распределенной среде. 8.1. Декомпозиция задачи и инкапсуляция ее решения Проектирование объектно-ориентированного программного обеспечения — это процесс перевода требований к ПО в проект, в котором с помощью объектов моделируется каждый аспект разрабатываемой системы и выполняемой ею работы. Центральное место в этом проекте отводится структуре и иерархии коллекций объектов, а также их взаимоотношениям и взаимодействиям. Для поддержки понятия модели ПО в C++ используется ключевое слово class. Существует два базовых типа моделей. Первый тип модели — масштабированное представление некоторого процесса, концепции или идеи. Этот тип модели используется для анализа или экспериментирования. Например, класс применяется для разработки модели молекулы, т.е. с помощью концепции С++-класса можно смоделировать гипотетическую структуру некоторого химического процесса, происходящего в молекулах. Программным путем можно затем изучить поведение молекулы при внедрении новых групп атомов. Второй тип модели ПО— воспроизведение некоторой реальной задачи, процесса или идеи. Цель этой модели — заставить некоторую часть системы ПО или приложения функционировать подобно ее "прототипу". В этом случае ПО занимает место некоторого компо- ента или некоторого физического предмета в неавтоматизированной системе. На- ример, мы можем использовать концепцию класса для моделирования калькулятора. Р корректном моделировании всех его характеристик и поведения можно создать пляр этого класса и использовать в качестве настоящего калькулятора. Про- ныи калькулятор здесь будет играть роль реального калькулятора. Таким обра- 4 ' • °Делированный нами класс может служить в качестве виртуального дублера л _ рого реального лица, места, предмета или идеи. Главное в программной моде- Ухватить суть реального предмета. Ряд n °Mno3Ulimi (decomposition) — это процесс разделения задачи и ее решения на ектам ЭДач' коллекций объектов и принципов взаимоотношений между этими объ- СТик ' Анал°гично инкапсуляция (encapsulation) — это моделирование характеристик РИо>т°в и поведения некоторого лица, места, предмета или идеи с помощью •Ляютг ™^КЦ-Ии class. Такое моделирование (инкапсуляция) и декомпозиция яв- °РИент СТью этапа проектирования объектно-ориентированного ПО. Объектно- Р ванные приложения, которые содержат распределенные объекты, вносят
272 Глава 8. Распределенное объектно-ориентированное программирование в процесс проектирования дополнительный уровень сложности. С точки зп "чистого" проектирования местоположение объектов в приложении не должно ять на разработку атрибутов и характеристик этих объектов. Класс — это модель и **" ли местоположение не является частью этой модели, то даже самое "крайнее" tv> C ^ , ч г расположение объектов (экземпляров этого класса) не должно иметь значение. Одн объекты существуют не в вакууме. Они взаимодействуют и обмениваются информа ей с другими объектами. Если объекты (участники взаимодействия) расположены разных компьютерах, и, возможно, даже в различных сетях, то оказывается, что cba тор местоположения объектов необходимо с самого начала включать в процесс пп ектирования ПО. И хотя насчет того, на каком именно этапе проектирования следуе рассматривать этот фактор, специалисты существенно расходятся во мнениях, тем не менее все единодушны в том, что его необходимо рассматривать. Дело в том, что обработка ошибок и исключений при взаимодействии объектов, расположенных в различных процессах или на различных компьютерах, отличается от обработки ошибок и исключений при взаимодействии объектов одного и того же процесса. Кроме того связи и взаимодействия между объектами одного процесса реализуются совершенно не так, как при расположении объектов в различных процессах, которые могут выполняться на разных компьютерах. Это нужно иметь в виду еще на раннем этапе проектирования. В распределенном объектно-ориентированном приложении вся его работа делится между объектами и реализуется в виде функций-членов различных объектов. Объекты должны быть логически разделены согласно определенной модели декомпозиции работ (Work Breakdown Model — WBM). Они могут быть разделены согласно моделям типа "клиент-сервер", "изготовитель-потребитель", равноправных узлов, "классной доски" или мультиагентной системы. Логическая структура каждой такой модели (с особенностями распределения объектов) показана на рис. 8.1. Во всех моделях, показанных на рис. 8.1, предполагается, что объекты могут быть на одном и том же или на разных компьютерах (главное — они принадлежат разным процессам). Уже сам факт принадлежности различным процессам делает объекты распределенными.1 Все модели представляют различные подходы к распределению работы приложения между объектами. 8.1.1. Взаимодействие между распределенными объектами Если объекты относятся к одному и тому же процессу, то в качестве средств м жобъектного "общения" можно использовать механизм передачи параметров, вызовь обычных методов и использование глобальных переменных. Если объекты принад лежат различным процессам, выполняемым на одном компьютере, то средств^ коммуникации между объектами могут служить файлы, каналы, очереди с дисцип ной обслуживания "первым пришел — первым обслужен", разделяемая память, буер ры обмена или переменные среды. Если же объекты "прописаны" на различных к пьютерах, то в качестве средств связи придется использовать сокеты, вызовы уда-* ных процедур и другие типы средств сетевого программирования. При этом * должны подумать не только о том, как будут общаться объекты в распределен * приложении, но и о том, посредством чего будет реализовано это общение. Объект Мы не включаем многопоточные программы в категорию распределенных
8.1. Декомпозиция задачи и инкапсуляция ее решения 273 *и3£ОТОВИТЕЛЪ-™ЭТ£ЕБИТЕЛЬ" ^пь£1§£1 КОМПЬЮТЕР 2 Объект А Сетевая или другая связь Клиент (потребитель) Объект В МОДЕЛЬ Р_АВНОТРАВНЫХ. УЗЛОВ _ КОМПЬЮТЕР 1 Узел КОМПЬЮТЕР 2 Объект А КОМПЬЮТЕР 3 Узел Объект С Узел Объект В ШдаЛЬ ^агсн^й^оом'^угшт^аг^ КОМПЬЮТЕР 1 КОМПЬЮТЕР 3 Источники знаний АГЕНТ 1 Объект А АГЕНТ 2 Объект В [К0МПЬЮТЕР4 Источники знаний АГЕНТ 3 Объект D "КЛАССНАЯ ДОСКА" УРОВЕНЬ А Частные решения УРОВЕНЬ В Частные решения ОПРЕДЕЛЕНИЕ ПРОБЛЕМЫ • специальные данные • информация • знания целевая установка (директивы) 1 г Источники знаний АГЕНТ 4 Объект С Объекты могут играть К роль потребителя, *—-^ изготовителя или обоих участников одновременно. Они взаимодействуют и сотрудничают при решении определенной проблемы Агенты или источники [\^ знаний взаимодействуют посредством "классной доски' п°требит „гическая структура и распределение объектов в моделях "изготовитель- ель , равноправных узлов, "классной доски" и мультиагентнои системы
274 Глава 8. Распределенное объектно-ориентированное программировани ориентированные приложения могут включать как простые типы данных, так вольно сложные, а именно классы, определенные пользователем. Такие классы ч ^° используются для связи между объектами. Поэтому связь между распределены СТ° объектами будет обеспечиваться не только с помощью простых встроенных ти И данных (например int, float или double), но и посредством классовых типов °В ределенных пользователем, без которых некоторые объекты не смогут выполн свою работу. Кроме того, необходимо позаботиться о том, чтобы у одних объект была возможность вызывать методы других объектов, расположенных в других ресных пространствах. Более того, необходимо предусмотреть возможность для о ного объекта "знать" о методах удаленных объектов. В то время как язык C++ подле живает средства объектно-ориентированного программирования, в нем не преду, смотрено никаких встроенных средств по обеспечению связи между распределенными объектами. Он не содержит никаких встроенных методов для локализации удаленных объектов и формирования к ним запросов. Для реализации связи между распределенными объектами разработан ряд протоколов. Двумя наиболее важными из них являются ПОР (Internet Inter-ORB Protocol — протокол, определяющий передачу сообщений между сетевыми объектами по TCP/IP) и RMI (Remote Method Invocation — вызов удаленных методов). С помощью этих протоколов могут общаться объекты, расположенные практически в любом мести сети. В этой главе мы рассмотрим методы реализации распределенных объектно- ориентированных программ с использованием упомянутых протоколов и спецификации CORBA (Common Object Request Broker Architecture). Спецификация CORBA представляет собой промышленный стандарт для определения отношений, взаимодействий и связей между распределенными объектами. ПОР и GIOP — два основных протокола, с которыми работает спецификация CORBA. Эти протоколы хорошо согласуются с протоколом TCP/IP. CORBA — самый простой и наиболее гибкий способ добавления средств распределенного программирования в среду C++. Средства, предоставляемые спецификацией CORBA, реализуют поддержку двух основных моделей объектно-ориентированного параллелизма, которые мы используем в этой книге: "классная доска" и мультиагентные системы. Поскольку спецификация CORBA отражает принципы объектно-ориентированного программирования, с ее помощью можно реализовать приложения довольного широкого диапазона: от миниатюрных до очень больших. В этой книге мы используем MICO2— открытую реализацию спецификации CORBA. MICO-реализация поддерживает основные CORBA-компоненты и службы. C++ взаимодействует с MICO посредством коллекции классов и библиотек классов. Спецификация CORBA поддерживает распределенное объектно- ориентированное моделирование на каждом его уровне. 8.1.2. Синхронизация взаимодействия локальных и удаленных объектов Для синхронизации доступа к данным и ресурсам со стороны нескольких ооъ тов, принадлежащих различным процессам, но расположенных на одном компь ре, можно использовать мьютексы и семафоры, поскольку каждый процесс, х 2 Все примеры использования CORBA-компонентов в этой книге реализованы с испол нием версии MICO 2.3.3 в операционной системе SuSE Linux и версии MICO 2.3.7 в опера „ ной системе Solans 8.
8.2. Доступ к объектам из других адресных пространств 275 нный от других, все же получает доступ к системной памяти компьютера. Эту йотде Память функционально можно рассматривать как разновидность памяти, систем ^^ Между процессами. Но если процессы распределены между различными разде ' ми то следует помнить, что разные компьютеры не имеют никакой об- коМПЬ ^^ и ПОЭТОму схемы синхронизации в этом случае должны быть реализована11 .другому. Синхронизация доступа (в зависимости от используемой WBM- НЫ и) может потребовать интенсивного взаимодействия между распределенными М£ ктами. Поэтому мы расширим традиционные методы синхронизации с помощью ° ммуникационных возможностей спецификации CORBA. 8.1.3. Обработка ошибок и исключений в распределенной среде Возможно, одной из самых сложных областей обработки исключительных ситуаций или ошибок в распределенной среде считается область частичных отказов. В распределенной системе могут отказать один или несколько компонентов, в то время как другие компоненты будут функционировать в "предположении", что в системе все в полном порядке. Если такая ситуация (например, отказ одной функции) возникает в локальном приложении, т.е. когда все компоненты принадлежат одному и тому же процессу, об этом нетрудно уведомить все приложение в целом. Но для распределенных приложений все обстоит иначе. На одном компьютере может отказать сетевая карта, а объекты, выполняемые на других компьютерах, могут вообще не "узнать" о том, что где-то в системе произошел отказ. Что случится, если один из объектов попытается связаться с другим объектом и вдруг окажется, что сетевые связи с ним оборвались? Если при использовании модели равноправных узлов (в которой мы формируем различные группы объектов по принципу решения различных аспектов проблемы) одна из групп откажет, то как об этом отказе "узнают" другие группы? Более того, какое поведение мы должны "навязать" системе в такой ситуации? Должен ли отказ одного компонента приводить к отказу всей системы? Если даст сбой один клиент, то должны ли мы прекратить работу сервера? А если откажет сервер, то нужно ли останавливать клиент? А что, если сервер или клиенты продемонстрируют лишь частичные отказы? Поэтому в распределенной системе, помимо "гонок" данных взаимоблокировок, мы должны также найти способы справляться с частичными от- ами компонентов. И снова-таки подчеркиваем, важно найти распределенный под- Д к С++-механизму обработки исключительных ситуаций. Для начала нас удовлетворят возможности, предоставляемые спецификацией CORBA. ц-2. Доступ к объектам из других адресных пространств вать екть1, Разделяющие одну область действия (видимости), могут взаимодейство- Объе ^ЧаЯ достУп ДРУГ к Другу по именам, псевдонимам или с помощью указателей. м°сть стУПен только в случае, если "видимо" его имя или указатель на него. Видимых vn eH ъектов определяется областью действия. C++ различает четыре основ- УРовня областей действия:
• 276 Глава 8. Распределенное объектно-ориентированное программирование блока; функции; файла; • класса. Вспомните, что блок в C++ определяется фигурными скобками { }, поэтому п сваивание значения Y переменной X в листинге 8.1 недопустимо, так как перемены Y видима только внутри блока. Функции main () неизвестно имя переменной у пределами блока, конец которого обозначен закрывающейся фигурной скобкой. // Листинг 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 A; another_object В;
8.2. Доступ к объектам из других адресных пространств 277 ^ic object *C; d - new dynamic_object; '^Something (A.doSomethingO ) ; B'Something(В.doSomething() ) ; cl>doMore (A.doSomething () ) ; //••• В листинге 8.2 объекты А и В находятся в одной области видимости, т.е. объект им для объекта А, а объект А видим для объекта В. Объект А может вызывать . циИ-члены объекта В, и наоборот. А что можно сказать об областях видимости, ели два объекта находятся на различных компьютерах? Что происходит, когда объект В создается другой программой и "получает прописку" совершенно в другом адресном пространстве? Как объект А узнает о существовании объекта В и как (что особенно важно) объект А узнает имя и интерфейс объекта В? Каким образом объект А сможет вызывать функции-члены, принадлежащие объекту В, если В — часть другой программы? В листинге 8.2 объекты А и В создаются во время компиляции, а объект С — во время выполнения. Все они являются частями одной и той же программы, обладают одной областью видимости, а их адреса принадлежат адресному пространству одного и того же процесса. Чтобы процесс мог выполнить инструкцию, ему нужно знать ее адрес. При компиляции программы, представленной в листинге 8.2, адреса объектов А и В хранятся в выполняемом файле. Следовательно, процесс, который выполняет программу из листинга 8.2, будет знать местоположение объектов А и В. Адрес объекту С присваивается во время выполнения программы, т.е. его точный адрес станет известен только тогда, когда будет вызвана функция new (). Однако указатель на объект С будет содержать адрес в пределах того же пространства, в котором размещаются объекты А и В, и, следовательно, процесс для получения доступа к объекту С воспользуется этим указателем. Таким образом, доступ к каждому объекту осуществляется на основе доступа к их адресам (прямого или косвенного). Имя переменной объекта— это просто псевдоним для его адреса. Если имя объекта попадает в рамки нашей области видимости, то мы можем получить к нем}' доступ. Проблема в том, как связать удаленный объект с нашей локальной областью видимости. Для того чтобы получить доступ к объекту D, который находится в Другом адресном пространстве, нам необходим некоторый способ ввода адреса удален- °го объекта в наш выполняющийся процесс, т.е. нужно научиться связывать уда- енныи объект с нашей локальной областью видимости. Нам требуется видимое имя, °рое бы служило псевдонимом для адреса в другом процессе, причем этот процесс « ет ВЬШОЛняться даже на другом компьютере. В некоторых случаях этот самый Р, °и компьютер может быть подключен к другой сети! Было бы весьма удобно за- Н пъ удаленный объект с помощью некоторого согласованного описания и получить с з ' ДЛЯ адРеса удаленного объекта. Имея ссылку, мы могли бы взаимодействовать Дет ООЪектом из нашей локальной области действия. Именно для таких нужд распре- °го программирования и можно использовать CORBA-реализацию. О л , ■'■ 'OR-доступ к удаленным объектам СТаНда СКТНал ссылка специального типа IOR (Interoperable Object Reference) — это ^ORBA ЫИ Ф°Рмат объектной ссылки для распределенных объектов. Каждый ъект имеет IOR-ссылку. IOR-ссылка— это дескриптор, который уникально
278 Глава 8. Распределенное объектно-ориентированное программирован Ие. идентифицирует объект. В то время как обычный указатель содержит простой м ный адрес для объекта, IOR-ссылка может содержать номер порта, имя хоста (имя пьютера в сети), объектный ключ и пр. В C++ для доступа к динамически создаваем^ объектам используется указатель. Указатель содержит информацию о том, где в пам *М компьютера расположен объект. При разыменовании указателя на объект использу И полученный адрес для доступа к членам этого объекта. Однако процесс разыменован * указателя на объект (с целью получения доступа к нему) требует больших усилий, ког * этот объект находится в другом адресном пространстве и, возможно, на другом компь тере. Указатель в этом случае должен содержать достаточно информации, чтобы coofi щить точное местоположение объекта. Если объект расположен в другой сети, указате должен содержать (прямо или косвенно) сетевой адрес, сетевой протокол, имя хоста адрес порта, объектный ключ и физический адрес. Стандартная IOR-ссылка действует как разновидность распределенного указателя на удаленный объект. Набор компонентов, содержащихся в IOR-ссылке под протоколом ПОР, показан на рис. 8.2. Понятие переносимой (portable) объектной ссылки — это важный этап на пути к достижению распределенной обработки данных. Оно позволяет использовать локальные ссылки на удаленные объекты практически везде (в Internet или intranet) и имеет важные последствия для мультиагентных систем, в которых агентам приходится перемещаться между системами и по всему пространству Internet. Стандарт IOR создает основу для мобильных объектов и распределенных агентов. После того как ваша программа получит доступ к IOR-ссылке объекта, можно использовать брокер объектных запросов (Object Request Broker— ORB) для взаимодействия с удаленным объектом посредством вызова методов, механизма передачи параметров, возврата значений и т.п. Логические компоненты IOR-ссылки: Хост Идентифицирует Internet-хост Порт Содержит номер порта TCP/IP, в котором целевой объект принимает запросы Объектный ключ Значение, которое однозначно преобразуется в конкретный объект Другие компоненты Дополнительная информация, которую можно использовать при обращениях, например для безопасности . i Рис. 8.2. Набор компонентов, содержащихся в IOR-ссылке под протоколом ПОР 8.2.2. Брокеры объектных запросов (ORB) ORB-брокер действует от имени программы. Он посылает сообщения удаленн му объекту и возвращает сообщения от него. Поведение ORB-брокера можно срав нить с посредником между локальными и удаленными объектами. ORB-брокер р шает все вопросы, связанные с маршрутизацией запроса от программы к удале ному объекту и с маршрутизацией ответа программе, принятого от удаленно объекта. Такое посредничество делает коммуникации между системами практи ски прозрачными. ORB-брокер избавляет программиста от необходимости пр граммирования сокетов между процессами, выполняющимися на различных к пьютерах. И точно так же он устраняет необходимость в программировании ка лов и очередей с FIFO-дисциплиной между процессами, выполняющимися
8.2. Доступ к объектам из других адресных пространств 279 мпьютере. Он берет на себя немалый объем сетевого программирования, °^Н° оого не обойтись при создании распределенных программ. Более того, он ^С3 пазличия между операционными системами, языками программирования СТ атными средствами. При программировании локальных объектов програм- Иа »больше не нужно беспокоиться о том, на каком языке реализованы удален- МИ бъекты, на какой платформе они выполняются и к какой сети они ?Ы писаны": Internet или локальной intranet. ORB-брокер использует IOR- ки чтобы упростить взаимодействие между компьютерами, сетями и объек- СС и Обратите внимание на то, что IOR-ссылка (см. рис. 8.2) содержит инфор- ю которая может быть использована для TCP/IP-соединений. Мы представи- лишь частичное описание IOR-компонентов, поскольку IOR-дескриптор должен быть "черным ящиком" для разработчика. ORB-брокер использует IOR-ссылки, чтобы найти объект назначения. Обнаружив объект, ORB-брокер активизирует его и передает аргументы, необходимые для вызова этого объекта. ORB-брокер ожидает завершения обслуживания запроса и возвращает вызывающему объекту ожидаемую информацию или исключение, если вызов метода оказался неудачным. Упрощенная последовательность действий, выполняемых ORB-брокером от имени локального объекта, показана на рис. 8.3. Действия, перечисленные на рис. 8.3, представляют упрощенную схему того, что делает ORB-брокер, взаимодействуя с удаленным объектом. Эти действия практически незаметны для локального объекта. Локальный объект вызывает один из методов удаленного объекта, а ORB-брокер делает "свою работу" от имени локального объекта. ORB-брокер выполняет большой объем обработки, заключенный всего лишь в нескольких строках кода. Обычно распределенное объектно-ориентированное приложение состоит по крайней мере из двух программ. Каждая программа имеет один или несколько объектов, которые взаимодействуют друг с другом, "пересекая" адресные пространства. Характер взаимодействия объектов определяется отношениями "клиент-сервер", "изготовитель-потребитель" или базируется на принципе равноправия (модель равноправных узлов). Следовательно, если У нас есть две программы, то одна будет действовать как клиент, а другая — как сервер, или одна — как изготовитель, а другая — как потребитель, либо обе они будут равноправными. В программе 8.1 реализован потребитель, который вызывает простои удаленный объект калькулятора. На примере этой программы демонстрирует- » как можно получить доступ к удаленному объекту, а также как инициализируется и используется ORB-брокер. УПРищЕННАЯ ПОСЛЕДОВАТЕЛЬНОСТЬ ДЕЙСТВИЙ ORB-БРОКЕРА ПРИ SagOBE МЕТОДА УДАЛЕННОГО ОБЪЕКТА 1- Найти vttoтт~, .ч _^ та удаленный объект. ивизировать модуль, содержащий искомый объект, если таковой еще не ■^Завизирован. З.Пе1 --рр^сдать аргументы удаленному объекту. —ожидать 1ръ -ать ответа после вызова метода удаленного объекта. Р уть локальному объекту информацию или исключение, если вызов удаленного ^■^1ода_оказался неуспешным. ^Ис. 8.3. уп ^кальмлг Роц1енная последовательность действий, выполняемых ORB-брокером от имени пого объекта
280 Глава 8. Распределенное объектно-ориентированное программирован // Программа 8.1 1 using namespace std; 2 #include "adding_machine_impl.h" 3 #include <iostream> 4 #include <fstream> 5 #include <string> 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 if stream In ("adding__machine. obj id") ; 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-брокер инициализируется. Строка 15 обеспечивает считывание из файла IOR-ссылки на объект adding_machine. Одно из прекрасных свойств IOR-ссылки состоит в том, что ее можно хранить как простую строк) и передавать другим программам. Передачу IOR-ссылки проще всего реализовать с помощью аргументов командной строки, переменных среды или файлов. 1U ссылку можно отправить по электронной почте или с помощью протокола передач файлов (File Transfer Protocol- FTP). IOR-ссылки совместно используют файловые системы, и их можно загружать с Web-страниц. Если некоторая программа имеет 1 ссылку на удаленный объект, то для доступа к нему можно использовать ORB-бро Y Другие методы связи между объектами с помощью IOR-ссылок будут рассмотр ниже в этой главе. Но для начала вполне достаточно использования файловых си * Итак, в программе 8.1 IOR-ссылка была получена путем преобразования объект ссылки в "строковую" форму (с использованием ORB-брокера удаленного кальк} ра) и записана в файл. При выполнении строки 18 локальный объект Orb преоор* . "строковую" IOR-ссылку обратно в объектную. В строке 19 эта объектная ссылк пользуется для реализации объекта adding_machine. Обратите внимание на то, при вызове методов этого объекта adding__machine выполняется соответству код удаленного калькулятора (см. строки 20, 21 и 22).
8.2. Доступ к объектам из других адресных пространств 281 **cbiT>subtractb50); machine зультат равен " « Machine->result() « endl; c°ut << овы этих методов сделаны в нашей локальной области видимости, они от- И хоТ вЫполняемому коду в другом адресном пространстве (в данном случае — да- Н°СЯ mv компьютеру). Для разработчика местоположение объекта Machine как #е к др; тает ИМеть значение. После создания (в строке 19) этот объект использу- Т любой другой объект C++. И хотя существуют весьма значительные различия СТС вызовами локальных и удаленных объектов1, объектно-ориентированное пред- МС ние тем не менее, поддерживается, и с точки зрения объектно-ориентирован- С программирования удаленные объекты ведут себя как локальные. Код, представ- ный в программе 8.1, является кодом клиентской части приложения (или кодом « тоебителя"), поскольку в нем используются возможности объекта adding__machine. Поэтому теперь (для получения завершенного приложения калькулятора) нам нужен код "ответной части", который реализует объект adding_machine. Код этого второго компонента представлен в программе 8.2. // Программа 8.2 1 #include <iostream> 2 #include <fstream> 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; CORBA: :String_var Ref = Orb->object_to_string ( <> * AddingMachine) ; ofstream Out("adding_machine.objidM); t\ °ut « Ref « endl; \* Out.closeO; 18 ^°^">imPl-is_ready (CORBA: : ImplementationDef : :_nil () ) ; !9 °^b->rUn(); 2 CORBA::release(AddingMachine); 21 } return(0); 22 23 3ировать к ВНимание на то, что программа-"изготовитель" также должна инициали- ^Ь1хкСОРидеКТ °ГЬ ^В стРоке Ю)- Это— одно из важных требований, предъявляе- -ориентированным программам, поскольку каждая программа реализует fewwc^ Уд«*тных объектов вносят задержку во времени, необходимость выполнять требования 03можность возникновения частичных отказов.
282 Глава 8. Распределенное объектно-ориентированное программирован! 1Ие. взаимодействие с удаленными объектами с помощью ORB-брокера. Именно поэ инициализация ORB-объекта— первое действие, которое должна выполнить CORu^ программа. В строке 12 объявляется реальный объект adding_machine. Это им тот объект, с которым в действительности связывается программа 8.1. В строк i° объектная ссылка на реальный объект adding_machine преобразуется в "строкову » форму, а затем записывается в обычный текстовый файл, чтобы ее можно было ft труда прочитать. После того как IOR-ссылка записана в файл, объект Orb ожидает проса. При каждом вызове одного из его методов этот объект выполняет соответ вующее арифметическое действие (сложение или вычитание). Значение результата передается посредством вызова метода result () объекта adding_machine. Программы 8.1 и 8.2 демонстрируют базовую структуру, которую должны иметь CORBA- программы. Код, создающий объект adding_machine, начинается с объявления его CORBA-класса. Каждый CORBA-объект начинается как IDL-проект (Interface Definition Language — язык описания интерфейсов). 8.2.3. Язык описания интерфейсов (IDL): более "пристальный" взгляд на CORBA-объекты Язык описания интерфейсов (IDL) — стандартный язык объектно- ориентированного проектирования, который используется для разработки классов, предназначенных для распределенного программирования. Он применяется для отображения интерфейса класса и отношений между классами, а также для определения прототипов функций-членов, типов параметров и типов значений, возвращаемых функциями. Одно из основных назначений языка IDL — отделить интерфейс класса от его реализации. Но для определения самих функций-членов и членов данных IDL не используется. Язык IDL определяет только интерфейс функции. Основные ключевые слова IDL перечислены в табл. 8.1. Таблица 8.1. abstract any attribute boolean case char const cell double exception Ключевые слова IDL enum factory FALSE fixed float in inout interface long module native Object octet oneway out raises readonly sequence short string struct supports typedef unsigned union ValueBase valuetype void wchar ___— Ключевые слова, перечисленные в табл. 8.1, представляют собой зарезервироь^ ные слова, используемые в CORBA-программах. Помимо определения интерфе1 функций для класса, язык IDL используется для определения отношений между &1 сами. IDL поддерживает:
8.2. Доступ к объектам из других адресных пространств 283 ипы, определенные пользователем; следовательности, определенные пользователем; • типы массивов; • рекурсивные типы; • семантику исключений; • модули (по аналогии с пространствами имен); • единичное и множественное наследование; • поразрядные арифметические операторы. Приведем 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-определения, должны иметь . idl-расширения. Прежде чем такой файл можно будет использовать, его необходимо преобразовать к С++-формату. Это преобразование можно выполнить с помощью препроцессора или отдельной программы. Все CORBA-реализации включают IDL-компиляторы. Существуют IDL-компиляторы для языков С, Smalltalk, C++, Java и др. IDL-компилятор преобразует IDL-определения в код соответствующего языка. В данном случае IDL-компилятор преобразует объявление интерфейса в легитимный С++- код. В зависимости от конкретной CORBA-реализации IDL-компилятор вызывается с использованием синтаксиса, который будет подобен следующему: ^1 adding_machine. idl ри выполнении этой команды создается файл, содержащий С++-код. Поскольку наше IDL-определение хранится в файле adding_machine. idl, MICO IDL-компилятор ст файл adding_machine. h, который будет содержать несколько каркасных С++- СОВ и CORBA-типов данных. Базовые IDL-типы данных приведены в табл. 8.2. ^f^jnua 8.2. Базовые IDL-типы данных L-rrvunu данных Диапазон Размер > 32 бит -2,5-2,5-1 > 16 бит 0-2v-l > 32 бит
284 Глава 8. Распределенное объектно-ориентированное программирование IDL-типы данных Диапазон unsigned short float double char string boolean octet any Размер 0-2,6-l IEEE с обычной точностью IEEE с двойной точностью ISO латинский-1 ISO латинский-1, за исключением ASCII NULL TRUE ИЛИ FALSE 0-255 Произвольный тип, идентифицируемый динами- Переменный Даже после того как IDL-компилятор создаст из определения интерфейса С++-код реализация методов интерфейсного класса остается все еще неопределенной. IDL- компилятор создает несколько С++-конструкций, которые предназначены для использования в качестве базовых классов. В листинге 8.3 показано два класса, сгенерированных MICO IDL-компилятором на основе файла adding_machine . 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<CORBA::Narrow_proto> *_narrow_helpers; static bool _narrow_helper2( CORBA::Object_ptr obj ); virtual void add( CORBA::ULong X ) =0;
8.2. Доступ к объектам из других адресных пространств 285 1 void subtract( CORBA::ULong X ) = 0; virtual CORBA::Long result() = 0; ^aSngimachineO O; prlVdding-machine ( const adding_machine& ); id operator=( const adding_machine& ); }; s adding_machine_stub : virtual public adding_machine{ PU 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 вместе с каркасными классами— результат его работы. Чтобы реализовать интерфейсы функций, объявленные в исходном IDL-файле, разработчик использует наследование. Например, в листинге 8.4 представлен определенный пользователем класс, который обеспечивает реализацию для одного из каркасных классов, созданных IDL-компилятором. // Листинг 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; yoid subtract (CORBA: :ULong X) Result = Result - X; CORBA: :Long result(void) return(Result); ^ add* 13 КаРкасных файлов, созданных IDL-компилятором из интерфейсного клас- У-jriachine, называется adding_machine_skel. Обратите внимание на то,
286 Глава 8. Распределенное объектно-ориентированное программировани что при выведении новых классов IDL-компилятор берет имя из определения hi фейса. Наш класс adding_machine_impl обеспечивает реализацию интерсЬ ^ функций, объявленного с использованием языка IDL. Во-первых, в ^ Са adding_machine_impl объявляется член данных Result. Во-вторых, здесь пре ^ гается реализация методов add(), subtract () и result (). В то время как инт^ фейсный класс adding_machine включает объявление этих методов, kj, ^ adding_machine_impl обеспечивает их реализацию. Определяемый пользовател С класс adding_machine_impl должен наследовать из базового класса множество фу» ций, полезных для распределенного программирования. В этом и состоит основн схема работы, связанной с CORBA-программированием. Интерфейсный класс предн значен для представления используемых интерфейсов. Назначение IDL-компилятора - сгенерировать реальные каркасные С++-классы, исходя из определения интерфейса Разработчик выводит класс из одного каркасных и обеспечивает реализацию методов определенных в интерфейсном классе, и членов данных, которые должны использоваться для хранения атрибутов объекта. Итак, создание реальных С++-классов из IDL определения представляет собой процесс, состоящий из трех действий. 1. Проектирование интерфейсов классов, отношений и иерархии с использованием языка IDL. 2. Использование IDL-компилятора для генерирования реальных каркасных С++- классов на основе IDL-классов. 3. Использование наследования для создания потомков из одного из нескольких каркасных классов и реализация методов интерфейса, унаследованных от каркасных классов. Мы рассмотрим этот процесс более детально ниже в этой главе. Но сначала познакомимся с базовой структурой программы потребителя. 8.3. Анатомия базовой CORBA-программы потребителя Одной из самых распространенных моделей для применения распределенного программирования является модель "изготовитель-потребитель". В этой модели одна программа играет роль "изготовителя", а другая — "потребителя". Изготовитель создает некоторые данные или предлагает ряд услуг, которыми пользуется потребитель (например, наша программа могла бы по требованию генерировать уникальные номерные знаки). Предположим, потребитель— это программа, которая создает запро сы на новые номерные знаки, а изготовитель — это программа, которая их генерир) ет. Обычно потребитель и изготовитель размещаются в различных адресных пр странствах. Компоненты такой программы и действия, которые должно содержа большинство CORBA-программ потребителей, представлены на рис. 8.4. Для взаимодействия с объектами, выполняемыми на других компьютерах или р положенными в других адресных пространствах, каждая программа— участи взаимодействия должна объявить ORB-объект. После этого программа-потребите может получить доступ к его функциям-членам. Как показано на рис. 8.4, ORB-ооъ инициализируется путем следующего вызова: CORBA::ORB_var Orb = CORBA::ORB_init(argc,argv, "mico-local-orb");
8.3. Анатомия базовой CORBA-программы потребителя 287 #jnC.ude-adding_machineJmpl.h" - Включение объявления для - реализации используемого объекта изготовителя II... ain(intargc,char*argvQ) ^CORBAnORB^varOrb = CORBA: :ORBjnit(argc,argv/V^^ ^^lnfadding_machine.ior"); string lor; getline(lnjor); ln.close(); ©Получение IOR-ссылки для удаленного объекта CORBA::Object_var Obj = Orb->string_to_object(lor.c_str()); radding_machine_var Machine = adding_machine::narrow(Obj); Приведение -ф IOR-ссылки к типу удаленного объекта Machine->add(5Q0) Machine->subtract(125), cout«"Result is " « Machine->result() « endl; return(O); 0 Вызов методов удаленного объ удаленного объекта } Рис. 8.4. Компоненты CORBA-программ потребителей и действия, которые они должны содержать При выполнении этой инструкции ORB-объект инициализируется. Для ORB- объектов используется тип CORBA: :ORB_var. В CORBA-реализациях объекты, тип которых помечается суффиксом _var, берут на себя заботу об освобождении базовой ссылки (в отличие от объектов, тип которых помечается суффиксом _ptr). Аргументы командной строки передаются конструктору ORB-объекта вместе с идентификатором orb___id. В данном случае идентификатором orb__id служит строка "mico- local -orb". Строка, передаваемая функции инициализации ORB_init (), зависит от конкретной CORBA-реализации. Полученный объект называют обслуживающим (servant object). После инициализации ORB-объекта и объектного адаптера разработчику CORBA- приложения необходимо позаботиться об IOR-ссылке для удаленного объекта {объектов). Как показано на рис. 8.4, IOR-ссылка считывается из файла ln9—machine. ior. IOR-ссылка была записана в этот файл в строковой форме. -объект используется для преобразования IOR-ссылки из строки снова в объект- * форму с помощью метода string_to_object (). Как показано на рис. 8.4, это алнзуется с помощью следующего вызова: ВА: :Object_var Obj = Orb->string__to_object(lor.c_str()); Qk . -ССЬ ФУНК1Дия lor. c_str () возвращает IOR-ссылку в строковой форме, а объект За Дет с°Держать IOR-ссылку в объектной форме. Объектная форма IOR-ссылки в C++ РетеРпевает процесс "сужения", который подобен операции приведения типа THnv к Рез^льтате этого процесса объектная ссылка приводится к соответствующему ''Рам екта- В данном случае "соответствующим" является тип adding__machine. Про- отребитель (см- Рис- 8.4) сужает IOR-объект, используя следующий вызов: Sumachine__var Machine = adding_machine: :_narrow(Obj ) ;
288 Глава 8. Распределенное объектно-ориентированное программирован При выполнении этой инструкции создается ссылка на объект adding_machine. Программа-потребитель может теперь вызывать методы о ИПа ленные в IDL-интерфейсе для класса adding_machine, например: ^ ^е" Machine->add(500); Machine->subtract(125); При выполнении этих инструкций вызываются методы add() и subtract () ленного объекта. Несмотря на то что рассматриваемая программа-потребитель но упрощена, она дает представление о базовых компонентах типичных CORKa программ потребителя или клиента. Однако программа-потребитель должна работ совместно с программой-изготовителем. Поэтому мы рассмотрим упрощенну CORBA-программу, которая действует как изготовитель для программы-потребител показанной на рис. 8.4. 8.4. Анатомия базовой 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) создает объект реализации в части В, используя следующий вызо adding_machine_impl *AddingMachine = new adding_machine_impl;
8.5. Базовый проект CORBA-приложения 289 glSl S&chineJmpLh- *- _ Включение объявления для реализации "используемого объекта adding_machine Инициализация —rrT^Sr^arOrb = CORBA::ORB jпit(argctargvt,•mico-local-orb,,); \+-Шк ORB-объекта С2§олЖ var Boa = Orb->BOAJnit(argc,argv,"mico-local-boa"); fw " adding„machinejmpl 'AddingMachine = new addJng_machinejmpl; * Q ^1тщ объектного адаптера CORBA::String_var lor = Orb->objectJo_string(AddingMachine); ofstreamOut(,,adding_machine.iorM); Out«lor; Out.close(); Boa->impUs~ready(CORBA::,mplementatJonDef::-nil^); Orb->run(); + экземпляра объекта Получение строковой в формы объектной IOR-ссылки и запись ее в файл _^ Ожидание запросов -|У от объектов- потребителей CORBA::release(AddingMachine); return(O); } Рис. 8.5. Основные компоненты, которые должна содержать CORBA-программа изготовителя При выполнении этой инструкции создается объект, который обеспечит реализацию услуг, потенциально запрашиваемых клиентскими объектами (или потребителями). Обратите также внимание на то, что в части С (см. рис. 8.5) программа- изготовитель использует объект Orb для преобразования IOR-ссылки в строку и записывает ее в файл adding_machine. ior. Этот файл можно передать с помощью FTP- протокола, по электронной почте, посредством протокола передачи гипертекстовых файлов (HTTP) вместе с Web-страницами, с помощью сетевой файловой системы NFS и т.д. Существуют и другие способы передачи IOR-ссылок, но файловый метод — earn простой. После записи IOR-ссылки программа-изготовитель просто ожидает за- Р сы от программ-клиентов (потребителей). Программа-изготовитель, представая на рис. 8.5, также представляет собой упрощенный вариант CORBA- Р граммы изготовителя (программы-сервера), тем не менее, она содержит все ос- !е компоненты, которые должна иметь типичная программа-изготовитель. 8-5. Базовый проект CORBA-приложения при К И3 пРогРамм> представленных на рис. 8.4 и 8.5, видно, что для CORBA- ссыл ения потребуются два ORB-объекта, объектный адаптер, метод передачи IOR- CORRa И П° кРаинеи мере один обслуживающий объект. Логическая структура А-приложения показана на рис. 8.6.
290 Глава 8. Распределенное объектно-ориентированное программиров После получения IOR-ссылки и приведения ее к соответствующему типу r ленного метода в программе клиента (потребителя) подобен вызову обычног °В ^Да~ в любой С++-программе. В CORBA-примерах этой книги предполагается исп 1еТ°Да ние протокола ПОР (Internet Inter ORB Protocol). Поэтому ORB-брокеры (см °ЛЬЗ°Ва* связываются с помощью протокола TCP/IP. IOR-ссылка должна содержать инсЬ^ ^ цию о местоположении удаленного объекта, достаточную для реализации ТГР^ связи. В качестве объектного адаптера обычно используется переносимый объе адаптер. Но для некоторых программ (более старых и простых) можно использ ^ базовый объектный адаптер. Различие между этими двумя адаптерами мы рассмо ТЬ ниже в этой главе. Каждое CORBA-приложение имеет один или несколько обсл^* вающих объектов, которые реализуют интерфейс, разработанный в IDL-классе П стейшие программы потребителя и изготовителя, показанные на рис. 8.4 и 8.5 м выполняться на одном компьютере в различных процессах или на различных комп ютерах. Если эти программы выполняются на одном компьютере, фал adding__machine. ior должен быть доступен из обеих программ. Если они выполняются на различных компьютерах, этот файл должен быть послан клиентскому компьютеру по FTP-протоколу, электронной почте, HTTP-протоколу и т.д. Детали компиляции и выполнения этих программ описаны в разделах "Профиль программы 8.1" и "Профиль программы 8.2". Проект простого CORBA-приложения Г ПРОЦЕСС А ПРОЦЕСС В Программа-потребитель ПОР Программа-изготовитель ORB Объектный адаптер {РОА или ВОА^ Реализация обслуживающего объекта . Получение IOR-ссылки из файлов, службы имен, с web-сайта, электронной почты и т.д Хранение IOR-ссылки в файлах, службах имен, на сервер6 электронной почты, web-сервере и Простое CORBA-приложение Рис. 8.6. Логическая структура CORBA-приложения
8.5. Базовый проект CORBA-приложения 291 f^Sib'программы 8.1 (цм* Программы ne_client_impl. ее ; пгюграмма представляет собой простой вариант программы-потребителя. Она связы- *^тея с CORBA-программой изготовителя, представленной на рис. 8.5. Она вы- ^пняет сложение с числом 500 с помощью калькулятора, а затем вычитает из ■ °^л£тата число 125. Результат выполнения операций отправляется в выходной llottKbbut с помощью метода results(). Требуемые библиотеки (гфб^З или mico2.3.7. [требуемые заголовки (Нет. [инструкции по компиляции и компоновке программ ЬДх^-роа adding__machine. idl fraco|$i+ -g -с adding_machine.ee -о adding__machine. о fmiea?c++ -д -с adding_machine__impl.ee -о adding_machine_impl.о пгасот6+^ -д -с adding_machine_client__impl.ee -о radd^^_machine_client_impl. о ш^Щ4'-д -о adding_machine_client adding_machine_client_impl.o bddlngilmachine_impl. о adding_machine. о - lmico2 .3.3 (Среда для тестирования fSu^'Linux 7.1 gnu C++ 2.95.2, Solaris 8 Workshop 7, MICO 2.3.3, MICO 2.3.7. Р^сфукции по выполнению i П^_^Ъ ВЫГ|ОЛНяемый файл adding_machine_client (например, b^§iiigjniachine_client). CORBA-программа изготовителя с именем >aa<^^_jnachine_server (представленная на рис. 8.5) должна быть запущена первой. Примечания ;еЛ|^увРемвни| когда будет вызвана программа adding_machine_clientf CORBA- UiSEPaMMa изготовителя должна уже выполняться. ; РРФиль программы 8.2 t!lSinporpaMMbi ! n9^chine_server_impl. се Ссание ^^^еин ~Мма пРЗДставляет собой простой вариант программы-сервера, пред- ^2ЙИпое°И На РИС" 8*5" °на пРинимает запросы на выполнение сложения и вычита- ^^^^ЗР?Навляет результаты этих операций.
292 Глава 8. Распределенное объектно-ориентированное программиров s.. Требуемые библиотеки • mico2.3.3 или mico2.3.7. , Требуемые заголовки |Нет. ! Инструкции по компиляции и компоновке программ |idl —poa adding_machine. idl Imico-c++ -g -с adding_machine. ее -о adding__machine. о |mico-c++ -g -с adding_machine_.iinpl.ee -о adding_machine_impl. 0 *mico~c++ -g -с adding__inachine_server__impl.ee -о I adding_machine__server_irapl. о *mico-ld -g -о adding__machine__server adding__machine_server__impl. 0 * adding_jcaachine__impl. о adding__machine. о - lmico2 .3.3 «. | Среда для тестирования fSuSE Linux 7.1 gnu C++ 2.95.2, Solaris 8 Workshop 7, MICO 2.3.3, MICO 2.3.7 > Инструкции по выполнению l Запустить выполняемый файл adding_machine__server (например, ! . /adding_jnachine_server). j Примечания !Нет. 8.5.1. IDL-компилятор IDL-компилятор представляет собой инструмент, предназначенный для перевода IDL-определений класса в С++-код. Этот код состоит из коллекции "каркасных" определений классов, перечислимых типов и шаблонных классов. Для CORBA-программ, приведенных в этой книге, в качестве IDL-компилятора используется MICO IDL- компилятор. В табл. 8.3 перечислены опции командной строки, которые чаще всего применяются при вызове этого IDL-компилятора. Таблица 8.3. Самые распространенные опции командной строки, применяемые при вызове IDL-компилятора Опции командной строки Описание --Ьоа Генерирует "каркасные" конструкции, которые используют базовый объектный адаптер (basic object adapter— BOA)- опция используется по умолчанию - -no-boa Отключает генерирование кода "каркасных" конструкций для --роа Генерирует "каркасные" конструкции, которые используют носимый объектный адаптер (portable object adapter — г^* --no-роа Отключает генерирование кода "каркасных" конструкШ1* РОА. Эта опция используется по умолчанию
8.5. Базовый проект CORBA-приложения 293 Окончание табл. 8.3 командной строки Описание Опции "^Tncluded-def s Генерирует код, который был включен с помощью директив "9en' #include _-version Выводит версию спецификации MICO n<def ine> Определяет макрос препроцессора. Эта опция эквивалентна ключу -D у большинства UNIX С-компиляторов l<path> Определяет путь поиска для директив # include. Эта опция эквивалентна ключу -1 у большинства UNIX С-компиляторов Ключи -boa и -роа (см. табл. 8.3) позволяют определить, на какой тип адаптера будут ориентированы создаваемые "каркасные" конструкции. Например, при выполнении следующей команды idl -роа -no-boa adding_machine. idl будет получен файл adding__machine. h, который содержит "каркасные" конструкции для РОА-адаптера (portable object adapter), и будет отключено генерирование "каркасных" конструкций для ВОА-адаптера (basic object adapter). При вводе команды idl -h будет сгенерирован полный список ключей IDL-компилятора. Если в поставке MICO надлежащим образом инсталлированы man-страницы, то ввод команды man idl обеспечит полное описание доступных IDL-ключей. Проектирование IDL-классов — первый шаг в CORBA-программировании. На следующем этапе необходимо определить способ хранения и считывания IOR-ссылок на удаленные объекты. 8.5.2. Получение IOR-ссылки для удаленных объектов ORB-класс содержит две функции-члена (string_to_object О Hobject_to_ string ())% которые можно использовать для преобразования IOR-объектов из строк в объекты типа Objectjtrs и обратно. Функция-член string_to_object () придает параметр типа const char * и преобразует его в объект типа Object_ptr. ^кция-член ob3^ct_to__string() принимает параметр типа Object_ptr и преоб- ORH еГ° В Указатель типа char *. Эти методы являются составной частью интерфейса ссыл"10^^ ^етод °bjact„to_string() используется для получения объектной IOR- пРог И СТР°ковой формы. IOR-сеылку, представленную в виде строки, можно передать Раммам клиентов (потребителей) различными способами, например: Ройная почта Разделяемые файловые системы (NFS-оборудование) ротокол Встраивание в HTML-документы * еты/сервлеты Аргументы командной строки Мая память Традиционные средства межпроцесной связи (IPC), т.е. кана- Л лъи FIFO-очереди и пр. именные соел,, rr.u.,un. ™™мв „ _™,..,„ м„им„
294 Глава 8. Распределенное объектно-ориентированное программирова Затем программа приема данных получает строковый вариант IOR-ссылк пользует функцию-член string_to_object () ORB-объекта для преобразованы т ИС ссылки в CORBA-объект ptr. Этот CORBA-объект ptr затем "сужается" (т е дится к соответствующему типу) и используется для инициализации локальног °* екта. В программах 8.1 и 8.2 для передачи IOR-ссылки между програ ° ^ потребителем и программой-изготовителем используются строковые формы к °И тов и файл. Строковую форму IOR-ссылок можно использовать для обеспеч ^ очень гибкой связи с удаленными объектами, которые могут размещаться практ 'Я ски в любом месте Internet, intranet или extranet. Существует реализация MlWcn (Wireless Mico) — открытая реализация спецификации wCORBA1, стандарт бесп водной спецификации CORBA, который можно использовать для улучшения моби ности объектов. Эта беспроводная спецификация позволяет реализовать связь п средством MIOR-ссылки (Mobile IOR). Она поддерживается для TCP-, UDP- и WAP WDP-механизмов передачи (Wireless Application Protocol Wireless Datagram Protocol) Мультиагентные и распределенные агентные системы также могут воспользоваться преимуществами стандартов IOR-ссылок. IOR- и MIOR-ссылки являются частью "строительных блоков" следующего поколения объектно-ориентированных Web- служб. Важно отметить, что хотя строковые IOR-ссылки обеспечивают гибкость и переносимость, они не могут идеально подходить ко всем ситуациям и конфигурациям. Передача файла, который содержит IOR-ссылку, — не слишком приемлемое требование для многих систем. Трудно с точки зрения практичности требовать от приложений клиента и сервера разделять одну и ту же файловую систему или сеть. А с точки зрения безопасности строковый вариант IOR-ссылки вообще исключается как достойный рассмотрения. Если приложение типа "клиент-сервер" велико по объему и достаточно разнотипно, то требование разделения строковой формы IOR-ссылки может оказаться слишком ограничивающим. CORBA-спецификация включает еще два стандарта для получения или передачи объектных ссылок: службы имен и маклерские службы. 8.6. Служба имен Стандарт службы имен обеспечивает механизм преобразования имен в объектные ссылки. Автор запроса на IOR-ссылку предоставляет в службу ИхМен имя, а она возвращает ему объектную ссылку, соответствующую этому имени. Служба имен действует как разновидность телефонного справочника, в которо. по имени ищется номер. Эта служба позволяет программам клиента (потребител находить по имени объектные ссылки. Эту службу можно использовать не только Ду ^ получения IOR-ссылок, но и для других ресурсов приложения. Получение объектн ссылки по имени называется связыванием по имени (name binding). Коллекция вар! тов связывания по имени соотносится с объектом именного контекста (пап о context). Чтобы проиллюстрировать понятие именного контекста, предположим, у нас есть приложение, которое предназначено для планирования маршрутов i стоит из большого количества различных объектов. Мы можем организовать гР>^сЯ объектов в соответствии с выполняемыми ими функциями. Одни объекты отно к группе файлового ввода-вывода данных, а другие — к группе безопасности. 1 wCorba - это стандарт CORBA для беспроводного взаимодействия удаленных ооъе Материалы по стандарту wCORBA доступны по адресу: www. omg .org.
8.6. Служба имен 295 использует также объекты, которые имеют отношение к видам транс- 0рИЛ°же автобус, автомобиль и велосипед. Каждое такое группирование образует дорта: п ^ коНтекст. Например, чтобы логически сгруппировать объекты, связанные цекотор нсПОрТа, можно создать контекст транспорта и связать с ним все виды с вида такое группирование позволяет сформировать именной контекст. Снача- транспор^^ наименование каждого вида транспорта с его IOR-ссылкой. Это и есть Ла СВЯ te по имени. Затем соотносим это связывание по имени с контекстом транс- СвАЗЫ Мы используем контексты для логической организации групп связанных объ- П°Р ' коллекция связанных именных контекстов образует именной граф (naming еКТ Ы Именные контексты представляются объектами. Поскольку именной контекст изуется как объект, он может участвовать в связывании по имени подобно любо- Р ому объекту. Это означает, что именной контекст может потенциально содер- ть другие именные контексты. Например, на рис. 8.7 показано несколько контек- ов включающих логическое представление нашего контекста транспорта. Обратите внимание на то, что последняя строка в контексте транспорта представляет собой имя airborne (воздушный вид транспорта). Для имени airborne существует еще один контекст, именуемый f lying_machines (летательные аппараты). Контекст f lying_machines содержит связи с несколькими объектами (на основе выполняемых функций). Контекст transportation (транспортные средства) вместе с контекстом f lying_machines образуют именной граф. На рис. 8.7 обратите внимание на то, что последний объект в контексте f lying_machines называется sonic (звуковые). Имя sonic связано с контекстом f ast_f lying_machines. Имя sonic имеет объектную ссылку 8888. Тем самым к именному графу добавляется еще один контекст. Это — пример именного контекста, содержащего другой именной контекст. Именной граф можно использовать для представления "многоплановой" структуры взаимосвязей в распределенном объектно-ориентированном приложении. Можно сказать, что именной граф фиксирует панораму распределенного приложения. Для мультиагент- ных систем именной граф можно использовать в качестве разновидности семантической сети (см. § 8.1). Несмотря на то что объекты могут быть разбросаны по различным аппаратным платформам, операционным системам, языкам программирования и географически отдаленным компьютерам, именной граф может представлять единую логическую структуру взаимоотношений и связей между объектами. На рис. 8.8 оказано альтернативное представление именного графа, приведенного на рис. 8.7. енные контексты этих двух рисунков совпадают, и в обоих случаях отчетливо ражены взаимоотношения между именными контекстами. На рис. 8.8 также зано, что существует путь от контекста transportation к контексту _~" -*-у1пд—machines и обратно к контексту transportation. н °°хода именного графа в процессе решения распределенной задачи приме- ног Известные алгоритмы обхода графов. При этом различные пути обхода имен- ет РаФа могут представлять различные решения задачи. Служба имен обеспечива- конт 3апроса доступом к именным контекстаАм и именным графам. К именным конт B^i ДОСТУп осуществляется через именные графы, а к связям — через именные КН. р L ^вязьтание обеспечивает прямое соответствие имени и объектной ссыл- "изгот МотРим программу 8.3, в которой представлен простой вариант сцек Теля » создающего связывание по имени и соотносящего это связывание Торым именным контекстом.
296 Глава 8. Распределенное объектно-ориентированное программирова КОНТЕКСТ •ТРАНСПОРТНЫЕ СРЕДСТВА" | Имя объекта boat car train bicycle airborne Объектная ссылка 1234 5678 9876 2345 ПППП ^ "■ ► + ШЩ КОНТЕКСТ -ЛЕТАТЕЛЬНЫЕ АППАРАТЫ" Имя объекта helicopter kite balloon sonic Объектная ссылка 9331 9221 0911 8888 * mm КОНТЕКСТ "СКОРОСТНЫЕ ЛЕТАТЕЛЬНЫЕ АППАРАТЫ0 Имя 1 объекта jet shuttle starship Объектная ссылка 9898 8899 8889 Рис. 8.7. Несколько различных именных контекстов велосипед поезд / КОЙТСКСГ ПГЩНОПОРТПЫЬ / контекст "скоростные летательные- ■ аппараты" ^межзвездный космический корабль Huff I fcKCT "4^ТАТ{-.ЛЬНЫё Рис. 8.8. Альтернативное представление именного графа
8.6. Служба имен 297 „ преград 8.3 #inc lude <iostream> lude <fstream> 2 include "permutation_impl.h" 3 #V7ine MICO_CONF_IMR с Jinclude <CORBA-SMALL.h> I «include <iostream h> J Jinclude <f*tream.h> я «include <umstd.h> _ о #include <mico/CosNammg.h> 10 \l int mainlint argc, char *argv[]) 13 { 14 CORBA: :ORB_var Orb = CORBA: :ORB_imt (argcargv, "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 ObjectRef erence = 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(O); 32 } 33 34 e 8.1- Семантические сети * Нтическая сеть (semantic network) — это одна из самых старых и простых схем Ни Тавления знаний. В основе семантической сети лежит графическое изображе- с еРаРхических взаимоотношений между объектами. На рис. 8.9 показана простая "ческая сеть, которая отображает знания о транспортных средствах в целом кретных транспортных средствах в частности.
298 Глава 8 Распределенное объектно-ориентированное программировав космический chhN Рис. 8.9. Простая семантическая сеть транспортных средств Овалы в семантической сети называются узлами, а линии — связями. Связи представляют существующие отношения между узлами. Узлы используются для представления объектов и фактов (или описателей). Некоторые связи являются дефинициональны- ми, а другие могут быть вычислены. Связи можно использовать для отображения наследования или подчиненности. Узлы и связи вместе выражают некоторые порции знаний. Например, изучив семантическую сеть, представленную на рис. 8.9, мы понимаем, что F-15— это транспортное средство, а также летательный аппарат, который имеет по крайней мере два крыла. Семантические сети используются для представления знаний, необходимых в ПО принятия решений. 8.6.1. Использование службы имен и создание именных контекстов При выполнении строки 22 серверная программа получает ссылку на службу име CORBA::Object_var NameService = Orb->resolve_initial_references("NameService"); Помимо получения объектных ссылок на хранилище реализаций (ImplementaU Repository) и хранилище интерфейсов (Interface Repository), метод ORB-объеь resolve_initial_references () используется для получения ссылки на слу* имен. Получив нужную ссылку, программа-сервер создает на ее основе именной ь текст (см. строку 23): CosNaming::NamingContext_var NamingConnexn = CosNaming::NamingContext::_narrow(NameService);
8.6. Служба имен 299 подходе мы получаем начальный именной контекст, который играет При ** действующего по умолчанию. Обнаружив службу имен и создав на- роль кон нНой контекст, серверная программа может добавлять в контекст пары чальныи ^ имени) "имя/объектная ссылка". Имена могут представлять собой (связыва меНОВ или другие контексты. Чтобы добавить в контекст пару объекту ная ССылка", необходимо сначала создать имя. Имена реализуются "имя/0 /-ORBA посредством структуры NameComponent. в стандартен г truct NameComponent { istring^var id, istring^var kind; В CORBA-реализации MICO структура NameComponent объявляется в файле sNaming. h. Структура NameComponent содержит два атрибута: id и kind. Первый тоибут используется для хранения текста имени, а второй представляет собой идентификатор, который можно использовать для классификации объекта, например так. //... CosNaming: :Name Ob^ectName; ObjееtName. length (1) ; ObjectName. id = Corba : : string_dup (" train" ) ; ObjectName. kind = Corba : : string_dup (" land_transportation") ; NamingContext->bind(ObjectName, ObjectRef erence) ; //... Здесь объявляется объект типа NameComponent. Атрибут id устанавливается равным значению "train", а атрибут kind— значению land_transportation. Очевидно, атрибут id должен быть описателем (дескриптором) объекта. Атрибут kind можно использовать для описания контекста или логической группы, к которой принадлежит этот объект. В данном случае он классифицирует поезд (train) как объект land__transportation (наземный вид транспорта). Метод bind () преобразует имя объекта ObjectName в объектную ссылку Obj ectRef erence и связывает ее с начальным именным контекстом. Имя может состоять из нескольких объектов типа NameComponent. Ьсли имя состоит только из одного объекта типа NameComponent, оно называется про- япъиц, а если из нескольких — составным. Если имя составное, то атрибут kind можно ис- ользовать для описания отношения (этот метод рассматривается в главе 12). В программа объект связывается с объектной ссылкой, которая соотносится с именованным кон- стом. После связывания с именным контекстом объект клиента может получить доступ <>нтексту посредством службы имен. В программах 8.1 и 8.2 для связи (посредством ли А ?ВОИ ЮК'Ссылки) между программами потребителя и изготовителя мы использова- 4* ил. А для связи клиента и сервера (см. программу 8.3) используется служба имен. л тали инсталляции и функционирования службы имен зависят от конкретной реа- oJtVy/t И* ЧЭеДа MICO включает программу nsd, которая реализует COS-совместимую Heofi ИМен- Прежде чем служба имен будет доступной для программы-потребителя, р^ мо Запустить демон mi cod и внести соответствующие элементы в хранилище к с аЧии. Чтобы узнать, как пользоваться программами nsd, micod и imr, обратитесь При етствующей документации и руководству по MICO (оно содержит множество Фрагл* Испш1Ьзования программ imr, nsd, micod и ird). В листинге 8.5 приведен Ляю, о Т Из сц«енария, используемого для настройки сервера в программе 8.3, позво- и сделать службу имен доступной для программы-потребителя.
300 Глава 8. Распределенное объектно-ориентированное программирован // Листинг 8.5. Сценарий внесения записи в хранилище // реализаций и запуска службы имен // mi cod -ORBIIOPAddr inet:hostname:рогtnumber —forward & imr create NameService poa "which nscT 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:hos tname:portnumber Этот сценарий можно использовать в сочетании с кодом сервера, приведенным в программе 8.3. Приведенный здесь сценарий реально позволяет автоматически запустить программу-сервер permutation_server. Обратите внимание на то, что имена hostname и portnumber в программе 8.5 необходимо заменить реальным именем компьютера, на котором выполняется сервер, и номером порта соответственно. 8.6.2. Служба имен "потребитель-клиент" Программа 8.3 связывает имя объекта с именным контекстом. Программа 8.4 содержит текст программы-потребителя, которая использует службу имен для доступа к связкам "объект-ссылка", которые были созданы в программе 8.3. Программа 8.3 генерирует перестановки любой строки символов, которую она получает. Для перестановки изменяется местоположение символов в строке. Например, эти строки Objcte JbOetc tbOjec Оjbeet JObetc Оjbeet JtObec представляют собой перестановки строки Object. Клиент передает серверу строк)' и сервер генерирует N перестановок. Сервер связывает имя "Inflection" с именным контекстом. Именно это имя программа-клиент должна задать, чтобы получит объектную ссылку из именного контекста. // Программа 8.4 1 int main(int argc, char *argv[]) 2 { 3 4 try{ 5 CORBA::ORB_var Orb = CORBA::0RB_init (argcargv,"mico-local-orb"); 6 object_reference Remote("NameService",Orb); 7 Remote.objectName("Inflection"); 8 permutation__var Client = permutation::_narrow(Remote.objectReference());
8.6. Служба имен 301 char Value[1000]; 9 strcpy(Value,"Common Object Request Broker"); 1° client->original(Value); 1J int N; 12 for(N = 0;N < 15;N++) 13 { cout << "Значение функции nextPermutation(] 15 << ciient->nextPermutation() « endl; 17 ) } catch(CosNaming::NamingContext::NotFound_catch &exc) { *8 cerr << " Исключение: объект не обнаружен." « endl; 9i catch (CosNaming: :NamingContext: : InvalidName_catch &exc) { „ cerr << "Исключение: некорректное имя." « endl; 23 > 24 25 return(0) ; 26 } Для доступа к соответствующему объекту именного контекста в программе- потребителе необходимо выполнить следующие три действия. 1. Получить ссылку на службу имен. 2. С помощью службы имен получить ссылку на соответствующий именной контекст. 3. С помощью именного контекста получить ссылку на соответствующий объект. Действие 1 реализуется путем вызова метода resolve_initial_ref erences (): CORBA: :Object_var NameService; NameService = Orb->resolve_initial_references ("NameService") ; Функция resolve_initial_ref erences () возвратит объектную ссылку на службу имен. В действии 2 эта ссылка используется для получения объектной ссылки на именной контекст: CosNaming: :NamingContext_var NameContext; ameContext = CosNaming: :NamingContext: :_narrow (NameService) ; Действии З значение объектной ссылки NameService "сужается", т.е. приводит- ^ оответствующему типу, в результате чего получается объектная ссылка на имен- По *ОНТекст NameContext. С помощью объекта NameContext программа- Ис Реоитель может вызвать его метод resolve(). Строки 24-27 из программы 8.3 злются для построения имени, которое и будет передано методу resolve (). Sen?™ {1)<- Naine^-'ld = C0RBA: :string_dup ("Inflection"); try |bj'klnd = CORBA::string_dup (""); 3ectReference = NameContext->resolve ( Name)
302 Глава 8. Распределенное объектно-ориентированное программирован!/! Метод resolve () возвращает объектную ссылку, связанную с заданным и объекта. В данном случае задано имя "Inflection". Обратите внимание на т Н^М такое же имя связывается с именным контекстом в программе 8.3 (строка 28) v T° программа-потребитель имеет объектную ссылку, она может ее "сузить", а затем ^ помощью получить доступ к удаленному объекту. Процесс получения объектной г ^ г- ССЫЛКИ на удаленный объект вполне тривиален, и поэтому имеет смысл его упростит тем инкапсуляции соответствующих компонентов в классе. • 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); //- -. } Программа 8.4 использует преимущества простого каркасного класса obj ect_ref erence, который мы создали с этой целью. В программе 8.4 (строка 6) обратите внимание на создание объекта Remote типа obj ect_ref erence. В строке 8 этот объект используется для получения ссылки на удаленный объект с помощью следующего вызова метода: Remote.obj ectReference(); После этого программа-потребитель получает доступ к удаленному объекту. Класс obj ect_ref erence обеспечивает выполнение некоторых необходимых действий и тем самым упрощает написание программы-потребителя. Конструктор класса obj ect_ref erence (он вызывается в строке 6 программы 8.4) реализован следующим образом. object_reference::objееt_reference(char *Service, CORBA::ORB_var Orb) { NameService = Orb->resolve_initial_references (Service); NameContext = CosNaming::NamingContext::_narrow ( NameService); } Этот конструктор получает ссылку на службу имен и создает объект клас NameContext. В строке 7 имя этого объекта передается методу objectName (). ^аТ \ для получения объектной ссылки, связанной с именем объекта, используется имени контекст. Метод obj ectName () реализован следующим образом. void objееt_reference::objectName(char *OName) { Name.length (1) ; Name[0].id = CORBA::string_dup (OName); Name[0].kind = CORBA::string_dup (""); try {
8.7. Подробнее об объектных адаптерах 303 ObiectReference = NameContext->resolve (Name); Ltch(. ■ •) * rr « " Problem resolving Name " « endl; throw; } } После вызова метода objectNameO программа-потребитель получает доступ ылке на удаленный объект. Теперь остается лишь вызвать метод b'ectReference() (это реализуется в строке 8 программы 8.4). В методе biectName () основную часть работы выполняет функция resolve (). Программы 8.3 8 4 образуют простое распределенное приложение "клиент-сервер", которое для доступа к объектным ссылкам вместо строковой формы IOR-ссылок использует службу имен. В сетях intranet или Internet можно использовать оба подхода. Эти же варианты применяются в качестве опорных структурных компонентов в контексте новой модели Web-служб. 8.7. Подробнее об объектных адаптерах Помимо службы имен и объекта именованного контекста, сервер в программе 8.3 также использует переносимый объектный адаптер. Вспомните, что адаптер (см. рис. 8.6) действует как своего рода посредник между ORB-брокером и обслуживающим объектом, который в действительности выполняет работу CORBA-объекта. Мы можем сравнить этот обслуживающий объект с "наемным" писателем, который пишет книгу от имени "подуставшей" знаменитости. С этой знаменитостью наперебой общаются журналисты, литературные агенты и юристы. Знаменитость удостаивается всех почестей, но реальную работу делает за него другой человек. CORBA- объект "публикует" интерфейс с внешним миром и играет роль "знаменитости" в CORBA-программе. Программа-клиент (или потребитель) взаимодействует с интерфейсом, который обеспечивает CORBA-объект, но реальную работу выполняет обслуживающий объект, играя роль "наемного" писателя. Обслуживающий объект имеет собственный протокол, который может отличаться от используемого UJRBA-объектом. CORBA-объект может предоставить С++-интерфейс для связи клиентом. Обслуживающий объект может быть реализован на Java, Smalltalk, ап и Других языках программирования. Объектный адаптер обеспечивает ин- Рфеис с обслуживающим объектом. Он адаптирует этот интерфейс, чтобы реали- я Услуживающего объекта была прозрачна для ORB-брокера и программы- нта. CORBA-реализация должна нормально поддерживать два типа объектных ^аптеров: Basic Object Adapter (BOA) и Portable Object Adapter (POA). Сначала стан- то ^ВА был ориентирован на использование ВОА-адаптера, но он не был доста- Кое ° Г ким' Поэтому и был разработан РОА-адаптер, который нашел более широ- Не РИМенение. ВОА-адаптер обладает минимальным набором средств, но его впол- Ко Но использовать для активизации объектных реализаций на базе информации, Рая содержится в хранилище реализаций (табл. 8.4).
304 Глава 8. Распределенное объектно-ориентированное программирована Таблица 8.4. Некоторые элементы, содержащиеся в хранилище реализаци" Элементы Описание Имя объекта Уникальный идентификатор для каждого с^бъе ' Режим активизации Разделяемый, неразделяемый, постоянный б лиотека permethod Путь Имя или путь выполняемого файла Список идентификаторов хранилища ВОА-адаптер, чтобы приступить к выполнению объекта изготовителя (сервера^ использует такие записи из хранилища реализаций, как режим активизации и путь' И хотя в ряде более простых примеров, приведенных в этой главе, используется ВОА- адаптер, мы рекомендуем для серьезной CORBA-разработки применять РОА-адаптер РОА-адаптер поддерживает: • прозрачную активизацию объекта; • транзитные объекты; • неявную активизацию обслуживающих объектов; • перманентные (постоянные) объекты за пределами сервера. Возможно, наиболее важной функцией РОА-адаптера является взаимодействие с обслуживающими объектами. CORBA-спецификация определяет обслуживающий объект следующим образом. Обслуживающий объект — объект языка программирования, который реализует запросы к одному или нескольким объектам. Обслуживающие объекты в общем случае существуют в контексте процесса сервера. Запросы на получение объектных ссылок обслуживаются ORB-брокером, действующим в качестве связующего звена, и трансформируются в вызовы конкретных обслуживающих объектов. Во время своего жизненного цикла объект может быть связан с несколькими обслуживающими объектами. Каждый обслуживающий объект должен иметь по крайней мере один РОА- адаптер. Но возможны и другие конфигурации (рис. 8.10). РОА-адаптерами управляют специальные объекты управления, или FCJA менеджеры. CORBA-спецификация определяет РОА-менеджер таким образом: РОА-менеджер — это объект, который инкапсулирует состояние обработки одного или нескольких РОА-адаптеров. С помощью РОА-менеджера разработчик может сделать запросы к соответствующим РОА-адаптерам с организацией очереди. Разраоот чик также может использовать РОА-менеджер для дезактивизации РОА-адаптеров. РОА- амки Сервер в программе 8.3 служит простым примером использования объектов адаптеров и РОА-менеджеров. Более подробное рассмотрение РОА выходит за р< нашей книги. За деталями обращайтесь к работе [20]. MICO-поставка также содер ряд примеров использования мощных средств РОА.
8.8. Хранилища реализаций и интерфейсов 305 К0НФИП>РАЦИЯ_1 Ю^ФИГУРАЦИЯ_2_ РОА Управление несколькими идентификаторами объектов у РОА Обслуживающий объект Обслуживающий объект конфигур.аУ!^5_3 РОА Обслуживающий объект РОА Обслуживающий объект Обслуживающий объект Обслуживающий объект Рис. 8.10. Возможные конфигурации отношений между РОА-агентами и обслуживающими объектами 8.8. Хранилища реализаций и интерфейсов ORB-брокер для определения местоположения объектов, когда строковые IOR- ссылки недоступны, использует хранилище реализаций. Хранилища реализаций представляют собой удобное место для хранения информации, связанной со спецификой среды (например, данные о системе безопасности, детали отладки и т.д.). Хранилище реализаций должно содержать информацию, достаточную для того, чтобы ORB-брокер мог отыскать путь объекта и выполняемый файл. Утилита imr в поставке MICO используется для управления хранилищем реализаций. Она позволяет отображать записи хранилища реализаций, вносить в него новые и удалять ненужные, например: create permutation persistent "4pwd4/permutation_server \ -ORBlmplRepoAddr inet: hostname :portnumber \ -ORBNamingAddr inet:hostnameiportnumber" IDL:permutation:1.0 \ фи выполнении этой команды в хранилище будет добавлена запись с перестанов- , имени. Местоположение выполняемого файла определяется как а /permutation_server. Эта запись также включает значения hostname °**tnumber, которые сообщают, где должна выполняться программа. Хранилище е 3аДий — весьма подходящее место для хранения информации этого типа об объ- и ' Та запись также устанавливает режим активизации (persistent). ORB-брокер с Льзует эту информацию для надлежащей инициализации выполнения программы • енем permutation_server. Полный список опций, доступных для программы * СоДержится в соответствующей документации. Помимо хранилища реализаций,
306 Глава 8. Распределенное объектно-ориентированное программировани используется хранилище интерфейсов, которое предназначено для хранения дИн ческой информации о каждом объекте. Хранилище интерфейсов можно использ И для динамического определения местоположения интерфейса с CORBA-объектами Л^ работы с хранилищем интерфейсов в MICO-реализациях CORBA-спецификации * пользуется программа ird. И хотя CORBA-спецификация описывает логические можности хранилищ реализаций и интерфейсов, все же некоторые ее дет (например, способы записи данных в хранилища и управления ими) зависят от к кретной среды, поставки и производителя. 8.9. Простые распределенные Web-службы, использующие CORBA-спецификацию Адреса для хранилищ реализаций и служб имен можно встроить в код HTML и использовать как часть CGI-обращения к Web-серверу. Этот метод с помощью CORBA- спецификации можно применить для реализации простых распределенных Web- служб. В листинге 8.6 представлен простой HTML-код. При щелчке на гиперссылке будет выполнен CORBA-клиент. Этот CORBA-клиент может затем получить доступ к серверу, используя адрес хранилища реализаций и службы имен, который был передан в CGI-команде HTML-кода. // Листинг 8.6. HTML-документ со встроенным обращением к // CORBA-программе (клиенту) <HTML> <HEAD> <TITLE> CORBA</TITLE> </HEAD> <BODY> <a href="http://www.somewhere.org/cgi-bin/client?- ORBImplRepoAddr+inet:hostname:port+- ORBNamingAddr+inet:hostname:port">Click</a> <P> </HTML> Здесь клиент ссылается на программу, которая должна получить доступ к CORBA- изготовителю (серверу). У клиента есть имя объекта, с которым ему необходимо связаться, а для дальнейших действий он использует службу имен. Этот метод не требует загрузки кода на компьютер пользователя. Совсем наоборот, код клиента, выполняясь на Web-сервере, должен получить доступ к CORBA-ориентированной программе- серверу независимо от ее местоположения (или в intranet-сети, подключенной к We серверу, или где-нибудь в другом месте Internet). Программа-клиент должна ответит HTML-браузеру, используя соответствующий CGI-протокол. Простая конфигураЦ1 Web-служб с CORBA-компонентами показана на рис. 8.11. Помимо протокола http, для запуска CORBA-ориентированных клиентов и с у веров можно использовать сетевой теледоступ telnet. Протоколы http и tein можно использовать для поддержки глобального распределения СОК компонентов. При проектировании распределенных компонентов, ориентир ванных на функционирование в сети Internet или intranet, важно не забыв о системе безопасности (соответствующем ПО и данных). И хотя реализа
8.10. Маклерская служба 307 предъявляемые к безопасности, выходят за рамки этой книги, мы иТреоов ^ ^ важную роль в любом проекте распределенной системы. Для подчерк иНф0рМациИ) имеющей отношение к безопасности, можно использо- хране илище реализаций. Любую CORBA-реализацию можно использовать вать Р с протоколом защищенных сокетов (Secure Socket Layer— SSL) и спе- да^ьной оболочкой SSH (Secure Shell). Простая архитектура Web-служб КОМПЬЮТЕР А КОМПЬЮТЕР В Программа-сервер Хранилище реализаций Хранилище интерфейсов Служба имен TCP/IP Программа-клиент р^--т---■-■-■ | Доступ к 1Р~здресам;^ г :-^фанад!ода реализаций ■ ^раЯилища ■интерфейсов f * Службы шеи. TCP/IP CGI http КОМПЬЮТЕР ПОЛЬЗОВАТЕЛЯ Е ► использует протокол html/CGI > вызывает программу-клиент Рис. 8.11. Простая конфигурация Web-служб с CORBA-компонентами 8-Ю. Маклерская служба юмимо строковых IOR-ссылок и службы имен, CORBA-спецификация включает ее прогрессивный и динамический метод получения объектных ссылок, име- . ^ьщ маклерской службой (trading service). Эта служба предлагает более интересно сравнению с рассмотренными выше) подход к взаимодействию с удален- 4 и объектами. Вместо взаимодействия со службой имен, клиент общается клером", у которого доступ к объектным ссылкам организован практически так » ак и в службе имен. Однако "маклер" связывает с объектными ссылками не Па Т^1С Имена' а описания и интерфейсы. В то время как служба имен содержит ссь ^Мя"ссь1лка", маклерская служба содержит пары "описания (интерфейсы)- а • Клиенты могут описать для "маклера" объект, который им нужен, и тот
308 Глава 8. Распределенное объектно-ориентированное программирование. ответит им соответствующей объектной ссылкой, если искомое соответствие будет обнаружено. Это — очень мощный поисковый метод. При этом клиент может н знать не только местоположения объекта, но и даже его имени. Это позволяет клиенту делать запросы на основе списка услуг, которые ему нужны, а не искать конкретный объект (запросы чего-то, которые не важно, где и кем будут удовлетворены). CORBA-спецификация определяет "маклер" следующим образом. Маклер — это объект, который поддерживает маклерскую службу в распределенной среде. Его можно представить себе в виде объекта, посредством которого другие объекты могут информировать о своих возможностях и сопоставлять свои потребности с объявленными (разрекламированными) возможностями. Рекламирование возможностей или услуг называется "экспортом". Совпадение с ними потребностей или обнаружение нужных услуг называется "импортом". Экспорт и импорт позволяют динамически обнаружить необходимые услуги и реализовать позднее связывание. И точно так же, как при связывании нескольких именных контекстов формируются именные графы, при связывании нескольких маклеров формируются маклерские графы. Именные и маклерские графы — это мощные методы представления знаний и функциональных возможностей. Именные и маклерские графы обеспечивают функционирование глобальных Web- и telnet-служб. Обход именных и маклерских графов может включать участки, которые потенциально имеют "ответвления" в какую-нибудь локальную сеть, intranet, extranet или Internet. Подобно именным контекстам маклеры обычно представляют определенные типы объектов. Например, мы могли бы позаботиться о том, чтобы маклеры одного типа имели доступ к объектам кредитных карточек, а маклеры другого — к объектам сжатия и шифрования. Можно создать маклеры, которые бы занимались объектами погоды и географии. А еще мы могли бы "научить" маклеры интересоваться финансовой деятельностью и страхованием. Объединив все эти маклеры, получим маклерский граф. Если один маклер будет работать от имени других, мы получим то, что можно назвать федерацией маклеров. Когда клиент описывает одному маклеру услуги, в которых он нуждается, а затем маклер общается со своими коллегами, чтобы найти эти услуги, то клиент и этот маклер включаются в федерацию маклеров. Это— самая мощная и гибкая форма "запроса, который не важно, где и кем будет удовлетворен". Когда федерация маклеров возвратит объектную ссылку, может оказаться, что она "родом бог-знает-откуда" и может быть реализована обслуживающим объектом (объектами), операционная система и язык программирования которого совершенно чужд программе клиента. Федерация маклеров обеспечивает доступ к очень большим и разнообразным коллекциям услуг. Следует иметь в виду, что CORBA- стандарт включает беспроводную спецификацию wCORBA, используемую для разработки мобильных агентных и мультиагентных систем. На рис. 8.12 показана базовая архитектура CORBA-ориентированного приложения типа "клиент-сервер", которое делает запросы к маклерам. Программа-клиент может взаимодействовать с маклером (или маклерами) напрямую или косвенно через федерацию маклеров. На рис. 8.12 обратите внимание на то, что после получения объектной ссылки осуществляется взаимодействие с ORB-брокером. Термины, связанные с темой программирования маклеров, приведены в табл. 8.5.
8.11. Парадигма "клиент-сервер" 309 Приложение типа "клиент-сервер", использующее маклерские объекты КОМПЬЮТЕР А КОМПЬЮТЕР С Программа-клиент ORB НОР Программа-сервер Объект клиента ORB Объектный адаптер (РОА или BOA) КОМПЬЮТЕР В ДЕЙСТВИЕ 1: получить объектную ссылку Маклерская!. служба 1 .1 ДЕЙСТВИЕ 1: получить объектную ссылку Маклерская служба 2 КОМПЬЮТЕР D Рис. 8.12. Базовая архитектура CORBA-ориентированного приложения типа "клиент- сервер", которое делает запросы к маклерам ^Таблица 8.5. Термины, связанные с темой программирования маклеров Термин Описание Экспортер Рекламирует услугу с помощью маклера. Экспортер может быть провайдером услуг или анонсировать услугу от имени кого-то другого Импортер Использует маклер для поиска услуг, соответствующих некоторому критерию. Импортер может быть потенциальным клиентом услуг или импортировать услугу от имени кого-то другого Предложение Содержит описание анонсируемой услуги. Описание состоит из име- услуги ни типа услуги, объектной ссылки и свойств объекта 8.11. Парадигма "клиент-сервер" Термины "клиент" и "сервер" часто применяются к различным видам программ- Ых приложений. Парадигма "клиент-сервер" состоит в разделении работы на две асти, представляемые процессами или потоками. Одна часть, клиент, создает запро- Ы на получение данных либо действий. Другая часть, сервер, выполняет эти запросы, оли запрашивающей и отвечающей стороны в большинстве случаев определяются °гикой самих приложений. Термины "клиент-сервер" используются на уровне
310 Глава 8. Распределенное объектно-ориентированное программированм операционной системы для описания отношений типа "изготовитель-потреби которые могут существовать между процессами. Например, если для взаимодей ЛЬ ' двух процессов используется FIFO-очередь, один из процессов "играет" роль сеп ИЯ а другой — роль клиента. Иногда клиент может "исполнить" роль сервера, если са дет получать запросы. Аналогично сервер будет выступать в роли клиента, если ^ потребуется обращаться с запросами к другим программам. Конфигурация "кли сервер" — основная архитектура распределенного программирования. При этом сервера обычно характеризует все приложение. Некоторые наиболее популяры типы программных серверов перечислены в табл. 8.6. Таблица 8.6. Основные типы программных серверов Типы программных серверов Описание Сервер приложений Файловый сервер Сервер баз данных Серверы транзакций Логические серверы Используется для обеспечения множества клиентов доступом к приложению. Вся работа приложения делится между клиентом и сервером, причем большая ее часть делается на сервере, а клиент (имея собственный процессор) выполняет только некоторую часть работы Действует как центральное хранилище для разделяемых документов, мультимедийных файлов, баз данных и т.д. Клиенты обычно представлены терминалами или рабочими станциями в сети. Клиент делает запрос на файлы или отдельные записи в этих файлах, затем файловый сервер передает запрос к клиенту. Файловый сервер поддерживает целостность данных и безопасность доступа к файлу Разбивает работу приложения между различными компьютерами в сетевой среде. Клиент формирует запросы на получение некоторого элемента данных, затем сервер баз данных находит эти данные и передает запрос клиенту. Сервер баз данных может обрабатывать сложные информационные запросы, для удовлетворения которых могут понадобиться мощности нескольких баз данных Используется для выполнения транзакций, которые происходят на компьютере или компьютерах, содержащих сервер транзакций. Каждое действие или обновление выполняется полностью без прерывания. При возникновении некоторых проблем все действия или обновления отменяются, и делается новая попытка выполнить транзакцию Используется для решения задач, которые требуют интенсивнь < символьных вычислений. Логический сервер способен отыска как неявно, так и явно заданную информацию в базе данных. Ло ческий сервер способен проследить некоторую информацию и с лать логический вывод об информации, которая не была явно вве на в базу данных. Логический сервер состоит из базы данных с оДН или несколькими встроенными механизмами логического выво Этот механизм используется для получения заключений и вывод от сервера. База данных состоит из правил, теорем, аксиом и пр дур. Чтобы удовлетворить запросы, логический сервер ДоЛ применять дедукцию, индукцию, силлогизмы и другие приемь^^—
8.12. Резюме 311 г снаЯ доска" и мультиагентные системы — это две основные архитектуры, ис- г ые в данной книге для поддержки параллельного и распределенного про- поль , ^оваНИЯ Особое внимание мы уделяем логическим серверам (см. табл. 8.6). ^ ский сервер — это специальный тип сервера приложений, который использу- ля решения задач, требующих интенсивных символьных и, возможно, парал- СТ ных вычислений. Процесс формирования некоторого вывода и дедукции часто Л лым бременем ложится на процессор и может значительно выиграть от исполь- ния параллельно работающих процессоров. Обычно чем больше процессоров ступно логическим серверам, тем лучше. Мультиагентные архитектуры и архитек- ' ««классной доски", рассматриваемые в главах 12 и 13, опираются на понятие распределенных логических серверов, которые могут совместными усилиями решать проблемы в сетевой среде, intranet или Internet. Несмотря на то что агентный подход и стратегия "классной доски" формируют архитектуру с уклоном в сторону равноправных узлов, они являются клиентами логических серверов. Распределенные объекты используются для реализации всех компонентов системы, a CORBA- спецификация позволяет упростить сетевое программирование. 8.12. Резюме Распределенное программирование включает программы, которые выполняются в различных процессах. Все процессы потенциально размещаются на различных компьютерах и, возможно, в различных сетях с различными сетевыми протоколами. Методы распределенного программирования позволяют разработчику разделить приложение на отдельно выполняемые модули, отношения между которыми можно определить на основе равноправия или как "изготовитель-потребитель". Каждый модуль имеет собственное адресное пространство и компьютерные ресурсы. Распределенное программирование позволяет использовать преимущества специальных процессоров, периферийного оборудования и других компьютерных ресурсов (например, серверов баз данных, приложений, почтовых серверов и т.д.). CORBA — это стандарт, применяемый для распределенного объектно-ориентированного программирования. В этой главе рассматриваются только CORBA-спецификации и CORBA- службы. Здесь вы должны были получить представление об этих базовых компонентах ° том, как можно построить простую распределенную программу. CORBA- спецификации для Web-служб, MAF, службы имен можно получить по адресу: • omg. org. За подробностями можно обратиться к книге [20]. Именные и маклер- е графы обеспечивают основу для мощного распределенного механизма представ- я знаний, который можно использовать в сочетании с мультиагентным програм- Р ванием. Они создают основу для следующего уровня интеллектуальных Web-служб.
РЕАЛИЗАЦИЯ МОДЕЛЕЙ SPMD И MPMD С ПОМОЩЬЮ ШАБЛОНОВ И MPI- ПРОГРАММИРОВАНИЯ В этой главе... 9.1. Декомпозиция работ для MPI-интерфейса 9.2. Использование шаблонных функций для представления MPI-задач 9.3. Упрощение взаимодействия между MPI-задачами 9.4. Резюме
^ J~J\EJSJ3J ■Jifi В сознательных действиях должен присутствовать существенный неалгоритмический компонент. - Роджер Пенроуз(Roger Penrose), The Emperor's New Mind Понятие параметризованного программирования поддерживается шаблонами. Основная идея параметризованного программирования — обеспечить максимально благоприятные условия для многократного использования ПО путем реализации его проектов в максимально возможной общей форме. Шаблоны функций поддерживают обобщенные абстракции процедур, а шаблоны классов — обобщенные абстракции данных. Обычно компьютерные программы уже представляют собой обобщенные решения некоторых конкретных проблем. Программа, которая суммирует два числа, обычно рассчитана на сложение любых двух чисел. Но если программа выполняет только операцию сложения, ее можно обобщить, "научив" выполнять Другие операции над двумя любыми числами. Если мы хотим получить самую общую Рограмму, можем ли мы остановиться лишь на выполнении различных операций над умя числами? А что если эти числа будут иметь различные типы, т.е. комплексные on еЦ*ественкые? Можно заложить в разработку программы выполнение различных пов^ЦИЙ НС только над любыми двумя числами, но и над значениями различных ти- п Или классов чисел (например, значениями типа int, float, double или ком- бин СНЫМи^' Кроме того, мы хотели бы, чтобы наша программа выполняла любую рную операцию на любой паре чисел — главное, чтобы эта операция была ле- Вп Для эти* двух чисел. Если мы реализуем такую программу, ее возможности С++ е Мн°гократного использования будут просто грандиозными. Эту возможность цИя РогРаммисту предоставляют шаблоны функций и классов. Такого вида обобщс- жно добиться с помощью параметризованного программирования.
314 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов Парадигма параметризованного программирования, поддерживаемая среде +, в сочетании с объектно-ориентированной парадигмой, также повдержив аМИ средствами C++, обеспечивают уникальный подход к MPI-программированию. Как C++, в сочетании с объектно-ориентированной парадигмой, также повдержива средствами C++, обеспечивают уникальный подход к MPI-программированию. Как °^ миналось в главе 1, MPI (Message Passing Interface — интерфейс передачи сообщений^ это стандарт средств коммуникации, используемых при реализации программ, треб щих параллелизма. MPI-интерфейс реализуется как коллекция, состоящая более чем 300 функций. MPI-функции охватывают большой диапазон: от порождения задач 3 барьерной синхронизации операций установки. Существует также С++-представлени° для MPI-функций, которые инкапсулируют функциональность MPI-интерфейса в набо классов. Однако в библиотеке MPI не используются многие преимущества объекты ориентированной парадигмы. Преимуществ параметризованного программирован и в ней также нет. Поэтому, несмотря на то что MPI-интерфейс весьма важен как стандарт, его "мощности" не позволяют упростить параллельное программирование. Да он действительно освобождает программиста от программирования сокетов и позволяет избежать многих ловушек сетевого программирования. Но этого недостаточно. Здесь может пригодиться кластерное программирование, а также программирование SMP- и МРР-приложений. Шаблонные и объектно-ориентированные средства программирования C++ могут оказаться весьма полезными для достижения этой цели. В этой главе для упрощения базовых SPMD- и MPMD-подходов'вместе с МР1-программированием мы используем шаблоны и методы объектно-ориентированного программирования. 9.1. Декомпозиция работ для MPI-интерфейса Одним из преимуществ использования MPI-интерфейса перед традиционными UNIX/Linux-процессами и сокетами является способность MPI-среды запускать одновременно несколько выполняемых файлов. MPI-реализация может запустить несколько выполняемых файлов, установить между ними базовые отношения и идентифицировать каждый выполняемый файл. В этой книге мы используем MPICH- реализацию MPI-интерфейса1. При выполнении команды $ mpirun -np 16 /tmp/mpi_examplel будет запущено 16 процессов. Каждый процесс будет выполнять программу с именем mpi_examplel. Все процессы могут использовать разные доступные процессоры. Кроме того, каждый процесс может выполняться на отдельном компьютере, если MPI работает в среде кластерного типа. Процессы при этом будут выполняться параллельно. Команда mpirun представляет собой основной сценарий, который отвечает за запуск MPI-заданий на необходимом количестве процессоров. Этот сценарий изолирует пользователя от подробностей запуска параллельных процессов на различных компьютерах. Здесь будет запущено 16 копий программы mpi_examplel. Несмотря на то что стандарт MPI-2 определяет функции порождения, которые можно использовать для динамического добавления программ к выполняемому MPI-приложению, этот ме тод не популярен. В общем случае необходимое количество процессов создается пр* запуске MPI-приложения. Следовательно, во время старта этот код тиражируете N раз. Описанная схема легко поддерживает модель параллелизма SPMD (SIMD), n скольку одна и та же программа запускается одновременно на нескольких процесс pax. Данные, с которыми каждой программе нужно работать, определяются по 1 Все MPI-примеры в этой, книге реализованы с использованием версий MPICH 1.1.2 и МР1СН 1- среде Linux.
9.1. Декомпозиция работ для MPI-интерфейса 315 ка программ. Этот метод старта одной и той же программы на нескольких про- ЗЗПорах можно развить, если нужно реализовать модель MPMD. Вся работа MPI- ЦСС оаммы делится между несколькими процессами, запускаемыми на старте про- ПР |^ы Информация о распределении "обязанностей" (т.е. кто что делает и какие пессы работают с какими данными) содержится в самой выполняемой программе. v шьютеры, задействованные в этой работе, перечисляются в файле rhines. arch (machines . Linux в данном случае) с использованием имени компь- а Местоположение этого файла зависит от конкретной реализации. В зависимо- от инсталляции, взаимодействие компьютеров, перечисленных в этом файле, бу- обеспечено либо командой ssh, либо UNIX/Linux-командой ' г'. 9.1.1. Дифференциация задач по рангу Во время старта процессов, включенных в МР1-приложение, MPI-среда назначает каждому процессу ранг и группу коммуникации. Ранг хранится как int-значение и служит в качестве идентификатора процесса для каждой MPI-задачи. Группа коммуникации определяет, какие процессы можно включить во взаимодействие типа "точка-точка". Сначала все MPI-процессы относят к группе, действующей по умолчанию. Заменить членов группы коммуникации можно, запустив приложения. После старта каждого процесса необходимо определить его ранг с помощью функции MPI_Coitun_rank (). Функция MPI_Coitun_rank () возвращает ранг вызывающего процесса. В первом аргументе, передаваемом функции, вызывающий процесс определяет, с каким коммуникатором он связывается, а его ранг возвращается во втором аргументе. Пример использования функции MPI_Comm_rank () показан в листинге 9.1. // Листинг 9.1. Использование функции MPI_Coiran_rank() int Tag = 33; int WorldSize; int TaskRank; MPI_Status Status; MPI_init (&argc, fcargv) ; MPI_Comm_rank(MPI_C0MM_WORLD/ &TaskRank) ; MPl_Coiran_size(MPI_COMM_WORLD/ &WorldSize) ; Коммуникатору MPI_COMM_WORLD по умолчанию при запуске назначаются все MPI- задачи. MPI-задачи группируются по коммуникаторам, которые определяют группу °ммуникации. В листинге 9.1 ранг возвращается в переменной TaskRank. Каждый роцесс должен иметь уникальный ранг. После определения ранга задаче передаются тветствующие данные либо определяется код, который ей надлежит выполнить. Усмотрим следующие варианты. ариант 1. Простая MPMD-модель Вариант 2. Простая SIMD-модель lf (TaskRank == i){ if (TaskRank == 1) { } Некоторые действия. // Используем одни данные. } //ankRank == 2){ if(TaskRank == 2){ } ^РУгие действия. // Используем другие данные. }
316 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов В первом варианте ранг используется для разграничения между процесса полняемой работы, а во втором — для разграничения данных, которые они д0 ВЫ обрабатывать. Несмотря на то что каждый выполняемый MPI-файл стартует НЫ ним и тем же кодом, модель MPMD (MIMD) можно реализовать с помощью п °Д и соответствующего ветвления программы. Аналогично после определения п °В данным процесса можно назначить некоторый тип, тем самым определив конк & ные данные, с которыми должен работать конкретный процесс. Ранг также исп Т зуется при передаче сообщений. MPI-задачи идентифицируют одна другую при мене сообщениями по рангам и коммуникаторам. Функции MPl_Send HMPI_Recv() используют ранг в качестве указания пунктов назначения и отпра ления соответственно. При выполнении вызова MPI_Send (Buff er, Count, MPI_LONG, TaskRank, Tag, Comm) ; будет отправлено Count значений типа long MPI-процессу с рангом, равным значению TaskRank. Параметр Buffer представляет собой указатель на данные, посылаемые процессу TaskRank. Параметр Count характеризует количество элементов в буфере Buffer, а не его размер. Каждое сообщение имеет тег. Этот тег позволяет отличить одно сообщение от другого, сгруппировать сообщения в классы, связать определенные сообщения с определенными коммуникаторами и пр. Тег имеет тип int, а его значение определяется пользователем. Параметр Comm представляет коммуникатор, которому назначается процесс. Если ранг и коммуникатор задачи известны, этой задаче можно посылать сообщения. При выполнении вызова MPI_Recv(Buf fer,Count,MPI_INT,TaskRank,Tag, Corran,&Status) ; будет получено Count значений типа int от процесса с рангом TaskRank. Инициатор вызова будет заблокирован до тех пор, пока не получит сообщение от процесса с рангом TaskRank и соответствующим значением тега (Tag). MPI-интерфейс для параметров ранга и тега поддерживает групповые символы. Такими групповыми символами являются значения MPI_ANY_TAG и MPXJ&NY_SOURCE. При использовании этих значений вызывающий процесс примет следующее полученное им сообщение независимо от его источника и тега. Параметр Status имеет тип MPI_Status. Информацию об операции приема можно получить из объекта Status. Параметр статуса содержит три поля: MPI_SOURCE, MPIJTAG и MPI_ERROR. Следовательно, объект Status можно использовать для определения тега и источника процесса- отправителя. При известном количестве процессов-участников можно точно определить отправителей сообщений и их получателей. Обычно для этого используется конкретное приложение. Распределение работы также зависит от приложения. Перед началом работы каждый процесс сразу же определяет, сколько других процессов включено в приложение. Это реализуется следующим вызовом: MPI_Comnusize (MPI_COM24_WORLD, &WorldSize) ; С помощью этой функции определяется размер группы процессов, связанных е ко кретным коммуникатором. В данном используется стандартный коммуникат р (MPI_COMM_WORLD). Количество процессов-участников возвращается в naPaMeTfL WorldSize. Этот параметр имеет тип int. Если каждому процессу известно значе WorldSize, значит, он знает, сколько процессов связано его коммуникатором.
9.1. Декомпозиция работ для MPI-интерфейса 317 о 1.2. Группирование задач по коммуникаторам Процессы связываются не только с рангами, но и с коммуникаторами. Коммуника- определяет область коммуникации для некоторого множества процессов. Все Т опессы, связанные с одним и тем же коммуникатором, относятся к одной и той же 'ппе коммуникации. Работу, выполняемую MPI-программой, можно разделить меж- группами коммуникаций. По умолчанию все процессы относятся к группе mpI C0MMJWORLD. Для создания новых коммуникаторов можно использовать функ- i№0~MPI_Coiran._create (). Список функций (с краткими описаниями), используемых яля работы с коммуникаторами, приведен в табл. 9.1. Благодаря использованию рангов и коммуникаторов MPI-задачи легко идентифицировать и различать. Ранг и коммуникатор позволяют структурировать программу как SPMD- или MPMD-модель либо как некоторую их комбинацию. Для упрощения кода MPI-программы мы используем ранг и коммуникатор в сочетании с параметризованным программированием и объектно-ориентированными методами. Шаблоны можно использовать не только применительно к аспекту различных данных SIMD- модели, но и к заданию различных типов данных. Это значительно упрощает структуру многих приложений, требующих выполнения большого объема одинаковых вычислений, но с различными типами данных. Для реализации модели MPMD (MIMD) мы рекомендуем использовать динамический полиморфизм (поддерживаемый объектами), параметрический полиморфизм (поддерживаемый шаблонами), объекты- функции и предикаты. Для разделения всего объема работы MPI-приложения эти методы используются в сочетании с рангами и коммуникаторами MPI-процессов. При использовании объектно-ориентированного подхода работа программы делится между семействами объектов. Все семейства объектов связываются с различными коммуникаторами. Соответствие семейств объектов различным коммуникаторам способствует модульности проекта MPI-приложения. Такой способ разделения также помогает понять, как следует применить параллелизм. Мы убедились, что объектно- ориентированный подход делает MPI-программы более открытыми для расширения, а также простыми для поддержки, отладки и тестирования. |||зблица9.1. Функции, используемые для работы с коммуникаторами Функции Описание #include "xnpi.h" MPI_Intercomm__create ( Создает inter-коммуникатор из двух intra- MPI_Comm LocalComm, коммуникаторов int LocalLeader, MPI_Coiran PeerComm, int remote_leader, int MessageTag, MPI__Comm *CommOut) ; I_Intercomm_merge ( Создает intra-коммуникатор из inter- коммуникатора MPI_Comm Comm, int High, MPI_Comm *CommOut); _Cartdim_get ( Возвращает декартову топологическую ин- MPI_Comm Comm, формацию, связанную с коммуникатором
318 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов Продолжение Функции Описание int MPI_Cart_create( MPI_Comm CommOld, int NDims, int *Dims/ int *Periods/ int Reorder, MPI_Comm *CoinmCart) ; int MPI_Cart_sub( MPI_Conim Comm, int *RemainDims, MPI_Comm *CoiranNew) ; int MPI_Cart_shift( MPI_Comm Comm, int Direction, int Display, int *Source, int *Destination); int MPI_Cart_map( MPI_Coinm 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 Comml, MPI_Comm Comm2, int *Result); Создает новый коммуникатор, к котором присоединяется топологическая информа Делит коммуникатор на подгруппы, которые образуют декартовы подсистемы более низкой размерности Считывает смещенные ранги источника и приемника при заданном направлении и величине смещения Преобразует процесс в декартову топологическую информацию Возвращает декартову топологическую информацию, связанную с коммуникатором Вычисляет координаты процесса в декартовой топологии при заданном ранге в группе Создает новый коммуникатор Вычисляет и возвращает ранг вызывающего процесса в коммуникаторе Вычисляет и возвращает ранг процесса в коммуникаторе при заданном декартовом местоположении Сравнивает два коммуникатора Comml иСошт2
9.1. Декомпозиция работ для МР1-интерфейса 319 Окончание табл. 9.1 Описание дор1__Сотт Commln, МР1_Со1ШП *CommOut) ; •nt- MPl-Comm^free ( MPl__Coiran *Comm) ; int MPI_Comm_group ( MPI_Coiran Comm, MPI-Group *Group); int MPI__Conun__size( MPI__Coiran Comm, int *Size); int MPI_Comm_split( MPI_Coiran Comm, int Color, int Key, MPI_Coiran *CoinmOut) ; int MPI_Comm_test_inter ( MPI__Comm Comm, int *Flag); int MPI_Comm_remote_group ( MPI_Comm Comm, MPI_Group *Group); int MPI_Comm_remote_size( MPI_Comm Comm, int *Size); Дублирует уже существующий коммуникатор со всей его кэшированной информацией Отмечает объект коммуникатора как освобожденный Получает доступ к группе, связанной с заданным коммуникатором Вычисляет и возвращает размер группы, связанной с заданным коммуникатором Создает новые коммуникаторы на основе цветов и ключей Определяет, является ли коммуникатор inter- коммуникатором Получает доступ к удаленной группе, связанной с заданным inter-коммуникатором Вычисляет и возвращает размер удаленной группы, связанной с заданным inter- коммуникатором 9.1.3. Анатомия MPI-задачи На рис. 9.1 представлена каркасная MPI-программа. Задачи, выполняемые этой программой, просто сообщают свои ранги MPI-задаче с нулевым рангом. Каждая rl-программа должна иметь по крайней мере функции MPI_Init () PI_Finalize(). ФункЧия MPI_Init () инициализирует MPI-среду для вызы- ающеи задачи, а функция MPI__Finalize () освобождает ресурсы этой MPI-задачи. Дая MPI-задача должна вызвать функцию MPI_Finalize () до своего заверше- я. Обратите внимание на обращения к функциям MPI_COMM_rank () -COMM_Size (). Они используются для получения значений ранга и количест- роцессов, которые принадлежат MPI-приложению. Эти функции вызываются v инством MPI-приложений. Вызов же остальных MPI-функций зависит от кон- ого приложения. MPI-среда поддерживает более 300 функций (подробная ин- ция представлена в соответствующей документации).
320 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблоно! #mclude <mpi.h> < Заголовочный MPI-файл inc Dest; int Tag = 50; int WorldSize; int TaskRank; string M; char Messageln[1000]; int N; strstream Buffer; MPI_Status Status; MPI_Init(&argc,&argc); < Функция инициализации MPI_Coiran_rank(MPI_COMM_WORLD,TaskRank) ; < Получение ранга задачи MPI_Coiran_size(MPI_COMM_WORLD,&:WorldSize) ; < Получение числа MPI-задач Dest = 0; < Сообщает, кто получает сообщение. N = 1; if(TaskRank != 0){ Buffer « "Ready To Work From Rank#" « TaskRank « ends; getline(Buffer,M); MPI_Send(const_char<char*>(M.data()),M.size()+1, MPI_CHAR,Dest,Tag,MPI_COMM_WORLD); < Отправка сообщения } else{ do{ cout « "From Supervisor11 « endl; MPI_Recv(MessageIn,100,MPI_CHAR,N, Tag,MPI_COMM_WORLD,&Status); < Получение сообщения cout « Messageln « endl; cout « "Received From " « Messageln « endl; N++; } while(N < WorldSize); } MPI_Finalize();< Завершение MPI } Рис. 9.1. MPI-программа 9.2. Использование шаблонных функций для представления MPI-задач Шаблоны функции позволяют обобщать процедуры для любого типа данных. Рассмотрим процедуру умножения, которая работает для любого типа данных (точнее, для типов данных, для которых операция умножения имеет смысл). template<class T> T multiplies(Т X, Т Y) { return( X * Y); } Для такой шаблонной функции, как эта, используются необходимые параметр для типа Т. Параметр Т означает некоторый тип данных, который будет реально > зан при реализации этого шаблона. Так, мы можем реализовать функи multiplies () следующим образом. //. . . multiplies<double>{3.2,4.5);
9.2. Использование шаблонных функций для представления МР1-задач 321 . 1ieS<int>(7/2); ^ti iies<rational>("7/2-f"3/4-); //••■ аметр Т заменяется типом double, int и rational соответственно, опреде- Здесь ^ ъал Точную реализацию операции умножения. Умножение для разных типов ЛЯЯ лПпрттеляется по-разному. Это означает, что для разных типов данных выполняет- данных опредс» ^ яличный код. Шаблонная фикция позволяет написать одну операцию умножения / е функции multiplies ()) и применить ее ко многим различным типам данных. 9.2.1. Реализация шаблонов и модель SPMD (типы данных) Параметризованные функции можно использовать с MPI-интерфейсом для обработки ситуаций, в которых все процессы выполняют одинаковый код, но работают с различными типами данных. Так, определив значение TaskRank процесса, мы можем распознать, с какими данными и данными какого типа должен работать процесс. В листинге 9.2 показано, как реализовать различные задачи для различных рангов. //Листинг 9.2. Использование шаблонных функций для // определения "фронта работ" МР1-задач int main (int argc, char *argv[]) { //... int Tag = 2; int WorldSize; int TaskRank; MPI__Status Status; MPI_lnit(&argc,&argv) ,- MPI_Comm_rank (MPI_COIYM_WORLD, &TaskRank) ; MPI_Comm_size (MPI_COm_WORLD, &WorldSize) ; //... switch (TaskRank) { case 1: multiplies<double>(3.2,4.6); break; case 2: multiplies<complex>(X,Y) break; //case n: //. . . } } Поскольку не существует двух задач с одинаковым рангом, все ветви в инструкции se листинга 9.2 будут выполнены различными MPI-задачами. Кроме того, такой тип раметризации можно распространить на контейнерные аргументы шаблонных пункций. Это позволит передавать одной и той же шаблонной функции различные еинеры объектов, содержащие различные типы объектов. Например, в листин- ■3 показана обобщенная шаблонная функция search ().
322 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов // Листинг 9.3. Использование контейнерных шаблонов в // качестве аргументов шаблонных функций template<T> bool search(T Key, graph<T>) { //. • ■ locate(Key) //. • • } //. . . MPI_Coiran_rank(MPI_COMM_.WORLD,&TaskRank); //- . . swi tch(TaskRank) { case 1: { graph<string> bullion; search<string> search{"gold", bullion); } break; 2: { graph<complex> Coordinates; search<complex>((X,Y)Coordinates); } break; //. В листинге 9.3 процесс, у которого TaskRank = 1, выполняет поиск в графе (graph) с именем bullion, содержащем string-объекты, а процесс, у которого TaskRank = 2, выполняет поиск в графе Coordinates, содержащем комплексные числа. Мы не должны изменять функцию search (), чтобы приспособиться к другим данным или типам данных, да и MPI-программа в этом случае имеет более простую структуру, поскольку мы можем многократно использовать шаблонную функцию поиска (search) для просмотра контейнера graph, содержащего данные любого типа. Использование шаблонов значительно упрощает SPMD-программирование. Чем более общей мы делаем MPI- задачу, тем более гибкой она становится. Кроме того, если некоторый шаблон прошел этап отладки и тестирования, надежность всех построенных на его основе МР1-задач можно считать довольно высокой, поскольку все они выполняют одинаковый код. 9.2.2. Использование полиморфизма для реализации MPMD-модели Полиморфизм— одна из основных характеристик объектно-ориентированно программирования. Если язык претендует на поддержку объектно-ориентированн программирования, он должен поддерживать инкапсуляцию, наследование и по морфизм. Полиморфизм— это способность объекта принимать множество форм- , лиморфизм поддерживает понятие "один интерфейс — множество реализаЦ Пользователь использует одно имя, или интерфейс, реализованный различными собами и различными объектами. Чтобы проиллюстрировать концепцию полиморф
Использование шаблонных функций для представления МР1-задач 323 м Класс vehicle, его потомков и простую функцию travel (), которая рассм у сс vehicle. На рис. 9.2 показана простая иерархия нашего семей- ислольз>еткл сТва класса vehicle. vehicle startEngine() moveForward() turnRight() turnLeftO stop() IK летательный аппарат ~к подводная лодка автомобиль вертолет Рис. 9.2. Иерархия семейства класса vehicle Самолеты, вертолеты, автомобили и подводные лодки — все это потомки класса vehicle (транспортные средства). Объект класса vehicle может заводить мотор, перемещаться вперед, поворачивать вправо, поворачивать влево, останавливаться и пр. В листинге 9.4 демонстрируется, как функция travel () использует объект класса vehicle для совершения компьютеризованного путешествия. // Листинг 9.4. // Функция travel(), которая в качестве параметра использует объект класса vehicle void travel (vehicle { *Transport) Transport->startEngine(); Transport->moveForward() ; Transport->turnLeft () ; Transports stop(); \nt main(int argC/ char *argv[]) //.. . car *Car; transportation = new VechicleO; travel(Car); ^^. Ция travel () принимает указатель на объект класса vehicle и вызывает методы класса vehicle. Обратите внимание на то, что функция main() в листинге 9.4
324 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов объявляет объект типа саг, а не vehicle, а также на то, что функции travel () сто объекта типа vehicle передается объект типа саг. Это возможно благодаря т С что в C++ указатель на класс может ссылаться на объект этого типа или на любой к ект, который является потомком этого типа. Поскольку класс саг является прои ным от класса vehicle, то указатель на тип vehicle может ссылаться на объект ти Д саг. Функция travel () написана без учета того, какими конкретно типами vehic] объектов она будет манипулировать. Для функции travel () вполне достаточно чт бы ее vehicle-объекты могли запускать мотор, двигаться вперед, поворачивать вл во, вправо и т.д. Если vehicle-объект способен выполнять эти действия, то функци travel () сможет справиться со своей работой. Обратите внимание на то, что на рис. 9.2 методы класса vehicle объявлены как виртуальные (virtual). Объявление методов виртуальными в базовом классе является необходимым условием динамического полиморфизма. В каждом из классов car, helicopter, submarine и airplane будут определены следующие функции. startEngine(); moveForward(); turnLeft(); turnRight(); stopO ; //... При этом объявление каждой функции будет соответствовать типу транспортного средства. Несмотря на то что транспортное средство каждого типа способно двигаться вперед, метод, в котором обеспечивается движение автомобиля, отличается от метода перемещения подводной лодки. Управление поворотом вправо у самолета отличается от управления таким же поворотом у автомобиля. Следовательно, транспортное средство каждого типа должно реализовать необходимые операции для получения законченного описания "своего" класса. Поскольку эти операции объявляются как виртуальные в базовом классе, они и являются кандидатами для реализации полиморфизма. Если vehicle-указатель, переданный функции travel (), в действительности ссылается на объект типа саг, то методами, вызываемыми в этой функции (startEngine (), moveForward () и пр.), реально окажутся те, которые определены в классе саг. Если vehicle-указатель, переданный функции travel (), в действительности ссылается на объект класса airplane, то методами, вызываемыми в этой функции, реально окажутся те, которые определены в классе airplane. Это и есть тот случай, когда много форм реализуется при одном интерфейсе. Несмотря на то что функция travel () вызывает только один набор методов, поведение этих методов может радикально отличаться в зависимости от того, указатель на объект какого vehicle-класса был назначен vehicle-указателю. Полиморфизм функции travel {) состоит в том, что при каждом вызове она может выполнять совершенно разные де ствия. И в самом деле, поскольку функция travel () использует указатель на клас vehicle, в будущем ее можно использовать для типов, производных от клас vehicle, которые были неизвестны или не существовали во время разработки ф> ции travel (). До тех пор пока будущие vehicle-классы будут наследовать кла vehicle и определять необходимые методы, ими можно будет управлять с помош функции travel (). Этот тип полиморфизма называется динамическим (run 1 polymorphism), поскольку функция travel () не знает точно, какие именно функ startEngine (), moveForward () или turnLef t () она будет вызывать, до тех F» пока программа не начнет выполняться.
2 Использование шаблонных функций для представления МР1-задач 325 тиП полиморфизма полезен при реализации MPI-программ, которые исполь- одель MPMD. Если MPI-задачи работают с указателями на базовые классы, то 3 юрФизМ позволяет MPI-классу также работать с любыми классами, производны- П° него. Предположим, что вместо объявления с указателем функция travel () М листинг 9.4) имела бы такое объявление: •& travel(vehicle Transport); том случае при обращении к функциям startEngine (), moveForward () и про- вызывались бы методы, принадлежащие только классу vehicle, и обращение изводным классам было проблематичным. Использование же указателя на класс hide и объявления методов в классе vehicle виртуальными (virtual) заставляют работать механизм полиморфизма. MPI-задачи, которые манипулируют указателями на базовые классы, могут точно так же использовать преимущества полиморфизма как функции travel () удается работать с любым типом vehicle-объекта (настоящим или будущим). Этот метод открывает большие перспективы для будущего кластерных приложений, а также приложений SMP (Symmetrical Multi/?rocessing — симметричная многопроцессорная обработка) и МРР (Massively Parallel Processing — массовая параллельная обработка), в которых необходимо реализовать модели MPMD. Чтобы понять, как модель MPMD работает в MPI-контексте, попробуем использовать нашу функцию travel () в качестве MPI-задачи, которая является частью подсистемы поиска. Все MPI-задачи отвечают за выполнение поисково-спасательных операций применительно к vehicle-объектам различного типа. Очевидно, что каждое транспортное средство (vehicle-объект) характеризуется различными способами движения. Несмотря на то что проблема, стоящая перед всеми MPI-задачами, заключается в выполнении поиска, все они будут иметь различные коды, поскольку все эти задачи используют различные виды vehicle-объектов, которые работают по- разному и требуют различных данных. Код, который содержится в листинге 9.5, необходимо запустить в нашей среде MPICH с помощью следующей команды. $ npirun -np 16 /tmp/search_n_rescue // Листинг 9.5. Реализация MPI-задачами простого ' I поиска и имитации спасения поврежденных '' объектов template<T> bool travel(vehicle "Transport, set<T> Location, . T Object) T-ransport->startEngine() ; Transport->moveForward(XDegrees); -ransport->turnLeft(YDegrees); •*-£ (Location, find (Transport->location () == Object) { // • . . rescue() main(int argC/ char *argv[])
326 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов } //... int Tag = 2; int WorldSize; int TaskRank; MPI_Status Status; MPI_Init (&argc, fcargv) ; MPI_Coiran_rank (MPI_COMM_WORLD, &TaskRank) ; MPI_Conim_size (MPI__COMM__WORLD, &WorldSize) ; //. . . switch(TaskRank) { case 1: { //. .. car * Car; set<streets> SearchSpace travel<streets>(Car, SearchSpace,Street); } break; case 2: { //... helicopter *BlueThunder; set<air_space> NationalAirSpace; travel<air_space> (BlueThunder, NationalAirSpace, AirSpace); //... } //case n: //- . , } Программа search_n_rescue будет запущена в 16 процессах, причем все процессы потенциально могут выполняться на различных процессорах, а все процессоры — находиться на различных компьютерах. Несмотря на то что все процессы выполняют один и тот же код, их действия могут радикально различаться (как и данные, с которыми они работают). Шаблоны и полиморфизм позволяют отличать одну MPI-задачу от другой (а значит, и данные, которые они будут использовать). Обратите внимание на то, что в листинге 9.5 MPI-процесс, у которого TaskRank = 1, будет использовать объект класса Саг и контейнер, содержащий streets-объекты. MPI-процесс, у которого TaskRan = 2, будет использовать объект класса helicopter и а1г_зрасе-объекты. Обе задач вызывают шаблонную функцию travel (). Поскольку шаблонная функция travel манипулирует указателями на класс vehicle, она может воспользоваться преимуте вами полиморфизма и выполнять операции с потомками класса vehicle. Это означа > что, хотя все MPI-задачи вызывают одну и ту же функцию travel (), действия, в полняемые этой функцией, различны. Обратите внимание на то, что в функц travel () нет инструкций case или if, которые бы пытались идентифицировать vehicle-объекта, с которым она работает. Конкретный vehicle-объект определ ся типом, на который используется указатель. Это MPI-приложение может рабо
q о Использование шаблонных функций для представления МР1-задач 327 «циально с 16 различными транспортными средствами, каждое из которых ха- П° оизуется собственным типом мобильности и областью поиска. Существуют Ра ие методы, которые можно использовать для реализации модели MPMD в среде и рУ но поЛиморфический подход обычно требует меньшего объема кода. Основные два типа полиморфизма, которые мы здесь демонстрируем, — это поли- пьбизм динамического связывания, поддерживаемый наследованием и виртуальными одами, и параметрический полиморфизм, поддерживаемый шаблонами. Функция avel () в листинге 9.5 использует оба типа полиморфизма. Полиморфизм, основный на наследовании, характеризует параметр vehicle *Transport, а парамет- ический полиморфизм— параметры set<T> и Т Object. Параметрический полиморфизм представляет собой механизм, благодаря которому один и тот же код используется для различных типов, передаваемых в качестве параметров. Различные типы полиморфизма, которые позволяют упростить MPI-задачи и сократить код, необходимый для реализации MPI-программы, перечислены в табл. 9.2. Таблица 9,2. Различные типы полиморфизма, которые можно использовать f?v|; , для упрощения МР1-задач Типы полиморфизма Механизмы Описание Динамический Наследование Вся информация, необходимая для определения и виртуальные того, какие виртуальные методы будет вызывать методы функция, неизвестна до выполнения программы Параметрический Шаблоны Механизм, в котором один и тот же код используется для различных типов, которые передаются как параметры 9.2.3. Введение MPMD-модели с помощью функций-объектов Функции-объекты используются в стандартных алгоритмах для реализации горизонтального полиморфизма. Полиморфизм, реализованный с помощью передачи параметра vehicle *Transport в листинге 9.5, является вертикальным, поскольку для функционирования необходимо, чтобы классы были связаны наследованием. При го- Р 3°нтальном полиморфизме классы связаны не наследованием, а интерфейсом. Все функции-объекты определяют операторную функцию operator (). Функции-объекты оляют разрабатывать MPI-задачи с использованием некоторой общей формы. '( фУнкция-объект Class some__class{ °Perator(); }; { Р ate<class T> T mpiTask(T X) //
328 Глава 9 Реализация моделей SPMD и MPMD с помощью шаблонов Т Result; Result = Х() //. .- } Шаблонная функция mpiTask () будет работать с любым типом Т, который им соответствующим образом определенную функцию operator (). //. .. MPI_Init(&argc,&argv); MPI_Comm__rank(MPI__COMM_WORLD/ &TaskRank) ; MPI_Coiran__size (MPI_COm_WORLD, &WorldSize) ; //. •• if(TaskRank == 0){ //- . . user_defined_type M; mpiTask(M); } if(TaskRank == N){ some_other_userdefined_type N; mpiTask(N); } // Этот горизонтальный полиморфизм не имеет отношения к наследованию или виртуальным функциям. Поэтому, если наша MPI-задача получит свой ранг, а затем объявит тип объекта, в котором определена функция operator (), то при вызове функции mpiTaskO ее поведение будет продиктовано содержимым метода operator (). Тогда, несмотря на идентичность всех процессов, запущенных посредством сценария mpirun, полиморфизм шаблонов и функций-объектов позволит всем MPI-задачам выполнять различную работу над различными данными. 9.3. Как упростить взаимодействие между МР1-задачами Помимо упрощения и сокращения размеров кода MPI-задачи с помощью полиморфизма и шаблонов, мы можем также упростить взаимодействие между Mrl" задачами, воспользовавшись преимуществами перегрузки операторов. Функции MPI_Send () и MPI_Recv () имеют следующий формат: MPI_Send (Buf f er, Count, MPI_LONG, TaskRank, Tag, Cornm) ; MPI_Recv(Buffer,Count,MPI_INT,TaskRank,Tag,Comm, &Status); При вызове этих функций необходимо, чтобы пользователь указал тип применяем здесь данных и буфер, предназначенный для хранения посылаемых или принима мых данных. Спецификация типа посылаемых или принимаемых данных мояс иметь довольно громоздкий вид и чревата последующими ошибками при передаче верного типа. В табл. 9.3 приведены прототипы MPI-функций отправки и присланных и их краткое описание.
9.3. Как упростить взаимодействие между МР1-задачами 329 "^Г^цаЭ.З. Прототипы MPI-функций отправки и приема данных Функции Описание iSSSSe "mpi.h" int MPl-Send (void 'Buffer,int Count, MPl-Datatype Type, int Destination, int MessageTag, MPI_Coiran 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, MPl_Datatype Type, int Destination, int MessageTag, MPl_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, !nt Destination, !nt MessageTag, MpI-.Comm Comm, J^-Request * Request ) ; Выполняет базовую отправку данных Инициализирует дескриптор для стандартной отправки данных Выполняет базовую отправку данных с синхронизацией Инициализирует дескриптор для стандартной отправки данных с синхронизацией Выполняет базовую отправку данных с сигналом готовности Инициализирует дескриптор для стандартной отправки данных с сигналом готовности Запускает отправку без блокировки
330 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов... Продолжение тпбл 9^ Функции int MPI_Issend (void *Buffer,int Count, MPI__Datatype Type, int Destination, int MessageTag, MPI_Coiran Comrn, MPI_Request *Request); int MPI_Irsend (void *Buffer,int Count, MPI_Datatype Type, int Destination, int MessageTag, MPI_Comm Comrn, MPI_Request *Request); int MPI_Recv (void *Buffer,int Count, MPI_Datatype Type, int source, int MessageTag, MPI_Comm Comrn, MPI_Status *Status); int MPI_Recv_init (void *Buffer,int Count, MPI_Datatype Type, int source, int MessageTag, MPI_Comm Comrn, MPI_Request *Request); int MPI_Irecv (void *Buffer,int Count, MPI_Datatype Type, int source, int MessageTag, MPI_Comm Comrn, 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 Comrn, MPI__Status *Status); Описание Запускает синхронную отправку без блокировки Запускает неблокирующую отправку данных с сигналом готовности Выполняет базовый прием данных Инициализирует дескриптор для приема данных Запускает прием данных без блокировки Отправляет и принимает сообщение
9.3. Как упростить взаимодействие между МР1-задачами 331 Окончание табл. 9.3 Функции Описание -Т7С j4pi_Sendrecv_replace Отправляет и принимает сообщение с ис- (void *Buffer/int Count, пользованием единого буфера Mpl__Datatype Type, int. Destination, int SendTag, int Source,int RecvTag, MPI__Comm Comm, MPI^Status *Status); Наша цель — обеспечить отправку и получение MPI-данных с помощью потокового представления iostream-классов. Данные удобно отправлять, используя следующий синтаксис. int X; float Y; user_defined_type Z; cout « X « Y « Z; //... Здесь разработчик не должен указывать типы данных при вставке их в объект cout. Для вывода этих данных трех типов достаточно определить оператор "«". Аналогично можно поступить при выделении данных из потокового объекта с in. //... int X; float Y; user_defined_type Z; cin » X » Y >> Z; //... В инструкции ввода данных их типы не задаются. Перегрузка операторов позволяет разработчику использовать этот метод для MPI-задач. Поток cout реализуется из класса ostream, а поток cin — из класса istream. В этих классах определены операторы «" и "»" для встроенных С++-типов данных. Например, класс ostream содержит ряд перегруженных операторных функций "«". °stream& °stream& °stream& °stream& °stream& °stream& °stream& °stream& 0streain& 0stream& °stream& operator«(char c) ; operator« (unsigned char c) ; operator« (signed char c) ; operator<<(const char *s) ; operator« (const unsigned char *s) ; operator« (const signed char *s) ; operator« (const void *p) ; operator«(int n) ; operator« (unsigned int n) ; operator«(long n) // адбс °Perator« (unsigned long n) ;
332 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов С помощью этих определений пользователь классов ostream и istream прим ет объекты cout и с in, не указывая типы передаваемых данных. Этот метод п * грузки можно использовать для упрощения MPI-взаимодействия. Мы рассмотп идею PVM-потока в главе 6. Здесь мы применяем тот же подход к созданию МРт* потока, используя структуру классов istream и ostream в качестве руководства лл разработки класса mpi_stream. Потоковые классы состоят из компонентов сост ния, буфера и преобразования. Компонент состояния представлен классом ios; ко понент буфера— классами streambuf, stringbuf или f ilebuf. Компонент преоб разования обслуживается классами istream, ostream, istringstrearn ostringstream, if stream и of stream. Компонент состояния отвечает за инкапсуляцию состояния потока. Класс ios включает формат потока, информацию о состоянии (работоспособное или состояние отказа), факт достижения конца файла (eof). Компонент буфера используется для хранения считываемых или записываемых данных. Классы преобразования предназначены для перевода данных встроенных типов в потоки байтов и обратно. UML-диаграмма семейства классов iostream показана на рис. 9.3. ИЕРАРХИЯ классов iostream I Т ios base IJ basic filebuf I J—Г-н ,^ . . '-I J basic '-J.J basic ios" 2E basic_ —, J streambuf basic, '-J J stringbuf L_-n basic_ —IJ ifstream Ij basic istream basic_ —j -I istringstream basic ostream _ 1 , I 1 ^J , ' basic_ — jJ iostream j -и £ basic_ — jJ ostringstream к " I T i basic_ —iJ stnngstream basic_ — jJ fstream Рис. 9.З. UML-диаграмма семейства классов iostream basic_ —i * ofstream 9.3.1. Перегрузка операторов"«" и "»" для организации взаимодействия между MPI-задачами Взаимоотношения и функциональность классов, показанных на рис. 9.3, можн использовать как своего рода образец для проектирования класса mpi_streaifi ■ И хотя проектирование потоковых MPI-классов требует больше предварительно
9.3. Как упростить взаимодействие между МР1-задачами 333 . ы по сравнению с непосредственным использованием функций MPI_Recv() Р рт send (), в целом оно делает MPI-разработку значительно проще. А если про Й м\1Ы с параллельной обработкой можно упростить, это нужно сделать обязательно, v ньшение сложности программ— весьма достойная цель для программиста. Здесь представляем лишь каркас класса mpi_stream. Но этого вполне достаточно для лучения понятия о конструкции потокового MPI-класса. После разработки класса • stream можно приступать к упрощению организации взаимодействия между MPl-задачами в большинстве MPI-программ. Листинг 9.6 содержит фрагмент из объявления класса mpi_stream. // Листинг 9.6. Фрагмент объявления II класса mpi_stream class mpios{ protected: int Rank; int Tag; MPI_Coinm 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 C); void rank(int R); void tag(int T); void comm(MPI_Comm C) ; ropi__stream &operator« (int X) ; rapi_.stream &operator« (float X) ; mPi__stream &operator<<(string X) ; mPi__stream &operator«(vector<long> &X); mPi_stream &operator«(vector<int> &X); mPi_stream &operator« (vector<f loat> &X); mPi_stream &operator«(vector<string> &X) ; mPi_stream &operator» (int &X) ; mPi_stream ^operator» (float &X) ; ^^stream ^operator» (string &X) ; ™Pi-Stream ^operator»(vector<long> &X); *'P^-Stream ^operator» (vector<int> &X) ; ^Pi^stream ^operator»(vector<float> &X); P^stream ^operator» (vector<string> &X) ;
334 Глава 9. Реализация моделей SPMD и MPMD с помощью шаблонов Для того чтобы сократить описание, мы объединили классы impi s*- и ompi_stream в единый класс mpi_stream. И точно так же, как классы istr ^ и ostream перегружают операторы "«" и "»", мы обеспечим их перегрузку в кл^ се mpi_stream. В листинге 9.7 показано, как можно определить эти перегружены операторы: // Листинг 9.7. Определение операторов "«" и *»" 11. . • mpi_stream &operator« (string X) { MPI_Send(const_cast<char*>(X.data()),X.size(), MPI_CHAR,Rank,Tag,Comm); return(*this); } // Упрощенное управление буфером. mpi_stream &operator«(vector<long> &X) { 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-приложениях, должны иметь операторы "«" и ">>", перегруженные с учетом каждого типа данных. Здесь мы продемонстрируем несколько простых перегруженных операторов. В каждом случае мы представляем упрощенный вариант управления буфером. На практике необходимо предусмотреть обработку исключений (на базе шаблонных классов) и распределение памяти (на базе классов-распределителей ресурсов). В листинге 9.7 обратите внимание на то, что класс mpios содержит коммуникатор, статус класса mpi_stream, номер буфера и значение ранга или тега (это— лишь одна из возможных конфигураЦ*11 класса mpi_stream— существует множество других). После того как клас mpi_stream определен, его можно использовать в любой MPI-программе. Взаим действие между MPI-задачами может быть организовано следующим образом. //. . . int X; float Y; vector<float> Z;
9.4. Резюме 335 • ci-ream Stream(Rank,Tag,MPI_WORLD_COMM); Stream << X « zf Stream « Y' /Л" » z- Stream >> *" Такой подход позволяет программисту, поддерживая потоковое представление, осТИТЬ MPI-код. Безусловно, в определение операторов "«" и ">>" необходимо ключить соответствующую проверку ошибок и обработку исключительных ситуаций. 9.4. Резюме Реализация SPMD- и MPMD-моделей параллелизма во многом выигрывает от использования шаблонов и механизма полиморфизма. Несмотря на то что MPI- интерфейс включает средства динамического С++-связывания, в нем не используются преимущества методов объектно-ориентированного программирования. Это создает определенные трудности для разработчиков, использующих стандарт MPI. Для упрощения MPMD-программирования можно успешно использовать такие свойства объектно-ориентированного программирования, как наследование и полиморфизм. Параметризованное программирование, которое поддерживается с помощью С++- шаблонов, позволяет упростить SPMD-программирование MPI-задач. Разделение работы программы между объектами — это естественный способ реализовать параллелизм в приложении. Для того чтобы облегчить взаимодействие между группами объектов, характеризующимися различной степенью ответственности за выполняемую работу, семейства объектов в MPI-приложении можно связать с коммуникаторами. Для поддержки потокового представления используется перегрузка операторов. Применение методов объектно-ориентированного и параметризованного программирования в рамках одного и того же MPI-приложения является воплощением муль- типарадигматического подхода, который упрощает код и во многих случаях уменьшает его объем. Тем самым упрощается отладка программ, их тестирование и поддержка. MPI-задачи, реализованные с помощью шаблонных функций, характеризуются оолее высокой надежностью при использовании различных типов данных, чем отдельно определенные функции с последующим обязательным выполнением операции приведения типа.
ВИЗУАЛИЗАЦИЯ ПРОЕКТОВ ПАРАЛЛЕЛЬНЫХ И РАСПРЕДЕЛЕННЫХ СИСТЕМ В этой главе... 10.1. Визуализация структур 10.2. Отображение параллельного поведения 10.3. Визуализация всей системы 10.4. Резюме
J~1s\EJSJ3J v-* "-}JrS "Мысли, не оформленные в словесную оболочку, — это вполне обычное явление. Наши идеи часто возникают на уровне ощущений. Мы вдруг начинаем чувствовать правильность решения проблем, над которыми бились долгое время, и только потом решаемся обозначить их на том или ином языке... Очень много идей приходят нам в головы в бессловесной форме..." — О. Коэхлер (О. Koehler), The Ability of Birds to Count Модель системы представляет собой своего рода информационное тело, "собранное" с целью изучения системы и лучшего ее понимания разработчиками и специалистами, которые должны ее поддерживать. При моделировании системы должны быть идентифицированы отдельные ее части, атрибуты, также действия, выполняемые системой. Моделирование — важный инструмент роцессе проектирования любой системы, поэтому очень важно добиться того, ооы разработчики до конца понимали систему, которую разрабатывают. Модели- г ание помогает выявить заложенный в систему параллелизм и понять, как имен- следует реализовать ее распределение. с нифицированный язык моделирования (Unifited Modeling Language— UML) ржит графические средства, используемые для проектирования, визуализации, н иР°вания и документирования артефактов системы программного обеспече- н " ЗЫк UML представляет собой фактический стандарт для моделирования объект- зна ентиРованных систем. Этот язык использует символы и условные знаки для обо- ЛИ ^я аРтефактов системы ПО, отображаемых с различных точек зрения и при раз- Ног И (*)окУсиРовке- Язык UML вобрал в себя методы объектно-ориентирован- Дж « анализа и проектирования, предложенные Гради Бучем (Grady Booch), * сом Рамбау (James Rumbaugh) и Айваром Джекобсоном (Ivar Jacobson)
338 Глава 10. Визуализация проектов параллельных и распределенных сист< в 1980-х и 1990-х годах. Он был принят рабочей группой по развитию станл объектного программирования (Object Management Group— OMG), междун ной организацией, состоящей из разработчиков ПО и производителей инсЬ " ционных систем и насчитывающей более 800 членов. Принятие UML дало n a ботчикам ПО не просто единый язык, а инструмент для анализа объектов, их сания, визуализации и документирования. В этой главе мы покажем, как можно визуализировать и смоделировать паралл hvk> и распределенную систему с помощью UML. Помимо помощи в разработке темы, моделирование позволяет идентифицировать области параллелизма (где им но?), понять необходимость применения синхронизации и взаимодействия Полей тем (когда именно?), а также продумать степень распределения объектов (как именно?). Мы рассматриваем диаграммные методы визуализации и моделирования параллельных систем со структурной и поведенческой точек зрения. Однако следует отметить, что классы, объекты и системы, используемые в этой главе как примеры служат целям демонстрации и необязательно отражают реальные классы, объекты или структуры, используемые в действительно существующей системе. 10.1. Визуализация структур При рассмотрении системы со структурной точки зрения акцент ставится на ее статических частях, т.е. нас интересует, как построены элементы системы. В этом случае изучаются атрибуты, свойства и операции, выполняемые системой, а также ее организация, устройство (состав компонентов) и взаимоотношение элементов в системе. В этом разделе рассматриваются диаграммные методы, используемые для моделирования: • классов, объектов, шаблонов, процессов и потоков; • организации объектов, работающих "в одной команде". Изображаемые при моделировании системы элементы могут быть концептуальными или физическими. 10.1.1. Классы и объекты Класс— это модель некоторой конструкции, характеризующейся определенными атрибутами и поведением. Это— описание множества понятий или объектов, которые обладают общими атрибутами. Класс — это базовый компонент любой объектно ориентированной системы. Классы можно использовать для представления реаль ных, концептуальных, аппаратных и программных конструкций. Для представлени классов, объектов и взаимоотношений, которые существуют между ними в параллел ной и/или распределенной системе, используется диаграмма класса (class diagram/■ Диаграмма класса позволяет отобразить атрибуты и услуги, предоставляемые классе а также ограничения, налагаемые на способ связи этих классов/объектов. Язык UML содержит средства для графического представления класса. Для пр стейшего изображения класса достаточно начертить прямоугольник и написать нем имя класса. При использовании только одного имени говорят, что это прос имя. С помощью диаграммы класса можно также отобразить атрибуты и услу ' предоставляемые пользователю этого класса (или операции, выполняемые э
10.1. Визуализация структур 339 м\ Чтобы включить в диаграмму атрибуты и операции, прямоугольник ото- КЛаС- тся с тремя горизонтальными отделениями. В верхнем отделении записыва- постое имя класса, в среднем — атрибуты, а в нижнем — операции. Разделы СТС А 'тов и операций можно пометить словами "атрибуты" и "операции" соответ- а но Имя класса должно быть указано в любом случае, а раздел атрибутов или С пий — по необходимости. Это значит, что если нужно указать один из разде- ° (атрибутов или операций), то другой отображается пустым. Различные способы представления класса показаны на рис. 10.1. student schedule а) student schedule Атрибуты string : StudentNumber string : Term map<string, vector<course> > : StudentSchedule map<string,vector<course> >::iterator : Schedulelterator Операции student_schedule (void) student_schedule(const student_schedule &Sched) ~student_schedule(void) scheduleDayOfWeek(string DayOfWeek) : vector<course> & student Schedule (void) : map<strmg,vector<course> > & studentSchedule(map<string/vector<course> > &X) : void snudentNumber (string SN) : void StudentNumber (void) : string term(string &X) : void term(void) : string operator=(const student_schedule ^Schedule) : student_schedule & operator==(student_schedule &Sched) : bool operator« (ostream &Out, student_schedule &Sched) : ostream & {friend} 6) student schedule Операции student_schedule (void) student_schedule(const student_schedule &Sched) -student_schedule (void) scheduleDayOfWeek(string DayOfWeek) : vector<course> & studentSchedule(void) : map<string/vector<course> > & studentSchedule(map<string,vector<course> > &X) : void StudentNumber (string SN) : void StudentNumber(void) : string term(string &X) : void term(void) : string °Perator=(const student_schedule ^Schedule) : student_schedule & °perator==(student_schedule &Sched) : bool Qperator« (ostream ScOut, student_schedule &Sched) : ostream & {friend} в) Ис- Ю.1. Различные способы представления класса
340 Глава 10. Визуализация проектов параллельных и распределенных систе! На рис. 10.1 представлен класс student_schedule. На рис. 10.1, а показано простейшее представление, рис. 10.1, tf содержит полную информацию о классе- ^ имя, атрибуты и операции, а рис. 10.1, в представляет имя класса и его операп^ (раздел, который должен содержать атрибуты, пуст). Если раздел атрибутов оставл И пустым, это означает, что данный класс имеет атрибуты, но их показывать в данн ** конкретном случае не нужно. Иногда используется дополнительный раздел, который служит для описания об занностей класса. Он размещается под разделом операций и может быть опущен. По обязанностями класса подразумевают то, что ему надлежит выполнить. Обязанности класса отображаются как "договорные" предложения, которые трансформируются в операции и атрибуты. Атрибуты трансформируются в типы данных и структуры данных, а операции— в методы (функции). Этот дополнительный раздел можно пометить словом "обязанности". Обязанности класса student_schedule можно изложить следующим образом: "возвращает расписание для студента на любой день недели при заданном номере студента, годе и периоде расписания". Обязанности класса отображаются в виде текста, причем каждая обязанность представляется в соответствующем разделе как короткое предложение или абзац. С помощью диаграммы класса можно отобразить объект, или экземпляр класса. Как и при использовании класса, простейшее представление объекта состоит в изображении прямоугольника, который содержит подчеркнутое имя объекта. Тем самым указывается именованный экземпляр класса. Именованный экземпляр класса можно сопровождать именем класса или обойтись без него. mySchedule именованный экземпляр mySchedule: student_schedule именованный экземпляр с именем класса Поскольку реальное имя объекта может быть известно только для программы, которая его объявляет, то в системной документации, возможно, имеет смысл указывать анонимные экземпляры классов. Анонимный объект класса можно представить следующим образом. :student_schedule Такой тип обозначения может оказаться удобным в случае, когда в системе существует несколько экземпляров класса. Несколько экземпляров класса можно представить двумя способами: в виде объектов и в виде классов. Количество экземпляров, которое может иметь класс, называется множественностью. Количество экземпляров класса (от нуля до бесконечности) можно указать диаграмме класса. Класс с нулевым количеством экземпляров является чистым о, рактпым классом. Он не может иметь ни одного объекта, явно объявленного с испо зованием этого типа. Количество экземпляров может иметь нижнюю и верхнюю Р ницы, которые также могут быть указаны на диаграмме класса. На рис. 10.2 показ возможные варианты обозначения нескольких экземпляров класса на диагра класса (с помощью графических средств или значения множественности). На рис. 10.2 множественность класса student_schedule указана как диапазон • •' а это означает, что наименьшее количество расписаний в нашей системе равно 1, большее — 7. Приведем еще несколько примеров обозначения множественности кла 1 Один экземпляр 1. . п От одного до заданного числа п 1. . * От одного до бесконечности 0 . . 1 От нуля до единицы
10.1. Визуализация структур 341 0 * От нуля до бесконечности * Бесконечное количество экземпляров Я зусловно, бесконечное количество экземпляров будет ограничено объемом вНутре] нней или внешней памяти. student__schedule student schedule 1..7 Операции student_schedule(void) student_schedule(const student_schedule & Sched)~student_schedule(void) scheduleDayOfWeek(string DayOfWeek): vector<course> & studentSchedule(void): map<string,vector<course> > & studentSchedule(map<string,vector<course> > &X): void studentNumber(string SN): void studentNumber(void): string term(string &X): void term(void): string operator=(const student_schedule &Schedule): student_schedule & operator==(student__schedule &Sched): bool operator«(ostream &Out, student_schedule &Sched): ostream & {friend} Рис. 10.2. Обозначение нескольких экземпляров класса с помощью графических средств и значения множественности 10.1.1.1. Отображение информации об атрибутах и операциях класса Диаграмма класса может содержать более подробную информацию об атрибутах и операциях класса. В разделе атрибутов можно указать тип данных и/или значение по умолчанию (если оно предусмотрено) для класса и значения атрибутов для объектов. Например, типы данных, содержащиеся в разделе атрибутов класса student__schedule, могут иметь следующий вид. StudentNumber : string Term string udentSchedule : map <string,vector<course> > cnedulelterator : map <string, vector<course> >: .-iterator Для объекта my Schedule эти атрибуты могут принимать такие значения. ^udentNumber : string = "102933" rm : string = "Spring" Им еТоды м°гут быть отображены с параметрами и с указанием типов возвращаемых u<*entSchedule(&X : map <string, *tud, entNumber() : string vector<course> >) : void
342 Глава 10. Визуализация проектов параллельных и распределенных систем Функция studentSchedule () принимает значение course для заданного ctv (course — это класс, который моделирует один курс обучения). Курсы для каждого ^ недели хранятся в векторе. Контейнер тар устанавливает соответствие строки ( НЯ недели) и вектора курсов (для заданного дня недели). Функция studentSchedul ?* возвращает void-значение, а функция studentNumber () — значение типа string На диаграмме класса можно также отобразить свойства атрибутов и onepai ° (методов). Свойства атрибутов помогают описать характер использования того и иного атрибута, что дает возможность судить о том, можно ли его изменять или це Так, для описания атрибутов используются три свойства: changeable, addOnl и frozen. Краткое описание этих свойств приведено в табл. 10.1. Для определени методов используются четыре свойства: isQuery, sequential, guarded и concurrent. Они также описаны в табл. 10.1. Свойства sequential, guarded и concurrent имеют отношение к параллельности выполнения методов. Свойство sequential описывает операцию, ответственность за синхронизацию которой лежит на инициаторе ее вызова. Такие операции не гарантируют целостности объекта. Свойство guarded описывает параллельно выполняемую операцию с уже встроенной синхронизацией. При этом guarded-операции означают, что в каждый момент времени возможен только один ее вызов. Свойство concurrent описывает операцию, которая позволяет ее одновременное использование. Операции, описываемые с помощью свойств guarded и concurrent, гарантируют целостность объекта. Гарантия целостности объекта применима к операциям, которые изменяют состояние объекта. Таблица 10.1. Свойства атрибутов и методов Свойства атрибутов Описание {changeable} На значения этого типа атрибута никакие ограничения не налагаются {addOnly} Для атрибутов, у которых значение множественности >1, можно добавлять дополнительные значения. Созданное значение невозможно удалить или изменить { frozen} После инициализации объекта значение атрибута изменить нельзя^ Свойства методов { isQuery} При выполнении метода этого типа состояние объекта остается неизменным. Этот метод возвращает значения {sequential} Пользователи этого метода для обеспечения гарантии последовательного доступа к нему должны использовать синхронизацию- При множественном параллельном доступе к этому методу дел° стность объекта подвергается опасности {guarded} Синхронизированный последовательный доступ к этому метод) встроен в объект; целостность объекта гарантируется { concurrent} К этому методу разрешен множественный параллельный досту целостность объекта при этом гарантируется
10.1. Визуализация структур 343 Гвойства guarded и concurrent можно использовать для отражения модели в\М (Parallel Random-Access Machine— параллельная машина с произвольным дос- ч Если метод считывает и/или записывает данные в память, доступную для дру- метода, который также считывает и/или записывает данные в ту же память, этот ол может быть описан как PRAM-алгоритм. При этом можно использовать соот- тствуюшие свойства, например, такие. PRAM-алгоритмы Свойства CR (Concurrent Read — параллельное чтение) concurrent C\V (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} Student Schedule : map <string, vector<course> > {changeable} Операции: scheduleDayOfWeek(&X : vector<course>/ Day : string) : void {guarded} StudentNumber () : string {isQuery, concurrent} Атрибут StudentNumber представляет собой константу типа string. После присвоения значение константы изменить нельзя. Если объект student_schedule используется для того же студента, но для различных периодов времени, то атрибуты егш и StudentSchedule должны быть модифицируемыми. Метод scheduleDayOf Week () принимает вектор курсов (vector<course>) для конкретно- ° ДНя неДели, хранимого в строке Day. Это — защищенная (guarded) операция. Она омещает расписание студента, соответствующее конкретному дню недели, в тар- ект StudentSchedule, изменяя тем самым его состояние. Синхронизация, раиваемая в этот объект, обеспечивается за счет использования мьютексов. Метод uentNumber () имеет два свойства: isQuery и concurrent. Этот метод возвра- константу StudentNumber и безопасен для одновременного доступа. Его вызов ^меняет состояния объекта, поэтому здесь и использовано свойство isQuery. а Диаграмме класса можно отобразить еще одно важное свойство атрибутов Tvn Рации ~~ их видимость. Свойство видимости описывает, кто может получить дос- д! атрибуту или вызвать операцию. Для представления этого свойства (уровня ви- сп СТ Используется соответствующий символ. Уровни видимости соответствуют фикаторам доступа, определенным в C++. Мвол видимости предваряет имя атрибута или операции (метода).
344 Глава 10. Визуализация проектов параллельных и распределенных систем Спецификаторы доступа Символы видимости public (+) Общий доступ pro tec ted (#) Доступ имеет сам класс и его потомки private (-) Доступ имеет только сам класс 10.1.1.2. Организация атрибутов и операций От того, как будут организованы атрибуты и операции в соответствующих отдел ниях диаграммы класса, зависит степень успешности использования этого класса А рибуты и операции можно упорядочить по алфавиту, уровню доступа или категориям Как оказалось, алфавитный порядок вряд ли поможет узнать, как могут называться те или иные атрибуты или операции (если документация находится в руках пользователя системы), или какие из них еще не определены (если документация используется в процессе разработки). Упорядочение по уровню доступа зарекомендовало себя гораздо лучше. В этом случае пользователь четко видит, какие атрибуты и операции являются, например, общедоступными (public) или закрытыми (private). Знание перечня защищенных (protected) членов поможет расширить возможности класса или специализировать его, используя механизм наследования. Такое упорядочение просто реализовать с помощью символов видимости (+, - и #) или С++- спецификаторов доступа (public, private и protected,). Существует несколько способов разбиения атрибутов и операций по категориям. Минимальный стандартный интерфейс определяет категории для операций, которые в свою очередь определяют атрибуты, поддерживающие эти операции. Составители минимального стандартного интерфейса руководствовались тем, что все классы должны определять такие операции и функции, которые делают его полезным. Вот список этих операций: • конструктор по умолчанию; • деструктор; • конструктор копии; • операции присваивания; • операции сопоставления на равенство; • операции ввода-вывода; • операции хеширования; • операции запросов. Этот список можно использовать в качестве основного перечня категорий для кла фикации операций, определяемых в классе. В этот перечень можно внести категории, торые позволяют указать дополнительные характеристики для атрибутов и операци Атрибуты: static const Операции: virtual pure virtual friend
10.1. Визуализация структур 345 Пои выборе категорий следует исходить из того, какая из них лучше всего описы- услуги, предоставляемые классом. Имя категории справа и слева заключается ** ойные угловые скобки («. . .»). На рис. 10.3 показано два возможных способа анизации атрибутов и операций для класса student_schedule, использующих: в двойные угловые^ мволы видимости и спецификаторы доступа (рис. 10.3, а) и категории минимально- го стандартного интерфейса (рис. 10.3, б). s tudent_schedu1e Атрибуты # string : StudentNumber # string : Term # map<string,vector<course> > : StudentSchedule # map<string/vector<course> >::iterator : Schedulelterator Операции « public » student_schedule(void) student_schedule{const student_schedule &Sched) ~student_schedule(void) scheduleDayOfWeek(string DayOfWeek) : vector<course> & а) символы видимости и спецификаторы доступа student_schedule « const » string : StudentNumber string : Term Атрибуты << конструктор >> student_schedule(void) Операции « конструктор копии » student_schedule(const student_schedule &Sched) « деструктор >> ~student_schedule(void) « операции запросов >> StudentSchedule(void) : map<string/vector<course> > & term(void) : string StudentNumber(void) : string scheduleDayOfWeek(string DayOfWeek) : vector<course> & « операции присваивания » StudentSchedule(map<string/vector<course> > &X) : void StudentNumber(string SN) : void term(string &X) : void operator=(const student_schedule ^Schedule) student_schedule & << операции ввода-вывода » operator«(ostream &Out, student_schedule ScSched) : ostream & {friend} к< операции сопоставления на равенство » operator==(student_schedule &Sched) : bool ) категории минимального стандартного интерфейса Ис- 10.3. Два способа организации атрибутов и операций в диаграмме »
346 Глава 10. Визуализация проектов параллельных и распределенных систем 10.1.1.3. Шаблонные классы Шаблонный класс представляет собой механизм, который позволяет в кач параметра в определении класса использовать тип. Шаблон определяет действи ^ торые выполняются над переданным ему типом. В C++ параметризованный класс ° дается с помощью ключевого слова template, template <class Type > classname {...}; Параметр Type представляет любой тип, передаваемый шаблону. Это может бь встроенный С++-тип или определенный пользователем класс. При объявлении пап метра Туре шаблон связывается с элементом, переданным ему в качестве параметои зованного типа. Например, класс student_schedule включает контейнер шар который содержит векторы объектов типа course для каждого дня недели. Как класс тар, так и класс vector являются шаблонными. map <string/vector<course> > StudentSchedule; Контейнер map использует для ключа тип string, а для значения — тип vector. Контейнер vector содержит объекты определенного пользователем типа course. Контейнер тар может отобразить соответствие между значениями двух любых типов данных, а контейнер vector содержать значения любого типа данных. map <int, vector <string> > Соответствие между числом и вектором строк map <int/ string> > Соответствие между числом и строкой vector <student_schedule> Вектор объектов класса student_schedule vector <map <int,string> > Вектор отображений, которые устанавливают соответствие между числом и строкой Шаблонные классы также представляются как прямоугольники. Параметризованный тип представляется как прямоугольник (меньшего размера), начертанный штриховой линией и расположенный в правом верхнем углу прямоугольника класса. Шаблонный класс может быть связанным или несвязанным,. При представлении несвязанного шаблонного класса в штриховом прямоугольнике отображается прописная буква Т, означающая несвязанный параметризованный тип. Для представления связанного шаблонного класса существует два способа. Один из них состоит в использовании символа класса, содержащего синтаксис C++ для объявления и связывания шаблонного класса, например: vector <string> Этот вариант называется неявным связыванием. В другом способе используется стерео тип зависимости bind (связать). Этот стереотип задает источник, который реализ ет шаблонный класс посредством использования реального параметризованного па. Этот вариант называется явным связыванием. Шаблонный объект является ре зацией шаблонного класса. Он обладает отношением зависимости с шаблон классом. С помощью стереотипа связать указывается имя параметра-типа. В штр ховом прямоугольнике отображаются соответствующие типы данных. Шаблон объект можно также рассматривать как детализацию шаблонного класса. Детал ция — это общий термин, означающий более высокий уровень представления инф г мации о том, что уже существует. Стереотипный индикатор «связать» детал рует шаблонный класс посредством реализации параметризованного типа. Спо представления шаблонного класса для контейнера тар представлены на рис. 10.4-
10.1. Визуализация структур 347 а) несвязанный класс map p тар тар б) связанный класс тар тар <string, vector<courses» г , - string . I vector<courses» ' map I string . ~| vector<courses> ' map | «bind» (реальные параметры) StudentSchedule Рис. 10.4. Способы представления связанного и несвязанного шаблонного класса 10.1.2. Отношения между классами и объектами Язык UML определяет три типа отношений между классами: • зависимости; • обобщения; • ассоциации. Зависимость определяет отношение между двумя классами. Если один класс зависит от другого, это означает, что изменение, внесенное в независимый класс, может повлиять на зависимый от него класс. Обобщение— это отношение между некоторой общей конструкцией и более конкретным типом этой конструкции. Под общей конструкцией подразумевается родительский класс (или суперкласс), а под более конкретным ее типом— сыновний класс (или подкласс). Потомок наследует свойства, атрибуты и операции родителя и может при этом определять собственные атрибуты и операции. Сыновний класс выводится из родительского, и его можно использовать в качестве заменителя родительского класса. Класс, не имеющий родителей (предков), на- ывается корневым, или базовым классом. Ассоциация— это структурное отношение, ко- орое означает, что объекты одного типа связаны с объектами другого типа, ссоциации между объектами двунаправлены. Например, если объект 1 связан с объ- ом I, то объект 2 связан с объектом 1. Ассоциация между двумя элементами ример, классами) называется бинарной связью, а между п элементами — п-арной. сисЪ ВИСИМость> обобщение и ассоциацию можно рассматривать как различные клас- Ще ~аЦИИ отношений, поскольку существует множество типов зависимостей, обоб- ИМе и асс°Циаций, которые можно определить. Каждая классификация отношений (нач ств^нный символ представления. Таким символом является отрезок прямой увенч анныи сплошной или пунктирной линией) между элементами, который может Ний отг» ЬСЯ стРелкои некоторого типа. Для более детального определения отноше- Ми ("vk КИ ПРЯМЫХ могут дополняться стереотипами и специальными обозначения- Сания имтеНИЯМИ"^ Стереотип— это метка, используемая для более подробного опи- Ки* и оа. ЭЛемента- Он представляется в виде имени, заключенного в угловые скоб- °пИсани Ме1^аемого наД элементом или рядом с ним. Например, на рис. 10.4 для <<bi Я Щаблонного объекта стереотип (<<связать>>)
348 Глава 10. Визуализация проектов параллельных и распределенных систем размещен рядом со стрелкой, которая отображает зависимость используемых об тов. Под "украшениями" понимаются текстовые или графические элементы л к ** ляемые к базовой интерпретации элемента и используемые для Документирован В сведений о спецификации элемента. Например, ассоциация отображается в виле * « » ^>^~ ОТ- резка сплошной линии между элементами. Агрегирование — это тип ассоциации торый выражает отношение "целое-часть". Для отображения агрегирования испо зуется отрезок сплошной линии, у которого один конец (прилегающий к "целому" элементу) венчается полым ромбом. Зависимость обозначается пунктирной направленной линией (со стрелкой), кот рая указывает на зависимую конструкцию. Отношение зависимости следует применять в случае, когда одна конструкция использует другую. Обобщение обозначается сплошной направленной линией со стрелкой, указывающей на родительский класс (суперкласс). Отношение обобщения следует применять в случае, когда одна конструкция выведена из другой. Ассоциация обозначается сплошной линией, которая соединяет одинаковые или различные конструкции. Отношение ассоциации следует применять в случае, когда одна конструкция структурно связана с другой. Некоторые стереотипы и ограничивающие условия, которые применяются к зависимостям, приведены в табл. 10.2. Эти стереотипы используются для отображения зависимостей между классами, интерактивными объектами, состояниями и пакетами. Стереотипы и ограничивающие условия, которые могут применяться к обобщениям и ассоциациям, приведены в табл. 10.3 и 10.4. Если стереотипы используют графические "украшения", они показаны в таблицах. Таблица 10.2. Стереотипы, применяемые к зависимостям Зависимость [источник!- — ->| приемник] Описание « friend » (« друг ») Стереотип Обусловливает, что: « bind » источник реализует шаблонный приемник, используя ре- (« связать ») альные параметры видимость источника распространяется на содержимое приемника « instanceOf » источник является экземпляром приемника; используется (« экземпляр ») для определения отношений между классами и объектами « instantiate » источник создает экземпляры приемника; используется для (« создать экземпляр ») определения отношений между классами и объектами « refine » источник представляет более высокий уровень детализа- (« уточнить ») Ции, чем приемник; используется для определения отношений между производным и базовым классами << use » источник зависит от открытого (public) интерфейса (« использовать ») приемника « become » объект-приемник совпадает с объектом-источником, но (<< стать ») в более поздний период жизненного цикла объекта; приемник может иметь другие значения, состояния и пр. ^
10.1. Визуализация структур 349 Окончание табл. 10.2 Зависимость l-pjgPoMHHK} ---->! приемник] « call » (« вызвать ») « сору » (« копировать ») « access » (« получить доступ ») « extend » (« расширить ») « include » (« включить ») Описание объект-источник вызывает метод приемника объект-приемник является точной и независимой копией объекта-источника исходному пакету предоставляется право ссылаться на элементы приемного пакета данный прецедент приемника расширяет поведение источника данный прецедент источника может включать прецедент приемника Ассоциации имеют еще один уровень детализации, который может быть применен к стереотипам, перечисленным в табл. 10.4: Имя Ассоциация может иметь имя (название), которое используется для описания природы отношений. К имени может быть добавлен треугольник, указывающий направление, в котором должно читаться имя. Роль Роль обозначает функцию, которую выполняет класс, представленный на одном конце линии ассоциации, относительно класса, представленного на другом конце этой линии. Множественность Обозначение множественности может использоваться для указания количества объектов, которые могут быть связаны с помощью данной ассоциации. Множественность можно отображать на обоих концах линии ассоциации. Передвижение Передвижение по ассоциации может быть однонаправленным, если объект 1 связан с объектом 2, но объект 2 не связан с объектом 1. ^Таблица 10.3. Стереотипы и ограничивающие условия, которые могут применяться к обобщениям Описание 9тереотип « implementation » (« Реализация ») ^Раничение { complete} Обусловливает, что: потомок наследует реализацию родителя, но не делает открытыми (public) его интерфейсы и не поддерживает их все потомки в обобщении получили имена, и никаких дополнительных потомков больше не было выведено
350 Глава 10. Визуализация проектов параллельных и распределенных chctgi Окончаниетпа^ ,п э Обобщение Описание | ПОТОМОК | [>| РОДИТЕЛЬ] Ограничение {incomplete} ({неполнота}) Ограничение {disjoint} ({несовместимость}) Ограничение {overlapping} ({перекрытие}) не все потомки в обобщении получили имена, и дополн тельные потомки могут быть выведены объекты родителя не могут иметь больше одного потомкя используемого в качестве типа объекты родителя могут иметь больше одного потомка используемого в качестве типа Таблица 10.4. Стереотипы и ограничивающие условия, которые могут применяться к ассоциациям Ассоциация Описание I ПГГКГИТ 1 I. .— J nRT->FKT Ъ I Тип navigation (передвижение) I ОБЪЕКТ :юп (пер* п—*п ОБЪЕКТ 2 Тип aggregation (агрегирование) I ЧАСТЬ \k -о| ЦЕЛОЕ"! Тип composition (композиция) | ЧАСТЬ \< 1#| ЦЕЛОЕ~~| Ограничение {implicit} ({неявное}) Ограничение {ordered} ({упорядоченность}) Свойство {changeable} ({модифицируемость}) Свойство {addOnly} ({расширяемость}) Свойство {frozen} ({жесткость}) Описывает однонаправленную (нереверсивную) ассоциацию, при которой объект 1 связан с объектом 2, но объект 2 не связан с объектом 1 Описывает связь "целое-часть", при которой "часть" во время своего существования связана не только с одним "целым" Описывает связь "целое-часть", при которой "часть" во время своего существования может быть связана только с одним "целым" Обусловливает, что отношение является концептуальным Обусловливает, что объекты на одном конце ассоциации упорядочены Описывает, что может быть добавлено, удалено и изменено между двумя объектами Описывает новые связи, которые могут быть добавлены к объекту на противоположном конце ассоциации Описывает связь, которая после добавления к объект) на противоположном конце ассоциации не может быть изменена или удалена ______ 10.1.2.1. Интерфейсные классы Интерфейсный класс используется для модификации интерфейса другого кла или множества классов. Такая модификация упрощает использование класса, Де его более функциональным, безопасным или семантически корректным. Пример" интерфейсных классов могут служить адаптеры контейнеров, которые являю
10.1. Визуализация структур 351 ггъю стандартной библиотеки шаблонов (Standard Template Library — STL). Адаптеры беспечивают новый открытый (public) интерфейс для таких контейнеров, как deque //fouble-flided queue— очередь с двусторонним доступом), vector (вектор) и list / писок). Рассмотрим пример. В листинге 10.1 представлено определение класса stack, торый ИСпользуется в качестве интерфейсного для модификации класса vector. // листинг 10.1. Использование класса stack в качестве ., интерфейсного класса template < class Container > class stack{ public: typedef Container::value_type value_type; typedef Container::size_type size_type; protected: Container c; public: bool empty(void) const (return c.empty();} size_type size (void) const {return c.sizeO; } 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 объявляется путем задания типа Container. stack < vector< T > > Stack; В данном случае типом Container является класс vector, но в качестве класса реализации для интерфейсного класса stack (вместо класса vector) можно использовать любой контейнер, который определяет следующие методы: empty () size() back() Push.back() Pop.back() Класс stack поддерживает семантически корректный интерфейс, традиционно принятый для стеков. Существует несколько способов отображения интерфейса. Один из них — круг, рядом с которым (чаще — под ним) записывается имя интерфейсного класса. Этот спо- показан на рис. 10.5, а. Для отображения операций класса stack можно также ис- Льзовать символическое обозначение класса (см. рис. 10.5, б). Здесь над именем сса отображается индикатор стереотипа «interface>>, обозначающий, что "- интерфейсный класс. Имя интерфейсного класса может начинаться с буквы "I", гда все операции этого класса будут заметнее отличаться от других классов. Для отображения отношений между классами stack и vector можно использо- Понятие реализации. Реализация — это семантическое отношение между классу ' в к°тором один из них предлагает "контракт" (интерфейсный класс), а другой Шолняет (класс реализации). В нашем примере класс stack определяет кон- К0 ' а класс vector его выполняет. Отношение реализации отображается отрез- Унктирной линии между двумя прямоугольниками классов с крупной полой
352 Глава 10. Визуализация проектов параллельных и распределенных cncrei стрелкой, указывающей на интерфейсный класс, т.е. на класс, который опрел контракт (рис. 10.5, в). Это изображение читается так: "Класс stack реализу ^ классом vector". Отношение между интерфейсным классом и его реализато °Я (средством реализации) также можно отобразить в виде "леденца на палоч* (рис. 10.5, г). Класс stack может быть реализован не только классом vector и классами list или deque. а stack «interface » stack value_type : Container::value_type size_type: Container::size_type Container empty(void): bool {const} size(void): size_type {const} top(void): value_type& top : const value_type & {const} push(const value_type& x): void pop(void): void a) класс stack в качестве интерфейсного класса б) использование стереотипа « interface » (« интерфейс ») «interface » stack value_type : Container::valueJtyp size_type: Container::size_type Container w-- empty(void): bool {const} size(void): size_type {const} top(void): value_type& top : const value_type & {const} push(const value_type& x): void pop(void): void в) класс stack реализуется посредством класса vector vector vector empty() size() back() push.back() pop.backQ г) использование "леденца на палочке" для обозначения реализации класса stack о stack Рис. 10.5. Способы представления интерфейсного класса
10.1. Визуализация структур 353 10.1.3. Организация интерактивных объектов Как видите, классы и интерфейсы можно использовать в качестве строительных ков (т.е. базовых элементов) при создании более сложных классов и интерфейсов, распределенной или параллельной системе возможно существование больших сложных структур, сотрудничающих с другими структурами, что создает объединена классов и интерфейсов, работающих вместе над достижением общих целей сис- емЫ- В языке UML такое поведение называется сотрудничеством. Упомянутые выше тооительные блоки могут включать как структурные, так и поведенческие элементы системы- Конкретная задача, которую запрашивает пользователь, может включать множество выполняемых вместе объектов. При этом для выполнения разных задач могу7 использоваться одни и те же объекты, взаимодействующие в разных случаях с различными элементами. Такая коллекция элементов (с учетом взаимодействия между ними) формирует сотрудничество. Понятие сотрудничества состоит из двух частей: структурной части, в которой акцент делается на характере организации и построении сотрудничающих элементов, и поведенческой, в которой основное внимание уделяется взаимодействию между элементами. (Об этом пойдет речь в следующем разделе.) Сотрудничество отображается в виде эллипса (начертанного пунктирной линией), содержащего название варианта сотрудничества. Имя сотрудничества должно быть уникальным. Оно представляет собой существительное или короткую фразу, состоящую из существительных, которые входят в словарный состав моделируемой системы. Структурные и поведенческие части сотрудничества отображаются внутри эллипса сотрудничества. Пример структурной части системы составления расписания показан на рис. 10.6. Структурная часть сотрудничества представляет собой сочетание классов ВАРИАНТЫ СОТРУДНИЧЕСТВА * Интерфейс ^ ' Рассмотрение4 у __ 'Генерирование4 * s запросов / 'ч курсов < ~~\ ответа scheduler г~| РОА black board I I filter_agent I >| SL ScheduleMap schedule_of courses 1 n blackboard k knowledge_source availability_ schedule I degree_plan 1 n course student_proflle Сотрудничество "Рассмотрение курсов" Рис. 10.6. Диаграмма сотрудничества для системы составления расписания
354 Глава 10. Визуализация проектов параллельных и распределенных сист< и интерфейсов, компонентов и узлов. Система, показанная на рис. 10.6, может жать множество вариантов сотрудничества. Каждый вариант сотрудничества vh ^ лен в системе, но его элементы — нет. Элементы одного варианта сотрудничеств **" гут быть использованы в другом варианте за счет иной организации * °" 10.2. Отображение параллельного поведения При отражении поведенческой характеристики системы акцент ставится на ее л намических аспектах. С этой точки зрения нас интересует, как ведут себя элементь системы при взаимодействии с другими элементами той же системы. Именно во взаимодействии одних элементов с другими и проявляются особенности параллелизма Диаграммы, используемые в этом разделе, позволяют смоделировать: • поведение объекта в течение его периода существования; • поведение объектов, которые совместно работают ради достижения конкретной цели; • поток управления с акцентом на определенном действии или последовательности действий; • синхронизацию действий элементов и взаимодействие между ними. В этом разделе также описаны диаграммы, используемые для моделирования распределенных объектов. 10.2.1. Сотрудничество объектов Сотрудничество объектов заключается в привлечении друг друга к работе с целью выполнения некоторой конкретной задачи. Они не вступают в постоянные отношения. Одни и те же объекты могут привлекаться разными объектами для выполнения различных задач. Сотрудничество объектов можно представить в виде диаграммы сотрудничества. Диаграммы сотрудничества имеют структурную и интерактивную части. Структурную часть мы рассмотрели выше. Интерактивная часть отображается в виде графа, вершинами которого являются объекты — участники рассматриваемого сотрудничества. Связи между объектами представляются ребрами. Ребра могут сопровождаться сообщениями, передаваемыми между объектами, вызовами методов и индикаторами стереотипов, которые позволяют подробнее отобразить характер связи. Связь между объектами имеет тип ассоциации. С двумя связанными объектами могу выполняться действия. В результате действия может измениться состояние одного ил двух объектов. Приведем примеры различных типов действий, связанных с объектами. create Объект может быть создан destroy Объект может быть разрушен са11 Операция, определенная в одном объекте, может быть вызвана дрУ гим объектом или им самим return Объекту7 возвращается значение send Объекту может быть послан сигнал
10.2. Отображение параллельного поведения 355 гт и вызове и выполнении любого метода возможно наличие передаваемых пара- в и возвращаемого значения (а также другие действия). МС Яти действия могут иметь место, если принимающий объект видим для вызы- тего. Для объяснения причины видимости объекта можно использовать еле- давшие стереотипы. ociation Объект видим по причине существования ассоциации (самый общий случай) parameter Объект видим, поскольку он является параметром для вызывающего объекта 1ОСа1 Объект видим, поскольку он имеет локальную область видимости для вызывающего объекта global Объект видим, поскольку он имеет глобальную область видимости для вызывающего объекта self Объект вызывает собственный метод Помимо перечисленных, возможно применение и других стереотипов. При вызове некоторого метода возможен вызов других методов иными объектами. Последовательность выполнения операций можно отобразить с помощью комбинации порядковых номеров и двоеточия, отделяющего имя метода от соответствующего номера. Комбинация порядковых номеров выражает последовательность, в которой выполняются операции. Например, на рис. 10.7 показана диаграмма сотрудничества, в которой используются порядковые номера. MainObject 1: « create » 2: Value := performAction (ObjectD) {local}! 1.1: initialize() {self} 1.1.1: initializeBQ {local} ObjectA 2.1: Value :=doAction() {parameter}, ObjectB 1.1.2: initializeC() {local} ObjectC ObjectD ис. 10.7. Диаграмма сотрудничества, использующая порядковые номера для обозначения последовательности выполнения операций Как показано на рис. 10.7, объект MainObject выполняет две операции в следующей последовательности: *• «create» Value := performAction(ObjectF) ри выполнении операции 1 объект MainObj ect создает объект Obj ectA. Объект в локален по отношению к объекту MainObject (поскольку имеет место ение объектов). Это инициирует первую последовательность операций во вло- Ст м Потоке управления. Для обозначения всех операций этой последовательно- Нен1 °льзУется число 1, за которым следует число, отражающее порядок их выпол- • 1так, первая операция последовательности 1 такова: Л: initialize ()
356 Глава 10. Визуализация проектов параллельных и распределенных систем Объект Ob j ectA вызывает собственный метод. Выполнение объектом собствен метода выражается соединительной линией, связывающей объект с самим собой и Г° дикатором стереотипа {self} ({сам}). Операция Ob j ectA: : initial ize() также **" пускает другую последовательность действий: а" 1.1.1: initializeB() 1.1.2: initializeCO В этой последовательности два других объекта (которые локальны по отношению к объекту Ob j ectA) инициализируются посредством вызова соответствующих методов инициализации. Операция 2: performAction(ObjectD) является началом еще одной вложенной последовательности действий. Объекту Ob j ectA передается объект ObjectD. Объект Ob j ectA вызывает операцию, определенную в объекте Ob j ectD: 2.1: doAction() Объект Ob j ectA имеет право вызвать эту операцию, поскольку объект ObjectD является параметром (переданным объектом MainObject), как отмечено стереотипом {parameter}. В результате выполнения этой последовательности действий объекту Ob j ectA возвращается значение и объекту MainObject также возвращается значение. Помимо комбинаций порядковых номеров, обозначение этих вложенных потоков управления можно усилить с помощью линии с зачерненной стрелкой, указывающей в направлении выполнения последовательности действий. 10.2.1.1. Процессы и потоки Процесс — это часть работы (кода), создаваемая операционной системой. Он включает один или несколько потоков, выполняемых в его адресном пространстве. Если потоков несколько, то один из них является основным (main thread). Несколько процессов могут выполняться параллельно. Потоки одного процесса могут выполняться параллельно с потоками других процессов. При использовании языка UML для отображения функционирования процессов и потоков каждый независимый поток выполнения считается активным объектом. Активный объект — это объект, который является владельцем процесса или потока. Каждый активный объект может активизировать то, чем он владеет. Активный класс— это класс, объекты которого являются активными. Активные классы можно использовать для моделирования группы процессов или потоков, которые разделяют одни и те же члены данных и методы. Объекты конкретной системы могут не иметь однозначной взаимосвязи с активными объектами. Как упоминалось в главах 3 и 4, при разделении программы на процессы и потоки следует учитывать, что методы объектов могут выполняться в отдельном процессе или отдельных потоках. Следовательно, при моделировании одного такого объекта его можно представить в виде нескольких активных объектов. Отношение между статическими и активными объектами можно изобразить с помощью диаграммы взаимодействия. В системе может быть несколько PVM- или MPI-задач, ил процессов, и каждую из них можно представить непосредственно как активный объект. Язык UML позволяет представить активный объект или класс таким же способом, как статический объект, за исключением того, что периметр прямоугольника, оо значающего этот объект или класс, обводится более жирной линией. В этом случа можно также использовать следующие два стереотипа: process thread
10.2. Отображение параллельного поведения 357 U яикаторы этих стереотипов позволяют отобразить различие между двумя типа- 'тивных объектов. На рис. 10.8 показана PVM-задача в виде активного класса М* - явного объекта. Диаграмма сотрудничества может состоять из активных объектов pvm_stream mutex Mutex; int Taskld; tnt Messageld; operator «(string X): pvm_stream & operator «(int X): pvm_stream & operator «(float X): pvm_stream & operator»(string X): pvm_stream & Рис. 10.8. Активный объект и класс pvm_stream 10.2.1.2. Отображение нескольких потоков выполнения и взаимодействия между ними В параллельной и распределенной системе возможно существование нескольких потоков выполнения, которые относятся к одному или нескольким процессам. Эти процессы и потоки могут выполняться в одной компьютерной системе с несколькими процессорами либо распределяться между несколькими различными компьютерами. Для представления каждого потока выполнения используется активный объект или класс. При создании активного объекта инициируется независимый поток выполнения. При разрушении активного объекта этот поток прекращает свое существование. Моделирование потоков в системе позволяет успешно осуществить управление, синхронизацию и взаимодействие между ними. В диаграмме сотрудничества для идентификации потоков используются числа и стрелки со сплошной заливкой наконечника. В диаграмме сотрудничества, которая состоит из активных объектов параллельной системы, имя активного объекта пред- тавляется порядковыми числами операций, выполняемых активным объектом. Ак- вныи обьект может вызвать метод, определенный в другом объекте, и приостано- ь выполнение до тех пор, пока этот метод не завершится. Стрелки используются только для отображения направления хода выполнения потока, но и природы его дения. Стрелки со сплошной заливкой наконечника используются для представ- я синхронного вызова, а стрелка с однореберным наконечником — для представ- асинхронного вызова. Поскольку один и тот же метод может быть вызван сразу Мо- * лькими активными объектами, то для описания синхронизации этого метода '"о использовать такие его свойства: Sequential Warded c°ncurrent Кото ^ ^ представлена диаграмма сотрудничс ства нескольких активных объектов, V с совместными усилиями" создают расписание студента. Объект blackboard
358 Глава 10 Визуализация проектов параллельных и распределенных систем FilterAgent 1: X := possibleSchedules() FilterAgent 2: suggestedSchedules(A) MajorAgent 1: X := currentDegreePlan() MajorAgent 2: Y := coursesTaken() MajorAgent 4: suggestionsForMajor(Z) FilterAgent SchedulerAgent Blackboard MajorAgent MajorAgent 3: : currentScheduleOfCoursesQ {self} SchedulerAgent 1: Blackboardl: X := masterList() produceMasterList() SchedulerAgent 2: possibleSchedules(D) ScheduleOfCourses — MinorAgent 3: I = currentScheduleOfCoursesQ MinorAgent 1: X := currentDegreePlan() MinorAgent 2: Y := coursesTaken() MinorAgent 4: suggestionsForMinor(B) MinorAgent Blackboard SuggestionForMajor SuggestionForMinor Schedule DegreePlan possibleSchedules(vector<ScheduleMap> &): void {guarded} possibleSchedules(void): vector<ScheduleMap> & {guarded} suggestionsForMajor(const courses &X): void {concurrent} suggestionsForMinor(const courses &X): void {concurrent} currentDegreePlan(void): ScheduleMap & {concurrent} produceMasterList(void) masterList(void): ScheduleMap & {guarded} suggestedSchedules(void): vector<ScheduleMap &> {guarded} suggestedSchedules(vector<ScheduleMap &>): void {guarded} II... Рис. 10.9. Диаграмма сотрудничества статических и активных объектов в системе составления расписаний используется для регистрации результатов предварительной работы и ее координа ции, а также представления итогового расписания, сгенерированного решателям задач активных объектов, именуемых в данном случае агентами (agent). Ma j orAgent Создает список имеющихся основных курсов MinorAgent Создает список имеющихся непрофилирующих курсов Fi Iter Agent Фильтрует список курсов и генерирует список возможных курс° ScheduleAgent Генерирует несколько вариантов расписаний на основе списка возможных курсов
10.2. Отображение параллельного поведения 359 Объект schedule_of „courses содержит все имеющиеся курсы. Объекты blackboard и schedule_of„courses доступны при параллельном обращении со стороны нескольких агентов. В данном варианте сотрудничества К< эти объекта видимы для всех агентов. Агенты MajorAgent, MinorAgent, • 1 terAgent и ScheduleAgent вызывают методы объекта blackboard. Агенты 'orAgent и MinorAgent вызывают методы объекта schedule_of_courses. При агенты Maj orAgent и MinorAgent имеют аналогичную последовательность общений к объектам blackboard и schedule_of „courses. Mai orAgent 1: currentDegreePlan () MinorAgent 1: currentDegreePlan () Ma-jcrAgent2 : coursesTaken () MinorAgent2 : coursesTaken () Mai orAgent 3 : scheduleOf Courses () MinorAgent 3 : scheduleOf Courses () MajorAgent4 : suggestionsForMaj or () MinorAgent4 : suggestionsForMinor () Как видите, к имени активного объекта, который вызывает эти методы, присоединяется порядковый номер. Оба объекта параллельно вызывают методы объектов blackboard и schedule_of_courses. Все эти методы параллельно синхронизированы и защищены от одновременного вызова. Методы masterList () и possibleCourses () имеют свойство guarded. Одни объекты могут модифицировать содержимое курсов, а другие— считывать его. Поэтому методы masterList () и possibleCourses () защищены разрешением только последовательного к ним доступа (EREW). 10.2.2. Последовательность передачи сообщений между объектами В то время как в диаграмме сотрудничества основное внимание уделяется структурной организации и взаимодействию объектов, совместно выполняющих некоторую задачу или реализующих прецедент (вариант использования системы), в диаграмме последовательностей акцент ставится на временном упорядочении вызовов методов или процедур, составляющих данную задачу или прецедент. В диаграмме последовательностей имя каждого объекта или конструкции отображается в собственном прямоугольнике. Все прямоугольники размещаются в верхней части диаграммы, вдоль ее оси X. В диаграмму следует включать только основных исполнителей прецедента и наиболее важные функции, в противном случае диаграмма будет перенасыщена деталями и утратит вою полезность. Объекты упорядочиваются слева направо, начиная с объекта или процедуры, которая является инициатором действия для большинства второстепенных ектов или процедур. Вызовы функций отображаются вдоль оси Y сверху вниз в по- Дке возРастания значения времени. Под каждым прямоугольником наносятся верти- ьные линии, представляющие "жизненные пути" (линии жизни) объектов. Стрелки 1лощной заливкой наконечника, направленные от линии жизни одного объекта - ии жизни другого, обозначают вызовы функций или методов (причем такая стрел- им ГД- напРавлена от инициатора вызова). Стрелки с "реберными" наконечниками Ил1 °°Ратное направление (т.е. к инициатору вызова), обозначая возврат из функции Хо * ОДа- Каждый вызов функции помечается ее именем. Помимо имени, при необ- г •, . СТи от°бражается информация об аргументах и условиях вызова, например:
360 Глава 10. Визуализация проектов параллельных и распределенных систем Функция или метод не выполнится, если заданное условие не будет истинным М тоды, которые должны быть вызваны несколько раз (например, при считывании е чений из структуры), предваряются признаком итерации (*). а" На рис. 10.10 показана диаграмма последовательностей для объектов системы состя ления расписания. Чтобы не перегружать эту демонстрационную диаграмму, количеств объектов в ней ограничено лишь тремя. В диаграммах последовательностей для паоал лельных объектов или процедур используются символы активизации. Символ активизаци представляет собой прямоугольник, отображаемый на линии жизни объекта. Наличи символа активизации означает активность объекта или процедуры. Символы активизации используются в случае, когда объект обращается к другому объекту без блокирования. Тем самым становится понятно, что объект или процедура продолжает выполняться или быть активной. На рис. 10.10 показано, что объект blackboard всегда активен. Он порождает объект schedule_agent и не блокируется. Объект schedule_agent вызывает метод blackboard.masterList О и ожидает получения от него списка курсов. Как упоминалось выше, возвращение метода обозначается стрелкой с "реберным" наконечником. Метод schedule„ag©nt затем вызывает один из собственных методов createSchedules (). Для обозначения вызова объектом одного из собственных методов используется специальный символ, состоящий из символа активизации и стрелки вызова. Символ активизации при этом накладывается на уже имеющийся символ активизации. Линия выходит из исходного символа активизации, а ее стрелка указывает на дополнительный символ. После передачи объектом schedule_agent результатов своей работы путем вызова метода blackboard.possibleSchedule () объект blackboard аннулирует его. Аннулирование обозначается большим символом "X" в конце линии жизни объекта. Стрелка вызова метода, исходящая из объекта blackboard и указывающая на символ "X", означает, что инициатором аннулирования является объект blackboard. Объект blackboard затем порождает объект Zilter_agent и опять-таки не блокируется. Объект f ilter_agent вызывает метод blackboard.poseibleSchedules () и ожидает получения от него вариантов расписаний. Объект f ilter__agent после этого вызывает один из собственных методов f ilterCourees (). После передачи результатов объект f ilter_agent ликвидирует себя. Объект blackboard последовательно вызывает собственные методы organ!zeSolut ion () и updateRecords (), а затем также ликвидируется. 10.2.3. Деятельность объектов Язык UML можно использовать для моделирования видов деятельности объектов — участников конкретной операции или прецедента. В этом случае строится диаграмма (видов) деятельности, которая представляет собой блок-схему, отражающую последовательные и параллельные действия (или виды деятельности) объектов, принимающих участие в выполнении конкретной задачи. На этой диаграмме с помощью стрелок указывается направление передачи управления для соответствующих видов деятельности. В то время как в диаграммах сотрудничества основное внимание уделяется передаче управления от объекта к объекту, в диаграммах последовательностей •* временному упорядочению потоков выполнения, в диаграммах деятельности акцен ставится на передаче управления от одного действия (или вида деятельности) к ДрУ гому. В результате действия (или вида деятельности) изменяется состояние объект или возвращается некоторое значение. Содержимое действия (или вида деятельн сти) называется состоянием действия (или вида деятельности). Состояние объек представляется в этом случае как конкретный момент в потоке выполнения.
10.2. Отображение параллельного поведения 361 ^blackboard 1 produceMasterList() « create » :schedule_agent masterLJst() I Возврат списка ' possibleSchedules(D) Ликвидация I « create » :filter_agent possibleSchedulesQ Возврат расписаний ^ ' Tt filterSchedules() Возврат Ъ Ликвидируется organizeSo!utions() гЦ updateRecords() X c- 10.10. Диаграмма последовательностей некоторых объектов системы составления расписаний
362 Глава 10. Визуализация проектов параллельных и распределенных систем Действие и деятельность имеют различия. Действия не могут быть логически п вергнуты декомпозиции или прерваны другими действиями или событиями. Приме ми действий могут служить создание или разрушение объекта, вызов метода или фут* ции. Деятельность можно разложить на составные части (другие виды деятельности) Б качестве примеров деятельности можно назвать программу, прецедент или процегш ру. Деятельность можно прервать событием, другим видом деятельности или действием Диаграмма (видов) деятельности представляет собой граф, узлы которого обозначают действия или виды деятельности, а ребра— безусловные переходы. Безусловность перехода состоит в том, что для того, чтобы он произошел, не требуется никакого события. Переход происходит сразу же по завершении предыдущего действия или вида деятельности. Эта диаграмма содержит ветви решений, символы начала, останова и синхронизации, которые объединяют несколько действий (или видов деятельности) или обеспечивают их разветвление. Состояния действий и видов деятельности представляются аналогичным образом. Для представления состояния действия или деятельности в языке UML используется стандартный символ блок-схемы, который обычно служит для отображения точек входа и выхода. Этот символ применяется независимо от типа действия или деятельности. Мы предпочитаем использовать стандартные символы блок-схемы, которые позволяют отличить действия ввода-вывода (параллелограмм) от действий обработки или преобразования (прямоугольник). Описание действия или вида деятельности, т.е. имя функции, выражения, прецедента или программы, отображается в соответствующем элементе графа. Состояние деятельности может дополнительно включать отображение действий входа и/или выхода. Действие входа— это действие, которое происходит, когда имеет место вход в состояние деятельности, а действие выхода — это действие, которое происходит непосредственно перед выходом из состояния деятельности. Эти действия являются первым и последним действиями соответственно, которые должны быть выполнены в состоянии активности. По завершении одного действия происходит немедленный переход к началу следующего. Переход обозначается стрелкой с двухреберным наконечником, направленной от одного состояния к другому (следующему). Переход, который указывает на состояние, называется входящим, а переход, обозначающий выход из состояния, — выходящим. Прежде чем произойдет выходящий переход, должно выполниться действие выхода, если таковое предусмотрено. Действие входа, если таковое предусмотрено, выполняется после того, как произойдет входной переход. Начало потока выполнения представляется в виде крупной закрашенной точки. Первый переход ведет из закрашенной точки к первому состоянию диаграммы. Точка останова, или состояние останова, диаграммы деятельности представляется крупной закрашенной точкой, заключенной внутри окружности. Диаграммы деятельности подобно блок-схемам имеют символ решения. Символ решения имеет форму ромба с одним входящим переходом и двумя (или более) выходящими переходами. Выходящие переходы сопровождаются условиями, которые определяют дальнейшее направление передачи управления. Это условие представляет собой обычное булево выражение. Выходящие переходы должны охватывать все возможные варианты ветвления. На рис. 10.11 показан символ решения, используемый при определении необходимости построения источника знаний. Иногда после завершения одного действия или вида деятельности начинается па раллельное существование нескольких потоков, выполняющих различные последов» тельности действий или виды деятельности. В отличие от блок-схемы, язык UM
10.2. Отображение параллельного поведения 363 пеляет символ, который можно использовать для представления момента, на- ° яя с которого несколько потоков выполняются параллельно. Для отображения 4 о момента используется символ синхронизации, который также служит для £ значения соединения параллельных путей. Этот символ имеет форму жирной онтальной линии с несколькими выходящими переходами (разветвление) или сколькими входящими переходами (соединение). Переходы, выходящие из ли- синхронизации, означают состояние действия или деятельности, которое прилит к выполнению нескольких потоков. Переходы, входящие в линию синхрони- ации означают необходимость синхронизации нескольких потоков, а линия синхронизации в этом случае используется для отображения ожидания до тех пор, пока все ветви не соединятся в единую ветвь (поток). Пример разветвления потоков и их соединения показан на рис. 10.12. РЕШЕНИЕ О [ИЗ нужен] [ИЗ не нужен] Сгенерировать сводный список курсов Создать ИЗ Рис. 10.11. Символ решения, используемый при определении необходимости построения источника знаний (ИЗ) Создать ИЗ РАЗВЕТВЛЕНИЕ Получить текущий план ^выдачи дипломов/ Получить курсы 'обучения СОЕДИНЕНИЕ Сгенерировать список основных курсов Получить 'расписание, курсов /И~% 10-12. Пример разветвления потоков из линии синхронизации и их соединения *"-источник знаний)
364 Глава 10. Визуализация проектов параллельных и распределенных систем При создании объекта MajorAgent вызывается его конструктор, котоп (см. рис. 10.12) инициирует три параллельных потока выполнения. После заверщен этих трех действий потоки соединяются в единый поток, назначение которого сое ит в выполнении действия по созданию списка основных курсов. Эту диаграмму можно разбить на три отдельных раздела, именуемь "плавательными дорожками". В каждой такой дорожке происходят действия или видь деятельности конкретного объекта, компонента или прецедента. "Плавательные дорожки" разделены на диаграмме вертикальными линиями. Одно действие (или вит деятельности) может происходить только в одной дорожке. Линии переходов и линии синхронизации могут пересекать одну или несколько дорожек. Действия или виды деятельности, обозначенные в одной и той же или различных дорожках, но находящиеся при этом на одном уровне, являются параллельными. Диаграмма деятельности с "плавательными дорожками" показана на рис. 10.13. Назначение этой диаграммы деятельности — смоделировать последовательность действий объекта blackboard, который генерирует сводный список курсов для системы составления расписаний. Объект blackboard (см. рис. 10.13) сначала принимает решение о том, нужно ли создавать объект MajorAgent. Если нужно, то вызывается конструктор объекта MajorAgent. Это приводит к созданию трех ветвей передачи управления. В двух из них действия выполняет объект blackboard ("получает текущий план выдачи дипломов" и "считывает курсы обучения"), а в третьей— объект ScheduleofCourses ("считывает расписание курсов"). Все эти действия— входные (поэтому для их обозначения используются параллелограммы). Затем три ветви объединяются в одну, и объект Maj orAgent выполняет действие, которое состоит в создании списка основных курсов. После того как объект blackboard выполнит "свое" действие, а именно "получит список основных курсов", происходит удаление объекта MajorAgent. Объект blackboard "генерирует сводный список курсов", и на этом деятельность рассматриваемых объектов прекращается. 10.2.4. Конечные автоматы С помощью конечных автоматов отображается поведение единой логической конструкции, определяющей последовательность ее преобразований в качестве ответов на внутренние и внешние события в течение ее линии жизни. Такой единой логической конструкцией может быть система, прецедент или объект. Конечные автоматы используются для моделирования поведения одного элемента. Элемент может реагировать на такие события, как процедуры, функции, операции и сигналы. Элемент может также отвечать на факт истечения времени. Когда происходит подобное событие, элемент реагирует на него определенным видом деятельности или путем выполнения некоторого действия, которое приводит к изменению состояния этого элемента или созданию некоторого артефакта. Выполняемое в этом случае действие должно зависеть от текущего состояния элемента. Под состоянием понимается сит>а ция, которая создается в результате выполнения элементом некоторого действия ил его ответа на некоторое событие в течение его линии жизни. Конечный автомат можно представить в виде таблицы или ориентированного графа' именуемого диаграммой состояний. На рис. 10.14 изображена UML-диаграмма состо ний для конечного автомата некоторого процесса. На этом рисунке показаны состо^ ния, через которые проходит процесс в период своей активности. Рассматриваемы
10.2. Отображение параллельного поведения 365 Blackboard [ИЗ нужен) [ИЗ не нужен] Получить текущий план /выдачи дипломов/ Получить курсы обучения Назначить список основных курсов —О Сгенерировать сводный список курсов MajorAgent Я Создать ИЗ Сгенерировать список основных курсов Ликвидировать ИЗ ScheduleOfCourses 'Получить г расписание/ курсов пос2 Диаграмма деятельности с "плавательными дорожками" леДовательность действий в системе составления расписаний отображающая Н сс м°жет иметь в системе четыре состояния: готовности, выполнения, ожида- Вос °СТанова. К наступлению этих четырех состояний процесса могут привести ь событий. Три из них происходят только при выполнении определенного
366 Глава 10. Визуализация проектов параллельных и распределенных систем условия. Событие блокирования происходит только в том случае, если процесс запп шивает операцию ввода-вывода или ожидает наступления некоторого события. Собь тие блокирования инициирует переход процесса из состояния выполнени ("бодрствования") в состояние ожидания ("сна"). "Пробуждение" процесса происходит или из-за события пробуждения или в результате завершения операции ввода- вывода. Событие пробуждения заставляет процесс перейти из состояния ожидания (исходного состояния) в состояние готовности (целевое состояние). Событие выхода происходит только в случае, если процесс выполнит все свои инструкции. Событие выхода заставляет процесс перейти из состояния выполнения в состояние ожидания Остальные события относятся к категории внешних и не подвластны процессу. Они возникают по некоторым внешним причинам, вынуждающим процесс перейти из некоторого исходного в некоторое целевое состояние. Сигнал перезапуска Выход [выполнение завершено] ^ Завершение Блокирование операции ввода-вывода Рис. 10.14. Диаграмма состояний для процессов Диаграммы состояний используются для моделирования динамических аспектов объекта, прецедента или системы. Диаграммы последовательностей, видов деятельности, сотрудничества и (добавленная) диаграмма состояний используются для моделирования поведения системы (или объекта) в период ее (его) активности. Структур" ная часть диаграммы сотрудничества и диаграмма классов позволяют смоделировать структурную организацию объекта или системы. Диаграммы состояний прекрасно подходят для описания поведения объекта вне зависимости от конкретного прецедента. Их следует использовать не для описания поведения нескольких взаимодействующих или сотрудничающих объектов, а для описания поведения объекта, системы или прецедента, который претерпевает ряд преобразований, причем именно в сл> чае, когда одно преобразование может быть вызвано несколькими событиями. "еЧ идет о таких логических конструкциях, которые весьма активно реагируют на внут ренние или внешние события.
10.2. Отображение параллельного поведения 367 В диаграмме состояний узлы представляют состояния, а ребра— переходы. Со- ния обозначаются прямоугольниками с закругленными углами, внутри которых сываются названия состояний. Переходы изображаются линиями с двухребер- стрелками, связывающими исходное и целевое состояния, причем острие елки должно указывать на целевое состояние. Существуют также начальное и конеч- р состояния. Начальное состояние представляет собой начало работы конечного томата. Оно обозначается черной точкой с ребром перехода к первому состоянию ав- омата. Конечное состояние, означающее, что система, прецедент или объект достигли онца своей линии жизни, отображается черной точкой, встроенной в окружность. Состояние имеет несколько частей (они перечислены в табл. 10.5). Состояние можно представить простым отображением его названия в центре соответствующей вершины диаграммы состояний (прямоугольника с закруглёнными углами). Если внутри этого прямоугольника необходимо отобразить также некоторые действия, то для названия состояния должен быть выделен отдельный раздел в верхней части прямоугольника. Действия перечисляются под этим разделом и должны иметь следующий формат отображения: метка [Условие] / действие или деятельность Рассмотрим пример: do / validate(data) Здесь do — это метка, которая используется для обозначения выполнения указанного действия до тех пор, пока объект находится в данном состоянии. Имя validate (data) — это имя вызываемой функции, a data — имя аргумента, с которым она вызывается. Если действие состоит в обращении к функции или методу, то аргументы желательно указывать. Таблица 10.5. Составные части состояния Части состояния Описание Название Уникальное имя состояния, которое отличает его от других состояний; состояние может не иметь имени Действия входа-выхода Действия, выполняемые при входе в состояние (состояние входа) или при выходе из него (состояние выхода) Подсостояния Вложенные состояния; подсостояния — это составные части состояния, которые могут быть активизированы последовательно или параллельно. Составное состояние, или суперсостояние, — это состояние, которое содержит подсостояния нутренние переходы Переходы, которые совершаются внутри состояния, не вызывая при этом изменения состояния мопереходы Переходы, которые совершаются внутри состояния, не вызывая изменения состояния, но приводящие к выполнению входного, а затем выходного действия сроненные события Список событий, которые происходят, пока объект находится в данном состоянии, но помещаются в очередь и обрабатываются, когда объект пребывает уже в другом состоянии
368 Глава 10. Визуализация проектов параллельных и распределенных систем Условие — это условное выражение, которое приводится к значению ЛОЖЬ ИСТИНА. Если условие дает значение ИСТИНА, выполняется действие или осущ ЛИ вляется деятельность, например: т" exit [data valid] / send(data) Действие выхода (exit) send (data) защищено выражением data valid, кот рое при вычислении может дать ложное или истинное значение. Если при выходе данного состояния это выражение даст значение ИСТИНА, то будет вызвана функци send (data). Использование выражения защиты необязательно. Переходы из одного состояния (объекта, системы или прецедента) в другое происходят при наступлении событий. Существует два вида переходов, которые могут осуществляться без изменения состояния (объекта, системы или прецедента) — это внутренние и самопереходы. Самопереход имеет место, когда возникновение конкретного события вынуждает объект выйти из текущего состояния. При выходе из него объект выполняет действие выхода (если таковое предусмотрено), а затем — действие, связанное с самопереходом (если таковое предусмотрено). Затем объект снова входит в прежнее состояние, выполняя при этом действие входа (если таковое предусмотрено). При внутренних переходах объект вовсе не выходит из текущего состояния и, следовательно, никаких действий (ни входного, ни выходного) не выполняет. На рис. 10.15 показана общая структура состояния, включающая действия входа и выхода, осуществляемую деятельность, а также внутренние и самопереходы. Самопереход обозначается линией, направленной назад к тому же состоянию. Название состояния Действие входа - Деятельность- Внутренний переход- -> entry / действие или функция -> do / деятельность -> поте /действие или функция Действие выхода \--> exit / действие или функция Рис. 10.15. Общая структура состояния Переход между разными состояниями означает, что между ними существует некоторое отношение. В то время, как объект находится в одном (исходном) состоянии, может произойти некоторое событие или могут создаться определенные условия, которые заставят этот объект перейти в другое (целевое) состояние. Таким образом, переход объекта из состояния в состояние инициируется событием. Один переход может иметь несколько параллельно существующих исходных состояний. В этом случае они соединяются перед осуществлением перехода. Один переход также может иметь несколько параллельно существующих целевых состояний, и тогда имеет место разветвление. Составные части перехода перечислены в табл. 10.6. Переход изобра кается линией, направленной от исходного состояния к целевому. Имя инициатора события отображается рядом с переходом. Подобно действиям и видам деятельности, события также могут быть защищены. Переход может быть безусловным, а это зн чит, что для его осуществления не требуется никакого специального события. Пр тг JN-oir т*ч исходного состояния объект немедленно переходит в целевое состояние.
10.2. Отображение параллельного поведения 369 "Таблица Ю.6. Составные части перехода Части перехода Описание 11 Исходное состояние Первоначальное состояние объекта; при оаществлении перехода объект выходит из исходного состояния Целевое состояние Состояние, в которое объект входит после осуществления перехода Событийный Событие, которое инициирует осуществление перехода. Переход инициатор может быть безусловным (т.е. не иметь инициатора), в эгом случае переход происходит сразу же после того, как объект завершит нее свои действия (виды деятельности) в исходном состоянии Защитное условие Булево выражение, связанное С событийным инициатором, которое обеспечивает осуществление перехода только в * 1учае, если при вычислении дает значение ИСТИНА Действие Действие, выполняемое объектом при осуществлении перехода; оно может быть связано с событийным инициатором и/или защитным условием 10.2.4.1. Параллельные подсостояния Подсостояние позволяет еще больше упростить описание модели поведения системы с параллелизмом. Подсостояние— это состояние, которое является составной частью другого состояния, именуемого суперсостоянием или составным состоянием. Такое представление означает, что состояние можно разбить на несколько подсостояний. Эти подсостояния могут существовать последовательно или параллельно. Параллелизм подсостояний означает, что один объект может быть занят в двух независимых поведенческих множествах. Это справедливо для нашего объекта "классной доски" (blackboard). При обработке каждого возможного расписания он должен обновлять соответствующие структуры и выполнять другие обслуживающие процедуры. Каждое подсостояние отображается в отдельном разделе. Подсостояния синхронизируются и объединяются перед выходом из составного состояния. Когда одно подсостояние подходит к концу, оно ожидает, пока другие состояния подойдут к концу, после чего Подсостояния снова соединяются в одно. На рис. 10.16 показана диаграмма состояний Для объекта blackboard, который генерирует расписание для студентов. Состояние Генерирование расписания (см. рис. 10.16) является составным. Его парал- сльные подсостояния называются Фильтрование и Обновление. Подсостояния отделя- ся пунктирной линией и представляются собственными конечными автоматами, Р чем каждый конечный автомат имеет свои начальное и конечное состояния. одсостоянии Фильтрование объект последовательно проходит через следующие со- ния: Фильтрование временных конфликтов, Балансировка и Персонификация. В под- оянии Поддержка объект проходит только через одно состояние: Обновление. Когда одсостояния Фильтрование и Поддержка (вернее, соответствующие им конечные маты) достигают своих конечных состояний, то перед выходом из составного со- ия Генерирование расписания происходит их объединение.
370 Глава 10 Визуализация проектов параллельных и распределенных систем ® Ожидание Назначение курсов Варианты расписаний Генерирование расписания Фильтрование 'Фильтровани временных ^конфликтов Балансировка Персонификация Поддержка 4 Обновление Составление вариантов расписаний Оценка приемлемости расписания [Расписание не приемлемо] [Расписание отменено] [Расписание одобрено] Отмена Обновление Рис. 10.16. Диаграмма состояний для объекта blackboard
10.3. Визуализация всей системы 371 10.2.5. Распределенные объекты Распределенные объекты — это объекты, выполняющиеся на различных процессах принадлежащих различным компьютерам. Диаграмма развертывания использу- для построения такой модели системы, в которой отображаются физические от- шения между ее программным и аппаратным компонентами. Диаграмма разверты- ния позволяет отобразить маршрутизацию компонентов и объектов в распределенной системе. Компоненты могут представлять собой выполняемые программы, библиотеки или базы данных. Поэтому весьма полезно четко представлять, где именно размещается в системе конкретный компонент или объект. Понять, как именно стоит распределить параллельные компоненты системы — задача непростая. Поэтому моделирование распределенных компонентов поможет в управлении конфигурацией, функционированием и производительностью системы. Диаграмма развертывания состоит из узлов и объектов или компонентов, которые размещаются в этих узлах. Узел — это вычислительное устройство или блок оборудования, который оснащен средствами хранения и обработки данных (например, это может быть отдельное периферийное устройство, компьютер, универсальная вычислительная машина или кластер компьютеров). Узлы этой диаграммы связаны между собой зависимостями. Эти зависимостями представляют, как компоненты взаимодействуют друг с другом. Направление зависимости означает, какой компонент осведомлен о существовании другого компонента. Даже если связь между узлами является двунаправленной, один компонент может не "знать" о том, с кем он связан. Существует два способа смоделировать местоположение компонентов или объектов в UML-диаграмме развертывания: посредством вложения или использования тегированного значения. Согласно первому способу компоненты, которые располагаются в узле, перечисляются внутри символьного обозначения узла. Второй способ предлагает отображать местоположение компонентов в символе компонента. Узлы являются частью диаграммы развертывания. В качестве символа узла используется куб. Куб может иметь два отдельных раздела: один будет содержать индикатор стереотипа, описывающий тип узла, а второй — список компонентов, относящихся к этому узлу (первый способ). При использовании символа компонента (второй способ) тегу location (местоположение) присваивается имя узла, в котором размещается данный компонент. Тег location имеет следующий формат: (location = имя узла} ler location может быть частью любой диаграммы, в которой местоположение °мпонентов является существенным фактором (например, в диаграммах сотрудни- ества. объектов или видов деятельности). На рис. 10.17 отображены два способа означения местоположения компонентов в распределенной системе. В части а это- рисунка показан символ узла, содержащий список компонентов, а в части б пределен символ активного объекта, в котором используется тег location. ■0-3. Визуализация всей системы чстема состоит из множества элементов, включая подсистемы, которые сотруд- а °т между собой с целью выполнения конкретных задач. Сотрудничество — это ирование конструкций, соединяемых в процессе регулярного взаимодействия.
372 Глава 10. Визуализация проектов параллельных и распределенных систем « процессор » М5 Состав: MajorAgent MinorAgent MajorAgent {location = M5} MinorAgent {location = M5} a) 6) Рис. 10.17. Способы отображения местоположения компонента в распределенной системе Рассмотренные в этой главе диаграммные методы позволяют разработчику взглянуть на систему с различных точек зрения, с различных уровней, как извне, так и изнутри. В этом разделе мы обсудим моделирование системы в целом. Это означает, что на самом высоком уровне моделирования следует отображать только основные компоненты или функциональные элементы. Диаграммные методы, предлагаемые для рассмотрения в этом разделе, используются для моделирования развертывания системы и ее архитектуры. И хотя этот раздел — последний в этой главе, моделирование и документирование системы в целом должно быть первым этапом ее проектирования и разработки. 10.3.1. Визуализация развертывания систем Развертывание системы — последний этап в ее разработке. При развертывании системы имеет смысл смоделировать реальные физические компоненты исполняемой версии системы. Диаграмма развертывания отображает конфигурацию элементов оборудования и программных компонентов. Программные компоненты представляют собой такие реальные выполняемые модули, как активные объекты (процессы), библиотеки, базы данных и пр. Диаграмма развертывания состоит из узлов и компонентов. Компоненты — это экземпляры физической реализации логических элементов. Например, класс— это логический элемент, который может быть реализован в виде одного или нескольких компонентов. Класс можно разделить на процессы или потоки, и каждый процесс или поток в диаграмме развертывания может быть компонентом. Компоненты класса могут выполняться на различных узлах одного компьютера (потоки/процессы) или различных компьютерах (процессы). Узел обозначается в виде куба. Узлы соединяются связями. Компоненты и узлы также могут соединяться связями. Как упоминалось выше, узел может содержать список компонентов, либо компонент может быть отображен отдельно от узла, но пр этом необходимо показать связь между ними. Компонент можно представить в вид прямоугольника с указанием тегов в его левой части. Имя компонента указывает внутри его символьного обозначения. Для отображения более крупных частей системы компоненты можно сгРУп"^?о вать в пакеты или подсистемы. Пример диаграммы развертывания показан на рис. 1 • ' Здесь пользователи подключаются к системе через intranet. Узлы являются час кластера компьютеров. Они группируются в пакет. Пользователи подключаю
10.3. Визуализация всей системы 373 кластеру как к единому элементу. В каждом узле перечисляются программные ком- оненты, которые на нем установлены. Взаимодействие между узлами обеспечивается посредством сетевого узла. И [пользователь пк ПОЛЬЗОВАТЕЛЬ ПК L КЛАСТЕР ^\^ ПОЛЬЗОВАТЕЛЬ ПК -_^\ ' INTR ^у « процессор » М5 Состав: blackboard.cpp Corba / I ^^ ANET « процессор » Мб Состав: pvm.h pthread.h MajorAgent « сеть » TCP/IP ^У^ ' « процессор » М7 Состав: Библиотека qt ^^ « процессор » М8 Состав: pvm.h pthread h MinorAgent ^^ ^^ ' « процессор » М9 Состав: Л 0 Рис. 10.18. Диаграмма развертывания, использующая пакеты 10.3.2. Архитектура системы Моделирование и документирование архитектуры системы — это ее описание па Мом высоком уровне. Гради Буч, Джеймс Рамбау и Айвар Джекобсон определяю! аРХитектуру как Набор важных решений по организации системы программного обеспечения. ныГюр структурных элементов и их интерфейсов, посредством которых составляется система, вместе с их поведением, определенным на периоды их сотр\днпчестнл, обье- Динение этих структ)рных и поведенческих элементов в более кр\пные под< и*, темы 11 архитектурный стиль, который направляет эту оркшнзацию — эги гпелч'н iы и п\ интерфейсы, их варианты взаимодействия и их композицию.
374 Глава 10. Визуализация проектов параллельных и распределенных систем Моделирование и документирование архитектуры системы должно охватывать логические и физические элементы, а также структуру и поведение системы на сам высоком уровне. Архитектура системы — это ее описание с различных точек зрения, но с акценто на структуре и организации системы. Ниже представлены различные точки зрения Прецедент Описывает поведение системы с точки зрения конечного (вариант использования) пользователя Процесс Описывает процессы и потоки, используемые в механизмах обеспечения параллелизма и синхронизации Назначение Описывает функции системы и услуги, предоставляемые конечному пользователю Реализация Описывает аппаратные компоненты, используемые для создания физической системы Развертывание Описывает программные компоненты и узлы, на которых они выполняются, в поставляемой системе Очевидно, что эти "поля зрения" (представления о системе) частично перекрываются и взаимодействуют между собой. Например, в описании назначения системы могут упоминаться прецеденты, а при описании ее реализации процессы часто представляют в качестве компонентов. Программные компоненты используются как в части реализации, так и части развертывания системы. При описании архитектуры системы очень полезно строить диаграммы, которые отражают каждый из перечисленных выше ее "портретов". Систему можно разложить на подсистемы и модули. Подсистемы и модули могут быть подвергнуты дальнейшей декомпозиции и разложены на компоненты, узлы, классы, объекты и интерфейсы. В языке UML подсистемы и модули, используемые на архитектурном уровне документации, называются пакетами. Пакет можно использовать для организации элементов в группу, которая описывает общую цель этих элементов. Пакет представляется в виде прямоугольника со вкладкой (ярлыком), расположенной над его верхним левым углом. Символ пакета должен содержать его название. Пакеты в системе могут связывать отношения, построенные на основе композиции, агрегирования, зависимости и наследования. Для того чтобы отличать один тип пакета от другого, можно использовать индикаторы стереотипов. На рис. 10.19 показаны пакеты, входящие в систему составления расписаний. Для системного пакета используется индикатор <<system» (<<система>>), а для пакета уровня подсистемы— индикатор «subsystem» («подсистема»). Подсистемы связаны с системой отношением агрегирования. Одни пакеты могут содержать другие пакеты. В этом случае имя пакета указывается во вкладке. На рис. 10.19 также показано содержимое каждой подсистемы. 10.4. Резюме Модель системы представляет собой своего рода информационное тел » "собранное" с целью изучения системы. При моделировании любой системы не ооо тись без документирования ее различных аспектов. Поскольку в создании систе> обычно занято множество людей, очень важно, чтобы все они пользовались одн* *
10.4. Резюме 375 « система » Система составления расписаний Интерфейс пользователя (клиент) « подсистема: Обучение « подсистема » X Статистическая информация Пролог-анализатор « подсистема » =3 Библиотеки QT/KDE NLI | £< подсистема » NLP NLG Логическая часть (сервер) « подсистема » Пополнение знаний « подсистема: NLP Механизм ^| логического вывода! « подсистема » I ' I "Классная доска7| | источник знаний | База знаний « подсистема » Толкование Рис. 10.19. Пакеты, используемые в системе составления расписаний (NLI — естественно- языковый интерфейс; NLP — обработка данных на естественном языке; NLG — генерация словаря естественного языка) языком. Таким языком стал унифицированный язык моделирования (United Modeling Language— UML), который представляет собой совокупность графических средств, используемых для проектирования, визуализации, моделирования и документирования артефактов системы программного обеспечения. Этот язык создан Гради Бучем, Джеймсом Рамбау и Айваром Джекобсоном. Язык UML стал фактическим стандартом для моделирования объектно-ориентированных систем. Его средства также успешно можно использовать для моделирования параллельных и распределенных систем в плане описания ее структурных и поведенческих аспектов. Диаграммы UML можно использовать для моделирования основных модулей сис- емы, отдельных объектов и системы в целом. Объект— это основная "единица" моделирования, используемая во многих диаграммах UML. Композиция, агрегирование, исимость и наследование — это некоторые из отношений, который могут сущест- агь между объектами. Для отображения поведения объектов и идентификации па- лелизма в системе используются диаграммы взаимодействия. Диаграммы сотруд- ества позволяют отобразить взаимодействие между объектами, совместно рабочими над выполнением некоторой конкретной задачи. Для представления содействия между объектами во времени используются диаграммы последова- - Юстей. С помощью диаграмм состояний можно отобразить действия одного объ- течение всего периода его существования. Для распределенных объектов пре- рена возможность указать их местоположение в системе.
376 Глава 10. Визуализация проектов параллельных и распределенных систем Диаграммы развертывания используются для моделирования системы с точки ния их поставки. Базовыми элементами диаграммы развертывания являются уал и компоненты. Узлы представляют блоки оборудования, а компоненты— части п граммного обеспечения. В символах узлов указывается, какие объекты или компонен установлены на них. При моделировании всей системы базовым элементом являете пакет. Пакеты можно использовать для представления систем и подсистем. Между пак тами могут существовать отношения, которые также отражаются на диаграмме.
ПРОЕКТИРОВАНИЕ КОМПОНЕНТОВ ДЛЯ ПОДДЕРЖКИ ПАРАЛЛЕЛИЗМА В этой главе... 11.1. Как воспользоваться преимуществами интерфейсных классов 11.2. Подробнее об объектно-ориентированном взаимном исключении и интерфейсных классах 11.3. Поддержка потокового представления 11А Пользовательские классы, создаваемые для обработки PVM-потоков данных 11.5. Объектно-ориентированные каналы и FIFO-очереди как базовые элементы низкого уровня 11.6. Каркасные классы 11.7. Резюме
S^y\3JSJ^J '*М f'*\ Ш )?*■• "Как только мы пересекаем черту, чтобы реализовать себя в компьютерной технологии, наши успехи начинают зависеть от способности нашего ума к эволюции. Мы становимся частью программного, а не аппаратного обеспечения." — Рей Курзвейл (Ray Kurzweil), The Age of Spiritual Machines При реализации параллелизма в программном обеспечении необходимо следовать одному важном)' правилу: параллелизм нужно обнаружить, а не внести извне. Иногда цель увеличения быстродействия программы не является достаточно оправданной для насаждения параллелизма в логику программы, которая по своей природе является последовательной. Параллелизм в проекте должен быть естественным следствием требований системы. Если параллельность определена в технических требованиях к системе, то следует с самого начала рассматривать варианты архитектуры и алгоритмы, которые поддерживают параллелизм. В противном случае необходимость параллелизма "всплывет" в уже существующей системе, которая изначально была нацелена лиш на выполнение последовательных действий. Такая участь часто постигает системы, ко торые первоначально разрабатывались как однопользовательские, а затем постепенна вырастали во многопользовательские, или системы, которые с функциональной точк зрения слишком далеко отошли от исходных спецификаций. В таких системах намер ние внести в систем)' параллелизм можно сравнить с попыткой "махать руками пос драки", и в этом случае для поддержки параллельности остается лишь делать архи турные "пристройки". В этой книге мы описываем методы реализации естественн параллелизма. Другими словами, если мы знаем, что нам нужно обеспечить пар< лизм, нас интересует, как это сделать, используя средства C++?
11.1. Как воспользоваться преимуществами интерфейсных классов 379 Мы представляем архитектурный подход к управлению параллелизмом в програм- используя преимущества С++-поддержки объектно-ориентированного програм- иоования и универсальности. В частности, С++-средства поддержки наследования, олиморфизма и шаблонов успешно применяются для реализации архитектурных ешений и программных компонентов, которые поддерживают параллельность. Методы объектно-ориентированного программирования обеспечивают поддержку десяти типов классов, перечисленных в табл. 11.1. Таблица 11.1. Типы объектно-ориентированных классов Типы классов Описание Шаблонный класс Контейнерный класс Виртуальный базовый класс Обобщенный код, который может использовать любой тип; реальный тип является параметром для тела этого кода Класс, используемый для хранения объектов во внутренней или внешней памяти Базовый класс, который служит прямой и/или косвенной основой для создания производных посредством множественного наследования; только одна его копия разделяется всеми его производными классами Класс, который поддерживает интерфейс для производных классов и который может быть использован только в качестве базового; используется как макет для построения других классов Класс, который используется для установки интерфейса других классов Класс, функции которого расширены за счет добавления новых членов к тем, которые были унаследованы от базового класса Класс, созданный для имитации некоторого элемента в конкретной предметной области; значение класса связано с этой предметной областью Класс, который содержит другие классы; имеет с этими классами отношения типа "целое-часть" Класс, реализация которого определена, что позволяет объявлять экземпляры этого класса; он не предполагается для использования в качестве базового класса и не предусматривает попыток создавать операции общего характера Класс (или коллекция классов), который имеет предопределенную структуру и представляет обобщенный характер функционирования безусловно, эти типы классов особенно полезны для проектов, в которых предпо- ается реализовать параллельность. Дело в том, что они позволяют внедрить Р нцип компоновки из стандартных блоков. Мы обычно начинаем с примитивных Понентов, используя их для построения классов синхронизации. Классы син- Р изации позволят нам создавать контейнерные и каркасные классы, рассчитан- на безопасное внедрение параллелизма. Каркасные классы представляют собой Абстрактный класс Интерфейсный класс Узловой класс Доменный класс Составной класс Конкретный класс Каркасный класс
380 Глава 11. Проектирование компонентов для поддержки параллелизма строительные блоки, предназначенные для таких параллельных архитектур бол высокого уровня, как мультиагентные системы и "доски объявлений". На каждо \ровис сложность параллельного и распределенного программирования уменьшаете благодаря использованию различных типов классов, перечисленных в табл. 11.1. IIiaK, начнем с интерфейсного класса. Интерфейсный (или адаптерный) класс ис- польз\ется для модификации или усовершенствования интерфейса другого класса пли множества классов. Интерфейсный класс может также выступать в качестве оболочки, созданной вокруг одной ичи нескольких функций, которые не являются членами конкретного класса Такая роль интерфейсного класса позволяет обеспечить обьсктно-ориентированный интерфейс с программным обеспечением, которое необязательно является объектно-ориентированным. Более того, интерфейсные классы позволяют упростить интерфейсы таких библиотек функций, как POSIX tin cads, PYM и MPI. Мы можем "обернуть" необъектно-ориентированную функцию в объектно-ориентированный интерфейс; либо мы можем "обернуть" в интерфейсный класс некоторые данные, инкапсулировать их и предоставить им таким образом обьектно-ориентированный интерфейс. Помимо упрощения сложности некоторых библиотек функций, интерфейсные классы используются для обеспечения разработчиков ПО согласующимся интерфейсом API (Application Programmer lnteita<e). Например, С++-нрограммисты, которые привыкли работать с iostream- классами, получат возможность выполнять операции ввода-вывода, оперируя категориями обьекпю-ориеитированиых потоков данных. Кривая обучения существенно М1шпмизир\ется, если новые методы ввода-вывода описать в виде привычного юмн-лш-предсдавления. Например, мы можем представить библиотеку средств передачи сообщений MPI как коллекцию потоков. impi_stream Streaml; mpi_stream Stream2; Streaml << Messagel « Message2 << Message3; Stream2 >> Message4; //. . . При таком подходе программист может целиком сосредоточиться на логике про- 1 раммы и не ломать голову над соблюдением требований к синтаксису библиотеки MPI. 11.1. Как воспользоваться преимуществами интерфейсных классов Зачдстлю полезно использовать инкапсуляцию, чтобы скрыть детали библиотек функций и обеспечить создание самодостаточных компонентов, которые годятся дл многократного использования. Возьмем для примера мыотекс, который мы рассма рнвллп в паве 7. Вспомним, что мыотекс— это переменная специального типа, и иольз\емля для синхронизации. Мыотекеы позволяют получать безопасный дост) к критическом) разделу данных или кода программы. Существует шесть основнь ф\пкцнй. предназначенных для работы с переменной типа pthread_mutex__. (14 )SIX Tin cads Mutex).
11.1. Как воспользоваться преимуществами интерфейсных классов 381 синопсис #include <pthread.h> nthread_mutex_destroy(pthread^mutex_t *mutex); nthread_mutex_init(pthread_jmitex_t *mutex, pthread_mutexattr_t *attr); Dthread_mutex_lock(pthread_jmitex_t *mutex); Dthread_mutex_timedlock(pthread_mutex_t *mutex) ; pthread_mutex_trylock(pthread_mutex__t *mutex); pthread__mutex_unlock(pthread_inutex_t *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, используем его для определения мьютексных переменных. Мы можем объявлять массивы мьютексов и использовать эти переменные как члены пользовательских классов. Инкапсулировав переменную типа pthread__mutex__t и мьютексные функции, воспользуемся преимуществами методов объектно- ориентированного программирования. Эти мьютексные переменные можно затем применять в качестве аргументов функций и значений, возвращаемых функциями. А поскольку мьютексные функции теперь связаны с переменной типа pthread_mutex_t, то там, где мы используем мьютексную переменную, эти функции также будут доступны. Функции-члены класса mutex определяются путем заключения в оболочку вызовов соответствующих Pthread-функций, например, так. Листинг 11.2. Функции-члены класса mutex ^utex::mutex(void) try{ int Value; Value = pthread_mutexattr_int(Attr); Value = pthread_mutex_init(Mutex,Attr);
382 Глава 11. Проектирование компонентов для поддержки параллелизма int mutex: : lock (void) { int RetValue; RetValue = pthread_mutex_lock(Mutex); //. . - return(ReturnValue); } Благодаря инкапсуляции мы также защищаем переменные типа pthread_mutex_t * и pthread_mutexattr_t *. Другими словами, при вызове методов lockO, unlockO, trylockO и других нам не нужно беспокоиться о том к каким мьютексным переменным или переменным атрибутов будут применены эти функции. Возможность скрывать информацию (посредством инкапсуляции) позволяет программист)^ писать вполне безопасный код. С помощью свободно распространяемых версий Pthread-функций этим функциям можно передать любую переменную типа pthread_mutex_t. Однако при передаче одной из этих функций неверно заданного мьютекса может возникнуть взаимоблокировка или отсрочка бесконечной длины. Инкапсуляция переменных типа pthread_mutex_t и pthread_mutexattr__t в классе mutex предоставляет программисту полный контроль над тем, какие функции получат доступ к этим переменным. Теперь мы можем использовать такой встроенный интерфейсный класс, как mutex, в любых других пользовательских классах, предназначенных для безопасной обработки потоков выполнения. Предположим, мы хотели бы создать очередь с многопоточной поддержкой и многопоточный класс pvm_stream. Очередь будем использовать для хранения поступающих событий для множества потоков, образованных в программе. На некоторые потоки возложена ответственность за отправку сообщений различным PVM-задачам. PVM-задачи и потоки выполняются параллельно. Несколько потоков выполнения разделяют единственный PVM-класс и единственную очередь событий. Отношения между потоками, PVM-задачами, очередью событий и классом pvm_strearn показаны на рис. 11.1. Очередь, показанная на рис. 11.1, представляет собой критический раздел, поскольку она совместно используется несколькими выполняемыми потоками. Класс pvm_stream — это также критический раздел и по той же причине. Если эти критические разделы не синхронизировать и не защитить, то данные в очереди и классе pvm_stream могут разрушиться. Тот факт, что несколько потоков могут одновременно обновлять либо очередь, либо код класса pvm_stream, открывает среду для "гонок". Чтобы не допустить этого, мы должны обеспечить нашу очередь и класс pvm_stream встроенными средствами блокировки и разблокировки. Эти средства также поддерживаются классом mutex. На рис. 11.2 показана диаграмма классов для наших пользовательских классов x_queue и pvm_stream. Обратите внимание на то, что класс x_queue содержит класс мьютекс, т.е. меЖ ду классами x_queue и мьютекс существует отношение агрегирования. Любая one рация, которая изменяет состояние нашего класса x_queue, может привест к "гонкам" данных, если, конечно, эту операцию не синхронизировать. Следов тельно, операции, которые добавляют объект в очередь или удаляют его из нее, ляются кандидатами для синхронизации. В листинге 11.3 приведено объявлеШ класса x_queue как шаблонного.
11.1. Как воспользоваться преимуществами интерфейсных классов pVM-программа PVM-задача А Критический раздел . _ _ _« | Очередь событий [ ^ г Поток А 1 г . Поток В i ' Класс pvm jstream Критический раздел PVM-задача В funcA() Событие 1 Событие 2 Класс pvm_stream Среда UNIX/Linux Рис. 11.1. Отношения между потоками, PVM-задачами, очередью событий и классом pvm_stream в PVM-программе x_queue pvm_stream Рис. 11.2. Диаграмма классов для пользовательских классов x_queue и pvm_stream // Листинг 11.3. Объявление класса x_queue template <class T> x_queue class{ Protected: queue<T> EventQ; inutex Mutex; //.. . Public: bool enqueue(T Object); T dequeue(void); >; //..
384 Глава 11 Проектирование компонентов для поддержки параллелизма Метод enqueue () используется для добавления элементов в очередь, а м dequeue () — для удаления их из очереди. Каждый из этих методов рассчитан на ^ пользование объекта Mutex. Определение этих методов приведено в листинге 114 // Гглсг'ант 11.4. Определение методов enqueue () и dequeue () template<class T> bool x_queue<T>::enqueue(T Object) { Ilutex.lock( ) ; EventQ.push(Object); Mutex.unlock(); } Lemplate<class T> T x_queue<T>::dequeue(void) { T Object; //. . . Mutex.lock(); Object = EventQ.front() EventQ pop(); Mutex.unlock(); //. . . return(Object); } Теперь очередь может функционировать (принимать новые элементы и избавляться от ненужных) в многопоточной среде. Поток В (см. рис. 11.1) добавляет элементы в очередь а поток А удаляет их оттуда. Класс mutex является интерфейсным классом. Он заключает в оболочку функции pthread_mutex_lock (), pthread_mutex_unlock(), pthread__mutex_init () и pthread_mutex_trylock (). Класс x_queue также является интерфейсным, поскольку он адаптирует интерфейс для встроенного класса queue<T>. Прежде всего, он заменяет интерфейсы методов push() и рор() методами enqueue () и dequeue (). При этом операции вставки и удаления элементов из очереди заключаются между вызовами методов Mutex. lock () и Mutex.unlock () ■ Поэтому в первом случае мы используем интерфейсный класс для инкапсуляции переменных типа pthread_mutex_t* и pthread_mutexattr_t*, а также заключаем в интерфейсную оболочку несколько функций из библиотеки Pthread. А во втором случае мы используем интерфейсный класс для адаптации интерфейса класса queue<T>. Еще одно достоинство класса mutex состоит в том, что его легко использовать в других классах, которые содержат критические разделы или области. Класс p\rm_stream (см. рис. 111) также является критическим разделом, поскольку оба потока выполнения (А и Б) имеют доступ к потоку данных. Опасность возник- нонення "гонок" данных здесь вполне реальна, поскольку поток А и поток Б могут п л\чить доступ к потоку данных одновременно. Следовательно, мы используем кла rnunex в пашем классе pvm_stream для обеспечения необходимой синхронизации. ''/ Листинг 11.5. Объявление класса pvm_stream class pvm_stream{ protected: mutex Mutex; int Taskld; int MessageId; / / . . . public:
2 Подробнее об объектно-ориентированном взаимном исключении... 385 pvm_stream & operator «(string X) ; pvin stream & operator «(int X) ; pvmlstream ^operator «(float X) ; pvitustream &operator»(string X) ; //••■ }; Как и в классе x_queue, объект Mutex используется применительно к функциям, оторые могут изменить состояние объекта класса pvm_stream. Например, мы могли пределить один из операторов "«" следующим образом. II листинг 11.6. Определение оператора « для / / класса pvm_stream pvnustream &pvm_stream: : operator« (string X) { pvm_pkbyte(const_cast<char *>(X.data() ) ,X.size() , 1) ; Mutex.lock(); pvm_send(TaskId,MessageId) ; Mutex.unlock(); //... return(*this) ; } Класс pvm_stream использует объекты Mutex для синхронизации доступа к его критическому разделу точно так же, как это было сделано в классе x_queue. Важно отметить, что в обоих случаях инкапсулируются pthread_mutex-4>yHKijHH. Программист не должен беспокоиться о правильном синтаксисе их вызова. Здесь также используется более простой интерфейс для вызова функций lock () и unlock (). Более того, здесь нельзя перепутать, какую pthread_mutex_t*-nepeMeHHyio нужно использовать с pthread_mutex^yHK4^MH. Наконец, программист может объявить несколько экземпляров класса mutex, не обращаясь снова и снова к функциям библиотеки Pthread. Раз мы сделали ссылку на Pthread-функции в определениях методов Kflaccamutex, то теперь нам достаточно вызывать только эти методы. 11-2. Подробнее об объектно-ориентированном взаимном исключении и интерфейсных классах "тобы справиться со сложностью написания и поддержки программ с паралле- измом, попробуем упростить API-интерфейс с соответствующими библиотеками. Некоторых системах, возможно, имеет смысл создать библиотеки Pthreads, MPI, ^кже стандартные функции использования семафоров и разделяемой памяти как СТь единого решения. Все эти библиотеки и функции имеют собственные протоко- 1 и синтаксис. Но у них есть много общего. Поэтому мы можем использовать интер- т1 Исные классы, наследование и полиморфизм для создания упрощенного и непро- Воречивого интерфейса, с которым непосредственно будет работать программист. 1 можем также скрыть от наших приложений детали реализации конкретной биб- °теки. Если приложение опирается только на методы, используемые в наших
386 Глава 11. Проектирование компонентов для поддержки параллелизма интерфейсных классах, то оно будет защищено от изменений, вносимых в реализа функций, обновлений библиотек и прочих "подводных" реструктуризации. В ко концов, работа над интерфейсом (интерфейсными классами) с компонентами пап лелизма и библиотека*ми функций позволит существенно понизить уровень слож сти параллельного программирования. Итак, рассмотрим подробнее, какие метол разработки интерфейсных классов можно реализовать для поддержки параллелизма 11.2.1. "Полуширокие" интерфейсы Базовый POSIX-семафор используется для синхронизации доступа к критическому раздел)' нескольких процессов, а базовый POSIX-поток— для синхронизации доступа к критическому разделу нескольких потоков. В обоих случаях используются переменные синхронизации и ряд функций, работающих с этихми переменными. Библиотеки MPI и PVM содержат примитивы передачи сообщений и обладают средствами порождения задач. Но интерфейсы этих библиотек различны. Нетрудно предположить, что работа прикладного программиста была бы эффективней, если бы он сосредоточил свое внимание на логике и структуре программы. Однако там, где семантика программы теряет свою ясность из-за необходимости использовать библиотеки, в которых попадаются аналогичные функции, а сами библиотеки отличаются синтаксисом и протоколами, у программиста возникают немалые трудности. Отсюда вытекает потребность универсализации интерфейса, который бы подходил для работы с разными библиотеками. Существует по крайней мере два подхода к разработке общего интерфейса для семейства, или коллекции классов. Объектно-ориентированный подход начинается с общего и переходит к частностям посредством наследования. Другими словами, возьмем минимальный набор характеристик и атрибутов, которыми должен обладать каждый член рассматриваемого семейства классов, а затем посредством наследования будем конкретизировать характеристики для каждого класса. При таком подходе по мере "спуска" по иерархии классов интерфейс становится все более "узким". Второй подход часто используется в коллекциях шаблонов. Шаблонные методы начинаются с конкретного и переходят к более общему посредством "широких" интерфейсов. "Широкий" интерфейс включает обобщение всех характеристик и атрибутов (см. книгу Страуструпа Язык программирования C++, 1997). Если бы нам пришлось применить к библиотекам средств параллелизма "узкий" и "широкий" интерфейсы, то согласно методу "узкого интерфейса" мы бы взяли от каждой библиотеки общие, или пересекающиеся, части (т.е. пересечение), обобщили их и поместили в базовый класс. И, наоборот, реализуя метод "широкого интерфейса", нужно было бы поместить в базовый класс все функциональные части каждой библиотеки (т.е. объединение), предварительно обобщив их. В результате пересечения мы получили бы меньший по объему да и менее полезный класс. А результат объединения, скорей всего, поразил бы каждого своей громоздкостью. Решение, которое интересует нас в данном случае, находится где-то посередине, т.е. нам нужны "полуширокие" интерфейсы. Начнем же мы с метода "узкого" интерфейса и обобщим его настолько, насколько это можно сделать в пределах иерархии одног класса. Затем используем этот "узкий" интерфейс в качестве основы для коллекШ классов, которые связаны не наследованием, а функциями. "Узкий" интерфейс до жен действовать в качестве стратегии сдерживания "ширины", до которой может р бухнуть "полуширокий" интерфейс. Другими словами, нам не нужно объединять о вально все характеристики и атрибуты; мы хотим получить объединение только
11.2. Подробнее об объектно-ориентированном взаимном исключении... 387 стей, которые логически связаны с нашим "узким" интерфейсом. Проиллюстрируем мысль на примере простого проекта интерфейсных классов для POSIX-семафора, Prhiead-мьютекса и Pthread-переменной блокировки. Безотносительно к реализации деталей, операции блокировки, разблокировки "пробной" блокировки являются характеристиками переменных синхронизации. Поэтому 1Ы создадим базовый класс, который будет служить "трафаретом" для целого семейства классов. Объявление класса 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; }; Обратите внимание на то, что методы синхронизации класса synchroniza- tion_variable объявлены виртуальными и инициализированы значением 0. Это означает, что они являются чисто виртуальными методами, что делает класс synchronization_variable абстрактным. Из класса, который содержит одну или несколько чисто виртуальных функций, объект прямым путем создать нельзя. Чтобы использовать этот класс, необходимо вывести из него новый класс и определить в нем все чисто виртуальные функции. Абстрактный класс — это своего рода трафарет с указанием того, какие функции должны быть определены в производном классе. Он предлагает интерфейсный проект для производных классов. Он отнюдь не диктует, как нужно реализовать методы, он лишь отмечает, какие методы должны быть представлены в выведенном классе, причем они не могут оставаться в нем чисто виртуальными. С помощью имен этих методов мы можем подсказать предполагаемое их поведение. Таким образом, проектный интерфейсный класс предлагает проект без реализации. Класс этого типа используется в качестве фундамента для будущих классов. Проектный класс гарантирует, что интерфейс будет иметь определенный вид [9]. Класс synchronization_variable обеспечивает интерфейсный трафарет для семейства переменных синхронизации. Для обеспечения различных вариантов реализации интерфейса мы используем наследование. Pthread-мьютекс — прекрасный кандидат для интерфейсного класса, поэтому мы определяем класс mutex как производный от класса synchronization_variable. Листинг 11.8. Объявление класса мьютекс, который наследует класс synchronization_variable ^ ass mutex : public synchronization_variable{ Protected: Pthread_mutex_t *Mutex; Pthread_mutexattr t *MutexAttr; //. . PubliC: lrit lock (void) ;
388 Глава 11. Проектирование компонентов для поддержки параллелизма 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 предназначен для защиты доступа к критическому разделу. Если один поток заблокировал мьютекс, он получает доступ к критическому разделу, защищаемому этим мьютексом. Иногда такая мера предосторожности оказывается излишне суровой. Возможны ситуации, когда вполне можно разрешить доступ нескольких потоков к одним и тем же данным, если ни один из этих потоков не модифицирует данные. Другими словами, в некоторых случаях мы можем ослабить блокировку критического раздела и "намертво" запирать его только для действий, которые стремятся модифицировать данные, разрешая при этом доступ для действий, которые предполагают лишь считывание или копирование данных. Такой вид блокировки называется блокировкой считывания (read lock). Блокировка считывания позволяет параллельный доступ к критическому разделу для чтения данных. Критический раздел может быть уже заблокированным одним потоком, но другой поток также может получить блокировку, если у него нет намерения изменять данные. Критический раздел может быть заблокирован для записи одним потоком, а другой поток может запросить блокировку для чтения этого критического раздела. Архитектура "классной доски" служит прекрасным примером структуры, которая может использовать преимущества "мьютексов считывания" и мьютексов более общего назначения. Под "классной доской" понимается область памяти, разделяемая параллельно выполняемыми задачами. "Классная доска" используется для хранения решений некоторой проблемы, которую совместными усилиями решает целая группа задач. По мере приближения задач к решению проблемы каждая из них записывав результаты на "классную доску" и просматривает ее содержимое с целью поиска р зультатов, сгенерированных другими задачами, которые могут оказаться полезным для нее. Структура "классной доски" является критическим разделом. В действительн сти мы хотим, чтобы одновременно только одна задача могла обновлять содержимо
2. Подробнее об объектно-ориентированном взаимном исключении... 389 и ссНой доски". Однако ее одновременное считывание мы можем позволить любо- количеству задач. Кроме того, если несколько задач уже считывает содержимое « ассной доски", нам нужно, чтобы оно не начало обновляться до тех пор, пока все и задачи не завершат чтение. "Мьютекс считывания" как раз подходит для такой ситуации, поскольку он может управлять доступом к "классной доске", разрешая его лько считывающим задачам и запрещая его для записывающих задач. Но если ре- ние проблемы будет найдено, содержимое "классной доски" необходимо обновить. В процессе обновления нам нужно, чтобы ни одна считывающая задача не получила доступ к критическому разделу. Мы хотим заблокировать доступ для чтения до тех пор пока не завершит обновление записывающая задача. Следовательно, нам нужно создать "мьютекс записи". В любой момент времени удерживать этот "мьютекс записи" может только одна задача. Поэтому мы делаем различие между мьютексом, который блокируется для считывания, но не для записи, и мьютексом, который блокируется для записи, но не для считывания. С использованием мьютекса считывания у нас может быть несколько параллельных считывающих задач, а с использованием мьютекса записи — только одна записывающая задача. Описанная схема является частью модели CREW (Concurrent Read Exclusive Write — параллельное чтение, монопольная запись) параллельного программирования. Для разработки спецификации нашего мьютексного класса нам нужно наделить его способностью выполнять блокировки считывания и блокировки записи. В библиотеке Pthreads предусмотрены мьютексные переменные блокировки чтения-записи и атрибутов: pthread_rwlock_t и pthread_rwlockattr_t Эти переменные используются совместно с 11 pthread_rwlock()-функциями. Мы используем наш интерфейсный класс rw_mutex для инкапсуляции переменных pthread_rwlock_t и pthread_rwlockattr_t, а также для заключения в оболочку Pthread-функций мьютексной организации чтения-записи. Синопсис #include <ptrhead.h> 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 *); lr*t Pthread_rwlock_unlock(pthread_rwlock_t *) ; *nt Pthread_rwlockattr_init(pthread_rwlockattr_t *); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *); lnt Pthread_rwlockattr_getpshared( const pthread_rwlockattr_t *, int *); lnt Pthread_rwlockattr_setpshared( pthread_rwlockattr_t *, int) ;
390 Глава 11. Проектирование компонентов для поддержки параллелизма runtime error // Листинг 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); //.. - }; Класс rw_mutex наследует класс mutex. На рис. 11.3 показаны отношения между классами rw_mutex, mutex, synchronization_variable и runtime_error. Пока мы создаем "узкии" интерфейс. На данном этапе мы заинтересованы в обеспечении минимального набора атрибутов и характеристик, необходимых для обобщения нашего класса mutex с использованием мьютексных типов и функций из библиотеки Pthread. Но после создания "узкого" интерфейса для класса mutex мы воспользуемся им как основой для создания "полуширокого" интерфейса. "Узкий" интерфейс обычно применяется в отношении классов, которые связаны наследованием. "Широкие" интерфейсы, как правило, применяют к классам, которые связаны функциями, а не наследованием. Нам нужен интерфейсный класс для упрощения работы с классами или функциями, которые принадлежат различным библиотекам, но выполняют подобные действия. Интерфейсный класс должен обеспечить программиста удобными рабочими инструментами. Для этого мы берем все библиотеки или классы с подобными функциями, отбираем все общие функции и переменные и после некоторого обобщения помещаем их в большой класс, который содержит все требуемые функции и атрибуты. Так определяется класс с "широким" интерфейсом. Но если включить в него (например, в класс rw_mutex) только интересующие нас функции и данные, мы получим "полуширокий" интерфейс. Его преимущества перед "широким" интерфейсом заключаются в том, что он позволяет нам получать доступ к объектам, которые связаны лишь функционально, и ограничивает множество методов, которыми может пользоваться программист, теми, которые содержатся в интерфейсном классе с узким "силуэтом". Это может быть очень важно при интеграции таких больших библиотек функций, как MPI и PVM с POSIX-возможностями параллелизма. Объединение MPI-, PVM- и POSIX-средств дает сотни функции Рис. 11.3. Отношения между классами rw_mutex, mutex, synchromzation_variable и runtime_error 77 synchronization variable
2 Подробнее об объектно-ориентированном взаимном исключении... 391 огичными целями# Затратив время на упрощение этой функциональности в ин- С Аейсных классах, вы позволите программисту понизить уровень сложности, свя- Т ный с параллельным и распределенным программированием. Кроме того, эти ин- Аейсные классы становятся компонентами, которые можно многократно использовать в различных приложениях. Чтобы понять, как подойти к созданию "полуширокого" интерфейса, построим рфейсный класс для 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); //. .. }; Синопсис <semaphore. h> int int sem_ int int int int int [int sem_init(sem_t *, sem_destroy (sem_t _t *sem_open(const sem_close(sem_t * int, *); char I; sem__unlink (const char sem_wait(sem_t *) sem_trywait (sem_t sem_post(sem_t *) sem_getvalue (sem_J '*>; t *, unsigned * *) ? int int, . *); int); . .); Обратите внимание на то, что класс semaphore имеет такой же интерфейс, как и наш класс mutex. Чем же они различаются? Хотя интерфейсы классов mutex и semaphore одинаковы, реализация функций lock (), unlock (), trylock () и тому °Добных представляет собой вызовы семафорных функций библиотеки POSIX. // Листинг 11.11. Определение методов 1оск(), unlock() и trylock() для класса semaphore xnt semaphore: : lock (void) //. . . return (sem_wait (Semaphore) ) ;
392 Глава 11. Проектирование компонентов для поддержки параллелизма int semaphore::unlock(void) { return(sem_post(Semaphore)); } Итак, теперь функции lock (), unlock (), trylock () и тому подобные заключают в оболочку семафорные функции библиотеки POSIX, а не функции библиотеки Pthread. Важно отметить, что семафор и мьютекс — не одно и то же. Но их можно использовать в аналогичных ситуациях. Зачастую с точки зрения инструкций, которые реализуют параллелизм, механизмы функций 1оск() и unlock () имеют одно и то же назначение. Некоторые основные различия между мьютексом и семафором указаны в табл. 11.2. Таблица 11.2. Основные различия между мьютексами и семафорами Характеристики мъютексов Характеристики семафоров Мьютексы и переменные условий Семафоры обычно разделяются между процессами, разделяются между потоками но их разделение возможно и между потоками Мьютекс деблокируется теми же по- Освобождать семафор должен необязательно тот токами, которые его заблокировали процесс или поток, который его удерживал Мьютекс либо блокируется, либо Семафоры управляются количеством ссылок. Стан- деблокируется дарт POSIX включает именованные семафоры Несмотря на важность различий в семантике (см. табл. 11.2), часто их оказывается недостаточно для оправдания применения к семафорам и мьютексам совершенно различных интерфейсов. Поэтому мы оставляем "полуширокий" интерфейс для функций lock(), unlock() и trylockO с одним предостережением: программист должен знать различия между мьютексом и семафором. Это можно сравнить с ситуацией, которая возникает с такими "широкими" интерфейсами таких контейнерных классов, как deque, queue, set, multiset и пр. Эти контейнерные классы связаны общим интерфейсом, но их семантика в определенных областях различна. Используя понятие интерфейсного класса, можно разработать соответствующие компоненты синхронизации для мьютексов, переменных условий, мью- тексов чтения-записи и семафоров. Имея такие компоненты, мы можем спроектировать безопасные (с точки зрения параллелизма) контейнерные, доменные и каркасные классы. Мы можем также применять интерфейсные классы для ооес- печения единого интерфейса с различными версиями одной и той же библиотеки функций при необходимости использования обеих версий (по разным причинам) в одном и том же приложении. Иногда интерфейсный класс может быть успешно применен для безболезненного перехода от устарелых функций к новым. Если мы хотим оградить программиста от различий, существующих между операцион ными системами, то наша цель — обеспечить его соответствующим АР1_И терфейсом, независимо от того, какая библиотека семафорных функций использ) ется в среде: System V или POSIX.
11.3. Поддержка потокового представления 393 11.3. Поддержка потокового представления Помимо использования интерфейсных классов для упрощения программирования создания новых "широких" интерфейсов библиотек средств параллелизма и пере- чи сообщений, имеет смысл также расширить существующие интерфейсы. Напри- объектно-ориентированное представление потоков данных можно расширить за счет использования каналов, FIFO-очередей и таких библиотек передачи сообщений, как PVM и MPI. Эти компоненты используются ради достижения межпроцессного взаимодействия (Inter-Process Communication — IPC), межпотокового взаимодействия (Inter-Thread Communication— ITC), а в некоторых случаях и взаимодействия между объектами (Object-to-Object Communicaton — ОТОС). Если взаимодействие имеет место между параллельно выполняемыми потоками или процессами, то канал связи может представлять собой критический раздел. Другими словами, если несколько процессов (потоков) попытаются одновременно обновить один и тот же канал, FIFO- очередь или буфер сообщений, непременно возникнет "гонка" данных. Если мы собираемся расширить объектно-ориентированный интерфейс потоков данных за счет включения компонентов из библиотеки PVM или MPI, нам нужно быть уверенными в том, что доступ к этим потокам данных будет безопасен с точки зрения параллелизма. Именно здесь могут пригодиться наши компоненты синхронизации, спроектированные в виде интерфейсных классов. Рассмотрим простой класс pvm_stream. // Листинг 11.12. Объявление класса pvm_stream/ который // наследует класс mios class pvm_stream : public mios{ protected: int Taskld; int Messageld; mutex Mutex; //... public: void taskld(int Tid) ; void messageld(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. Интерфейс этого класса далек от совершенства. Поскольку этот асс предназначен для обработки данных любого типа, мы должны расширить опре- ления операторов "«" и ">>". А так как мы планируем использовать класс ^—Stream в многопоточной программе, мы должны быть уверены в том, что объ- класса pvm_stream безопасен для потоков. Поэтому мы включаем в качестве
394 Глава 11. Проектирование компонентов для поддержки параллелизма члена нашего класса pvm_stream класс mutex. Поскольку сообщение может бь направлено для конкретной PVM-задачи, класс pvm_stream инкапсулирует для н активный буфер. Наша цель— использовать классы ostream и istream в качест "путеводителя" по функциям, которые должен иметь класс pvm_stream. Вспомним что классы ostream и istream являются классами трансляции. Они переводят ти пы данных в обобщенные потоки байтов при выводе и обобщенные потоки байтов в конкретные типы данных при вводе. Используя классы istream и ostream, программисту не нужно погружаться в детали вставки в поток или выделения из потока данных того или иного типа. Мы хотим, чтобы и поведение класса pvm_stream было аналогичным. Библиотека PVM располагает различными функциями для каждого типа данных, которые необходимо упаковать в буфер отправки или распаковать из буфера приема. Например, функции pvm_pkdouble () pvm_pkint() pvm__pkf loat () используются для упаковки double-, int- и float-значений соответственно. Аналогичные функции существуют и для других типов данных, определенных в C++. Мы бы хотели поддерживать наше потоковое представление, т.е. чтобы ввод и вывод данных можно было представить как обобщенный поток байтов, который перемещается в программу или из нее. Следовательно, мы должны определить операторы вставки (<<) и извлечения (>>) для каждого типа данных, который мы собираемся использовать при обмене сообщениями между PVM-задачами. Мы также моделируем состояние потока данных в соответствии с классами istream и ostream, которые содержат компонент ios, предназначенный для хранения состояния этого потока. Поток данных может находиться в состоянии ошибки либо в одном из различных состояний, которые выражаются восьмеричным, десятичным или шестнадцатеричным числом. Поток также может пребывать в нормальном, заблокированном или состоянии конца файла. Класс pvm_stream должен не только содержать компонент, который поддерживает состояние потока данных, но и методы, которые устанавливают заданное или исходное состояние PVM-задачи, а также считывают его. Наш класс pvm_stream для этих целей содержит компонент mios. Этот компонент поддерживает состояние потока данных и активного буфера отправки и приема информации. На рис. 11.4 представлены две диаграммы классов: одна отображает отношения между основными классами библиотеки iostream, а вторая — отношения между классом pvm_stream и его компонентами. Обратите внимание на то, что классы istream и ostream наследуют класс ios. Класс ios поддерживает состояние потока данных и состояние буфера, используемого классами istream и ostream. Наш класс mios исполняет ту же роль в отношении класса pvm_stream. Классы istream и ostream содержат определения операторов "«" и ">>". Эти же операторы определены и в нашем классе pvm_stream. Поэтом), хотя наш класс pvm_stream не связан с iostream-классами наследованием, между ними существует интерфейсная связь. Мы используем интерфейс iostream-классов в качестве "полуширокого" интерфейса для классов pvm_stream и mios. Обратите внимание на то, что класс mios (см. рис. 11.4) наследуется классом pvm_stream. Если мы хотим поддерживать потоковое представление с помощью класса pvm_stream, то для этог как раз подходит понятие интерфейсного класса.
11.3. Поддержка потокового представления 395 ИЕРАРХИЯ iostream-классов ios base ' "U.j ^ I т basic filebufl basic ios ± и v. basic_ —.J streambuf -4? basic istream 3—.-,-. basic ostream J_-r-, 3^1_? /\ basic__ —• J ifstream basic_ — |J istringstream — —I т . г basic_ —.J iostream ± basic_ — |J stringstream —;—i т . basic_ —, J ostringstream basic_ —, J fstream basic_ —, J stringbuf _L- basic_ — | J ofstream pvm_stream i mutex Рис. 11.4. Диаграмма классов, отображающая отношения между основными классами библиотеки iostream, и диаграмма класса pvm_stream 11.3.1. Перегрузка операторов "«" и "»" для PVM-потоков данных Итак, рассмотрим определение операторов "«" и ">>" для класса pvm_stream. ператор вставки (<<) используется для заключения в оболочку функций —Send () и pvm_pk. Вот как выглядит определение этого операторного метода. листинг 11.13. Определение оператора "<<" для класса pvm_stream class -Stream &pvm_stream::operator«(int Data)
396 Глава 11. Проектирование компонентов для поддержки параллелизма reset(); pvm_pkint(&Data,1,1); pvm_send(Taskld,Messageld) ; //.. . return(*this); } Подобное определение существует для каждого типа данных, которые будут обрабатываться с использованием класса pvm_stream. Метод reset () унаследован от класса mios. Этот метод используется для инициализации буфера отправки данных Taskld и Messageld— это члены данных класса pvm_stream, которые устанавливаются с помощью методов taskld () и messageld (). Определяемый здесь оператор вставки позволяет отправлять данные PVM-задаче с помощью стандартной записи операции вывода в поток. int Value = 2004; pvm_stream MyStream; MyStream « Value; Оператор извлечения данных (») используется подобным образом, но для получения сообщений от PVM-задач. В действительности оператор "»" заключает в оболочку функции pvm_recv () и pvmupk (). Определение этого операторного метода выглядит так. // Листинг 11.14. Определение оператора "»" для класса // pvm_stream pvm_stream &pvm_stream: :operator» (int &Data) { int Bufld; //. . . Bufld = pvm_recv(Taskld,Messageld); StreamState = pvm_upkint(&Data,1,1); 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. При этом он работает с более знакомыми для него объектно-ориентированными потоками данных- В данном случае поток данных представляет буфер сообщений, а элементы, которЫе
11.3. Поддержка потокового представления 397 помещаются в него или извлекаются оттуда, представляют сообщения, которыми обвиваются между собой 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. PVM-ЗАДАЧАА pvm_stream Класс mutex Оператор « : {pvm_send()} Оператор » : {pvm_receive()} PVM-ЗАДАЧА В -► PVM-поток байтов -> |||fpai4«io.w<<w J Поток 2 : вызывает г операцию *«tt 1 ~ —~~~^—-~~^^ pvm_stream Класс mutex Оператор « : { pvm_send()} Оператор » : { pvm_receive()} вызывает операцию"«" Рис. 11.5. Взаимодействие потоков выполнения и класса pvnustream с буферами pvirusend и pvn\_receive Критическими разделами являются не только буферы отправки и приема данных. Класс mios, используемый для хранения состояния класса pvm_stream, также является критическим разделом. Для защиты этого компонента можно использовать класс mutex. При обращении к операторам вставки и извлечения данных можно использовать объект Mutex. // Листинг 11.15. Определение операторов "«" и "»" для класса pvm_stream PviiUstream &pvm_stream: :operator« (int Data) Mutex.lock(); reset(); pvm_pkint(&Data/l/l); Pvm_send(TaskId/MessageId); Mutex.unlockO ; //... return(*this);
398 Глава 11. Проектирование компонентов для поддержки параллелизма } pvm_stream &pvm_strearn::operator»(int &Data) { int Bufld; //. . . Mutex.lockO ; Bufld = pvm_recv(TaskId/MessageId); StreamState = pvm_upkint(&Data,1,1); Mutex.unlock(); return(*this); } Этот вид защиты позволяет сделать класс pvm_stream безопасным. Здесь мы не представили код обработки исключений или другой код, который бы позволил предотвратить бесконечные отсрочки или взаимную блокировку. Основная идея в данном случае — сделать акцент на компонентах и вариантах архитектуры, которые пригодны для поддержки параллелизма. Интерфейсный класс mutex и класс pvm_stream можно использовать многократно, и оба они поддерживают параллельное программирование. Предполагается, что объекты класса pvm_stream должны использоваться PVM- задачами при отправке и приеме сообщений. Но это не является жестким требованием. Для того чтобы пользователь мог применить концепцию класса pvm_stream к своим классам, для них необходимо определить операторы вставки («) и извлечения (»). 11.4. Пользовательские классы, создаваемые для обработки PVM-потоков данных Чтобы понять, как определенный пользователем класс можно использовать совместно с классом pvm_stream, попробуем усовершенствовать возможности PVM- палитры, представленной в главе 6. Класс палитры представляет простую коллекцию цветов. Для удобства будем сохранять цвета в векторе строк (vector<string>) с именем Colors. Начнем с объявления класса spectral_palette, который содержит friend- объявления для операторов вставки (<<) и извлечения (>>). // Листинг 11.16. Объявление класса spectral_palette class spectral_palette : public pvm_object{ protected: //. . . vector<string> Colors; public: spectral_palette(void); //. . . friend pvm_stream &operator>>(pvm_stream kin, spectral_palette &Obj); friend pvm_stream &operator<<(pvm_stream &Out, spectral_palette &Obj); //. . .
11.4. Пользовательские классы, создаваемые для обработки... 399 Обратите внимание на то, что класс spectral_palette в листинге 11.16 насле- т класс pvm__object. Класс pvm_ob:ject тем самым обеспечивает своего наследника доступом к идентификатору задачи и идентификатору сообщения. Вспомните, что идентификаторы задачи и сообщения используются во многих 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 <Obj.Colors.size() - 1) { Source.append(" "); } } Out.reset(); Out.taskId(Obj.Taskld); Out.messageId(Obj.Messageld); Out « Source; return(Out); } Рассмотрим подробнее определение этой операции вставки в листинге 11.17. Поскольку класс pvm_stream работает только со встроенными типами данных, цель пользовательского оператора "<<" — перевести пользовательский объект в последовательность значений встроенных типов данных. Этот перевод является одной из основных обязанностей классов, "отвечающих" за потоковое представление данных. Жданном случае объект класса spectral_palette должен быть переведен в строку Цветов", разделенных пробелами. Список цветовых значений сохраняется в строке ource. Рассматриваемый процесс перевода позволяет применить к объекту этого ^асса оператор "<<", который был определен для строкового типа данных. Имея определения этих операторов, API-интерфейс программиста становится более удоб- Ым, чем при использовании оригинальных версий функций библиотеки Pthread, ^IX и MPI. Ведь теперь объект класса spectral_palette можно переслать из од- °и PVM-задачи в другую, используя такую привычную операцию вставки («).
400 Глава 11. Проектирование компонентов для поддержки параллелизма // Листинг 11.18. Использование объектов классов // pvm_stream и spectral_palette pvm_stream TaskStream; spectral_palette MyColors; //. .- TaskStream.taskld(20001); TaskStream.messageld(l); TaskStream « MyColors; //.. • Здесь объект MyColors пересылается в соответствующую PVM-задачу. На рис. Ц.б показаны компоненты, используемые для поддержки объектов TaskStream и MyColors. Каждый компонент на рис. 11.6 можно детализировать и оптимизировать в отдельности. Каждый представленный здесь уровень обеспечивает дополнительный слой изоляции от сложности этих компонентов. В идеале на самом высоком уровне программист должен заниматься только деталями, связанными с данной предметной областью. Такой высокий уровень абстракции позволяет программисту самым естественным образом представлять параллелизм, который вытекает из требований предметной области, не углубляясь при этом в синтаксис и сложные последовательности вызовов функций. Компоненты, представленные на рис. 11.6, следует рассматривать лишь как малую толику библиотеки классов, которую можно использовать для PVM-программ и многопоточных PVM-программ. Те же методы можно применять для взаимодействия между параллельно выполняемыми задачами, которые не являются частью PVM-среды. Ведь существует множество приложений, которые требуют реализации параллельности, но не нуждаются во всей полноте функционирования механизма PVM-среды. Для таких приложений вполне достаточно использования функций exec (), fork () или pvm_spawn (). Примерами таких приложений могут служить программы, которые требуют создания нескольких параллельно выполняемых процессов, и приложения типа "клиент-сервер". Для таких неРУМ- или неМР1- приложений также может потребоваться организация межпроцессного взаимодействия. Для параллельно выполняемых процессов, создаваемых посредством f ork-exec- последовательности вызовов или функций pvm_spawn, имело бы смысл поддерживать потоковое представление данных. Понятие объектно-ориентированного потока данных можно также расширить с помощью каналов и FIFO-очередей. TaskStream: pvm_stream SL mios I включает mutex spectral_palette:MyColors SL pvm_object включает vector <string> Рис. 11.6. Компоненты, используемые для поддержки объектов TaskStream и MyColors
11.5. Объектно-ориентированные каналы и FIFO-очереди... 401 11.5. Объектно-ориентированные каналы и FIFO-очереди как базовые элементы низкого уровня Приступая к разработке объектно-ориентированных каналов, начнем с рассмотрения базовых характеристик и поведения каналов в целом. Канал представляет собой средство взаимодействия между несколькими процессами. Для того чтобы процессы могли взаимодействовать, необходимо обеспечить между ними передачу информации определенного вида. Эта информация может представлять данные или команды, предназначенные для выполнения. Обычно такая информация преобразуется в последовательность данных и помещается в канал, а затем считывается процессом с другого конца канала. При считывании из канала данные снова преобразуются, чтобы обрести смысл для считывающего процесса. В любом случае при передаче от одного процесса другому эти данные должны где-то храниться. Мы называем область хранения информации буфером данных. Для размещения данных в этом буфере и извлечения их оттуда необходимо выполнять соответствующие операции. Но прежде чем говорить о выполнении таких операций, необходимо позаботиться о существовании самого буфера данных. Объектно-ориентированный канал должен обладать средствами, которые поддерживают операции создания и инициализации буфера данных. После завершения взаимодействия между процессами буфер данных, используемый для хранения информации, становится ненужным. Это означает, что наш объектно-ориентированный канал должен "уметь" удалять буфер данных после его использования. Из этого "введения в каналы" вырисовываются по крайней мере пять основных компонентов, которыми должен обладать объектно-ориентированный канал: • буфер; • операция вставки данных в буфер; • операция извлечения данных из буфера; • операция создания/инициализации буфера; • операция ликвидации буфера. Помимо этих пяти базовых компонентов, канал должен иметь два конца. Один конец предназначен для вставки данных, а другой — для их извлечения. К этим двум концам могут получать доступ различные процессы. Чтобы наше описание канала было полным, мы должны включить в него порт ввода и порт вывода, к которым могут подключаться различные процессы. В результате мы получаем уже семь базовых компонентов, составляющих описание нашего объектно-ориентированного канала: порт ввода; порт вывода; буфер; операция вставки данных в буфер; операция извлечения данных из буфера; операция создания/инициализации буфера; операция ликвидации буфера.
402 Глава 11. Проектирование компонентов для поддержки параллелизма Эти компоненты образуют минимальный набор характеристик, составляют описание канала. Уточнив базовые компоненты, можно поразмыслить о том, как п разработке объектно-ориентированного канала лучше всего использовать суще вующие системные API-интерфейсы или структуры данных. В разработке каналов п пробуем для начала применить те же методы (инкапсуляцию и перегрузку операторов), которые мы использовали при разработке класса pvm_stream. Обратите внимание на то, что пять из семи выше перечисленных базовых компонентов являются общими для многих основных структур данных и типов контейнеров, которые обычно используются для операций ввода-вывода. В большинстве случаев UNIX/ Linux-средства работы с файлами поддерживают: • буферы; • операции вставки данных в буфер; • операции извлечения данных из буфера; • операции создания буфера; • операции удаления буфера. Для инкапсуляции функций, предоставляемых системными UNIX/Linux- службами, мы используем понятие интерфейсных С++-классов и создаем объектно- ориентированные версии сервисных функций ввода-вывода. Если в случае с классом pvm_stream для библиотеки PVM нам приходилось начинать "с нуля", то здесь мы можем воспользоваться преимуществами существующей стандартной библиотеки C++ и библиотеки классов iostreams. Вспомните, что библиотека классов iostreams поддерживает объектно-ориентированную модель потоков ввода и вывода. Более того, эта объектно-ориентированная библиотека оснащена поддержкой буферизации данных и всех операций, связанных с использованием буфера. На рис. 11.7 показана простая диаграмма класса basic__iostream. basic iostream I if Компонент буфера Компонент состояния V basic_ streambuf _Е_ basic ios Компонент преобразования I basic istream 1 basic ostream Рис. 11.7. Диаграмма классов, компоненты класса basic_iostream отображающая основные Основные компоненты класса basic_iostream можно описать тремя видам классов: компонент буфера, компонент преобразования и компонент состояния [^ J- Компонент буфера используется в качестве области промежуточного хранения оа тов информации. Компонент преобразования отвечает за перевод анонимных поел
11.5. Объектно-ориентированные каналы и FIFO-очереди... 403 овательностей байтов в значения и структуры данных соответствующих типов, также за перевод структур данных и отдельных значений в анонимные последова- ельности байтов. Компонент преобразования отвечает за обеспечение программиста потоковым представлением байтов, в котором все операции ввода-вывода незави- имо от источника и приемника обрабатываются как поток байтов. Компонент состояния инкапсулирует состояние объектно-ориентированного потока и позволяет определить, какой тип форматирования применим к байтам данных, которые содержатся в компоненте буфера. Компонент состояния также содержит информацию о том, в каком режиме был открыт поток: дозаписи, создания, монопольного чтения, монопольной записи, а также о том, будут ли числа интерпретироваться как шестна- дцатеричные, восьмеричные или двоичные. Компонент состояния также можно использовать для определения состояния ошибки операций ввода-вывода, выполняемых над компонентом буфера. Опросив этот компонент, программист может определить, в каком состоянии находится буфер, условно говоря, в хорошем или плохом. Эти три компонента представляют собой объекты, которые можно использовать совместно (для формирования полнофункционального объектно-ориентированного потока) или в отдельности (в качестве вспомогательных объектов в других задачах). Пять из семи базовых компонентов нашего потока уже реализованы в библиотеке классов iostreams. Поэтому нам остается лишь дополнить их компонентами портов ввода и вывода. Для этого мы можем рассмотреть системные средства поддержки потоков. В среде UNIX/Linux создать канал можно с помощью вызовов системных функций (листинг 11.19). // Листинг 11.19. Использование системного вызова для // создания канала int main(int argc, char *argv[]) { int Fd[2]; pipe(Fd); } Функция pipe () предназначена для создания структуры данных канала, которую можно использовать для взаимодействия между родительским и сыновним процессами. При успешном обращении к функции pipe () она возвращает два дескриптора файла. (Дескрипторы файлов представляют собой целые значения, которые используются для идентификации успешно открытых файлов.) В этом случае дескрипторы сохраняются в массиве Fd. Элемент Fd[0] используется при открытии файла для Чтения, а элемент Fd[l] — при открытии файла для записи. После создания эти два Дескриптора файлов можно использовать при вызове функций read О и write(). >нкция write () обеспечивает вставку данных в канал посредством дескриптора £ 1 ], а функция read () — извлечение данных из канала посредством дескриптора [0]. Поскольку функция pipe () возвращает дескрипторы файлов, доступ к каналу * °Жно получить с помощью системных средств работы с файлами. Для определения * аксимально возможного количества доступных дескрипторов файлов, открытых од- м процессом, можно использовать системную функцию sysconf (_SC_OPEN_MAX), Для определения размера канала — функцию pathconf (_PC_PIPE_BUF).
404 Глава 11. Проектирование компонентов для поддержки параллелизма Эти два файловых дескриптора представляют наши логические порты ввода и вода соответственно. Мы также используем их для связи с библиотекой клас iostreams. В частности, они обеспечивают связь с классом буфера. Компонент буфе iostreams-классов имеет три семейства классов. Эти три типа буферных классов пеп числены в табл. 11.3. Таблица 11.3. Три типа буферных классов Типы классов Описание bas i c_s treambu f Описывает поведение различных потоковых буферов с целью управления входными и выходными последовательностями символов bas i c_s tr ingbu f Связывает входные и выходные последовательности с последовательностью произвольных символов, которая может быть использована для инициализации или доступна в качестве строкового объекта basic_f ilebuf Связывает входные и выходные последовательности символов с файлом Рассмотрим подробнее класс basic_filebuf. Тогда как класс basic_streambuf используется в качестве объектно-ориентированного буфера в операциях ввода-вывода с применением стандартного потока, а класс basic_stringbuf — в качестве объектно- ориентированного буфера для памяти, класс basic_f ilebuf применяется в качестве объектно-ориентированного буфера для файлов. Рассмотрев интерфейс для класса bas ic_f ilebuf и интерфейс для классов преобразования (basic_if stream, basic_of stream и basic_f stream), можно найти способ связать дескрипторы файлов, возвращаемые системной функцией pipe (), с объектами класса basic_iostream. На рис. 11.8 показаны диаграммы классов для семейства f stream-классов. basic fstream basic filebuf v basic iostream basic ifstream I basic filebuf basic istream basic ofstream 1 basicjilebuf \ 7 basic_ostream Рис. 11.8. Диаграммы классов для семейства f stream-классов
11.5. Объектно-ориентированные каналы и FIFO-очереди... 405 Обратите внимание на то, что все классы basic_ifstream, basic__of stream basic_f stream содержат класс basic_f ilebuf. Следовательно, чтобы упростить здание объектно-ориентированного канала, мы можем использовать любой класс из емейства f stream-классов. Мы можем связать дескрипторы файлов, возвращаемые истемной функцией pipe (), либо с помощью конструкторов, либо с помощью функции-члена attach(). Синопсис #include <fstream> // UNIX-системы ifstream(int fd) fstreamdnt fd) ofstream(int fd) // gnu C++ void attach (int fd) ; 11.5.1. Связь каналов с iostream-объектами с помощью дескрипторов файлов Существует три iostream-класса (if stream, of stream и f stream), которые мы можем использовать для подключения к каналу. Объект класса if stream используется для ввода данных, объект класса of stream— для их вывода, а объект класса f stream можно применять и в том и в другом случае. Несмотря на то что непосредственная поддержка дескрипторов файлов и потоков ввода-вывода не является частью стандарта ISO, в большинстве UNIX- и Linux-сред поддерживается С++- ориентированный iostream-доступ к дескрипторам файлов. В библиотеке GNU C++ iostreams предусмотрена поддержка дескриптора файла в одном из конструкторов классов if stream, of stream и f stream и в методе attach () , определенном в классах if stream и of stream. UNIX-компилятор языка C++ компании Sun также поддерживает дескрипторы файлов с помощью одного из конструкторов классов if stream, of stream и f stream. Поэтому при выполнении следующего фрагмента кода //... ^t Pd[2]; ^ipe(Fd); ifstream IPipe(Fd[0]) ; °fstream 0Pipe(Fd[l]); /Дут созданы объектно-ориентированные каналы. Объект IPipe будет играть Р ль входного потока, а объект OPipe— выходного. После создания эти потоки жно применять для связи между параллельно выполняемыми процессами с ис- ьзованием потокового представления и операторов вставки (<<) и извлечения )- Для С++-сред, которые поддерживают метод attachO, дескриптор файла Но связать с объектами классов if stream, of stream или f stream, используя следующий синтаксис>
406 Глава 11. Проектирование компонентов для поддержки параллелизма // Листинг 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 <unistd.h> 2 #include <iostream.h> 3 #include <fstream.h> 4 #include <math.h> 5 #include <sys/wait.h> 6 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.closeO ; 33 34 } 35 36 }
11.5. Объектно-ориентированные каналы и FIFO-очереди... 407 Вспомните, что значение 0, возвращаемое функцией fork(), принадлежит сы- внему процессу. В программе 11.1 канал создается при выполнении инструкции, асположенной на строке 17. А при выполнении инструкции, расположенной на тооке 29, родительский процесс открывает канал для записи. Файловый дескриптор Fd Г1 ] означает "записывающий" конец канала. К этому концу канала (благодаря вы- obv конструктора на строке 29) присоединяется объект класса of stream. К "считывающему" концу канала присоединяется объект класса if stream (строка 23). Сыновний процесс открывает канал для чтения и получает доступ к дескриптору файла, поскольку он вместе со средой родителя наследует и дескрипторы файлов. Таким образом, любые файлы, которые открыты в среде родителя, будут оставаться открытыми и в среде наследника, если операционная система не получит явные инструкции, основанные на системной функции f cntl. Помимо наследования открытых файлов, маркеры внутрифайловых позиций остаются там, где они были в момент порождения сыновнего процесса, чтобы сыновний процесс также получил доступ к маркеру позиции. При изменении позиции в родительском процессе маркер сыновнего также смещается. В этом случае мы могли бы реализовать потоковое представление данных, не создавая интерфейсный класс. Просто присоединив файловые дескрипторы канала к объектам классов of stream и if stream, мы сможем использовать операторы вставки (<<) и извлечения (»). Аналогично любой класс, в котором определены операторы "»" и "<<", может выполнять операции вставки данных в канал и извлечения их оттуда без какого-либо дополнительного программирования. В программе 11.1 родительский процесс помещает значение М_Р1 в канал (строка 30), а сыновний процесс извлекает это значение из канала, используя оператор ">>" (строка 24). Инструкции по выполнению и компиляции этой программы приведены в разделе "Профиль программы 11.1". j Профиль программы 11.1 Имя программы lprogramll-l.ee I Описание \Программа 11.1 демонстрирует использование объектно-ориентированного потока р использованием анонимных системных каналов. Для создания двух процессов, ^которые взаимодействуют между собой с помощью операторов вставки («) и извлечения (»), программа использует функцию fork(). \ Требуемые заголовки i , г ait.h>, <unistd.h>, <iostream.h>, <f stream.h>, <math.h>. Инструкции по компиляции и компоновке программ |с++ -о programll-1 programll-l.ee : сРеда для тестирования .Solaris 8, SuSE Linux 7.1. I Инструкции по выполнению f r/programll-i
408 Глава 11. Проектирование компонентов для поддержки параллелизма Компилятор gnu C++ также поддерживает метод attach (). Этот метод можн пользовать для связи файловых дескрипторов с объектами классов ifSb И° и of stream (листинг 11.21). Геа1п // Листинг 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[l] ) объект класса of stream связывается с файловым дескриптором канала. Теперь любая информация, которая будет помещена в объект Out, в действительности запишется в канал. Использование операторов извлечения и вставки для выполнения автоматического преобразования формата является основным достоинством использования семейства f stream-классов в сочетании с канальной связью. Возможность применять пользовательские средства извлечения и вставки избавляет программиста от определенных трудностей, которые могут иметь место при программировании каналов связи. Поэтому вместо явного перечисления размеров данных, записываемых в канал и читаемых из него, при управлении доступом для чтения-записи мы используем только количество передаваемых через канал элементов, что существенно упрощает весь процесс. К тому же такое "снижение себестоимости" немного упрощает параллельное программирование. Рекомендуемый нами метод состоит в использовании архитектуры, в основе которой лежит принцип "разделяй и властвуй". Главное — правильно расставить компоненты "по своим местам" — и программирование станет более простым. Например, поскольку канал связывается с объектами классов of stream и if stream, мы можем использовать информацию, хранимую компонентом ios, для определения состояния канала. Компоненты преобразования iostreams-классов можно использовать для выполнения автоматического преобразования данных, помещаемых в один конец канала и извлекаемых из его другого конца. Использование каналов вместе с iostream-классами также позволяет программисту интегрировать стандартные контейнеры и алгоритмь с использованием межпроцессного взаимодействия на основе канала. На рис. 11-У п казаны взаимоотношения между объектами классов if stream, of stream, канало и средствами вставки и извлечения при организации межпроцессного взаимодействи Для чтения данных из канала и записи данных в канал можно также использов семейство классов f stream и функции-члены read () и write (). 11.5.2. Доступ к анонимным каналам с использованием итератора ostream_iterator Канал можно также использовать с итераторами ostream_iterator и istr ^ iterator, которые представляют собой обобщенные объектно-ориентирова указатели. Итератор ostream_iterator позволяет передавать через канал Ие
11.5. Объектно-ориентированные каналы и FIFO-очереди... 409 йнеры (те- списки, векторы, множества, очереди и пр.). Без использования *° ггеадюбъектов и итератора ostream_iterator передача контейнеров объектов бы- *°б i очень громоздкой и подверженной ошибкам процедурой. Операции, которые дос- Ла яля классов ostream_iterator и istream_iterator, перечислены в табл. 11.4. тупны /V ПРОЦЕСС А ПРОЦЕСС В ,;2ЁШШЬ. ostream КАНАЛ (средство g°™ вставки) L Utebuf fd д fpil «mm sz ! Конец записи Конец чтения [.(средство 4 извлечения) f ■ (ci[0j Рис. 11.9. Взаимоотношения между объектами классов if stream, of stream, каналом и средствами вставки и извлечения при организации межпроцессного взаимодействия Щ|6лица/11.4. Операции, доступные для классов ostreairuiterator '■$$$%%*•' • * и istream_iterator Итераторы Операции Описание istream_iterator °stream_iterator а == b а != b *а + 4- Г Г 4-4- 4-4- Г Г 4-4- Отношение эквивалентности Отношение неэквивалентности Разыменовывание Инкремент (префиксная форма) Инкремент (постфиксная форма) Инкремент (префиксная форма) Инкремент (постфиксная форма) Обычно эти итераторы используются вместе с iostreams-классами и стандартными °ритмами. Итератор ostream_iterator предназначен только для последова- ьно выполняемой записи. После доступа к некоторому элементу программист не ет вернуться к нему опять, не повторив всю итерацию сначала. При использова- этих итераторов канал обрабатывается как последовательный контейнер. Это озирает, что ostre, алгс Что при связывании канала с iostreams-объектами посредством итератора am_iterator и файловых дескрипторов мы можем применить стандартный t_ Ритм обработки данных для ввода их из канала и вывода их в канал. Причина того, и итераторы можно использовать вместе с каналами, состоит в связи, которая
410 Глава 11. Проектирование компонентов для поддержки параллелизма существует между итераторами и iostreams-классами. На рис. 11.10 предст на диаграмма, отображающая отношения между итераторами ввода-выво ^ iostreams-классами. и Объектно- КонтейнерА| ориентированный ;-кон тайнее В! Объект ^копирование ofstream /т PIPE A ,'fefrear" I 1 I Объект"" $«w Sftrrr ostream^ iterator^ Рис. 11.10. Отношения между итераторами ввода-вывода и iostreams-классами На рис. 11.10 также показано, как эти классы взаимодействуют с объектно-ориентированным каналом. Рассмотрим подробнее, как итератор ostream_iterator используется с объектом класса ostream. Если инкрементируется указатель, мы ожидаем что он будет указывать на следующую область памяти. Если же инкрементируется итератор ostream_iterator, он перемещается на следующую позицию выходного потока. Присваивая значение разыменованному указателю, мы тем самым помещаем это значение в область, на которую он указывает. Присваивая значение итератору ostream_iterator, мы помещаем это значение в выходной поток. Если выходной поток связан с объектом cout, это значение отобразится на стандартном устройстве вывода. Мы можем объявить объект класса ostream_iterator следующим образом. ostream_iterator<int> X(cout,"\n"); Тогда X является объектом типа ostream_iterator. При выполнении операции инкремента Х++; итератор X перейдет к следующей позиции выходного потока. А при выполнении этой инструкции присваивания *Х = Y; значение Y будет отображено на стандартном устройстве вывода. Дело в том, что оператор присваивания "=" перегружен для использования объекта класса ostream. В результате объявления ostream__iterator<int> X(cout, "\n"); будет создан объект X с использованием аргумента cout. Второй аргумент в констр, торе является разделителем, который автоматически будет размещаться после кажд го int-значения, вставляемого в поток данных. Объявление итерат у ostream_iterator выглядит следующим образом (листинг 11.22). // Листинг 11.22. Объявление класса ostream_iterator template <class _Tp> class ostream_iterator { protected: ostream* _M_stream; const char* _M_string; public:
11.5. Объектно-ориентированные каналы и FIFO-очереди... 411 edef output_iterator_tag iterator_category; r^edef void value_type; tvpedef void dif ference_type; tvpedef void pointer; typedef void reference; ctream__iterator (ostream& s) : _M_stream(& s) , JM_string(0) {} ostream__iterator (ostream& s, const char* c) : _M_s trearn (& s) , JM_s tring ( с) { } ostream_iterator<_Tp>& operator=(const _Tp& value) { *JM_stream « value; if (_M_string) *_M_stream « _M_string; return *this; ostream_iterator<_JTp>& 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 j-0 int main (int argc, char *argv[]) 12 ^ int Size,Pid,Status,Fdl[2],Fd2[2]; ri Pipe(Fdl); pipe(Fd2); I strstream Buffer; ^ char Value[50]; ^ float Data; 19 ^ect°r<float> X(5,2.1221), Y; 20 ^uffer « Fdl[0] « ends; 2 Buffer » Value; 22 setenv("Fdin"#Value,1); 23 Bnf^r-Clear(); 24 t !fer « Fd2[l] « ends; 25 Buffer » value; 26 *etenv("Fdout",Value,1); Fla = fork();
412 Глава 11. Проектирование компонентов для поддержки параллелизма 27 if(Pid != 0){ 28 ofstream OPipe; 29 OPipe.attach(Fdl[l]); 30 ostream_iterator<float> OPtr(OPipe,"\n"); 31 OPipe << X.sizeO « 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 pe » Data; 40 Y.push_back(Data); 41 } 42 wait(&Status); 43 ostream_iterator<float> OPtr2(cout,"\n"); 44 copy (Y.begin () ,Y.end() /OPtr2) ; 45 OPipe.close(); 46 IPipe.close(); 47 } 48 else{ 49 execl("./programll-2b",иргодгат11-2Ь"/NULL); 50 } 51 52 return(0); 53 } В строках 21 и 25 системная функция setenv () используется для передачи значений файловых дескрипторов сыновнему процессу. Это возможно благодаря тому, что сыновний процесс наследует среду родительского процесса. Мы можем устанавливать переменные среды в программе с помощью вызова функции setenv (). В данном случае мы устанавливаем их следующим образом. Fdin=filedesc; Fdout=filedesc; Сыновний процесс затем использует системный вызов getenv() для считывания значений переменных Fdin и Fdout. Значение переменной Fdin будет представлять "считывающий конец" канала для сыновнего процесса, а значение переменной Fdout — "записывающий". Использование системных функций setenv () и getenv( J обеспечивает простую форму межпроцессного взаимодействия (interprocess communication — IPC) между родительским и сыновним процессами. Каналы создаются при выполнении инструкций, приведенных в строке 14. Родительский проце присоединяется к одному концу канала для операции записи с помощью метод attach () (строка 29). После присоединения любые данные, помещенные в объе OPipe типа of stream, будут записаны в канал. Итератор типа ostream_itera подключается к объекту OPipe при выполнении следующей инструкции (строка 5 )• ostream_iterator<float> OPtr(OPipe,"\n"); Теперь итератор OPtr ссылается на объект OPipe. После каждой порции поме мых в канал данных будет вставляться разделитель "\п". С помощью итератора мы можем поместить в канал любое количество float-значений. При этом мы м связать с каналом несколько итераторов различных типов. Но в этом случае нео ^ мо, чтобы на "считывающем" конце канала данные извлекались с использование.
11.5. Объектно-ориентированные каналы и FIFO-очереди... 413 соответствующих типов. При выполнении следующей инструкции из програм- РаТ?? о в канал сначала помещается количество элементов, подлежащих передаче: *"ipe « X.sizeO « endl; г ми элементы отправляются с использованием одного из стандартных С++- алгоритмов: copy(X.begin(),X.end(),OPtr); Алгоритм сору () копирует содержимое одного контейнера в контейнер, связан- й с итератором приемника. Здесь итератором приемника является объект OPtr. Объект OPtr связан с объектом OPipe, поэтому при выполнении алгоритма сору () /"уместившегося" в одной строке кода) в канал переписывается все содержимое кон- ейнера. Этот пример демонстрирует возможность использования стандартных алго- питмов для организации взаимодействия между различными частями сред параллельного или распределенного программирования. В данном случае алгоритм сору () пересылает информацию от одного процесса другому (из одного адресного пространства в другое). Эти процессы выполняются параллельно, и алгоритм сору () значительно упрощает взаимодействие между ними. Мы подчеркиваем важность этого подхода, поскольку, если есть хоть какая-то возможность упростить логику параллельной или распределенной программы, ею нужно непременно воспользоваться. Ведь межпроцессное взаимодействие — это один из самых сложных разделов параллельного или распределенного программирования. С++-алгоритмы, библиотека классов iostreamS и итератор типа ostream_iterator как раз и позволяют понизить уровень сложности разработки таких программ. Использование манипулятора flush (в строке 33) гарантирует прохождение данных по каналу. Инструкции по выполнению и компиляции этой программы, а также заголовки, которые необходимо в нее включить, приведены в разделе "Профиль программы 11.2". .. ,W«!p"w*> *4».*"*f » фЬфйль программы 11.2 мя программы •писание - Шмма использует библиотеку классов iostreams и итератор типа ._,_„. ^iterator для пересылки содержимого векторного контейнера через ано- ЙР^Уемые заголовки &.aB'fFrifchm>» <fstream>, <vector>, <iterator>, <stdlib.h>, <string.h>, Ьл^Р^кции по компиляции и компоновке программ |f*fv?P Programll-2 programll-2.cc ! для тестирования y :.aw ~-]и по выполнению J'
414 Глава 11. Проектирование компонентов для поддержки параллелизма В программе 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 argcchar *argv[]) 20 { 21 char Value[50]; 22 int Fd[2]; 23 float Data; 24 vector<float> X; 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<float> OPtr(OPipe,"Nn"); 36 IPipe » NumElements; 37 for(int N = 0;N < NumElements;N++) 38 { 39 IPipe » Data; 40 X.push_back(Data); 41 } 42 OPipe « X.sizeO « endl; 43 transform(X.begin() ,X.end() ,OPtr,N) ; 44 OPipe « flush; 45 return(O); 46 47 } Сыновний процесс считывает элементы данных из канала, помещает их в вектор, а затем выполняет математические преобразования над каждым элементом вектор • после чего отправляет их назад родительскому процессу. Математические преоор вания (строка 43) выполняются с использованием стандартного С++-алгорит. transform и пользовательского класса multiplier. Алгоритм transform приме ет к каждому элементу контейнера операцию, а затем результат этой операции п • щает в контейнер-приемник. В данном случае контейнером-приемником служиТ ект Opt г, который связан с объектом OPipe. Заголовки, которые необходимо в чить в программу 11.2.1, приведены в разделе "Профиль программы 11.2.1".
11.5. Объектно-ориентированные каналы и FIFO-очереди... 415 Рпрофияь программы 11.2.1 Щи* программы jprograirul-2b.ee ! Описание : огоамма представляет собой код сыновнего процесса, который запускается про- ^ аммой11.2- В этой программе для получения содержимого контейнера, отправ- I Энного из программы 11.2, используется объект класса ifstream. Для отправки че- (оез канал обработанной информации родительскому процессу в программе используется объект класса ostream_iterator и стандартный алгоритм transform. требуемые заголовки !<iostream>, algorithm>, <fstream>, <vector>, <iterator>, <stdlib.h>, lustring.h>, <unistd.h>. f Инструкции по компиляции и компоновке программ fc+i* -o'programll-2b programll-2b.ee j Инструкции по выполнению |эта программа запускается программой 11.2. Несмотря на то что классы библиотеки iostream, итераторы типа istream_iterator и ostream__iterator упрощают программирование канала, они не изменяют его поведение. По-прежнему остаются в силе вопросы блокирования и проблемы, связанные с корректным порядком открытия и закрытия каналов, рассмотренные в главе 5. Но использование основных механизмов тех же методов объектно- ориентированного программирования все же позволяет понизить уровень сложности параллельного и распределенного программирования. 11.5.3. FIFO-очереди (именованные каналы), iostreams-классы и итераторы типа ostream__iterator Методы, которые мы использовали для реализации объектно-ориентированных онимных каналов, обладают двумя недостатками. Во-первых, любым процессам, ко- рые взаимодействуют с другими процессами, нужен доступ к файловым дескрипто- Р * i, возвращаемым при вызове системной функции pipe (). Поэтому существует Р лема получения этих файловых дескрипторов для всех процессов-участников. по ПР°5лема легко решается, если процессы связаны отношением "родитель- омок (как в программах 11.1, 11.2 и 11.2.1), но в этом случае возникает другая кан СМа ходит' во-вторых, что процессы, которые используют неименованные щ х' Должны быть связаны отношениями. Это требование можно обойти с помо- тУРа FTPМЫ "еРедачи Дескриптора. Для решения этой проблемы используется струк- Дост (First In — First Out — первым прибыл, первым обслужен). Самое большое ее сВяз СТВо Как раз и состоит в том, что к ней могут получить доступ процессы, не Ые никакими отношениями. Процессы должны выполняться на одном компь-
416 Глава 11. Проектирование компонентов для поддержки параллелизма ютере — это единственное, что должно их связывать. При этом процессы могут з каться программами, реализованными на разных языках программирования и пользованием различных парадигм программирования (например, обобщен <- или объектно-ориентированной). При групповых вычислениях и при использ ** нии других конфигураций равноправных элементов можно воспользоваться п имуществами FIFO-очередей (иногда называемых именованными каналами) скольку в UNIX- и Linux-среде FIFO-структура имеет имя (определяемое пользователем) и ее (в отличие от анонимных каналов) можно сравнить с "капитальнь сооружением". FIFO — однонаправленная структура, а это значит, что пользовател именованного канала в среде UNIX должен открыть его либо для чтения, либо для записи, но не для того и другого одновременно. Именованные каналы, созданные в среде UNIX, остаются в файловой системе до тех пор, пока они не будут явно удалены с помощью вызова из программы функции unlink () или выполнения соответствующей команды из командной строки (например, команды rm). Именованным каналам при их создании присваивается эквивалент имени файла. Любой процесс, которому известно имя канала и который обладает необходимыми правами доступа, может открыть его, прочитать из него данные и записать их туда. Чтобы связать анонимные каналы с объектами классов if stream и of stream, мы использовали нестандартное связывание с файловым дескриптором. Нестандартность ситуации вытекает из того, что "брак" между файловыми дескрипторами и iostreams-объектами пока не "освящен" стандартом ISO C++. Поэтому безопаснее использовать FIFO-структуры. К FIFO-файлу специального типа можно получить доступ с помощью имени в файловой системе, в которой "официально" поддерживается связывание с объектами С++-классов if stream и of stream. Поэтому точно так же, как мы упрощали межпроцессное взаимодействие (IPC) с помощью iostream-классов и анонимного канала, мы упрощаем доступ к FIFO-структуре. FIFO-структура, основные функции которой совпадают с функциями анонимного канала, позволяет распространить возможности взаимодействия на классы, не связанные никакими родственными отношениями. Однако каждая программа— участник взаимодействия должна при этом "знать" имена FIFO-структур. Это требование, казалось бы, напоминает ограничение, с котороым мы встречались при использовании файловых дескрипторов. Однако FIFO — это все же "шаг вперед". Во-первых, при открытии анонимного канала только система определяет, какие файловые дескрипторы доступны в данный момент. Это означает, что программист не в состоянии полностью контролировать ситуацию. Во-вторых, существует ограничение на количество файловых дескрипторов, котороми располагает система. В-третьих, поскольку FIFO-структурам имена присва ваются пользователем, то количество таких имен не ограничивается. Файловые дес рипторы должны принадлежать файлам, открытым ранее (и причем успешно], а FIFO-имена — это всего лишь имена. FIFO-имя определяется пользователем, а <ра ловые дескрипторы— системой. Имена файлов связываются с объектами класс if stream, f stream и of stream с помощью либо конструктора класса либо мет ореп(). В программе 11.3.1 для связывания объектов классов of stream и if str с FIFO-структурой используется конструктор. // Программа 11.3.1 14 using namespace std; 15
11.5. Объектно-ориентированные каналы и FIFO-очереди... 417 const int FMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; 17 18 int main(int argc, char *argv[]) 19 { 20 2i int Pid,Status,Size; 22 double Value; 25 mkf ifо("/tmp/channel.1",FMode); 26 mkf ifo("/tmp/channel.2",FMode); 28 vector<double> Х(ЮОДЗ.О); 29 vector<double> Y; 30 ofstream OPipe("/tmp/channel.Iй,ios::app); 31 ifstream IPipe("/tmp/channel.2"); 32 OPipe << X.sizeO « endl; 33 ostream_iterator<double> Optr(OPipe,"\n"); 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.endO , -13 .0) « endl; 48 49 return(O); 50 } В программе 11.3.1 используется две FIFO-структуры. Вспомните, что FIFO- структуры являются однонаправленными компонентами. Поэтому, если процессы Должны обмениваться данными, то необходимо использовать по крайней мере две FIFO-структуры. В программе 11.3.1 они называются channel. 1 и channel. 2. Обратите внимание на установку флагов полномочий для FIFO-структур (строка 16). Эти полномочия означают, что владелец FIFO-структуры имеет право доступа для чтения записи, а все остальные — право доступа только для чтения. При выполнении стро- и 30 FIFO-структура channel. 1 будет открыта только для вывода данных. Тот же ре- ультат можно было бы получить следующим образом: 0pipe. open (" /tmp/channel. 1 -, ios: : app) ; Используемые здесь параметры алгоритма open () означают, что FIFO-структура УДет открыта в режиме дозаписи. В программе 11.3.1 алгоритм сору () используется вставки объектов в объект OPipe типа f stream и косвенно в FIFO-структуру. Мы и бы также использовать здесь объект типа f s trearn: Stream OPipe("/tmp/channel.1", ios::out I ios::app); этом случае взаимодействие процессов было бы ограничено выводом данных об-к ° В Режиме Дозаписи. Если бы мы не использовали флаг ios: :app, попытка типа ofstream создать FIFO-структуру (см. строку 30) была бы неудачной.
418 Глава 11. Проектирование компонентов для поддержки параллелизма К сожалению, такой вариант работать не будет. Создание FIFO-структур наход в компетенции функции mkfifo(). В строках 45 и 46 программы 11.3.1 FIFO* структуры удаляются из файловой системы. С этого момента любые процессы торых открыты FIFO-структуры, еще в состоянии получить к ним доступ. Однако имен больше не существует. Поэтому такие процессы не смогут использовать ал ритм open О или создать новые объекты типа of stream или if stream на осно имени, которое было "отсоединено". В строках 32-34, объекты типа ostream iterator и of stream используются для вставки элементов в FIFO-структуру. Обп^ тите внимание на то, что программа 11.3.1 не образует никаких ветвлений и не создает сыновних процессов. Программа 11.3.1 зависит от другой программы, которая должна считывать информацию из FIFO-структуры channel. 1 или записывать информацию в FIFO-структуру channel. 2. Если такая программа не будет работать одновременно с программой 11.3.1, последняя останется заблокированной. Детали реализации приведены в разделе "Профиль программы 11.3.1". : Профиль программы 11.3.1 Имя программы programll-За.ее Описание Для пересылки контейнерного объекта через FIFO-структуру используются объекты типа ostream_iterator и ofstream. Для извлечения информации из FIFO- структуры применяется объект типа ifstream. Требуемые заголовки <unistd.h>, <iomanip>, <algorithm>, <fstream.h>, <vector>, <iterator> <strstream.h>, <stdlib.h>, <sys/wait .h>, <sys/types.h>, <sys/stat.h> <f cntl .h>, <numeric>. Инструкции по компиляции и компоновке программ C++ -о programll-За programll~3a.ee Среда для тестирования SuSE Linux 7.1, gee 2.95.2, Solaris 8, Sun Workshop 6. Инструкции по выполнению ./programll-За & programll-3b Примечания Сначала запускается программа 11.3.1. Программа 11.3.2 содержит инструкцию sleep, которая восполняет собой отсутствие реальной синхронизации. Программа 11.3.2 считывает данные из FIFO-структуры channel. 1 и записыва информацию в FIFO-структуру channel. 2. // Программа 11.3.2. Считывание данных из FIFO-структуры // channel.1 и запись информации в
11.5. Объектно-ориентированные каналы и FIFO-очереди... 419 FIFO-структуру channel.2 no using namespace std; 11 12 class multiplied ^3 double X; 14 public: -15 multiplier (double Value) { X = Value;} double &operator()(double Y) { X = (X * Y);return(X);} >; int main(int argcchar *argv[]) { double Size; double Data; vector<double> X; multiplier R(1.5); sleep(15); fstream IPipe("/tmp/channel. 1") ; ofstream OPipe("/tmp/channel. 2",ios::app); if(IPipe.is_open()){ IPipe » Size; } else{ exit(l); } cout « "Количество элементов " << Size << endl; for(int N = 0;N < Size;N++) { IPipe » Data; X.push_back(Data); } OPipe « X.sizeO « endl; ostream_iterator<double> Optr(OPipe,"\n"); transform(X.begin() ,X.end() ^ptr^) ; OPipe « flush; OPipe.close(); IPipe.closeO ; return(0); Обратите внимание на то, что в программе 11.3.1 FIFO-структура channel. 1 отбывается для вывода данных, а в программе 11.3.2 та же FIFO-структура channel. 1 — ввода данных. Следует иметь в виду, что FIFO-структуры действуют как однона- равленные механизмы связи, поэтому не пытайтесь пересылать данные в обоих на- равлениях! Достоинство использования iostreams-классов в сочетании с FIFO- руктурами состоит в том, что мы можем использовать iostreams-методы применительно к FIFO -структурам. Например, в строке 30 мы используем метод is__open() ССа basic_f ilebuf, который позволяет определить, открыта ли FIFO-структура. и она не открыта, то программа 11.3.2 завершается. Детали реализации програм- 1.3.2 приведены в разделе "Профиль программы 11.3.2".
420 Глава 11. Проектирование компонентов для поддержки параллелизма Профиль программы 11.3.2 Имя программы - programll-ЗЬ. ее * Описание Программа считывает объекты из FIFO-структуры с помощью объекта типа if stream. Для пересылки данных через FIFO-структуру здесь используется итера- «тор типа ostream_iterator и стандартный алгоритм trans form. ' Требуемые заголовки ><unistd.h>, <iomanip>, <algorithm>t <fstream.h>, <vector>, <iterator> :<strstream.h>, <stdlib.h>, <sys/wait .h>,<sys/types.h>, <sys/stat.h>, -<f cntl ,h>, <numeric>. I Инструкции по компиляции и компоновке программ C++ -о programll-3b programll-3b.ee ; Среда для тестирования fSuSE Linux 7.1, gee 2.95.2, Solaris 8, Sun Workshop 6.0. j Инструкции по выполнению iprogramll.3a & programll-3b \ Примечания :Сначала запускается программа 11.3.1. Программа 11.3.2 содержит инструкцию, [Sleep, которая восполняет собой отсутствие реальной синхронизации. 11.5.3.1. Интерфейсные 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» (f if о &Out, float X) ; //. . . >; В этом случае мы можем легко создавать объекты класса fifo с помощью констр) ра, а также передавать их как параметры и принимать в качестве значений, возвр щаемых функциями. Мы можем использовать их в сочетании с классами стандартн
11.6. Каркасные классы 421 тейнеров. Применение такой конструкции значительно сокращает объем кода, бходимого для функционирования FIFO-механизма. Более того, "классовый" под- создает условия для обеспечения типовой безопасности и вообще позволяет про- мисту работать на более высоком уровне. 11.6. Каркасные классы Под каркасным понимается класс (или целая коллекция классов), который имеет за- оанее определенную структуру и представляет обобщенную модель поведения. Точно так же, как программы обеспечивают общие решения для конкретных задач, каркасные классы предоставляют конкретные решения для классов задач. Другими словами, каркас приложений содержит общую направленность выполнения кода для целого диапазона программ, которые решают задачи подобным образом. Поскольку каркас приложений представляет одно решение для семейства задач, то их можно назвать обощенными автономными мини-приложениями. Каркасный класс служит своего рода проектом для мини-приложения. Он предлагает фундаментальную структуру (скелет), которую должно иметь приложение, не навязывая никаких деталей. Каркасный класс определяет отношения, распределяет обязанности, намечает порядок действий и протоколы между частями ПО в объектно-ориентированной архитектуре. Например, мы можем спроектировать класс языкового процессора, который должен содержать общую схему работы для целого диапазона приложений. Эта схема должна определить действия, которые необходимо выполнить для преобразования некоторого входного языка в заданный выходной формат. Такой каркас состоит из нескольких общих частей ПО: • компоненты проверки достоверности; • компоненты выделения лексем; • компоненты грамматического разбора; • компоненты синтаксического анализа; • компоненты лексического анализа. Эти части ПО можно объединить, чтобы сформировать уже знакомую нам программную конструкцию (листинг 11.24). 'I Листинг 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) ; language_processor::process_input(void)
422 Глава 11. Проектирование компонентов для поддержки параллелизма getString(); validateStringO ; 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 (при надлежа щем его тестировании и отладке) позволяет ускорить разработку широкого диапазон приложений. Понятие каркасного класса также полезно использовать при разработке прилож ний, к которым предъявляются требования параллелизма. Так, использование are ных каркасов и каркасов "классной доски" фиксирует базовую структуру параллелиз. и схемы работы в этих структурах. Майкл Вулдридж в своей книге [51] предлага следующий обобщенный цикл управления агентами.
11.6. Каркасные классы 423 —^ритм: цикл управления агентами В = ВО while true do get next percept p Б = brf(B,p) I = deliberate(B) f]= plan(B,I) execute(П) end while Эта модель поведения реализуется широким диапазоном рациональных агентов. Если вы разрабатываете программу, в которой используются рациональные агенты, то скорее всего эта последовательность действий будет реализована в вашей программе. На фиксации последовательностей действий такого типа и "специализируются" каркасные классы. Для цикла управления агентами функции brf (), deliberate () и plan () должны быть объявлены чисто виртуальными функциями. Цикл управления агентами определяет, в каком порядке и как должны вызываться эти функции, а также сам факт того, что они должны быть вызваны. Однако конкретное содержание функции определит производный класс. При надлежащем определении цикла управления агентами будет решен целый класс проблем. Ведь системы, состоящие из множества параллельно выполняющихся агентов, постепенно становятся стандартом для реализации приложений параллельного программирования. Такие системы часто называют мультиагентными системами. Агентно-ориентированные системы мы рассмотрим в главе 12, а пока отметим, что агентные каркасные классы позволяют понизить уровень сложности разработки мультиагентных систем, что очень ценно в свете того, что мультиагентные системы становятся предпочтительным вариантом архитектуры для реализации средне- и крупномасштабных приложений, которые требуют реализации параллелизма или массового параллелизма. Каркасные классы обеспечивают своих потомков не только планом действий (что весьма полезно для параллельных или распределенных систем), но и такими компонентами синхронизации, как объектно-ориентированные мьютексы, семафоры и потоки сообщений. Структура "классной доски" — полезное средство для взаимодействия множества агентов— представляет собой критический раздел, поскольку сразу несколько агентов должны иметь возможность одновременно считывать из нее информацию и записывать ее туда. Следовательно, каркасный класс должен обеспечить базовую структуру для отношений между агентами, компонентами синхронизации и классной доской". Например, листинг 11.25 содержит два метода, которые каркасный класс мог бы использовать для доступа к "классной доске". / Листинг 11.25. Определение методов recordMessge() и getMessage() для класса agent_framework X{nt a9ent__framework: rrecordMessage(void) Mutex.lockO ; BlackBoardStream « Agent[N].message(); Mutex.uniockO ; agent_framework:igetMessage(void)
424 Глава 11. Проектирование компонентов для поддержки параллелизма Mutex.lockO ; BlackBoardStream » Values; Agent[N].perceive(Values); Mutex.unlock(); } Здесь каркасный класс должен защищать доступ к "классной доске" с помощью объектов синхронизации. Поэтому, когда агенты считывают сообщения с "классной доски" или записывают их туда, синхронизация уже будет обеспечена каркасным классом. Программисту не нужно беспокоиться о синхронизации доступа к "классной доске". Базовая структура агентно-ориентированного каркасного класса agent_f ramework показана на рис. 11.11. МУЛЬТИАГЕНТНАЯ СИСТЕМА agent_framework содержит ссылку на объект Blackboard I а объектно- ориентированные условные переменные объектно- ориентированные мьютексы ЦИКЛ УПРАВЛЕНИЯ АГЕНТАМИ "с"'В]йГ" i ^.^ Задача Предложения, данные,... Частные решения agent_framework Ь содержит ссылку на объект Blackboard объектно- ориентированные условные переменные объектно- ориентированные мьютексы ЦИКЛ УПРАВЛЕНИЯ АГЕНТАМИ ШОДЕЛк """':ВОГ '' MPI-поток сообщений, PVM-поток сообщений, FIFO-структуры или каналы, используемые для взаимодействия агентов Среда UNIX/Linux Рис. 11.11. Базовая структура каркасного класса agent_f ramework
11.7. Резюме 425 Обратите внимание на то, что каркасный класс инкапсулирует объектно- ентированные мьютексы и переменные условий. Агентно-ориентированный кар- ный класс (см. рис. 11.11) для организации взаимодействия процессов в MPI- либо pVM-ориентированной системе должен использовать MPI- либо PVM-потоки сообще- ий Вспомните, что эти потоки сообщений были разработаны как интерфейсные классы, что позволяет программисту для доступа к PVM- или MPI-классу использовать ostreamS-представление. Если MPI- или PVM-классы не используются, агенты могут взаимодействовать через сокеты, каналы или даже общую память. В любом случае мы оекомендуем реализовать примитивы синхронизации с помощью интерфейсных классов, которые упрощают их использование. Структура "классной доски", показанная на рис. 11.11, является объектно-ориентированной и использует преимущества универсальности, обеспечиваемой шаблонными классами, что также упрощает реализацию параллелизма. Агенты, выполняемые параллельно, представляют эффективную модель параллельного и распределенного программирования. 11.7. Резюме Проблемы параллельного программирования, представленные в главе 2, можно эффективно решить, используя "строительные блоки", рассмотренные в этой главе. Роль интерфейсного класса в упрощении использования библиотек функций трудно преувеличить. Интерфейсный класс вносит логичность API-интерфейса путем заключения в оболочку вызовов функций из таких библиотек, как MPI или PVM. Интерфейсные классы обеспечивают типовую безопасность и возможность многократного использования кода, а также позволяют программисту работать в привычной "системе координат", как с PVM- или MPI-потоками данных. Межпроцессное взаимодействие (IPC) упрощается путем связывания канала или потоков сообщений с iostreams-объектами и перегрузки операторов вставки («) и извлечения (») для пользовательских классов. Класс ostream_iterator доказывает свою полезность в "оптовой" пересылке контейнерных объектов и их содержимого между процессами. Итераторы типа ostream_iterator и istream_iterator также обеспечивают связующее звено между стандартными алгоритмами и IPC-компонентами и методами. Поскольку модель передачи сообщений используется во многих параллельных и распределенных приложениях, то любой метод, который упрощает передачу различных типов данных между процессами, упрощает программирование приложения в целом. К таким способам упрощения относится использование iostreams-классов и итераторов типа ostream_iterator и istream_iterator. Каркасный класс представлен здесь как базовый строительный блок параллельных приложений. Мы рассматриваем ассы» подобные классам мьютексов, переменных условий и потоков, как компоненты изкого уровня, которые должны быть скрыты от программиста в каркасном классе (где ° возможно!). При создании средне- и крупномасштабных приложений, которые тре- Уют реализации параллелизма, программист не должен "застревать" на этих низкоуровневых компонентах. В идеале для удовлетворения требований параллельной обратим каркасный класс должен быть строительным блоком базового уровня и обеспечи- нас "готовыми" схемами равноправных элементов и взаимодействия типа "клиент- рвер . мы Можем использовать различные типы каркасных классов: для обработки Сел, баз данных или применения агентов, технологии "классной доски", GUI и т.д.
426 Глава 11. Проектирование компонентов для поддержки параллелизма Метод, который мы предлагаем для реализации параллелизма, состоит в построен^ приложений на базе коллекции каркасных классов, которые уже оснащены надлежащ ми компонентами синхронизации, связанными соответствующими отношениям В главах 12 и 13 мы подробнее остановимся на каркасных классах, которые поддеру», вают параллелизм. Мы также рассмотрим использование стандартных С++-алгоритмок контейнеров и объектов-функций для управления процессом создания множества задач или потоков в приложениях, требующих параллелизма.
РЕАЛИЗАЦИЯ АГЕНТНО- ОРИЕНТИРОВАННЫХ АРХИТЕКТУР В этой главе... 12.1. Что такое агенты 12.2. Понятие об агентно-ориентированном программировании 12.3. Базовые компоненты агентов 12.4. Реализация агентов в C++ 12.5. Мультиагентные системы 12.6. Резюме
</\^JSJZJ х ~*?S f Нам предстоит сделать еще немало, прежде чем мы поймем, как люди описывают свои задачи и какую роль эти описания играют в решении задач. Но мы уже знаем достаточно для того, чтобы предположить, что используемые людьми описания, представленные как в виде высказываний, так и в форме рисунков, могут быть сымитированы компьютерами. — Герберт А. Саймон (Herbert A. Simon), Machine as Mind (Android Epistemology) Если бы последовательное (процедурное) программирование позволяло находить решения в любых ситуациях, то не было бы необходимости для развития технологий параллельного и распределенного программирования. Во многих случаях методы последовательного программирования просто не отвечают требованиям и опыту современных пользователей компьютеров. В процессе поиска разработчиками новых подходов к решению все возрастающих проблем и создаются альтернативные модели программного обеспечения. Программисты находят более эффективные способы организации ПО. Структурное программирование было шагом вперед по сравнению с процедурным (изобиловавшим безусловными переходами), объектно-ориентированное программирование сменило структурное. Во многих отношениях агенты и агентно-ориентированное программирование можно рассматривать как очередную (более высокую) ступень развития программирования. Агенты представляют иной (более сложный) метод организации и представления распределенных/параллельных программ.
12.1. Что такое агенты 429 12.1. Что такое агенты Когда объектное программирование впервые заявило о себе, сама трактовка понятия объекта вызвала большие споры. Подобные разногласия вызывает и трактовка понятия агента. Одни определяют агенты как автономные постоянно выполняющиеся программы, которые действуют от имени пользователя. Однако это определение можно применить и к UNIX-демонам или даже некоторым драйверам устройств. Другие дополняют это определение тем, что агент должен обладать специальными знаниями пользователя, должен выполняться в среде, "населенной" другими агентами, и обязан действовать только в рамках заданной среды. Эти требования должны исключать другие программы, которые можно было бы до некоторой степени считать агентами. Например, многие агенты электронной почты действуют автономно и могут работать по многих средах. Кроме того, в различных кругах программистов для описания агентов появились такие термины, как софтбот, т.е. программный робот (softbot), база знаний (knowbot), программный брокер (software broker) и интеллектуальный объект (smart object). В этой главе мы многократно будем определять термин агент. Начнем с простых согласованных частичных определений и построим определение, которое бы устраивало С++-программистов. Существует определение, согласно которому агент определяется как некоторая сущность, функционирующая постоянно и автономно в среде, в которой выполняются другие агенты и процессы. Хотя весьма заманчиво принять это определение и развить его, мы не будем этого делать, поскольку оно "с таким же успехом" описывает и другие виды программных конструкций. Многие объектно-ориентированные компоненты функционируют постоянно и автономно в среде, в которой выполняются другие процессы и существуют другие агенты. И в самом деле, многие CORBA-ориентированные системы типа "клиент-сервер" вполне соответствуют этому описанию! Поэтому, если мы заменим в этом определении слово агент словом объект, оно в точности опишет многие объектно-ориентированные системы. Если заглянуть в более официальный источник, Foundation for Intelligent Physical Agents (FIPA), то в соответствии с ним термин агент определяется следующим образом: Агент — это главный исполнитель в домене. Он обладает одной или несколькими сервисными возможностями, образующими единую и комплексную модель выполнения, которая может включать доступ к внешнему ПО, пользователям (людям) и средствам связи. Несмотря на то что это определение имеет более структурированную форму, оно также нуждается в дальнейшем уточнении, поскольку под это определение попадают многие серверы (объектно-ориентированные и нет). Это определение в таком виде включило бы слишком много типов программ и программных конструкции. И хотя мы опираемся на FIPA-спецификацию, это базовое определение требует дальнейшей проработки. "12.1.1. Агенты: исходное определение Одной из причин, по которой слово объект может заменить слово агент во многих определениях и описаниях агента, состоит в том, что агенты по сути основаны на °оъектах. И в самом деле, наше первое требование к определению агента заключается том, что оно в первую очередь должно удовлетворять определению объекта1, т.е. мы При использовании термина объект в определении агента мы включаем родственные для него поня- гля из области искусственного интеллекта: исполнитель и дЗрейм.
430 Глава 12. Реализация агентно-ориентированных архитектур имеем в виду, что агент — это объект определенного вида. В этой главе особый акцен делается на том, что отличает агента от других категорий объектов. Исходя из тог что C++ поддерживает интерфейсные, контейнерные и каркасные классы, мы може с таким же успехом ввести и агентные классы. Это приводит нас ко второму требова нию, выдвигаемому к определению агентов в С++-среде. В C++ агент реализуется с использованием понятия класса. Типы классов отличаются друг от друга тем, как они функционируют, или тем, как они структурированы. Например, контейнерный класс описывает объект, используемый для хранения других объектов. Интерфейсный класс применяется для описания объекта, который преобразует или адаптирует интерфейс другого объекта. Каркасный класс описывает объект, который содержит шаблон, или образец поведения, являющегося общим для целого семейства других объектов. Агентные классы предназначены для определения объектов, которые обладают тем, что Иогав Шохам (Yohav Shoham) описывает как психическое (интеллектуальное) состояние: "Психическое состояние должно включать такие компоненты, как представления, возможности, варианты выбора и обязательства". Это психическое состояние зачастую описывается моделью убеждений, желаний и намерений (Belief, Desires and Intentions— BDI). Мы расширяем модель BDI, чтобы включить в нее действия. Теперь в нашем первом определении агент описывается как часть ПО, отвечающая следующим требованиям. 1. Это определенный тип объекта (т.е. не все объекты являются агентами). 2. Его реализация использует понятие класса (для агентов весьма существенны инкапсуляция, наследование и полиморфизм). 3. Он содержит набор поведенческих вариантов и атрибутов, которые должны включать убеждения, желания, намерения и действия. В рамках нашего изложения материала агенты по определению являются рациональными программными компонентами. Прежде чем мы перейдем к дельнейшему определению агентов, рассмотрим типы агентов, которые реализуются чаще всего. 12.1.2. Типы агентов Существует несколько категорий агентов. Несмотря на то что не все агенты можно отнести к одной из них, с их помощью все же можно описать большинство агентов, которые уже нашли практическое применение. В табл. 12.1 перечислено пять основных категорий агентов. Очевидно, существуют и агенты смешанного типа, которые можно отнести к нескольким категориям одновременно, поскольку для распределения агентов по категориям нет никаких жестких правил. Эти категории представлены для удобства и используются в качестве отправной точки в попытке классифицировать агенты, которые, возможно, вам придется разрабатывать или использовать в своей работе. В табл. 12.1 не указаны компоненты, которые должны иметь агенты. Здесь определены лишь виды деятельности, которые характерны для агентов той или иной категории. При этом следует понимать, что эти категории не являются исключительной сферой агентов. Подобным образом по категориям можно разделить и другие классы ПО (например, экспертные и объектно-ориентированные системы). В нескольких случаях единственным отличием может оказаться сам факт того, что мы говорим оо агентах, а не об объектах или экспертных системах.
12.1. Что такое агенты 431 Таблица 12.1. Пять основных категорий агентов Категории агентов Описание Интерфейсные агенты Представляют следующее поколение взаимодействия между человеком и компьютером. Эти агенты обеспечивают новый пользовательский интерфейс с компьютером Агенты поиска Выполняют различные виды поиска информации Агенты Патрулируют, наблюдают, отслеживают (выполняемые дейст- мониторинга/управления вия), управляют и контролируют устройства и условия, данные и процессы Агенты сбора данных Уполномочены запросить некоторые данные или услуги от имени пользователя Агенты поддержки Обеспечивают анализ и синтез информации, интерпретацию принятия решений условий и данных, планирование действий и оценку результата 12.1.3. В чем состоит разница между объектами и агентами Агент прежде всего должен отвечать условиям объектной ориентации. Это означает, что агенты и объекты имеют больше общего, чем многие специалисты хотели бы это признать. Именно функциональная и конструктивная составляющие объектов сближают их с агентами. Объекты по определению самодостаточны и проявляют определенную автономность. Если степень автономности пересекает определенный порог, и объекту предоставляются такие когнитивные (познавательные) структуры данных, как те, что характерны для модели BDI, то такой объект является агентом. Автономный рациональный объект является агентом.2 Объект считается рациональным в случае, если он обладает: • методами, которые реализуют некоторую форму дедукции, индукции или абдукции; • членами данных, которые представляют собой реализации когнитивных структур данных. Следует иметь в виду, что в объектно-ориентированном программировании подпрограммы, определенные для класса, называются методами, а в C++— функциями- членами. Переменные или компоненты данных, определенные для класса, называются атрибутами, а в C++ — членами данных. Если некоторые функции-члены используются Для реализации дедукции, индукции или абдукции с использованием членов данных, °торые представляют собой реализации когнитивных структур данных, то такой ъект является рациональным. Если рациональный объект при этом пересекает определенный порог автономности, то это и есть агент. Мы намеренно избегаем термина интеллектуальный. В настоящее время неизвестно, будем ли мы п-либо создавать интеллектуальное программное обеспечение. Но бесспорно то, что мы можем созда- ва*пъ рациональное ПО на основе хорошо понимаемого логического формализма.
432 Глава 12. Реализация агентно-ориентированных архитектур Когнитивные структуры данных — это структуры, используемые для представлен таких интеллектуальных компонентов, как убеждения, намерения, обязательства п шения, настроения и знания. Например, мы могли бы обозначить структуру убежл ний, используя С++-множество (set). set<statements> Beliefs; struct statement{ //... float ArrivalTime; float DepartureTime; string Destination; //... >; Здесь инструкции связаны с составлением расписания для некоторого вида общественного, транспорта. Коллекция этих инструкций хранится в С++-множестве set<statements> и представляет "убеждения" агента. Это именно то, что мы подразумеваем под членами данных, которые являются реализациями когнитивных структур данных. Агент должен объявить член данных соответствующим образом. class agent{ set<statements> Beliefs; //. .. }; В классе agent для обработки множества Beliefs, чтобы сформировать намерения, обязательства или планы, используется дедукция, индукция или абдукция. Из нашего определения агентов следует, что, если мы имеем дело с рациональным автономным объектом, то мы имеем дело с агентом. Если он не рациональный, то он не агент, он — просто объект. А о степени автономности мы поговорим подробнее ниже в этой главе. 12.2. Понятие об агентно-ориентированном программировании Агентно-ориентированное программирование — это процесс назначения работы, порученной программе, одному или нескольким агентам. В декомпозиции работ (Work Breakdown Structure — WBS) в этом случае участвуют только агенты. Если всю работу, которую должна выполнить программа, можно назначить одному или нескольким агентам, мы имеем дело с чистой агентно-ориентированной программой, в которой весь необходимый объем проектирования и разработки требует только агентно-ориентированного программирования. Во многих ситуациях наряду с агентами в приложении будут задействованы и другие виды объектов и систем, которые не являются агентно-ориентированными, и, следовательно, такое программирование нельзя назвать агентно-ориентированным. Подобное сотрудничество часто имеет м ^ сто, когда агенты участвуют в работе серверов баз данных, серверов приложени и других типов объектно-ориентированных систем. При создании систем ПО — лио полностью агентно-ориентированных, либо только частично — создаются рациональ ные объектно-ориентированные программные компоненты.
12.2. Понятие об агентно-ориентированном программировании 433 0Z1 \ Дедукция, индукция и абдукция Яедукция, индукция и абдукция — это процессы, используемые для того, чтобы сделать вывод на основании набора утверждений или коллекции данных. Процесс дедукции позволяет механизму рассуждений прийти к заключению, оценив множество утверждений. Если эти утверждения (посылки) истинны, и механизм рассуждений следует соответствующим правилам генерирования вывода, то это дает основания утверждать, что непременно истинны и следствия, например: Все фигуры с тремя сторонами являются треугольниками. Данная фигура имеет три стороны. Эта фигура — треугольник. <— Вывод получен по дедукции. Правила генерирования вывода— это руководящие принципы и ограничения, которые определяют, как механизм рассуждений может переходить от одного утверждения к другом}'. Правила генерирования вывода определяют, когда утверждения логически эквивалентны, и условия, при которых одно утверждение может быть преобразовано в другое. Основные правила генерирования вывода приведены в конце этого раздела. Процесс индукции позволяет механизму рассуждений делать вывод на основании множества утверждений, являющихся фактами, например: Вчера шел дождь. Позавчера шел дождь. Дождь шел всю прошлую неделю. Завтра будет идти дождь. <— Вывод получен по индукции. Тогда как следствия, полученные в процессе дедукции объявляются непременно истинными (если правила генерирования вывода были применены корректно), то заключения, к которым приходят в процессе индукции, имеют лишь некоторую вероятность быть истинными. Насколько близко эта вероятность приближается к 100%, зависит от характера и контекста утверждений, а также данных, на которые они опираются. Процесс абдукции позволяет механизму рассуждений сделать наиболее правдоподобный вывод на основе набора утверждений или данных, например, так. Предметы одежды обвиняемого были обнаружены на месте преступления. Между обвиняемым и покойником недавно произошел бурный конфликт. ДНК обвиняемого была обнаружена на месте преступления. овиняемый виновен в свершении преступления. <— Вывод получен по абдукции. АеДукция, индукция и абдукция — это три основных процесса логического мышления. х роль в логике можно сравнить с ролью вычислений и арифметики в математике. пособность корректно переходить от посылок (утверждений, данных и фактов) заключениям является процессом, который мы называем рассуждением.
434 Глава 12. Реализация агентно-ориентированных архитектур Основные правила генерирования вывода 1. Модус поненс (правило отделения) 2. Модус толленс (принцип фальсификации) p>q -q p>q Р •ч 3. Гипотетический p>q q<r .*. р > г 5. Конструктивная, (p>q)-(r>s) .". q v s 7. Упрощение p.q .\р 9. Сложение Р л pvq СИЛЛОГИЗМ дилемма .\~р 4. Дизъюнктный силлогизм p>q ~Р ■\q 6. Поглощение p>q •*• Р > (Р ' Ч) 8. Конъюнкция Р q /.p.q 12.2.1. Роль агентов в распределенном программировании Возникновение распределенных программ было вызвано практической необходимостью. Нетрудно представить, что существует некоторый ресурс, который нужен программе, но этот ресурс размещен на другом компьютере или в сети. Под такими ресурсами часто понимают базы данных, Web-серверы, серверы электронной почты, серверы приложений, принтеры и крупные запоминающие устройства. Подобными ресурсами обычно управляет часть ПО, именуемая сервером. Другая часть ПО, которой необходимо получить доступ к ресурсам, называется клиентом. Тот факт, что ресурсы и клиент расположены на различных компьютерах, приводит к необходимости использования распределенных архитектур. В большинстве случаев не имеет смысла объединять эти программы в одну большую и выполнять ее на одном компьютере и в едином адресном пространстве. Более того, существует множество программ, ра3" работанных в различное время, разными разработчиками и для разных целей, но которые
12.2. Понятие об агентно-ориентированном программировании 435 т успешно использовать преимущества друг друга. Приложение, которое исполь- ало эти программы, эволюционировало определенным образом и в итоге " потужило звание" распределенного приложения. Поскольку эти программы отделе- дрУГ от друга, каждая из них должна иметь собственное адресное пространство "свои" ресурсы. Когда эти программы используются для совместного решения зада- они образуют распределенное приложение. Оказывается, что архитектура распределенной программы обнаружила высокую степень гибкости, что позволило применить ее к крупномасштабным приложениям. Во многих приложениях необходимость в распределенной архитектуре обнаруживается довольно поздно, "когда поезд уже ушел". Но если заранее идентифицировать такую необходимость, можно с успехом использовать соответствующие методы проектирования программного обеспечения. Если вы уже точно знаете, что вам нужно разрабатывать распределенное приложение, то следующий вопрос должен прозвучать так: "как именно оно должно быть распределено?". От ответа на этот вопрос будет зависеть, какую модель следует использовать в этом случае. Несмотря на существование множества различных моделей (равноправных узлов и типа "клиент/сервер"), в этой книге мы остановимся только на двух: мультиагентной архитектуре и архитектуре "классной доски". Оба эти вида архитектуры могут использовать преимущества агентов, поскольку агенты представляют собой самодостаточные, автономные и рациональные программные структуры. Рациональность агентов заключается в том, что им известно их назначение. И обычные объекты имеют цель, но агенты "знают", какова эта цель. Идентификация назначения каждого аспекта ПО — вполне естественный процесс. На этапе проектирования нетрудно продумать цель отдельной части ПО, и поэтому простейшая форма декомпозиции ПО состоит в том, чтобы назначить агенту его цель. Затем приходит черед понять, агентов какого класса лучше всего уполномочивать на выполнение той или иной работы. Поскольку агент— это единица модульности в агентно-ориентированной программе (agent-oriented program — AOP), то проблема распределения сводится к поиску средств взаимодействия множества агентов. Процесс проектирования исходного класса агента вбирает в себя все то, что необходимо Для идентификации отдельных составных частей распределенной программы. Справившись с созданием агентов как действительно рациональных объектов, мы сможем воспользоваться преимуществами CORBA-спецификации для разработки действительно распределенных мультиагентных систем. CORBA скрадывает сложность распределенного программирования и взаимодействия посредством сетей (intranet и Internet). Обзор средств распределенного программирования с использованием ^ORBA-сиецификации приведен в главе 8. Поскольку агенты являются объектами, этот обзор CORBA-средств имеет силу и для агентов. В главе 6 рассмотрена система vM (Parallel Virtual Machine— параллельная виртуальная машина). Систему PVM акже можно использовать для значительного упрощения взаимодействия между ентами, существующими в различных процессах или на разных компьютерах. Аген- р * Можно реализовать как CORBA-объекты, либо их можно назначить отдельным 1-процессам. В обоих случаях взаимодействие агентов упрощается в значительной ненн. Если в одном приложении задействовано несколько агентов, то такое при- *ение представляет собой мультиагентную систему. Если агенты расположены на Р °м к°мпыотере, то для взаимодействия между собой они могут использовать КБА-, PVM- или MPI-средства (Message Passing Interface). Агенты в различных про- сах также могут использовать такие традиционные методы межпроцессного взаи- ВИЯ (*^С), как FIFO-структуры, разделяемую память и каналы. В распределен- программировании есть три основные проблемы.
436 Глава 12. Реализация агентно-ориентированных архитектур 1. Идентификация декомпозиции ПО распределенного решения. 2. Реализация эффективного и рационального взаимодействия между распре ленными компонентами. 3. Обработка исключительных ситуаций, ошибок и частичных отказов. Несмотря на то что для реализации п. 2 в понятии класса агента нет ничего таког что было бы свойственно только агентам, смысл п. 1 и 3 почти подразумевается в с мой сути агента. Рациональность каждого агента определяет его назначение, а следовательно, и роль, которую он будет играть в решении ПО. Поскольку агенты самодостаточны и автономны, то хорошо продуманный класс агента должен включать необходимые меры по обеспечению их отказоустойчивости. 12.2.2. Агенты и параллельное программирование При размещении агентов в среде с несколькими процессорами или параллельно выполняющимися потоками вы получаете такие же преимущества, как и при распределенном программировании, но с той лишь разницей, что сотрудничество между агентами программировать в этом случае гораздо проще. Для передачи сообщений между агентами, которые коллективно решают задачи некоторого вида, также можно использовать PVM- и MPI-среды. И снова-таки, рациональность агентов облегчает понимание, как следует провести декомпозицию работ для параллелизма. В параллельном программировании, как правило, встречаются такие проблемы. 1. Эффективное и рациональное разделение работы между несколькими компонентами. 2. Координация параллельно выполняющихся программных компонентов. 3. Разработка соответствующего взаимодействия (когда это необходимо) между компонентами. 4. Обработка исключительных ситуаций, ошибок и частичных отказов (если агенты функционируют на отдельных компьютерах). Мультиагентные параллельные архитектуры часто характеризуются как слабосвязанные, т.е. им присущ минимум взаимодействия и взаимозависимости. Каждый агент знает свою цель и обладает методами для ее достижения. В то время как п. 3 не подвластен классу агента, п. 1, 2 и 4 можно легко управлять с помощью классов агентов. Например, при использовании агентов влияние п. 2 уменьшается, поскольку каждый агент рационален, имеет цель, а также способы и средства ее достижения. Поэтому вся ответственность смещается с алгоритма координации и управления на действия каждого агента. Влияние п. 4 также уменьшается, поскольку агенты самодостаточны, рациональны и автономны, а кроме того, хорошо продуманный класс агента долясе включать необходимые меры по обеспечению отказоустойчивости агентов. Посколь ку состояние агента инкапсулировано, ответственность за защиту критических разде* лов в объекте агента целиком возлагается на класс агента. Агент должен приводи в исполнение собственные стратегии доступа к данным. Возможные стратегии дрсту па, из которых могут выбирать агенты, перечислены в табл. 12.2.
12.3. Базовые компоненты агентов 437 [^^лица 12.2. Стратегии доступа Типы алгоритмов чтения-записи Значение Монопольное чтение, монопольная запись (Exclusive Read Exclusive Write) Параллельное чтение, монопольная запись (Concurrent Read Exclusive Write) Монопольное чтение, параллельная запись (Exclusive Read Concurrent Write) Параллельное чтение, параллельная запись (Concurrent Read Concurrent Write) Класс каждого агента должен определить, какая именно стратегия доступа приемлема в мультиагентной среде. В ряде случаев реализуются не просто отдельные стратегии доступа, перечисленные в табл. 12.2, а их комбинации. Это позволяет упростить параллельное программирование, поскольку разработчик может работать на более высоком уровне и не беспокоиться о построении мыотексов, семафоров и пр. Муль- тиагентные решения позволяют разработчику не погружаться в детали координации вызова каждой функции и организации доступа к данным. Каждый агент имеет цель. Каждый агент рационален, а следовательно, обладает определенной логикой для достижения своей цели. Процесс программирования в этом случае больше напоминает делегирование задач, а не координацию задач, которая характерна для традиционного параллельного программирования. Поскольку агентно-ориентированное программирование — это объектно-ориентированное программирование специального вида, применительно к агентам используется более декларативный вид параллельного программирования по сравнению с традиционным процедурно-ориентированным программированием, которое часто реализуется такими языками, как Fortran или С. Разработчик лишь определяет, что нужно сделать и какие агенты должны это сделать, т.е. выходит, что параллелизм практически сам заботится о себе. При этом всегда существует некоторый объем программирования, связанного с координацией и организацией взаимодействия, но агентно-ориентированное программирование сводит этот необходимый объем к минимуму. Однако обо всех этих "плюсах" можно говорить лишь при условии существования классов агентов. Очевидно, кто-то должен спроектировать классы агентов и написать их код. Теперь самое время разобраться в том, что Должен содержать класс агента. 12.3. Базовые компоненты агентов Агент объявляется с использованием ключевого слова class. Компоненты агента Должны состоять из С++-членов данных и функций-членов. Логическая структура класса агента показана на рис. 12.1. Класс агента (см. рис. 12.1) определяет типичные методы инициализации, чтения записи, которые должен иметь практически любой объект. В "джентльменский на- Р входят конструкторы, деструкторы, операторы присваивания, обработчики несений и т.д. Атрибуты этого класса включают переменные состояния, опреде- «°Щие объект. Если же ограничиться перечнем этих атрибутов и методов, мы полу-
438 Глава 12. Реализация агентно-ориентированных архитектур чим только традиционный объект. Рациональный компонент создают когнитивн структуры данных и методы рассуждений (логического вывода). А ведь именно циональный компонент трансформирует "обычный" объект в агент. Класс агента Атрибуты класса Методы класса Когнитивные структуры данных Методы рассуждений Традиционные объектно- ориентированные компоненты Рациональные компоненты Рис. 12.1. Логическая структура класса агента 12.3.1. Когнитивные структуры данных Под структурой данных понимается набор правил, применяемых для логической организации данных, а также правила доступа к этой логической организации. Именно метод организации определяет, как данные должны быть концептуально структурированы и какие операции доступа могут быть применены к этой структуре. Если для типов данных вообще и абстрактных типов данных (abstract datatypes — ADT) в частности важно, что хранить, то для структур данных важно, как хранить. Например, целочисленный тип данных определяет некоторую "сущность", которая характеризуется наличием компонента данных и некоторого количества арифметических операций (например, сложение, вычитание, умножение, деление и т.д.). Этот компонент данных не имеет дробной части и состоит из отрицательных и положительных чисел. Спецификация типа данных ничего не "говорит" о том, как целые числа нужно использовать или как к ним получить доступ. Однако спецификация структуры данных (например, стека) определяет список элементов, сохраняемых по принципу "последним прибыл— первым обслужен" (last-in-first-out— LIFO). Структура данных стека также определяет, что элементы из нее можно извлекать только по одному за раз и причем только из вершины стека. Другими словами, элемент, помещенный в стек последним, должен быть извлечен из него раньше остальных элементов. Это означает, что структура данных стека определяет не только характер организации элементов, но и характер доступа к ним (т.е. как элементы можно помещать в структуру, опрашивать, изменять, удалять и т.п.). Когнитивные структуры данных ограничивают правила организации данных и доступа к ним такими, которые относятся к области логики и эпистемологии. Особенности когнитивных структур данных определяются правилами логического вывода, методами рассуждений (т.е. дедукцией, индукцией и абдукцией), понятиями эпистемологических данных, знания, обоснования, убеждений, посылок, высказываний, ошибочных доказательств и заключении. Из нашего определения когнитивных структур данных намеренно исключены такие относящиеся психике человека понятия, как воображение, паранойя, беспокойство, счастье, грусть и т.п. Нас интересует рациональное эпистемологическое, а не интеллектуальное ПО.
12.3. Базовые компоненты агентов 439 как для традиционных структур данных вполне обычными являются, например, ритмы сортировки и поиска, то для когнитивных структур данных более прием- Методы рассуждений. Абстрактные типы данных, используемые вместе с когни- вными структурами данных, часто включают следующие: вопросы события факты вРемя предположения заблуждения убеждения Цель утверждения обоснование заключения Безусловно, с когнитивными структурами данных можно сочетать и другие типы данных, но приведенные выше являются характеристиками программ, которые используют такие рациональные программные компоненты, как агенты. Эти абстрактные типы обычно реализуются как типы данных, объявленные с помощью ключевых слов struct или class. Например, так. struct question{ class justification{ string Requiredlnformation; time EventTime; target_ob:ject QuestionDomain; bool Observed; string Tense; bool Present; string Mood; //... //... }; }; Шаблонные и контейнерные С++-классы можно использовать для организации таких когнитивных структур данных, как знания, например, так. class preliminary_knowledge{ //.. . map<question,belief> Opinion; map<conclusion/justification> SimpleKnowledge; set<propositions> Argument; //. . . }; "•2.3.1.2. Методы рассуждений Иод методами рассуждений (см. рис. 12.1) понимают дедукцию, индукцию и абдукцию. (Краткое описание этих методов приведено в параграфе 12.1.) Несмотря на то ° в агентно-ориентированной архитектуре требуется их использование, не сущест- конкретных ссылок на то, как они реализуются. Дедукция, индукция и абдукция °сятся к процессам высокого уровня. Подробности реализации этих процессов — °е дело разработчика ПО. Рассуждение — это процесс выведения логического за- чения на основании посылок, истинность которых предполагается или точно усилена. Не существует единственно правильного способа реализации процесса , рдений, иногда называемого машиной (или механизмом) логического вывода. При на практике применяется несколько распространенных способов реализации # пР°Цесса. Например, можно использовать методы прямого построения цепочки .бдений от исходных посылок к целевой гипотезе) или обратного построения
440 Глава 12. Реализация агентно-ориентированных архитектур цепочки (рассуждений от целевой гипотезы к исходным посылкам). Нашли здесь применение методы анализа целей и средств, а также такие алгоритмы обхода графов, как "поиск вглубь" (Depth First Search — DFS) и "поиск в ширину" (Breadth First Search — BFS) Существует также целая совокупность методов доказательства теорем, которые можно использовать для реализации методов рассуждений и механизмов логического вывода Здесь важно отметить, что класс агента может иметь один или несколько методов рассуждений. Описание самых основных способов их реализации приведено в табл. 12.3. Таблица 12.3. Основные способы реализации методов рассуждений Способы реализации методов Описание рассуждений Обратное построение цепочки Управляемый целями метод, в котором процесс начинается с предположения, утверждения или гипотезы и стремится найти подтверждающие доказательства Прямое построение цепочки Управляемый данными метод, который начинается с анализа имеющихся данных или фактов и приходит к определенным выводам Анализ целей и средств Использует множество операторов для последовательного решения подзадач до тех пор, пока не будет решена вся задача в целом Эти методы достаточно понятны и широко доступны во многих библиотеках, оболочках и языках программирования. Эти методы являются "строительными блоками" для базовых методов рассуждений. Чтобы понять, как происходит процесс рассуждения, используем одно из правил генерирования вывода, а именно модус поненс (правило отделения), и построим простой метод рассуждения. Возьмем следующее утверждение. Если существует автобусный маршрут из Детройта в Нью-Йорк, то Джон поедет в отпуск. Если мы выясним, что автобусный маршрут из Детройта в Нью-Йорк действительно существует, то будем знать, что Джон поедет в отпуск. Правило модус поненс имеет следующий формат. Р -» Q Р Здесь: Р = Если существует автобусный маршрут из Детройта в Нью-Йорк, Q = Джон поедет в отпуск. Мы могли бы спроектировать простой агент обеспечения решения, который позволи нам узнать, поедет Джон в отпуск или нет. Этому агенту нужно узнать все возможное оо тобусных маршрутах. Предположим, у нас есть список автобусных маршрутов: Толедо-Кливленд Детройт-Чикаго Янгстаун-Нью-Йорк Кливленд-Колумбус Цинциннати-Детройт Детройт-Толедо Колумбус-Нью-Йорк Цинциннати-Янгстаун
12.3. Базовые компоненты агентов 441 Каждый из этих маршрутов представляет обязательство, взятое на себя компанией ABC Bus Company. Если наш агент получит доступ к расписанию автобусных маршру- этой компании, то приведенный выше список маршрутов можно будет использо- ать для представления некоторой части убеждений нашего агента. Возникает вопрос: как перейти от списка маршрутов к убеждениям? Для начала попробуем разработать простую структуру утверждений. struct existing_trip{ string From; time Departure; string To; time Arrival; }; Затем попытаемся использовать контейнерный класс для представления убеждений нашего агента в отношении автобусных маршрутов. set<existing__trips> BusTripKnowledge; Если определенный автобусный маршрут содержится в множестве BusTripKnowledge, то наш агент убежден в том, что в указанное время автобус непременно отправится по этому маршруту из пункта отправления в пункт назначения. Итак, мы можем зафиксировать любой маршрут в соответствии с заданной структурой //... existing__trip Trip; Trip. From. append (" Toledo") ; Trip.To.append( "Cleveland") ; Trip.Departure("4:30"); Trip.Arrival("5:45"); BusTripKnowledge. insert (Trip) ; //... Если мы поместим каждый маршрут в множество BusTripKnowledge, то убеждения нашего агента об автобусных перевозках компании ABC Bus Company будут полностью^ описаны. Обратите внимание на то, что прямого маршрута из Детройта в Нью-Йорк не существует. Но Джон может добраться в Нью-Йорк из Детройта более сложным путем, осуществив следующие переезды автобусом: из Детройта в Толедо; из Толедо в Кливленд; из Кливленда в Колумбус; из Колумбуса Нью-Йорк. Поэтому, несмотря на то что компания ABC Bus Company не предоставляет прямо- марщрута (из пункта А в пункт Б), она позволяет совершить переезд с большим ко- чеством промежуточных остановок. Задача состоит в следующем: как об этом мо- т узнать наш агент? Агент на основе своих знаний об автобусных маршрутах должен адать некоторым алгоритмом генерирования вывода о том, существует ли мар- Р> из Детройта в Нью-Йорк. Мы используем простой цепной метод. Просматрива- о Лементы множества BusTripKnowledge и находим первый маршрут из Дет- ^ Та~~ из Детройта в Чикаго. Опрашиваем атрибут То этого элемента. Если бы он Равен значению "Нью-Йорк", процесс поиска был бы прекращен, поскольку мы
442 Глава 12. Реализация агентно-ориентированных архитектур нашли нужный маршрут. В противном случае сохраняем найденный (промежуто ный) маршрут в стеке. Затем ищем маршрут с атрибутом From, равным "Чикаго" При этом может оказаться, что таких маршрутов не предусмотрено вообще. Поскол ку далее хранить элемент множества, соответствующий маршруту "Детройт-Чикаго" нет никакого смысла, мы удаляем его из стека, сделав пометку, что этот маршрут уж был рассмотрен. Затем повторяем поиск маршрута с отправлением из Детройта. Находим такой маршрут: "Детройт-Толедо". Проверяем, не равен ли его атрибут То значению "Нью-Йорк", и поскольку наши надежды не оправдались, сохраняем этот элемент в стеке. Затем ищем маршрут с атрибутом From, равным "Толедо". Находим маршрут "Толедо-Кливленд" и также помещаем его на хранение в стек. После этого просматриваем маршруты в надежде найти элемент, у которого атрибут From был бы равен значению "Кливленд". Для каждого найденного маршрута проверяем значение атрибута То. Если он равен значению " Нью-Йорк", то промежуточные маршруты, помещенные в стек, представляют в целом маршрут из Детройта в Нью-Йорк, начало которого находится на "дне" стека, а его конечный пункт — в вершине. Если мы пройдем по всему списку маршрутов и не найдем ни одного с атрибутом То, равным значению "Нью-Йорк", или иссякнут возможные варианты проверки атрибута То для верхнего элемента стека, то мы, извлекая верхний элемент из стека, будем искать следующий элемент, значение атрибута From которого совпадает со значением атрибута То элемента, расположенного в вершине стека. Этот процесс повторяется до тех пор, пока стек не опустеет или мы все-таки не найдем нужный маршрут. Для определения, существует ли маршрут из пункта А в пункт Б, используется в данном случае упрощенный метод DFS (Depth First Search — "поиск вглубь"). Наш простой агент будет использовать этот DFS-метод для выяснения, существует ли маршрут из Детройта в Нью-Йорк. Выяснив этот факт, агент может обновить свои убеждения насчет Джона. Теперь агент убежден, что Джон поедет в отпуск. Предположим, мы внесли дополнительное предусловие относительно отпуска Джона. Если Джон обслужит 15 или больше новых клиентов, его доходы превысят (>) 150000. Если доходы Джона превысят 150000 и существует маршрут из Детройта в Нью- Йорк, то Джон отправится в отпуск. Теперь агент должен выяснить, превышают ли доходы Джона сумму 150000 и существует ли маршрут из Детройта в Нью-Йорк. Чтобы выяснить положение дел насчет доходов Джона, агент должен сначала узнать, обслужил ли Джон хотя бы 15 новых клиентов. Предположим, мы уверяем программного агента в том, что Джон оо- служил 23 новых клиента. Затем агент должен убедиться в том, что его доходы превышают 150000. На основе содержимого множества BusTripKnowledge агент сумел прийти к выводу о существовании маршрута из Детройта в Нью-Йорк. На основании убеждений об автобусных маршрутах и 23 новых клиентах агент использует процесс прямого построения цепочки (т.е. рассуждений от исходных посылок к целевои гипотезе) и приходит к заключению, что Джон таки поедет в отпуск. Формат рассуждений этого процесса имеет такой вид. А -> В (В и С) -> D А С D
12.3. Базовые компоненты агентов 443 Здесь: А = Если Джон обслужит не менее 15 новых клиентов, В = Доходы > 150000, С s Существует автобусный маршрут из Детройта в Нью-Йорк, D = Джон поедет в отпуск. В этом примере агент убеждается, что элементы А и С истинны. С использованием правил ведения рассуждений агент заключает, что элементы В и D равны значению ИСТИНА. Следовательно, агент делает вывод о том, что Джон поедет в отпуск. Подобный вид обработки имеющихся данных можно было бы применить к агенту в ситуации, когда у директора фирмы в подчинении находятся сотни или даже тысячи служащих, и он хотел бы, чтобы агент регулярно составлял почасовой график работы для своих служащих. Директор намерен затем получать от агента справку о том, кто работал, кто находился в отпуске по болезни, а кто — в очередном отпуске и т.д. Агент должен обладать знаниями и полномочиями устанавливать график работы. Каждую неделю агент должен представлять ряд приемлемых графиков работы, очередных отпусков и сведений о пропусках по болезни. Агент в этом случае для получения результата использует простой метод прямого построения цепочки и метод DFS. Чтобы реализовать этот вид рассуждений, мы использовали такие типы данных, как struct и классы стеков и множеств. Эти классы используются для хранения знаний, предположений и методов рассуждений. Они позволяют реализовать когнитивные структуры данных (Cognitive Data Structures— CDS). Для поддержки процесса рассуждений, а именно при опросе наших структур данных (стека и множества) мы использовали DFS-методы. При сочетании метода прямого построения цепочки и метода DFS создается процесс, в соответствии с которым одно предположение может быть подтверждено на основе уже принятых предыдущих. Это очень важный момент, поскольку наш агент при достижении цели должен знать, что в действительности следует считать корректным. Такой подход также влияет на отношение к вопросам параллельного программирования. Тот факт, что агент рационален и действует в соответствии с правилами построения рассуждений, позволяет разработчику сосредоточиться на корректном моделировании задачи, выполняемой агентом, а не на стремлении явно управлять параллелизмом в программе. Минимальные требования параллелизма, выражаемые тремя "китами" — декомпозицией, взаимодействием и синхронизацией (decomposition, communication, synchronization — DCS), — по большей части относятся к архитектуре агента. Каждый агент для своего поведения имеет логическое обоснование. Это обоснование должно опираться на хорошо определенные и хорошо понимаемые правила ведения рассуждений. Декомпозиция зачастую выражается в простом назначении агенту одного или нескольких основных указаний (директив). Декомпо- Ция работ в этом случае должна иметь естественный характер и в конце концов выразиться в параллельных или распределенных программах, которые нетрудно под- рживать и развивать. Взаимодействие агентов проще представить, чем взаимодей- ие анонимных модулей, поскольку границы между агентами более четки очевидны. Каждый агент имеет цель, которая лежит на поверхности. Знания, или формация, необходимые каждому агенту для достижения его цели, в этом случае ко определяются. Чтобы позволить агентам взаимодействовать, разработчик мо- использовать простые MPI-функции или средства взаимодействия объектов, ко- рые являются частью любой CORBA-реализации. При обеспечении взаимодействия тов самыми сложными являются следующие моменты:
444 Глава 12. Реализация агентно-ориентированных архитектур • посредством чего должно происходить взаимодействие; • кому нужно взаимодействовать; • когда должно происходить взаимодействие; • какой формат должно иметь взаимодействие. Ответы на эти вопросы должны быть изначально заложены в проект агентов. Теперь осталось лишь определиться с физической реализацией взаимодействия агентов. Для этого можно воспользоваться библиотеками, которые поддерживают параллелизм. Наконец, что касается проблем синхронизации, то с ними можно легко справиться, поскольку именно логическое обоснование агента сообщает ему, когда он может и должен выполнять действия. Следовательно, сложные вопросы синхронизации сводятся к простым вопросам сотрудничества. Благодаря этому упрощается и задача разработчика в целом. Теперь рассмотрим базовую структуру агента и возможности его реализации в C++. 12.4. Реализация агентов в C++ Рассмотрим упрощенный вариант предыдущего примера агента и продемонстрируем, как его можно реализовать в C++. Цель этого агента — составлять график отпусков и выполнять подготовку к поездкам владельца компании ABC Auto Repair Company. В компании работают десятки служащих, и поэтому у хозяина нет времени заботиться о проведении своего очередного отпуска. Кроме того, если хозяин не получит определенного объема прибыли, об отпуске не может быть и речи. Поэтому владельцу компании хотелось бы, чтобы агент распланировал его отпуска равномерно по всему году при условии процветания фирмы. По мнению владельца компании, главное, чтобы агент работал автоматически, т.е. после инсталляции на компьютере о нем можно было не беспокоиться. Когда агент определит, что подошло время для отпуска, он должен предъявить план проведения отпуска, забронировать места в отеле и проездные билеты, а затем по электронной почте представить хозяину маршрут. Владелец должен побеспокоиться только о формировании задания для агента. Он должен указать, куда желает отправиться и какой объем прибыли необходимо получить, чтобы запланированная поездка состоялась. Теперь рассмотрим, как можно спроектировать такой агент. Вспомним, что рациональный компонент (см. рис. 12.1) класса агента состоит из когнитивных структур данных и методов рассуждений (стратегий логического вывода). Когнитивные структуры данных (CDS) позволяют хранить убеждения, предположения, знания, заблуждения, факты и пр. Для доступа к этим когнитивным структурам данных в процессе решения проблемы и выполнения задач класс агента использует стратегии логического вывода. Для реализации CDS-структур данных и методов построения рассуждений можно использовать ряд контейнерных классов и алгоритмов, которые содержатся в стандартной библиотеке C++. 12.4.1. Типы данных предположений и структуры убеждений Этот агент обладает убеждениями о показателях авторемонтной мастерской. У®е ждения составляют информацию о том, сколько клиентов обслуживается в час, каков загрузка ремонтных секций в день и общий объем продаж (запчастей и услуг) за неко
12.4. Реализация агентов в C++ 445 ый период времени. Кроме того, агент знает, что владелец фирмы любит путеше- овать только автобусами. Поэтому агент хранит информацию об автобусных мар- \тах которые могут для отпускника оказаться привлекательными. В программе, сыщенной математическими вычислениями, используются в основном целочис- енные значения и числа с плавающей точкой. В графических программах участвуют пиксели, линии, цвета, геометрические фигуры и пр. В агентно-ориентированной программе основными типами данных являются предположения, правила, утверждения литералы и строки. Для построения типов данных, свойственных агентно- ориентированному программированию, мы будем опираться на объектно- ориентированную поддержку, предусмотренную в C++. Итак, рассмотрим объявление класса предположения (листинг 12.1). // Листинг 12.1. Объявление класса предположения template<class C> class proposition { protected: list<C> UniverseOfDiscourse; bool TruthValue; public- virtual bool operator()(void) = 0; bool operator&&(proposition &X) ; bool operator||(proposition &X); bool operatorj j(bool X); bool operator&&(bool X) ; operator void*(); bool operator!(void); bool possible(proposition &X); bool necessary(proposition &X); void universe(list<C> &X); //.. . }; Предположение представляет собой утверждение, тема (предмет) которого подтверждается или отрицается предикатом. Предположение может принять значение ИСТИНА или ЛОЖЬ. Предположение можно использовать для фиксации одного убеждения, которое есть у агента. Кроме того, в качестве предположения может быть представлена некоторая другая информация, которая предлагается агенту и которую агент необязательно воспринимает как убеждение. Для представления предположении используется когнитивный тип данных, который должен быть таким же функциональным в агентно-ориентированной программе, как целочисленные и вещест- енные типы данных в математических программах. Поэтому, чтобы обеспечить не- °торые основные операторы, применимые к предположениям, мы используем С++- редства перегрузки операторов. В табл. 12.4 показано, как такие операторы преобразуются в логические. Класс proposition (см. листинг 12.1) представляет собой упрощенную версию сокращённым набором функциональных возможностей). Назначение этого клас- — сделать использование типа данных proposition таким же простым и естест- ньщ, как использование любого другого С++-типа данных. Обратите внимание на Дующее объявление в классе proposition: Vlrtual bool operator()(void) = 0;
446 Глава 12. Реализация агентно-ориентированных архитектур Таблица 12.4. Преобразование операторов в логические Пользовательские С++-операторы Распространенные логические операторы ~ " && ~ II I ~ possible ♦ necessary □ Это объявление чисто виртуального метода. Если в классе объявлен чисто виртуальный метод, это означает, что данный класс — абстрактный, и из него нельзя создавать объекты, поскольку в нем отсутствует определение этого метода. Метод лишь объявлен, но не определен. Абстрактные классы используются для определения стратегий и являются своего рода проектами производных классов. Производный класс должен определить все виртуальные функции, которые он наследует от абстрактного класса. В данном случае класс proposition используется для определения минимального набора возможностей, которыми может обладать класс-потомок. Необходимо также отметить еще одну важную особенность класса proposition (см. листинг 12.1): это шаблонный класс. Он содержит такой член данных: list<C> UniverseOfDiscourse; Этот член данных предполагается использовать для хранения значения предметной области, к которой относится предположение. В логике область рассуждения содержит все легальные сущности, которые могут рассматриваться при обсуждении. Здесь мы используем контейнер list. Поскольку в общем случае темы обсуждения могут быть самыми разными, мы используем контейнерный класс. Список UniverseOfDiscourse мы объявляем защищенным (protected), а не закрытым (private), чтобы к нему могли получить доступ все потомки класса proposition. Классу proposition также "знакомы" такие понятия модальной логики, как логическая необходимость и вероятность, которые весьма полезны в агентно- ориентированном программировании. Модальная логика позволяет агенту различать такие определения, как "вероятно, ИСТИНА" и "несомненно, ИСТИНА". Основные операторы, используемые для выражения логической необходимости и вероятности, перечислены в табл. 12.4. Мы определяем эти методы только в описательных целях; их реализация выходит за рамки рассмотрения в этой книге. Но они являются частью классов предположений, которые мы успешно применяем на практике. Чтобы сделать класс proposition "годным к употреблению", выведем из него новый класс и назовем его trip_announcement. Класс trip_announcement представляет собой утверждение о существовании автобусного маршрута из некоторого исходного пункта (отправления) в пункт назначения. Например, предположим, что существует автобусный маршрут из Детройта в Толедо. Эта информация позволяет сформулировать высказывание, которое может быть либо истинным, либо ложным. Если бы нас интересовало, когда это высказывание истинно или ложно, мы бы воспользовались понятиями временной логики. Временная логика— это логика времени. Агенты так*е применяют обоснования, зависящие от времени. Но в данном случае все предположения относятся к текущему времени. Это утверждение декларирует, что в данное
12.4. Реализация агентов в C++ 447 оемя существует автобусный маршрут из Детройта в Толедо. Агент должен "уметь" удостовериться в этом и либо "довериться" этому факту, либо отвергнуть го как ложное высказывание. Теперь можно рассмотреть объявление класса trip announcement, представленное в листинге 12.2. // Листинг 12.2. Объявление класса trip_announcement class trip_announcement : public proposition<trip_announcement>{ protected: string Origin; string Destination; stack<trip_announcement> Candidates; public: 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(1ist<trip__announcement>::iterator I, string TempOrigin); stack<trip_announcement> candidates(void); friend bool operator||(bool X,trip_announcement &Y); friend bool operator&&(bool X,trip_announcement &Y); //. .. }; Обратите внимание на то, что класс trip_announcement наследует класс proposition. Вспомните, что класс proposition является шаблонным и требует задания параметра, определяющего тип. Объявление class trip_announcement : public proposition<trip__announcement> обеспечивает класс proposition требуемым типом. Кроме того, важно отметить, что класс trip__announcement определяет операторный метод operator (). Следовательно, наш класс trip_announcement — конкретный, а не абстрактный. Теперь мы можем объявить и использовать предположение типа trip_announcement непосредственно в программе агента. В классе trip_announcement определены следующие дополнительные члены данных: Origin destination Candidates эти члены данных используются для указания пунктов отправления и назначения тооусного маршрута. Если автобусный маршрут требует пересадки с одного автобуса Другой и несколько остановок в пути, то член данных Candidates будет содержать лный путь следования. Следовательно, объект класса trip__announcement пред- вляет собой утверждение об автобусном маршруте и пути следования. В классе ^-.announcement также определены некоторые дополнительные операторы. Эти
448 Глава 12. Реализация агентно-ориентированных архитектур операторы позволяют уравнять класс trip_announcement "в правах" со встроенными типами данных языка C++. Помимо убеждений относительно автобусных маршру. тов, агент также обладает убеждениями, связанными с показателями успешности функционирования рассматриваемой компании. Эти убеждения отличаются по структуре, но в основном содержат высказывания, которые могут быть истинными либо ложными. Итак, мы снова используем класс proposition в качестве базового Б листинге 12.3 представлено объявление класса pef ormances tatement. // Листинг 12.3. Объявление класса performancestatement class performancestatement : public proposition<performancestatement>{ int Bays; float Sales; float PerHour; public: bool operator() (void); bool operator==(const performancestatement &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,performancestatement &Y); friend bool operator&&(bool X,performancestatement &Y); //. . . }; Обратите внимание на то, что этот класс также обеспечивает шаблонный класс proposition параметром. class performancestatement : public proposition<performances tatement> {...} Благодаря этому объявлению класс proposition теперь определен для объектов типа performancestatement. Класс performancestatement используется для представления убеждений об объеме продаж, количестве обслуженных клиентов (в час) и загрузке ремонтных секций в день. Для каждого из перечисленных убеждений о том, что агент имеет в соответствующей области, существует отдельное высказывание. Эта информация хранится в таких членах данных: Bays Sales PerHour Такие высказывания, как "По секции 1 объем продаж составил 300 тыс. долл., обслужено 10 клиентов в час, а коэффициент загрузки равен 4", можно представить с помощью объекта класса perf ormance_statement. Итак, наш класс агента имеет две категории убеждений, реализованных в виде данных, тип которых выведен и класса proposition. На рис. 12.2 представлена UML-диаграмма классов trip_announcement и perf ormances tat ement. Эти классы предназначены Д-71 хранения структуры убеждений агента.
12.4. Реализация агентов в C++ 449 performance_ statement performance^ I statement proposition I Рис. 12,2. UML-диаграмма классов trip_announcement И performance_statement 12.4.2. Класс агента Классы, представленные на рис. 12.2, образуют фундамент для когнитивных структур данных агента, которые делают агента рациональным. Именно рационализм класса агента отличает его от других типов объектно-ориентированных классов. Рассмотрим объявление класса агента, приведенное в листинге 12.4. // Листинг 12.4. Объявление класса agent class agent{ private: performance_statement Manager1; performance_statement Manager2; Performance'statement МападегЗ; tr ip_announcement Tr ip1; trip_announcement Trip2; t]fip_announcement Trip3 ; list<trip_announcement> TripBeliefs; list<performance_statement> PerformanceBeliefs; Public: agent(void); bool determineVacationAppropriate(void); bool scheduleVacation(void); void updateBeliefs(void); void setGoals(void); void displayTravelPlan(void); *. //.
450 Глава 12. Реализация агентно-ориентированных архитектур Как и классы предположений, класс агента представляет собой упрощенную версию. Полный листинг объявления класса, который можно было бы использовать на практике, занял бы три или четыре страницы. Но для описательных целей, которые мы преследуем в этой книге, приведенного вполне достаточно. Итак, класс agent содержит два контейнера-списка. list<trip_announcement> TripBeliefs; list<performance_statement> PerformanceBeliefs; Контейнеры типа list — это стандартные С++-списки. Каждый список используется для хранения коллекции текущих убеждений агента. "Мировоззрение" нашего простого агента ограничено знаниями об автобусных маршрутах и характеристиках успешности его владельца. Содержимое этих двух контейнеров представляет полные знания агента и набор его убеждений. Если в этих списках есть утверждения, в которые агент больше не верит, их следует удалить. Если в процессе рассуждений агент обнаруживает новые утверждения, они добавляются в список уже существующих убеждений. Агент имеет постоянный доступ к информации об автобусных маршрутах и эффективности ведения бизнеса его владельца и при необходимости может обновлять свои убеждения. Помимо убеждений, агент имеет цели, которые иногда представляются как желания в модели убеждений, желаний и намерений (Beliefs, Desires, Intentions — BDI). Цели поддерживают основные директивы, выдаваемые агенту клиентом. В нашем случае цели сохраняются в высказываниях, приведенных ниже. performance_statement Managerl; performance_statement Manager2; performance_statement Manager3; trip_announcement Tripl; trip_announcement Trip2; trip_announcement Trip3; Следует иметь в виду, что мы значительно упрощаем представление целей и директив в классе агента. Но все же этого достаточно, чтобы понять, как построены эти структуры. Три Manager-утверждения содержат цели, связанные с эффективностью бизнеса, которые должны быть удовлетворены, прежде чем владелец фирмы сможет хотя бы подумать об отпуске. Три Trip-утверждения содержат автобусные маршруты, по которым владелец фирмы хотел бы прокатиться при условии успешности его бизнеса. Убеждения вместе с директивами образуют базовые когнитивные типы данных, которыми располагает агент. Используемые агентом стратегии логического вывода вместе с этими когнитивными типами данных образуют когнитивную структуру данных агента (Cognitive Data Structure — CDS). На базе CDS-структуры формируются рациональный компонент и характерные особенности класса агента. Помимо контейнеров, в которых хранятся убеждения и структуры, которые в свою очередь хранят директивы и цели, большинство классов агентов имеют контейнеры, предназначенные для хранения намерений, обязательств или планов агента. Агент получает директивы от своего клиента, а затем использует свою способность делать выводы и совершать действия, направленные на выполнение этих директив. Результат рассуждений и выполнения агентом действий часто сохраняется в контейнере с его намерениями, обязательствами или планами. Что касается нашего простого агента, то для хранения намерений или планов отдельного контейнера не ему требуется. Однако он должен зафиксировать путь следования (с пересадками и остановками) предполагаемой отпускной поездки на автобусе. Эта информация хранится в контейнере Candidates.
12.4. Реализация агентов в C++ 451 Я мерения или планы должны быть обработаны аналогичным образом. Если агент жет выполнить директивы, он распланирует поездку и по электронной почте подобно сообщит об этом своему владельцу. Агент приступает к своим обязанностям момент создания объекта. Фрагмент конструктора агента представлен в листинге 12 5 // Листинг 12.5. Конструктор класса agent agent:: agent (void) setGoals(); updateBeliefsO ; if(determineVacationAppropriate()){ displayTravelPlan(); scheduleVacation(); cout « "Сообщение о возможности отпуска." « endl; } else{ cout « "В данное время отпуск нецелесообразен." « endl; } } 12.4.2.1. Цикл активизации агента Многие определения агентов включают требования непрерывности и автономности. Идея состоит в том, что агент должен непрерывно выполнять поставленные перед ним задачи без вмешательства оператора. Агент обладает способностью взаимодействовать со своей средой и (до некоторой степени) контролировать ее благодаря наличию цепи обратной связи. Непрерывность и автономность часто реализуются в виде событийного цикла, при выполнении которого агент постоянно получает сообщения и информацию о событиях. Эти сообщения и события агент использует для обновления своей внутренней модели мира, намерений и предпринимаемых действий. Однако непрерывность и автономность — понятия относительные. Одни агенты должны активизироваться каждую микросекунду, в то время как другие — лишь один раз в год. А в случае профаммного обеспечения полетов в дальний космос агент может иметь цикл даже больше одного года. Поэтому мы не будем акцентировать внимание на физических событийных циклах и постоянно активных очередях сообщений. Такая организация может подходить для одних агентов, но оказаться непригодной для других. Мы пришли к выводу, что лучше всего здесь применить понятие логического цикла. Логический цикл может (или не может) быть реализован как событийный. Логический цикл может длиться от одной наносекунды до некоторого количества лет. Общий вид про- того логического цикла активизации агента показан на рис. 12.3. Область рассуждения (см. рис. 12.3) представляет все, с чем наш агент может леги- мно взаимодействовать. Эта область может состоять из файлов, информации от °ртов или устройств сбора данных. Получаемая информация должна быть представ- а в виДе предположений или утверждений (высказываний). Обратите внимание на уществование цепи обратной связи от выходных данных агента к входным. Наш т (см. листинг 12.4) активизируется только несколько раз в год. Следовательно, смысла помещать его в постоянно выполняющийся событийный цикл. Наш агент ен периодически активизироваться в течение года для выполнения своих задач. стинге 12.5 представлен конструктор агента. При активизации агент устанавливает
452 Глава 12. Реализация агентно-ориентированных архитектур цели, обновляет убеждения, а затем определяет уместность отпуска. Если отпуск возможен, агент предпринимает некоторые действия и по электронной почте уведомляет об этом владельца фирмы. Если же отпуск в данное время нецелесообразен, владелец получает от агента сообщение другого содержания. Активизировать Просмотреть область рассуждения Обновить убеждения Применить методы рассуждения Совершить действие [Директивы не выполнены] <> г ё Просмотреть директивы [Директивы выполнены] Дезактивизировать Рис. 12.3. Общий вид простого логического цикла активизации агента 12.4.2.2. Стратегии логического вывода агента Этот агент обладает способностями рассуждать, реализованными частично классом proposition и частично методом determineVacationAppropriate () • Вспомните, что в классе proposition объявлен метод operator () =0 в виде чисто виртуальной функции. Поэтому в производном классе необходимо реализовать метод operator (). Мы используем этот оператор, чтобы объект предположения мо самостоятельно определить свою "суть", т.е. понять, истинно данное предположи ние или ложно. Это означает самодостаточность классов предположений. Имени в самодостаточности и состоит фундаментальный принцип объектно-ориентирован ного программирования: класс представляет собой самостоятельную конструкцию.
12.4. Реализация агентов в C++ 453 инкапсулирующую его характеристики и поведение. Итак, одной из основных линий поведения класса предположений и его потомков является способность определять, истинно данное предположение или нет. Для реализации этого средства используется перегрузка операторов и объекты-функции. Рассмотрим фрагменты определения класса proposition и определений его потомков. /Листинг 12.6. Фрагменты определений класса I proposition и его потомков template <class C> bool proposition<C>::operator&&( proposition &X) return((*this)() &&X()); template <class C> bool proposition<C>::operator||( proposition &X) return((*this)() || X()); template<class C> proposition<C>::operator void*(void) return((void*)(TruthValue)); bool trip_announcement::operator()(void) list<trip_announcement>::iterator I; if(directTrip()){ return(true); } I = UniverseOfDiscourse.begin(); if(validTrip(I,Origin)){ return(true); } return(false),- Операторы " | |" и "&&", используемые в классах предположений, позволяют определить, истинно данное предположение или ложно. В каждом из этих определений операторов в конечном счете вызывается метод operator (), определенный в классе- потомке. Обратите внимание на определение оператора " | | " (см. листинг 12.6). Этот оператор определен следующим образом. template <class О bool proposition<C>::operator|| , (proposition &X) return ((*this) () || XO) ; •Зго определение позволяет использовать следующий код. ^riP-.announcement A; Performance_statement В; lf «A | | в) { ^ // Какие-нибудь действия.
454 Глава 12. Реализация агентно-ориентированных архитектур При вычислении выражений А или В будет вызван оператор operator (). Каждый класс предположений определяет поведение оператора operator () по-своему. На_ пример, в классе trip_announcement оператор operator () определяется так. bool trip_announcement::operator()(void) { list<trip_announcement>::iterator I; if (directTripO ) { 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.4. Реализация агентов в C++ 455 // листинг 12.7. Определения методов validTripO и . directTripO bool trip_announcement::validTrip(list<trip_announcement>:: iterator I, string TempOrigin) 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<trip_announcement>::iterator I; = find(UniverseOfDiscourse.begin(), UniverseOfDiscourse.end(), *this); if (I == UniverseOfDiscourse.end()){ TruthValue = false; return(false); TruthValue = true; } return(true);
456 Глава 12. Реализация агентно-ориентированных архитектур В обоих методах validTrip () и directTrip () используется алгоритм find () Из стандартной библиотеки C++. UniverseOfDiscourse— это контейнер, который содержит убеждения агента и подготовленные для него утверждения. Вспомните, что одним из первых действий, предпринимаемых агентом, является вызов метода updateBelief s (), который заполняет контейнер UniverseOf Discourse. Определение метода updateBelief s () приведено в листинге 12.8. // Листинг 12.8. Обновление убеждений void agent::updateBeliefs(void) { performance_statement TempP; TempP.sales(203.0); TempP.perHour(100.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 информация, поступающая в списки TripBelief s и Perf ormanceBelief s, представляет новые высказывания, которые агент получает о приемлемых маршрутах и эффективности авторемонтной мастерской. Эти высказывания оцениваются относительно директив, выданных агенту. Установкой директив агента занимается метод setGoals (). (Его определение приведено в листинге 12.9.) // Листинг 12.9. Метод установки целей агента void agent::setGoals(void) { Manager1.perHour(15.0) ; Manager1.bays(8); Manager1.sales(12 3.2 3 ) ; Manager2.perHour(25.34) ; Manager2.bays(4); Manager2.sales(12.33) ; Manager3.perHour(34.34) ; МападегЗ.sales(100000. 12) ; Manager3.bays(10); Tripl.origin("Detroit"); Tripl.destination("Chicago"); Trip2.origin("Detroit") , Trip2.destination("NY") Trip3.origin("Detroit") Trip3.destination("Windsor") }
12.4. Реализация агентов в C++ 457 Эти директивы сообщают агенту о том, что его владелец хотел бы отправиться оТпуск из Детройта в Чикаго, из Детройта в Нью-Йорк или из Детройта в Виндзор. Помимо маршрутов, также устанавливаются финансовые цели. Чтобы отпуск состоялся необходимо достижение одной или нескольких таких целей. После установки целей агент обновляет свои убеждения, и его следующая задача будет определена в зависимости от целей и убеждений при условии возможности планирования отпуска. И тогда вызывается второй компонент методов рассуждений агента: DetermineVacationAppropriate () Этот метод передает контейнер UniverseOf Discourse каждому из объектов предположений. После этого он использует утверждение, выраженное в следующей форме: (A v В v С) Л (Q v R v S) --> W Это выражение можно озвучить так: если хотя бы одно из утверждений каждой группы истинно, то элемент W примет значение ИСТИНА. Для нашего агента это означает, что если достигнута хотя бы одна из целей эффективности биснеса и существует хотя бы один из приемлемых автобусных маршрутов, то отпуск можно планировать. Определение метода determineVacationAppropriate () представлено в листинге 12.10. // Листинг 12.10. Второй метод рассуждений bool agent::determineVacationAppropriate(void) { bool TruthValue; Manager1.universe(PerformanceBeliefs), Manager2.universe(PerformanceBeliefs), МападегЗ.universe(PerformanceBeliefs), Tripl.universe(TripBeliefs); Trip2.universe(TripBeliefs); Trip3.universe(TripBeliefs); TruthValue = ((Managerl || Manager2 || МападегЗ) && (Tripl || Trip2 || Trip3)); return(TruthValue); } Обратите внимание на то, что списки TripBelief s и Perf ormanceBelief s являются аргументами метода universe () объектов Trip и Manager. Именно здесь объекты предположений получают информацию из предметной области (UniverseOf Discourse). Прежде чем объект класса proposition вызовет оператор operator (), его контейнер UniverseOf Discourse должен заполниться имеющимися у агента данными. В листинге 12.10 при вычислении выражения ((Managerl || Manager2 || МападегЗ) && (Tripl || Trip2 || Trip3)); Ценивается шесть предположений (посредством выполнения оператора "| |"). ератор " | |" для каждого предположения выполняет оператор operator (), торый для определения истинности предположения использует список iverseOfDiscourse. Следует иметь в виду, что классы trip_announcement rtor^ance__statement наследуют довольно много функций класса proposition. сгингах 12.6 и 12.7 было показано, как определяется оператор operator () для Са trip__announcement, а в листинге 12.11 приведено определение оператора erator () Для класса per f ormance_statement.
458 Глава 12. Реализация агентно-ориентированных архитектур // Листинг 12.11. Класс performance_statement bool performance_statement::operator()(void) { bool Satisfactory = false; list<performance_statement>::iterator I; I = UniverseOfDiscourse.begin(); while(I != UniverseOfDiscourse.end() && !Satisfactory) { if(((*I).bays() >= Bays) || ((*I).sales() >= Sales) || ( (*I) .perHourO >=PerHour)){ Satisfactory = true; } I++; } return(Satisfactory); } Оператор operator () для каждого класса proposition играет "свою" роль в способности класса агента делать логические выводы. В листинге 12.6 показано, как вызывается оператор operator () при каждом вычислении оператора " | | " или "&&" для класса proposition или для одного из его потомков. Именно такое сочетание методов operator (), определенных в proposition-классах, и методов класса agent образует стратегии логического вывода для класса agent. В дополнение к операторам "||" и "&&", определенным в классе proposition, классы trip_announcement и perf ormance_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 || Y()); } bool operator&&(bool X,trip_announcement &Y) { return(X && Y()); }
12.4. Реализация агентов в C++ 459 Обратите внимание на то, что в определении этих функций-"друзей" (благодаря ссылке на элемент Y ()) также используется вызов функции operator (). Эти функции определяются и в классе perf ormance_statement. Наша задача— сделать использование proposition-классов таким же простым, как использование встроенных типов данных. В классе proposition также определен другой оператор, который позволяет использовать предположение естественным образом. Рассмотрим следующий код. //.-- trip_announcement A; if (A){ //... Некоторые действия. } Как в этом случае компилятор тестирует объект А? При выполнении инструкции if () компилятор стремится найти в скобках значение целочисленного типа данных или типа bool. Но тип объекта А совсем другой. Мы хотим, чтобы компилятор воспринимал объект А как высказывание, которое может быть либо истинным, либо ложным. При таких обстоятельствах функция operator () не вызывается. Поэтому для получения нужного эффекта мы определяем оператор void*. Эту функцию- оператор можно определить следующим образом. template<class C> proposition<C>: .-operator void* (void) { return((void*)(TruthValue)); } Это определение позволяет предположение любого типа, представленное "в единственном числе", протестировать как значение истинности. Например, когда наш класс agent собирается отправить по электронной почте владельцу фирмы сообщение, содержащее путь следования, агенту нужно определить, какой маршрут отвечает заданным требованиям. В листинге 12.13 представлен еще один фрагмент из методов обработки автобусных маршрутов. // Листинг 12.13. Метод displayTravelPlan() void agent::displayTravelPlan(void) stack<trip_announcement> Route; 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();
460 Глава 12. Реализация агентно-ориентированных архитектур Обратите внимание на то, что объекты Tripl, Trip2 и Trip3 тестируются так как будто они имеют тип bool. Метод candidates () просто возвращает путь следования, соответствующий заданному маршруту. Таким образом, разработка стратегий логического вывода и когнитивных структур данных становится проще благодаря использованию перегрузки операторов и С++-шаблонов. Именно стратегии логического вывода и когнитивные структуры данных делают объект рациональным. С++- программист для разработки агентов использует конструкцию класса, а для реализации когнитивных структур данных (CDS) — контейнерные объекты в сочетании со встроенными алгоритмами. Класс, который содержит CDS-структуры, становится рациональным, а рациональный класс — агентом. 12.4.3. Простая автономность Поскольку наш простой класс агента не требует выполнения традиционного "цикла активизации", нам нужны другие средства, которые бы периодически активизировали агент без вмешательства человека. Возможны ситуации, когда агент нужно запускать на выполнение лишь иногда или только при определенных условиях. Среды UNIX/Linux оснащены утилитой crontab, которая представляет собой пользовательский интерфейс "хрон-системы" (хрон— это демон ОС UNIX, исполняющий предписанные команды в соответствии со строго определенными значениями даты и времени, указанными в специальном файле с именем crontab). Утилита crontab позволяет организовать периодическое выполнение одной или нескольких программ. Задания для утилиты crontab можно назначать с указанием месяца, дня недели, дня (месяца), часов и минут. Для использования утилиты crontab в нашем случае необходимо создать текстовый файл, который будет содержать график активизации агента. Записи этого файла должны иметь следующий формат: минуты часы день месяц день недели команда Каждый элемент записи может принимать следующие значения: минуты 0-59 часы 0-23 день 1-31 месяц 1-12 день недели 1-7(1 — понедельник, 7 — воскресенье) команда может быть любой UNIX/Linux-командой, а также именем файла, который содержит агенты Созданный в таком формате текстовый файл передается "хрон-системе" с помощью следующей команды: $cront ab NameOfCronFi I е Например, предположим, у нас есть файл activate . agent, содержимое которого имеет такой вид. 15 8 * * * agentl 0 21 * * 6 agent2 * * 1 12 * agent3
12.5. Мультиагентные системы 461 После выполнения с г on tab-команды $crontab activate.agent агент agent 1 будет активизироваться каждый день в 8:15, агент agent2 — каждое воскресенье в 21:00, а агент agent3 — каждый раз при наступлении первого декабря. Хрон-файлы можно при необходимости добавлять или удалять. Хрон-файлы могут содержать ссылки на другие хрон-задания, позволяя таким образом агенту "самому" перепланировать свою работу. Так, для обеспечения чрезвычайно гибкой, динамичной и надежной процедуры активизации агентов можно использовать сценарии оболочки в сочетании с утилитой crontab. Чтобы получить полное описание утилиты crontab, обратитесь к оперативным страницам руководства (manpages— гипертекстовые страницы консультативной информации, поясняющие действие конкретных команд): $man crontab или $man at Средства crontab и at представляют собой простейший способ автоматизации или регулярного запуска агентов, который не требует постоянного выполнения циклов активизации. Эти утилиты надежны и гибки. Однако для реализации автоматической активизации агента также можно использовать хранилище, или репозиторий, реализаций и брокер объектных запросов (object request brokers — ORB), который мы рассматривали в главе 8. Стандартные CORBA-реализации также предоставляют средства организации событийных циклов. 12.5. Мультиагентные системы Мультиагентные системы— это системы, в которых задействовано несколько агентов, обладающих способностью в процессе решения некоторой задачи взаимодействовать, сотрудничать, "договариваться" или соперничать. У С++-разработчика программного обеспечения есть несколько вариантов для реализации мультиагент- ных систем. Агенты можно реализовать в отдельных потоках выполнения с помощью API-интерфейса POSIX thread. В этом случае одна программа разбивается на несколько потоков, каждый из которых содержит один или несколько агентов. Следовательно, агенты одного потока будут разделять одно и то же адресное пространство. Это позволяет агентам легко взаимодействовать путем использования глобальных переменных и простой передачи параметров. Если компьютер, на котором выполняется программа, содержит несколько процессоров, то агенты могут выполняться параллельно. В этом случае каждый агент должен быть оснащен объектами синхронизации (см. главы 5 и 11) и компонентами обработки исключительных ситуаций (см. главу 7). Мультиагентные системы, реализованные посредством многопоточности, представляют самое простое решение, но тем не менее ограничивающее агентов рамками од- °го компьютера. Более гибкий подход к созданию мультиагентных систем предос- авляет CORBA-реализация. Стандарт CORBA (помимо ядра спецификации CORBA) одержит спецификацию мультиагентного средства (multi-agent facility— MAF). <~0-реализацию, которую мы используем в CORBA-примерах этой книги, можно Р менять для реализации агентов, которые способны взаимодействовать через сети ernet, intranet и локальные сети. С++-привязка CORBA-стандарта имеет полную ДДержку объектно-ориентированного представления и, следовательно, поддержку нтно-ориентированного программирования. В главе 13 мы рассмотрим, как можно -1ьзовать библиотеки PVM и MPI для поддержки агентов в контексте параллель- 11 распределенного программирования.
462 Глава 12. Реализация агентно-ориентированных архитектур 12.6. Резюме Агенты — это рациональные объекты. Агентно-ориентиро ванное программирова- ние _ это свежий взгляд на старые проблемы декомпозиции, взаимодействия и синхронизации, которые являются обязательной частью каждого проекта параллельного или распределенного программирования. С++-поддержка перегрузки операторов контейнеров и шаблонов обеспечивает эффективные средства реализации широкого диапазона классов агентов. Будущие системы с массовым параллелизмом и большие распределенные системы будут опираться на агентно-ориентированные реализации, поскольку практически не существует других путей построения таких систем. Несмотря на "вводный" характер примеров создания агентов, представленных в этой главе, они вполне обеспечивают основу для понимания практических принципов построения агентных систем. Для развертывания мультиагентных систем можно использовать общедоступные и популярные библиотеки POSIX thread API, MICO, PVM и MPI. Мультиагентные системы можно использовать для реализации решений, которые требуют параллельного или распределенного программирования. В этой книге представлены два основных варианта архитектуры для параллельного и распределенного программирования: первый представляют агенты, а второй — "классные доски" (которые предполагают использование агентов). О том, как использовать "классные доски" для реализации решений параллельного и распределенного программирования, мы поговорим в следующей главе.
РЕАЛИЗАЦИЯ ТЕХНОЛОГИИ "КЛАССНОЙ ДОСКИ" С ИСПОЛЬЗОВАНИЕМ PVM-СРЕДСТВ, ПОТОКОВ И КОМПОНЕНТОВ C++ В этой главе... 13.1. Модель "классной доски" 13.2. Методы структурирования "классной доски" 13.3. Анатомия источника знаний 13.4. Стратегии управления для "классной доски" 13.5. Реализация модели "классной доски" с помощью CORBA-объектов 13.6. Реализация модели "классной доски" с помощью глобальных объектов 13.7. Активизация источников знаний с помощью потоков 13.8. Резюме
JTJ\3JSJ^J Kf:i: lltl ff • £&* 1 J*J %i м.к s? ?fc r^£v *^' „/" 4- ••' ^' •M "Человеческий разум гораздо сложнее, чем любой компьютер, но будущая цель развития компьютерной техники — достичь уровня "мышления" не отдельного индивидуума, а умственного потенциала целого общества..." — Тимоти Феррис (Timothy Ferris), The Universe and Eye Одна из основных целей в параллельном программировании — разбить всю работу, предусмотренную для выполнения программой, на множество задач, которые могут при необходимости выполняться с определенной степенью параллелизма. Эта цель труднодостижима. Довольно сложно так провести декомпозицию работ (Work Breakdown Structure— WBS), чтобы создать соответствующий фундамент для параллелизма и обеспечить корректные и эффективные результаты работы. Для достижения этой цели мы используем методы моделирования и специальные архитектурные решения. На практике на этапе моделирования самой задачи и ее решения стараются выявить естественный параллелизм. Не следует в решение вносить параллелизм искусственно. Если задача и ее решение смоделированы надлежащим образом, то необходимый параллелизм обнаружится сам собой. Архитектура "классной доски" облегчает такой процесс моделирования. В частности, модель "классной доски" позволяет организовать и концептуализировать параллельность и взаимодействие компонентов в системе, которая требует применения параллельного или распределенного программирования.
13.1. Модель "классной доски" 465 13.1 • Модель "классной доски" Модель "классной доски" — это технология совместного решения задач. "Классная доска" используется для регистрации и координации действий, а также организации взаимодействия между двумя или больше программными решателями задач. Таким образом, в модели "классной доски" существует два основных типа компонентов: "классная доска" и решатели задач. "Классная доска" представляет собой централизованный объект, к которому имеет доступ каждый из решателей задач. Решатели задач могут считывать содержимое "классной доски" и изменять его. Содержимое "классной доски" в различные моменты времени различно. Исходное содержимое "классной доски" включает задачу, которую необходимо решить, а также информацию, представляющую начальное состояние задачи, ее ограничения, цели и требования. По мере того как решатели задач продвигаются к получению решения, на "классной доске" фиксируются промежуточные результаты, гипотезы и выводы. Промежуточные результаты, записанные одним решателем задач, могут действовать как катализатор для других решателей задач, считывающих содержимое "классной доски". На "классную доску" записываются предварительные решения. Решения, признанные неудовлетворительными, "стираются", и процесс поиска новых решений продолжается. В отличие от сеансов непосредственной связи, решатели задач используют "классную доску" не только для передачи частичных результатов, но и поиска друг друга. В некоторых конфигурациях "классная доска" действует как рефери, информируя решателей задач о факте достижения решения или выдавая сигнал начать либо завершить работу. "Классная доска"— это активный объект, а не просто область памяти. В некоторых случаях "классная доска" определяет, каких решателей задач нужно привлечь и какое ее содержимое следует принять или отвергнуть. "Классная доска" может преобразовывать или интерпретировать результаты, полученные от решателей задач одной группы, чтобы ими могли воспользоваться решатели задач другой группы. Решатель задач — это программное средство, которое обычно обладает специальными знаниями или возможностями обработки получаемой информации в пределах некоторой предметной области. Решатель задач может быть довольно простой функцией, которая, например, переводит значение температуры по Цельсию в значение по Фаренгейту, или достаточно сложным интеллектуальным агентом, который обрабатывает медицинские диагнозы. В модели "классной доски" эти решатели задач называются источниками знаний. Чтобы решить задачу с использованием "классной доски", необходимо наличие двух или больше источников знаний, которые обычно обладают различной специализацией. Модель "классной доски" более подходит для задач, разделяемых на отдельные подзадачи, которые можно решать независимо (или почти независимо) от Других. В базовой конфигурации архитектуры "классной доски" каждый решатель занимается "своей" частью задачи, т.е. он "видит" только часть общей задачи, с которой работает. Если решение одной части задачи зависит от решения другой ее части, то Массная доска" используется для координации действий решателей задач и объединена частных решений. Решатели задач, задействованные в архитектуре "классной дос- > не должны быть одинаковыми. Каждый из них может быть реализован по-своему. пример, одни решатели задач могут быть реализованы с использованием объектно- °Риентированных технологий, а другие — как функции. Более того, решатели задач мо- /Т использовать совершенно различные парадигмы решения. Например, решатель А
466 Глава 13. Реализация технологии "классной доски"... для решения своей подзадачи может применять метод обратного построения цепочки (т.е. ведения рассуждений от целевой гипотезы к исходным посылкам), а решатель В — метод от противного. При этом также необязательно, чтобы решатели задач были реализованы с помощью одного и того же языка программирования. Модель "классной доски" не определяет никакой конкретной структуры ни для самой "классной доски", ни для источников знаний. Как правило, структура "классной доски" зависит от конкретной задачи.1 Реализация источников знаний также зависит от специфики решаемой задачи. "Классная доска" — это концептуальная модель, описывающая отношения без представления структуры самой "классной доски" и источников знаний. Модель "классной доски" не диктует количество используемых источников знаний или их назначение. "Классная доска" может быть единственным глобальным или распределенным объектом, компоненты которого расположены на нескольких компьютерах. Системы "классной доски" могут состоять из нескольких "классных досок", и каждая из них "занимается" решением определенной части исходной задачи. Это делает модель "классной доски" чрезвычайно гибкой. Модель "классной доски" поддерживает параллельное и распределенное программирование. Во-первых, источники знаний, работая над решением части общей задачи, могут выполняться одновременно. Во-вторых, источники знаний могут быть реализованы в различных потоках или отдельных процессах одного или нескольких компьютеров. "Классная доска" может быть разделена на несколько отдельных частей, позволяющих параллельный доступ со стороны нескольких источников знаний. "Классная доска" легко поддерживает такие архитектурные варианты, как CREW (concurrent read, exclusive write— параллельное чтение и монопольная запись), EREW (exclusive read, exclusive write — монопольное чтение и монопольная запись) и MIMD (multiple- instruction, multiple-data— множество потоков данных и множество потоков команд). Мы реализуем "классную доску" как глобальный объект или коллекцию объектов, а источники знаний — как отдельные потоки. Поскольку потоки разделяют одно и то же адресное пространство, к "классной доске", реализованной как глобальный объект или семейство объектов, будут получать доступ все потоковые источники знаний. Если источники знаний реализовать как отдельные процессы, выполняющиеся на одном или нескольких компьютерах, то "классную доску" имеет смысл реализовать как CORBA-объект или как коллекцию CORBA-объектов. Вспомните, что CORBA-объекты можно использовать для поддержки как параллельной, так и распределенной модели вычислений. Здесь мы используем технологию CORBA для поддержки "классной доски" как разновидность распределенной памяти, совместно используемой задачами, выполняющимися в различных адресных пространствах. Эти задачи могут быть PVM-типа (Parallel Virtual Machine— параллельная виртуальная машина), задачами, порождаемыми традиционными f ork-exec-вызовами функций, или задачами, порождаемыми библиотечными функциями posix_spawn (). Две конфигурации памяти для реализации технологии "классной доски" показаны на рис. 13.1. В обоих случаях (см. рис. 13.1) все источники знаний имеют доступ к "классной доске". Источники знаний, размещенные в различных адресных пространствах, Несмотря на то что "классную доску" можно использовать для решения для многих аналогичных дач, вряд ли это возможно для совершенно различных классов задач, т.е. многократное использова "классной доски" обычно ограничено близкими по своей сути задачами. Дело в том, что пространство }* шений в этом случае тесно связано с конкретной задачей, а компонент правил тесно связан с пространстве решений, что не позволяет использовать "классную доску " для решения задач более широкого диапазона.
13.2. Методы структурирования "классной доски" 467 должны иметь сетевую связь с "классной доской", реализованной как один или несколько CORBA-объектов. Если источники знаний реализованы как PVM-задачи то их связь с "классной доской" можно построить на основе передачи сообщений. Такая конфигурация обеспечивает чрезвычайно гибкую модель решения задач. ПАМЯТЬ "КЛАССНОЙ ДОСКИ" (КОНФИГУРАЦИЯ 1) Источники знаний расположены в различных адресных пространствах « CORBA-объект » "Классная доска" L ПРОЦЕСС 1 Сетевая связь или межпроцессное взаимодействие « процесс » ИЗ-, ПРОЦЕСС 2 « процесс » ИЗ 2 ПРОЦЕСС 3 « процесс » И33 ПАМЯТЬ "КЛАССНОЙ ДОСКИ" (КОНФИГУРАЦИЯ 2) Источники знаний расположены в одном адресном пространстве ПРОЦЕСС 1 Адресное пространстве L_ ПОТОК 1 « глобальный объект » "Классная доска" — — - « поток » H3i ПОТОК 2 | « ПОТО | ИЗ 7 '- ПОТОК 3 << поток » ИЗ з | о 1 * К» : 1 Рис. 13.1. Две конфигурации памяти для реализации технологии "классной доски" (ИЗ — источник знаний) 13.2. Методы структурирования "классной доски" Методов структурирования "классной доски" не существует. Однако большинство реализаций этой технологии имеют определенные характеристики и атрибуты. Ис- ХоДНое содержимое "классной доски" обычно включает часть пространства решения
468 Глава 13, Реализация технологии "классной доски"... задачи. Пространство решений должно содержать все частные и полное решения задачи. Например, предположим, что у нас есть механизм поиска изображений автомобилей в Internet. Этот механизм поиска может обрабатывать растровое или векторное изображение, чтобы определить, содержит ли оно изображение автомобиля, и если содержит, то отвечает ли оно параметрам поиска. Допустим, этот механизм поиска разработан с использованием модели "классной доски". Каждый источник знаний имеет свою специфику: один— специалист в области идентификации изображений покрышек, другой — идентифицирует зеркала задней обзорности, третий — эксперт по дверным ручкам для автомобилей и т.д. Каждая деталь автомобиля представляет малую часть пространства решений. Одни части пространства решений содержат полное изображение автомобиля с различных точек зрения (т.е. сверху, снизу, под углом 45° и т.д.), а другие — только отдельные детали автомобилей, например, фронтальную и заднюю части, крышу или багажник. На "классной доске" размещается растровое или векторное изображение, и отдельные источники знаний пытаются идентифицировать детали изображения, которые могут быть частями автомобиля. Если некоторая часть пространства решений совпадает с какой-нибудь частью изображения, эта часть изображения будет записана в другую часть "классной доски" как частное решение. Один источник знаний может поместить на "классную доску" дверную ручку идентифицируемого автомобиля, другой — дверцу. Если эти две части информации оказались на "классной доске", то какой-нибудь третий источник знаний может использовать эту информацию как вспомогательную при идентификации передней части автомобиля в исследуемом изображении. После того как будет идентифицирована передняя часть, она также размещается на "классной доске". Каждый из этих различных способов идентификации изображения автомобиля представляет часть пространства решений. Пространство решений иногда организуется иерархически. В нашем примере с автомобилем на вершине иерархии могут находиться полные изображения автомобиля, следующий уровень может состоять из различных видов передних и задних частей, еще один уровень может содержать двери, багажники, капоты, ветровые стекла и колеса. Каждый уровень описывает в этом случае меньшее, возможно, менее характерное изображение некоторой части автомобиля. Источники знаний могут работать одновременно на нескольких уровнях иерархии. Пространство решений также можно организовать в виде графа, в котором каждый узел представляет некоторую часть решения, а каждое ребро — отношения между двумя частными решениями. Пространство решений может быть представлено в виде одной или нескольких матриц, а каждый элемент матрицы будет содержать в этом случае полное или частное решение. Представление пространства решений— это важный компонент архитектуры "классной доски". Именно характер задачи часто определяет, как должно быть распределено пространство решений. Помимо компонента пространства решении, "классная доска" обычно имеет один или несколько компонентов (эвристических) правил. Компонент правил используется для определения того, какие источники знаний стоит использовать и какие решения принимать или отвергать. Компонент правил можно также применить для перевода частных решений с одного уровня иерархии пространства решений на другой. Компонент правил позволяет назначать ПР^" оритеты источникам знаний. Некоторые источники знаний могут "зайти в тупик • "Классная доска" может "снять отметку" с одной группы источников знаний в польз) другой, а также использовать компонент правил, чтобы предложить источникам знании более потенциально подходящие гипотезы на основе уже сгенерированных частнЫ*
13.2. Методы структурирования "классной доски" 469 гипотез. Помимо пространства решений и компонента правил, "классная доска" часто содержит начальные значения, значения ограничений и вспомогательные цели. В некоторых случаях "классная доска" может содержать одну или несколько очередей событий, используемых для приема входных данных либо из пространства задачи, либо от источников знаний. Логическая схема базовой архитектуры "классной доски" показана на рис. 13.2. Архитектура "классной доски" ^ВЗАИМОДЕЙСТВИЕ Включает: Т\ j эвристические ограничения задачи,! метазнания об источниках знаний Программная | ||Ш^| модель решения | ™^^ | j;> « процесс/поток » И31 « процесс/поток » ИЗ 2 « процесс/поток » И33 « процесс/поток » ИЗп * Если ИЗ (источник знаний) является процессом, взаимодействие может быть реализовано через сеть или средства межпроцессного взаимодействия. Если ИЗ — поток, связь можно осуществить посредством передачи параметров Рис. 13.2. Логическая схема базовой архитектуры "классной доски" "Классная доска" (см. рис. 13.2) имеет ряд сегментов, а каждый сегмент— различные реализации. Это говорит о том, что "классная доска" — это нечто большее, чем просто область глобальной памяти или традиционные базы данных. Хотя на рис. 13.2 показаны только основные компоненты, которые имеют многие "классные доски", этот вид архитектуры не ограничивается таким составом. К числу дополнительных компонентов потенциально можно отнести модели контекстов задачи и модели предметной области, которые могут оказаться полезными для решателей задач при навигации по пространству решений. С++-подцержка объектно-ориентированного проектирования и программирования прекрасно сочетается с требованиями гибкости, которые обычно предъяв- яются к модели "классной доски". Большинство архитектур "классной доски" может Ыть смоделировано с использованием С++-классов. Вспомните, что классы можно ис- ользовать для моделирования человека, местности, предмета или идеи, а "классные ски используются для решения задач, в которых часто участвуют люди, местности, редметы или идеи. Поэтом)7 весьма уместно применять С++-классы для моделирования "Ьектов, которые содержит "классная доска". В своих реализациях модели "классной
470 Глава 13. Реализация технологии "классной доски"... доски" мы используем преимущества контейнерных С++-классов и стандартных алгоритмов. Помимо встроенных классов, мы создаем интерфейсные классы для мьютексов и других переменных синхронизации, используемых в реализации "классной доски" Поскольку к "классной доске" могут получить доступ сразу несколько источников знаний одновременно, это означает, что она является критическим разделом, доступ к которому нуждается в синхронизации. Поэтому вместе с другими компонентами "классной доски" мы будем использовать здесь и объекты синхронизации. 13.3. Анатомия источника знаний Источники знаний представляются как объекты, процедуры, множества правил, логические утверждения, а в некоторых случаях и целые программы. Источники знаний включают часть условий и часть действий. Если "классная доска" содержит информацию, которая удовлетворяет части условий некоторого источника знаний, то его часть действий активизируется. Инглемор (Englemore) и Морган (Morgan) в своей работе [14] четко описывают обязанности источника знаний. Каждый источник знаний отвечает за знание условий, при которых он может внести свой вклад в решение. Каждый источник знаний имеет предусловия, т.е. условия, которые должны быть записаны на "классной доске" и существовать до того, как будет активизировано тело источника знаний. Источник знаний можно рассматривать как большое правило. Главное, чем отличается правило от источника знаний, состоит в степени детализации знаний. Часть условий этого большого правила называется предусловием источника знаний, а часть действий — его телом. Здесь Инглемор и Морган не определяют ни единой детали части условий или части действий источника знаний. Они представляют собой логические конструкции. Часть условий может иметь форму простого значения булевого флага на "классной доске" или сложной последовательности событий, поступающих в очередь событий в пределах определенного периода времени. Аналогично часть действий источника знаний может быть выражена простой инструкцией, выполняющей операцию присваивания переменной некоторого выражения, или механизмом прямого построения цепочки в экспертной системе. Это описание широты диапазона еще раз подчеркивает гибкость модели "классной доски". Для наших целей вполне достаточно конструкции С++-класса и понятия объекта. Каждый источник знаний должен быть объектом. Часть действий источника знаний должна быть реализована в виде методов объекта, а часть условий — в виде его членов данных. Если объект находится в определенном состоянии, то его часть действий должна быть активизирована. Проще говоря, мы реализуем источники знаний в виде потоков или процессов. Следовательно, для каждого потока и для каждого процесса должен существовать только один источник знаний. Применяя к "классной доске" PVM-механизм, источник знаний будет эквивалентом PVM-задачи. Логическая схема источника знаний показана на рис. 13.3. Часть "Условия" каждого источника знаний обновляется "из закромов" "классно! доски", а часть "Действия" источников знаний обновляет ее содержимое. Обратит внимание на то (см. рис. 13.3), что между пространством процесса и источником зна ний (или между пространством потока и источником знаний) существует взаимно од нозначное отношение. Важным атрибутом источника знаний является его автоно* ность. Каждый источник знаний является специалистом в своей области и поч
13.4. Стратегии управления для "классной доски" 471 не зависит от других решателей задач. Это составляет одно из требуемых качеств для параллельной программы. В идеале задачи в параллельной программе могут выполняться одновременно, почти не нуждаясь во взаимодействии с другими задачами. Такое поведение в точности описывает схему модели "классной доски". Источники знаний действуют независимо, и любое взаимодействие осуществляется посредством "классной доски". Поэтому источник знаний (с его точки зрения) действует в одиночку, получая дополнительную информацию от "классной доски" и записывая на "классную доску" свои изыскания. О деятельности других источников знаний и их стратегиях поведения ему ничего не известно. В модели "классной доски" задача делится на ряд автономных или полуавтономных решателей задач. В этом и состоит преимущество модели "классной доски" перед другими моделями. В самой гибкой конфигурации источники знаний должны быть интеллектуальными агентами. Агент должен быть совершенно самодостаточным и способным действовать самостоятельно при минимальной потребности к взаимодействию с "классной доской". Именно интеллектуальный агент представляет самую грандиозную перспективу для реализации крупномасштабного параллелизма. :<<пр6цесс/потЬк^> Источник;зианий л ^ •директивы | .обласгь поиска • требования| ' база зианий | * стратегии логического вывода * методы поиска * стратегии решения*задачи - •.■■•О- '" Условия■ л ■.■Действий Рис. 13.3. Логическая схема источника знаний 13.4. Стратегии управления для "классной доски" В реализации модели "классной доски" предусмотрено несколько уровней управления, обеспечивающих возможность параллельного функционирования источников знаний. На самом нижнем уровне их схемы синхронизации должны защищать целостность "классной доски". "Классная доска" является критическим разделом, поскольку она представляет собой совместно используемый модифицируемый ресурс. В параллельной среде доступ со стороны источников знаний для чтения и записи должен 1#гь скоординирован и синхронизирован. Координация и синхронизация может Ючать блокировку файлов, семафоры, мьютексы и т.д. Этот уровень управления не ключается непосредственно в решение, над которым работают источники знаний. ° Можно назвать вспомогательным уровнем управления, и он не должен зависеть от еЦИфики задачи, решаемой с помощью "классной доски". В нашем архитектурном
472 Глава 13. Реализация технологии "классной доски"... подходе этот уровень управления реализуется интерфейсными классами (напримеп классами мьютекса и семафора, использованными в главе 11). Вспомните, что дейст вия, инкапсулированные в этих классах, не зависят от приложения, в котором они используются. Для параллельных реализаций "классной доски" на этом уровне выбирается один (или больше) из четырех типов параллельного доступа, которыми должны обладать алгоритмы или эвристические правила источников знаний для физической реализации "классной доски". Другими словами, пользователи "классной доски" могут использовать EREW-, CREW-, ERCW- или CRCW-доступ. Именно характер доступа определяет, как будут использованы примитивы синхронизации. Описание упомянутых здесь типов доступа приведено в табл. 13.1. Таблица 13.1. Четыре типа параллельного доступа, используемых в модели "классной доски" PRAM-модели Описание 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 — множество программ и множество потоков данных), известная также как MIMU (multiple-instruction, multiple-data— множество потоков команд и множество потоков данных), поскольку каждый источник знаний (решатель задачи) имеет собственную область специализации. Однако сама природа задачи иногда может дать право на использование такой популярной модели, как SPMD (Single Program Multiple Data —
13.4. Стратегии управления для "классной доски" 473 одна программа, несколько потоков данных). В этом случае уровень управления поудит дгодинаковых источников знаний, но передаст им различные параметры. На следующем уровне управления определяется, что делать с решением или частными решениями, записанными на "классной доске". Этот уровень управления должен оценить, могут ли источники знаний остановить работу, и является ли сгенерированное решение приемлемым, неприемлемым, частично приемлемым и т.д. Именно этим уровнем управления завершается видимость "классной доски" и всех частных или предварительных решений. Именно здесь осуществляется руководство общими стратегиями решения задач коллективными усилиями. В соответствии со структурой "классной доски" и источников знаний модель "классной доски" предполагает существование компонента управления, но не определяет, как он должен быть структурирован. Иногда компонент управления является частью "классной доски", а иногда он реализуется источниками знаний. В некоторых случаях компонент управления реализуется модулями, которые являются внешними по отношению к "классной доске". Компонент управления также может быть реализован любым сочетанием предыдущих вариантов. Источники знаний совместно ищут решение задачи. Следует отметить, что некоторые задачи имеют несколько решений. Одни из них могут находиться глубже в пространстве поиска, чем другие; поиск одних решений может быть более затратным по сравнению с поиском других, а некоторые решения могут быть недостаточно хорошо продуманы. Компонент управления не только руководит коллективными стратегиями поиска, выполняемого источниками знаний, но и контролирует частные или предварительные решения, чтобы убедиться, что источники знаний не реализуют какую-нибудь непрактичную стратегию поиска. Компонент управления выявляет бесконечные циклы, тупики или рекурсивные регрессии. Более того, компонент управления включается в выбор наилучших или наиболее подходящих источников знаний для данной задачи. По мере продвижения источников знаний к искомому решению компонент управления может разгрузить одни источники знаний за счет других. Стратегия управления должна быть тесно связана с со стратегиями поиска, которыми руководствуются источники знаний. Важно помнить, что все источники знаний могут использовать различные стратегии поиска и методы решения задачи. И хотя они работают с общей "классной доской", источники знаний по своей сути автономны и самодостаточны. Следовательно, этот уровень управления имеет двустороннее взаимодействие с источниками знаний. Возможные конфигурации управления и их уровни в архитектуре "классной доски" показаны на рис. 13.4. Обратите внимание на то, что в первой из представленных конфигураций (см. рис. 13.4) механизм управления содержится в самой "классной доске", а не в отдельном модуле и не в источниках знаний. В этой конфигурации блок управления проектируется как часть класса "классной доски". Поскольку на уровнях 2 и 3 необходимо двустороннее взаимодействие, имеет смысл, чтобы "классная доска" порождала процессы или потоки, которые будут содержать источники знаний. Если "классная Доска" порождает процессы или потоки, ей нетрудно получить доступ к идентификационному номеру любого потока или процесса. Это позволяет "классной доске" легко ередавать сообщения источникам знаний и осуществлять управление процессами Потоками. Если "классной доске" по некоторой причине нужно прекратить деятель- ость конкретного источника знаний, то доступ к идентификатору потока или процесса делает эту задачу очень простой. Обратите внимание на то, что в одном из пред- тавленных на рис. 13.4 вариантов блок управления является внешним по отношению классной доске" и источникам знаний. В этом случае идентификационный номер °Тока или процесса должен быть явным образом связан с модулями управления.
474 Глава 13. Реализация технологии "классной доски"... ВАРИАНТ 1 Компонент управления является частью "классной доски" УРОВЕНЬ 2 УРОВЕНЬ 3~ Включает: эвристические ограничения задачи, метазнания об источниках знаний Программная модель решения Контуюпь и ■ -управление. истрчнйками знаний., . взаимодействие |« процесс/поток » H3i « процесс/поток » И31 ВАРИАНТ 2 Компонент управления является частью "классной доски" и источников знаний "КЛАССНАЯ ДОСКА" Г "• I Частные решения j Компонент управления | Начальные значения Контроль и управление источникам v .знаний i « процесс/поток » И31 Компонент управления! « процесс/поток » И31 Компонент управления ВАРИАНТ 3 Компонент управления является внешним по отношению к "классной доске" и источникам знаний "КЛАССНАЯ ДОСКА" | п I Частные решения I ' к OJ X к S X о 5 3 5 Ф 05 огр о. О. оде s Начальные значения « процесс/поток » M3i Компонент „управления: « процесс/поток » ИЗп Рис. 13.4. Конфигурации управления и их уровни в архитектуре "классной доски"
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 475 13.5. Реализация модели "классной доски" с помощью CORBA-объектов Вспомните, что CORBA-объект (см. главу 8) является независимым от платформы распределенным объектом. К CORBA-объектам могут получать доступ процессы, выполняющиеся на одном или на разных компьютерах, подключенных к сети. Это делает CORBA-объекты кандидатами для использования в PVM-средах, когда программа делится на ряд процессов, которые могут (или не могут) выполняться на одном и том лее компьютере. Обычно PVM-среда используется для передачи сообщений при вторичной роли общей памяти (если она вообще существует). Введение понятия разделяемого и доступного по сети объекта существенно усиливает вычислительные мощности PVM-среды. Следует иметь в виду, что с помощью CORBA-объектов можно смоделировать все, что позволяют смоделировать нераспределенные объекты. Это означает, что PVM-задачи, которые имеют совместный доступ к CORBA-объектам, могут получать доступ к контейнерным объектам, объектам оболочки, шаблонов, доменов и другим видам вспомогательных объектов. В данном случае мы хотели бы, чтобы PVM-задачи имели доступ к объектам "классной доски". Поэтому модель передачи сообщений мы дополняем совместным доступом к сложным объектам. Помимо PVM- задач, получающих доступ к распределенным CORBA-объектам, к ним также могут обращаться задачи, порожденные функциями posix_spawn() или fork-exec. Эти задачи выполняются в отдельных адресных пространствах одного и того же компьютера, но могут, тем не менее, связываться с CORBA-объектами, которые расположены либо на том же, либо на другом компьютере. Поэтому, несмотря на то что все задачи, созданные с помощью функций posix_spawn() или fork-exec, должны размещаться на одном компьютере, CORBA-объекты могут располагаться на любом компьютере. 13.5.1. Пример использования CORBA-объекта "классной доски" Чтобы продемонстрировать наше представление о CORBA-ориентированной классной доске", рассмотрим ее реализацию, предложенную разработчиками из компании Ctest Laboratories. И хотя полное описание этого варианта выходит за рамки нашей книги, мы все же остановимся на самых важных аспектах "классной доски" и источников знаний, имеющих отношение к нашему архитектурному подходу к параллельному программированию. "Классная доска" реализует услуги программно- ориентированного консультанта по составлению расписания учебных курсов. Классная доска" решает задачи планирования учебных курсов для студента типичного колледжа. Студенты часто сталкиваются с проблемой "неудобного" расписания за- ятии. Во время регистрации курсов всегда существует конкуренция за места в ауди- ориях. В какой-то момент важные для студента курсы попросту "закрываются". Ведь е зря существует печально известное правило, соответствующее дисциплине обслу- вания очереди: "первым пришел — первым обслужен". Поэтом)7 во время регистра- и' когДа десятки тысяч студентов пытаются записаться на ограниченное количест- ^^курсов, важным фактором выступает своевременность. Студент желает пройти Хрсы, которые дают право на получение диплома. В идеале эти курсы должны быть г* несены во времени. Кроме того, студент хотел бы поддерживать определенную нук> нагрузку и иметь свободное время для домашних и факультативных занятий.
476 Глава 13. Реализация технологии "классной доски"... Проблема состоит в том, что, когда студент готов взять выбранный им курс, прием на него может уже оказаться закрытым, и вместо него ему предлагаются другие курсы которые его интересуют в меньшей степени. Курсы-заменители увеличивают стоимость и продолжительность обучения студента в колледже, что с точки зрения студента является негативным фактором. Но если курсы-заменители отвечают "посторонним" интересам студента (имеются в виду хобби или перспективные цели), то такие курсы- заменители могут оказаться допустимыми. Кроме того, существует ряд факультативных кусов, которые могут также давать право для "выхода на диплом". Студент хотел бы получить оптимальный набор курсов, который бы позволил ему в запланированные сроки (или досрочно) претендовать на диплом, оставаясь при этом в рамках намеченного бюджета с максимальной гибкостью участвуя в учебном процессе. Для решения этой задачи студент использует работающую в реальном масштабе времени программу составления расписания учебных курсов, основанную на технологии "классной доски". Важно отметить, что "классная доска" имеет доступ реального времени к академической характеристике студента и текущим курсам (с открытым или закрытым приемом) в любой момент периода регистрации. Кроме того, "классная доска" имеет доступ к дипломному плану студента, академическим требованиям для реализации этого плана, расписанию "готовности" студента посещать занятия, данным о его целях и предпочтениях и т.д. Все эти элементы моделируются с помощью C++- и CORBA- классов и образуют компоненты "классной доски". Для упрощения нашего примера мы рассмотрим только следующие четыре источника знаний: • консультант по общеобразовательным курсам; • консультант по основным курсам; • консультант по факультативным курсам; • консультант по непрофилирующим курсам. Итак, рассмотрим фрагмент CORBA-интерфейса "классной доски". // Листинг 13.1. CORBA-объявления, необходимые для нашего // класса "классной доски" typedef sequence<long> courses; 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 — обеспечить доступ для чтения и записи со стороны источников знаний. В данном случае при разделении "классной доски" необходимо предусмотреть сегменты для каждого источника знаний. ^т На практике каждый сегмент источника знаний должен содержать один или несколько станоар них контейнерных С++-классов, используемых в качестве очередей данных и очередей событий. Ьезо ность каждого контейнера обеспечивается за счет компонентов синхронизации.
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 477 позволяет источникам знаний получать доступ к "классной доске" посредством CRCW-стратегии. Другими словами, несколько типов источников знаний могут получить доступ к "классной доске" одновременно, но источники знаний одинакового типа должны быть ограничены применением CREW-стратегии. Любой метод или сЬункция-член, с помощью которого источники знаний будут получать доступ к "классной доске", должен быть определен в интерфейсном классе black__board. Класс courses объявляется с использованием типа CORBA, и поэтому его можно применять в качестве параметра и значений, возвращаемых методами при взаимодействии между источниками знаний и "классной доской". Поэтому эти объявления класса blackboard courses Minor; courses Major; бурут использованы для представления информации, которая либо записывается на "классную доску", либо считывается с нее. Тип courses — это синоним для CORBA- типа sequence<long>, полученный в результате использования typedef- объявления. Тип sequence<long> в CORBA представляет собой вектор (массив) переменной длины. Это означает, что переменные типа courses используются для хранения массива элементов типа long. Каждый long-элемент предназначен для хранения кода курса. Каждый код курса, представляет курс обучения, предлагаемый в колледже. Поскольку C++ не имеет типа sequence, то объявление sequence<long> преобразуется в С++-класс. Этот класс имеет такое же имя, как sequence<long> typedef: courses. Процесс преобразования из CORBA-типов в типы C++ происходит во время IDL-компиляции при построении CORBA-приложения. IDL-компилятор должен перевести объявление sequence<long> в С++-код. С++-класс courses должен автоматически включать перечисленные ниже функции. allocbuf () freebuf() get_buffer() length() operator[] release() replace() maximum() Источники знаний будут взаимодействовать с "классной доской" с помощью этих методов. Объявление sequence<long> "невидимо" для источников знаний; они видят" только класс courses. Поскольку CORBA поддерживает такие типы данных, как структуры (struct), классы, массивы и последовательности, источники знаний могут обмениваться с "классной доской" высокоорганизованными объектами. Это позволяет программист)^ поддерживать объектно-ориентированное представление при °омене данными с "классной доской". Поддержка объектно-ориентированного пред- тавления (где это необходимо) является важным фактором понижения уровня слож- °сти параллельного программирования. Способность просто считывать с "классной Доски и записывать на нее сложные объекты или даже иерархии объектов упрощает рограммирование в параллельных приложениях. Нет необходимости выполнять реооразование из примитивных типов данных в сложные объекты: можно совер- ать °бмен сложными объектами напрямую.
478 Глава 13. Реализация технологии "классной доски"... 13.5.2. Реализация интерфейсного класса blackboard Обратите внимание на то, что в интерфейсном классе (см. листинг 13.1) нет объявлений переменных. Вспомните, что интерфейсный класс в CORBA-реализации ограничивается только объявлением методов. В интерфейсном классе не существует компонентов, предназначенных для хранения информации. CORBA-классы должны тесно контактировать с С++-реализациями до конца работы приложения. Реальные реализации методов и необходимых переменных вносятся в производный класс (выведенный из этого интерфейсного класса). Производный класс, выведенный из интерфейсного класса black_board, представлен в листинге 13.2. // Листинг 13.2. Фрагмент класса реализации для // интерфейсного класса black_board #include "black_board.h" #include <set.h> class blackboard : virtual public POA_black_board{ protected: //. - . set<long> SuggestionForMajor; set<long> SuggestionForMinor; set<long> SuggestionForGeneral; set<long> SuggestionForElective; 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: :Staticlmplementation
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 479 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::0bjectld &, 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; Это означает, что данный класс нельзя использовать напрямую. Из него необходимо вывести производный класс, в котором будут определены реальные функции- члены для всех объявлений чисто виртуальных функций. Класс POA_black_board, представленный в листинге 13.2, содержит требуемые определения для всех чисто ьиртуальных функций-членов. Что касается нашего класса "классной доски", то для реализации действий самой "доски" и источников знаний используются С++-методы. Днако источники знаний реализованы частично в языке C++ и частично в языке ло- ического программирования Prolog.2 Но поскольку C++ поддерживает мультиязыковую та конфигурация обусловлена тем, что Prolog имеет множество таких встроенных средств, как Р Ция унификации, возврат к предыдущему состоянию и поддержка логики предикатов, которые И пшеном случае (без использования языка Prolog) пришлось бы реализовать в C++ "с нуля ". В этой кни- (P<*pa6t примеров, в которых мы "смешиваем" C++ с языком Prolog, используется версия S\VI-Prolog ]отка университета в Амстердаме) и ее С++-библиотека интерфейсов.
480 Глава 13. Реализация технологии "классной доски"... и мультипарадигматическую разработку, к средствам C++ можно вполне добавить достоинства языка Prolog. В C++ мы можем либо породить Prolog-задачи (с помощью posix__spawn()- или fork-exec-функций), либо получить доступ к среде Prolog через ее интерфейс с незнакомыми языками профаммирования, который позволяет Prolog-среде общаться непосредственно с C++ и наоборот. Независимо от того, на каком языке создана реализация источников знаний — C++ или Prolog, объект "классной доски" должен взаимодействовать только с С++-методами. 13.5.3. Порождение источников знаний в конструкторе "классной доски" "Классная доска" реализуется как распределенный объект, использующий CORBA- протокол. В данном случае одной из основных целей "классной доски" является порождение источников знаний. Это важный момент, поскольку "классная доска" должна иметь доступ к идентификационным номерам задач. Начальное состояние "классной доски" (оно устанавливается в конструкторе) включает информацию о студенте, его академической характеристике, текущем семестре, требованиях для получения диплома и т.д. С помощью "классной доски", исходя из начального состояния, определяется, какие источники знаний следует запустить в работу. Иначе говоря, оценив начальную задачу и исходное состояние системы, "классная доска" составляет список запускаемых на выполнение источников знаний. Каждый источник знаний имеет соответствующий двоичный файл, а для хранения имен этих файлов "классная доска" использует контейнер Solvers. Позже, при функционировании конструктора, с помощью функционального объекта (или объекта-функции) и алгоритма f or_each () порождаются источники знаний. Вспомните, что любой класс, в котором определена операторная функция operator (), можно использовать как функциональный объект. Объекты-функции, как правило применяют совместно со стандартными алгоритмами вместо функций или в дополнение к ним. Обычно везде, где можно использовать обычную функцию, ее можно заменить объектом-функцией. Чтобы определить собственный функциональный объект, необходимо определить операторный метод operator (), придав ему соответствующий смысл, указав список параметров и тип возвращаемого им значения. Наша CORBA-реализация "классной доски" может поддерживать источники знаний, реализованные с помощью PVM- задач, традиционных UNIX/Linux-задач или отдельных потоков, использующих библиотеки POSIX thread. По типу задач, порождаемых в конструкторе, можно определить, с какими именно задачами будет работать "классная доска": с POSIX-потоками, традиционными UNIX/Linux-процессами или PVM-задачами. 13.5.3.1. Порождение источников знаний с помощью PVM-задач Конструктор "классной доски" содержит следующий вызов алгоритма. for_each(Solve.begin(),Solve.end(),Task); Алгоритм f or_each () применяет операторный метод объекта-функции (созданног для класса задачи) к каждому элементу контейнера Solve. Этот метод используется Д-71 порождения источников знаний в соответствии с моделью MIMD, при реализации торой все источники знаний имеют различную специализацию и работают с разли ными наборами данных. Объявление этого класса задач приведено в листинге 13-4.
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 481 // Листинг 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<char *>(X.data())/NULL,0,"",1, &Tid[N]);N++; } blackboard::blackboard(void) { task Task; vector<string> Solve; // Determine which KS to invoke //. .. Solve.push_back(KS1); Solve.push_back(KS2); Solve.push_back(KS3); Solve.push_back(KS4); f or__each (Solve. begin () , Solve. end () ,Task) ; } Этот класс task инкапсулирует порожденный процесс. Он содержит идентификационный номер задачи (поскольку у нас используется PVM-задача). В случае применения стандартных UNIX/Linux-процессов или Pthread-потоков, он должен содержать идентификационный номер процесса или потока. Этот класс действует как интерфейс между создаваемым процессом или потоком и "классной доской". "Классная доска здесь является основным компонентом управления. Она может управлять PVM- задачами с помощью их идентификационных номеров. Кроме того, "классная доска" может использовать групповые PVM-операции для синхронизации PVM-задач с использованием барьеров, организации PVM-задач в логические группы, которые Должны отрабатывать определенные аспекты решаемой задачи, и сигнализации членов группы с помощью соответствующих тегов сообщений. Групповые PVM-операции перечислены и описаны в табл. 13.2. Особый интерес для нашей "классной доски" представляют операции Pvm_barrier () и pvm_joingroup(), поскольку существуют ситуации, в которых классная доска" не запускает новые источники знаний до тех пор, пока определенная Труппа источников знаний не завершит свою работу. Для блокирования вызывающего роцесса до нужного момента (до окончания обработки данных соответствующими сточниками знаний) можно использовать операцию pvm_barrier (). Например,
482 Глава 13. Реализация технологии "классной доски"... "классная доска" в качестве консультанта по выбору курсов обучения не будет активи зировать источник знаний, отвечающий за составление расписания, до тех пор, пок не представят свои предложения источники знаний, которые специализируются на основных, общеобразовательных, второстепенных и факультативных курсах. Поэтому "классная доска" будет использовать операцию pvm_barrier () для ожидания завершения работы этой группы PVM-задач. На рис. 13.5 представлена UML-диаграмма видов деятельности, которая позволяет понять, как синхронизируются источники знаний и "классная доска". Барьер синхронизации здесь реализуется с помощью операций pvm_barrier () и pvm_j oingroup (). Реализация операторной функции для объекта задачи приведена в листинге 13.5. Таблица 13.2. Групповые PVM-операции Операции Описание int pvm_joingroup( char *groupname), int pvm_lvgroup( char *groupname); int pvm_gsive( char *groupname); int pvm_gettid( char *groupname/ int inum); int pvm_getinst( char *groupname/ int taskid); int pvm__barrier ( char *groupname, int count); int pvm_bcast{ char *groupname/ int messageid); int pvm_reduce( void *operation, void *buffer, int count, int datatype, int messageid, cha r * groupname, int root); Вносит вызывающий процесс в группу groupname, а затем возвращает int-значение, которое представляет собой номер процесса в этой группе Удаляет вызывающий процесс из группы groupname Возвращает int-значение, которое представляет собой количество членов в группе groupname Возвращает int-значение, равное идентификационному номеру задачи, выполняемой процессом, который идентифицируется именем группы groupname и номером экземпляра inum Возвращает int-значение, которое представляет собой номер экземпляра, связанный с именем группы groupname и процессом, выполняющим задачу с идентификационным номером taskid Блокирует вызывающий процесс до тех пор, пока count членов в группе groupname не вызовут эту функцию Передает всем членам группы groupname сообщение, хранимое в активном буфере отправки, связанном с номером messageid Выполняет глобальную операцию operation во всех процессах группы groupname
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 483 "Классная доска" [ИЗ не нужен] /\ [ИЗ нужен] I Получение текущего дипломного плана Получение выбранных курсов Прием предлагаемых курсов Составление основного списка курсов / Получение / основного списка курсовУ г— ИЗ "Основные курсы* ИЗ "Второстепенные курсы*8 ИЗ "Общеобразовательные курсы* ИЗ "Факультативные курсы" Создание ИЗ Составление списка курсов ИЗ "Составитель расписания" Создание ИЗ Создание вариантов расписаний I : Ис«13.5. UML-диаграмма видов деятельности, отображающая синхронизацию "классной Доски" и источников знаний
484 Глава 13. Реализация технологии "классной доски"... // Листинг 13.5. Определение функции operator() // в классе task void task::operator()(string X) { int cc; pvm_mytid(); cc = pvm_spawn(const_cast<char *>(X.data())/NULL/0,"",1, &Tid[N]);N++; } Функция-оператор operator () используется для порождения PVM-задач. Имя задачи содержится в элементе X. data (). При обращении к функции pvm_spawn () (см. листинг 13.5) создается одна задача, а ее идентификационный номер сохраняется в элементе Tid[N]. (Подробнее о функции pvm_spawn () и вызове PVM-задач см. главу 6.) Класс task используется для создания функциональных объектов (объектов- функций). При выполнении алгоритма for_each(Solve.begin(),Solve.end(),Task); вызывается функция operator (), которая выполняет объект Task. Эта операция заставляет активизироваться источники знаний, содержащиеся в контейнере Solve. Алгоритм for_each() гарантирует активизацию всех источников знаний. Если используется модель SIMD, то в алгоритме f or_each () нет никакой необходимости. Вместо него прямо в конструкторе "классной доски" мы используем вызов функции pvm_spawn(). В листинге 13.6 как раз и показано, как при использовании модели SIMD можно запустить множество PVM-задач из конструктора "классной доски". // Листинг 13.6. Запуск PVM-задач из конструктора // класса task void task::operator()(string X) { int cc; pvm_mytid(); cc = pvm_spawn(const_cast<char *>(X.data()),NULL,0,"",1, &Tid[N]);N++; } 13.5.3.2. Связь "классной доски" и источников знаний Согласно коду, приведенному в листинге 13.6, порождается 20 источников знании. Сначала все они выполняют одинаковый код. После их порождения "классная доска должна отправить сообщения с указанием, какую роль они будут играть в процессе решения задачи. При использовании данной конфигурации источники знании и "классная доска" являются частью PVM-среды. После создания источники знании будут взаимодействовать с "классной доской" путем соединения с портом, на котором она размещается, или по ее адресу в сети intranet или Internet. Для этого источникам знаний понадобится объектная ссылка на "классную доску". Эти ссылки можно "зашить" в код источников знаний, или они могут прочитать их из файла конфигур3" ции либо получить из службы имен. Имея ссылку, источник знаний взаимодействуй с ORB-брокером (Object Request Broker— брокер объектных запросов), чтобы найти
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 485 удаленный объект, содержащий реальные данные (знания) и активизировать его. Для нашего примера мы назначаем "классной доске" конкретный порт и запускаем CORbA-объект "классной доски" с помощью следующей команды. blackboard -ORBIIOPAddr inet:porthos:12458 По этой команде запускается наша программа "классной доски" с подключением к порту 12458 хоста porthos. Запуск CORBA-объекта зависит от используемой CORBA- реализации. В данном случае мы используем "открытую" CORBA-реализацию Mico\ При выполнении программы blackboard реализуется экземпляр "классной доски", который в свою очередь порождает источники знаний. В созданных источниках знаний жестко закодирован номер порта, по которому они будут связываться с "классной доской". Фрагмент кода реализации источника знаний, который связывается с CORBA-ориентированным объектом "классной доски", представлен в листинге 13.7. // Листинг 13.7. Код источника знаний, который связывается // с CORBA-ориентированной wклассной доской" 1 #include "pvm3.h" 2 using namespace std; 3 #include <iostream> 4 #include <fstream> 5 #include <string.h> 6 #include <strstream> 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: :0bject_var Obj =0rb->bind( "IDL:blackboard: 1. 0й, "inet:porthos:12458"); 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[1] = 253212; 26 string FileName; 27 strstream Buffer; Pid = pvm_mytid(); Buffer « "Результат." « Pid « ends; Buffer » FileName; *1 ofstream Fout(FileName.data()); BlackBoard->suggestionsForMajor(Courses); 28 29 30 Для всех CORBA-примеров этой книги мы использовали реализацию Mico 2.3.3 в среде Linux « Mko 2.3.7 в ОС Solans 8.
486 Глава 13. Реализация технологии "классной доски"... 33 Fout.closeO ; 34 pvm_exit() ; 35 return(0); 36 } 37 В строке 11 (см. листинг 13.7) инициализируется ORB-брокер. При выполнении строки 12 осуществляется связывание имени объекта blackboard с портом 12458 и возвращается ссылка на CORBA-объект в переменной Obj. Строку 16 можно расценивать как разновидность операции приведения типа, чтобы переменная BlackBoard ссылалась на объект "правильного размера". После того как источник знаний реализовал объект BlackBoard, он может вызывать любой метод, объявленный в интерфейсе black_board, код которого приведен в листинге 13.1. Обратите внимание на создание в строке 13 объекта Courses. Вспомните, что тип courses изначально был определен как CORBA-тип sequence. Здесь источник знаний использует класс courses, созданный во время IDL-компиляции. Добавление элементов в этот класс можно представить как добавление элементов в любой массив. При выполнении строк 24 и 25 в объект Courses добавляются два элемента, а в строке 32 содержится вызов метода, которому в качестве параметра передается объект Courses: BlackBoard->suggestionsForMajor(Courses) При выполнении этого вызова информация о курсах обучения записывается на "классную доску". Аналогично следующие методы courses currentDegreePlan(); courses suggestedSchedule(); можно использовать для считывания информации с "классной доски". Поэтому для общения с "классной доской" источнику знаний достаточно иметь ссылку на объект Black__board. Объект Black_board может располагаться в любом месте сети intranet или Internet. Найти реальное местоположение удаленного объекта — забота исключительно ORB-боркера. (Процесс отыскания и активизации CORBA-объектов рассматривается в главе 8.) Поскольку объект Black_board имеет идентификационные номера PVM-задач, он может управлять этими задачами и обмениваться сообщениями (отправлять и получать их) непосредственно с источниками знаний. Аналогично источники знаний могут напрямую взаимодействовать друг с другом, используя традиционный обмен PVM-сообщениями. Важно отметить следующее: после того как окажется, что не существует больше никаких системных PVM-вызовов, деструктор объекта Black_board должен вызвать метод pvm_exit(), а каждый источник знаний — метод pvm_exit (). Тем самым из PVM-среды будут удалены ненужные больше объекты, но обработка данных, не связанная с этими объектами, может продолжаться. 13.5.3.3. Активизация источников знаний с помощью POSIX-функции spawn () Реализация источников знаний в рамках PVM-задач особенно полезна в ситуации, если задачи должны выполняться на разных компьютерах. Каждый источник знании в этом случае может воспользоваться преимуществами любого специализированного ресурса, которым может быть оснащен конкретный компьютер. К таким ресурсам можно отнести быстродействующий процессор, базу данных, специальное периферийное оборудование и наличие нескольких процессоров. PVM-задачи можно также использовать
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 487 на одном компьютере с несколькими процессорами. Но поскольку взаимодействие с нашей "классной доской" легко реализовать путем подключения к порту, для реализации источников знаний, не мудрствуя лукаво, мы можем также использовать традиционные UNIX/Linux-процессы. Если источники знаний создаются в стандартных UNIX/Linux-процессах, а компьютер содержит несколько процессоров, то источники знаний могут выполняться параллельно на этих процессорах. Но если источников знаний больше, чем процессоров, возникает необходимость в многозадачности. На рис. 13.6 показаны два простых архитектурных варианта, которые можно использовать с CORBA-ориентированной "классной доской" и UNIX/Linux-процессами. ВАРИАНТ 1 "Классная доска" и источники знаний (ИЗ) находятся на одном компьютере. Все ИЗ реализованы в различных процессах. КОМПЬЮТЕР А > г > « CORBA-объект » "Классная доска" \ДРЕСНОЕ ПРОСТРАНСТВО 1 « процесс » И31 \ДРЕСНОЕ 1Р0СТРАНСТВС « процесс » И33 )3 А П А П ЦРЕСНОЕ РОСТРАНСТВО 2 « процесс » И32 ДРЕСНОЕ РОСТРАНСТВО « процесс » И34 4 Источники знаний порождаются Функциями posix_spawn() или fork-exec(). ВАРИАНТ 2 "Классная доска" находится на одном компьютере, а все источники знаний (ИЗ) на другом. Все ИЗ реализованы в различных процессах. КОМПЬЮТЕР С « CORBA-объект » | "Классная доска" | КОМПЬЮТЕР В 1 АДРЕСНОЕ ПРОСТРАНСТВО 1 I « процесс » H3i АДРЕСНОЕ ПРОСТРАНСТВО « процесс » И33 )3 АДРЕСНОЕ ПРОСТРАНСТВО 2 « процесс » И32 АДРЕСНОЕ ПРОСТРАНСТВО 4 « процесс » И34 Источники знаний порождаются функциями posix__spawn() или fork-exec(). Рис. 13.6. Два архитектурных варианта использования CORBA-ориентированной "классной Доски" и UNIX/Linux-процессов В варианте 1 CORBA-объект и источники знаний размещаются на одном компьютере, и каждый источник знаний имеет собственное адресное пространство. Другими словами, каждый источник знаний порожден с помощью функции posix_spawn() или семейств а функций fork-exec. В варианте 2 CORBA-объект размещается на од- ом компьютере, а все источники знаний — на другом, но в различных адресных про- странствах. В обоих вариантах CORBA-объект действует как разновидность общей амяти для источников знаний, поскольку все они получают доступ к нему и могут облениваться информацией через "классную доску". При этом важно помнить о сущест- °вании основного преимущества CORBA-объекта — он имеет более высокую органи- аЦию, чем простой блок памяти. "Классная доска" — это объект, который может
488 Глава 13. Реализация технологии "классной доски"... состоять из структур данных любого типа, объектов и даже других "классных досок" Такой вид организации не может быть реализован простым использованием базовых функций доступа к общей памяти. Поэтому CORBA-реализация обеспечивает идеальный способ разделения сложных объектов между процессами. В подразделе 13.5.3.1 описано создание PVM-задач, которые реализуют источники знаний. Здесь мы изменяем конструктор, включая в него вызовы функции posix_spawn () (с той же целью можно использовать алгоритм f or_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 <spawn.h> blackboard::blackboard(void) { //... 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_soureel",NULL}; posix_spawn(&Pid, ,,knowledge_sourcel", &N, &M, argv,NULL) ; //.. . } В листинге 13.8 инициализируются атрибуты и действия, необходимые для порождения задач, после чего с помощью функции posix_spawn() создается отдельный процесс, который предназначен для выполнения источника знании knowledge_sourcel. После создания этого процесса "классная доска" получает к нему доступ через его идентификационный номер, сохраняемый в параметре Pid. Кроме "классной доски", используемой в качестве средства связи, возможно и стандартное межпроцессное взаимодействие (IPC), если "классная доска" расположена на одном компьютере с источниками знаний. "Классная доска"— самый простой способ взаимодействия между источниками знаний, хотя в конфигурации размещения "классной доски" на отдельном компьютере можно использовать с этой целью сокеты. В этом случае управление, осуществляемое "классной доской" над источниками знаний, будет более жестким и обусловленным в любой момент времени содержимым "классной доски", а не сообщениями, передаваемыми непосредственно источникам знаний. Прямую пересылку сообщений легче реализовать при использовании
13.5. Реализация модели "классной доски" с помощью CORBA-объектов 489 "классной доски" в сочетании с PVM-задачами. В этом случае источники знаний сами настраивают себя на основе содержимого "классной доски". Но "классная доска" все же имеет определенный "рычаг" управления источниками знаний, поскольку ей "известны" идентификационные номера всех процессов, содержащих источники знаний. Как модель MPMD (MIMD), так и модель SPMD (SIMD), также поддерживаются использованием функции posix_spawn(). В листинге 13.9 представлен класс, который можно использовать в качестве объекта-функции при выполнении алгоритма f or_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_f ile_actions_init (&N) ; Command.append("/tmp/"); Command.append(X); char *const argv[] = {const_cast<char*>(Command.data()) ,NULL}; posix_spawn(&Pid,Command.data(), &N,&M,argv/NULL); Command.erase(Command.begin(),Command.end()); //... } Мы инкапсулируем атрибуты, необходимые для функции posix_spawn(), в классе child__process. Инкапсуляция всех данных, требуемых для вызова этой функции в классе, упрощает ее использование и обеспечивает естественный интерфейс с атрибутами процесса, который создается с ее помощью. Обратите внимание на то, что в классе child_process мы определили функцию operator () (см. листинг 13.9). Это означает, что класс child_process можно использовать в качестве функционального объекта при выполнении алгоритма f or_each (). По мере того как "классная доска" решает, какие источники знаний необходимо активизировать для решения задачи, она сохраняет их имена в контейнере Solve. Позже при выполнении конструктора "классной доски нужные источники знаний активизируются с помощью алгоритма f or_each (). '/ Конструктор. ^bild_process Task; f°r__each(Solve.begin() ^olve.endO ,Task) ; При выполнении этого конструктора для каждого элемента контейнера Solve вызы- вается метод operator (), код которого приведен в листинге 13.9. После активизации
490 Глава 13. Реализация технологии "классной доски"... источники знаний получают доступ к ссылке на объект "классной доски" и могут приступать к решению свой части задачи. И хотя источники знаний здесь не являются PVM-задачами, они связываются с "классной доской" таким же способом (см. подраздел 13.5.3.2) и так же выполняют свою работу. Дело в том, что межпроцессное взаимодействие между стандартными UNIX/Linux-процессами отличается от межпроцессного взаимодействия, которое возможно с использованием PVM-среды. Кроме того, PVM-задачи могут располагаться на разных компьютерах, в то время как процессы, созданные с помощью функции posix_spawn(), могут существовать только на одном и том же компьютере. Если процессы, созданные функцией posix_spawn() (либо семейством функций fork-exec), необходимо использовать в сочетании с моделью SIMD, то в дополнение к объекту "классной доски" для назначения источникам знаний конкретных областей задачи, которые они должны решать, можно использовать параметры argc и argv. В случае, когда "классная доска" находится на одном компьютере с источниками знаний, и она активизирует источники знаний в своем конструкторе, то формально "классная доска" является для них родителем, а потомки наследуют от родителя переменные среды. Переменные среды "классной доски" можно использовать в качестве еще одного способа передачи информации источникам знаний. Этими переменными среды можно легко управлять, используя следующие функции. #include <stdlib.h> //. .. setenv(); unsetenv(); putenv(); Если источники знаний реализуются в процессах, которые созданы с помощью функции posix_spawn () (или fork-exec), то их программирование не выходит за рамки обычного CORBA-программирования с доступом ко всех средствам, предлагаемым CORBA-протоколом. 13.6. Реализация модели "классной доски" с помощью глобальных объектов Выбор CORBA-ориентированной "классной доски" вполне естествен в условиях, когда источники знаний должны быть реализованы в среде intranet или Internet, или когда в целях соблюдения модульного принципа организации, инкапсуляции и так далее каждый источник знаний реализуется в отдельном процессе. Однако в распределении "классной доски" необходимость возникает не всегда. Если источники знаний можно реализовать в рамках одного процесса или на одном компьютере, то лучше всего в этом случае организовать несколько потоков, поскольку при таком варианте быстродействие выше, расходы системных ресурсов меньше, а сама работа (настройка) — проще. Взаимодействие между потоками легче организовать, поскольку потоки разделяют одно адресное пространство и могут использовать глобальные переменные. Ведь тогда "классную доску" можно реализовать как глобальный объект, доступный всем потокам в процессе. При реализации источников знаний в виде потоков в рамках одной программы отпадает необходимость в межпроцессном взаимодействии, использовании сокетов или какого-либо другого типа сетевой связи. Кроме того, в этом случае оказывается ненужным дополнительный
13.6. Реализация модели "классной доски" с помощью глобальных объектов 491 уровень CORBA-протокола, поскольку можно обойтись разработкой обычных С++- классов. Если многопоточная программа рассчитана на использование одного компьютера с несколькими процессорами, то потоки могут выполняться параллельно на доступных процессорах. В SMP- и МРР-системах потоковая конфигурация "классной доски" весьма привлекательна. В общем случае при использовании потоков достигается самая высокая производительность. Потоки часто называют облегченными процессами, поскольку они не требуют таких же расходов системных ресурсов, как традиционные UNIX/Linux-процессы. В библиотеке POSIX threads (Pthreads) предусмотрено практически все, что нужно для создания источников знаний и управления ими. На рис. 13.7.1-13.7.3 представлены три базовые конфигурации распределения процессов для "классной доски" и источников знаний. РАСПРЕДЕЛЕНИЕ ПРОЦЕССОВ 1 Источники знаний реализованы как потоки одного процесса, которые разделяют "классную доску" как глобальный объект. ЕДИНСТВЕННЫЙ ПРОЦЕСС Адресное пространство |« глобальный объект » blackboard поток 1 потока ..—_—__— —_ ~__ «поток» j | << поток» | I ИЗ, | j И32 | ; потока' |< >''>« ПОТОК » Рис. 13.7.1. Базовая конфигурация распределения процессов для "классной доски" и источников знаний (вариант 1) Поскольку "классная доска" реализована в многопоточной среде, то для синхронизации доступа к "классной доске" можно использовать Pthread-мьютексы и переменные условий, которые необходимо инкапсулировать в интерфейсных классах, как описано в главе 11. Кроме того, для координации и синхронизации работы, выполняемой источниками знаний, можно использовать функции pthread_cond_signal () и pt^read_cond_broadcast (). Поскольку "классная доска" сама создает потоки, ей будет нетрудно получить доступ к идентификационным номерам всех источников знаний. то означает, что "классная доска" может при необходимости аннулировать поток, используя функцию pthread_cancel (). Кроме того, "классная доска" способна синхро- изировать выполнение источников знаний с помощью функции pthread_j oin (). омимо уже перечисленных достоинств многопоточной реализации (высокое быстродействие и простота использования потоков и глобального объекта "классной дос- )» существует также проблема обработки ошибок и исключительных ситуаций.
492 Глава 13. Реализация технологии "классной доски"... В общем случае эта проблема решается проще в рамках одного процесса и одного компьютера, чем при использовании нескольких процессов и нескольких компьютеров. На рис. 13.8 показаны уровни сложности, связанные с обработкой ошибок и исключительных ситуаций при использовании различных конфигураций. РАСПРЕДЕЛЕНИЕ ПРОЦЕССОВ 2 Источники знаний, реализованные как процессы, и CORBA-объект "классной доски" выполняются на одном компьютере, а для связи друг с другом используют механизм межпроцессного взаимодействия. ОДИН КОМПЬЮТЕР С НЕСКОЛЬКИМИ ПРОЦЕССАМИ « CORBA-объект » blackboard ПР | Механизм межпроцессного взаимодействия | ОЦЕСС1 « процесс » И31 ПРОЦЕСС 2 « процесс » из2 ПРОЦЕСС 3 « процесс » И33 s Рис. 13.7.2. Базовая конфигурация распределения процессов для "классной доски" и источников знаний (вариант 2) РАСПРЕДЕЛЕНИЕ ПРОЦЕССОВ 3 Источники знаний реализованы как процессы, выполняющиеся на разных компьютерах, и используют для связи с CORBA-объектом "классной доски" НОР-протокол. НЕСКОЛЬКО ПРОЦЕССОВ НА НЕСКОЛЬКИХ КОМПЬЮТЕРАХ КОМПЬЮТЕР 1 КОМПЬЮТЕР 1 « CORBA-объект » blackboard i НОР (протокол сетевой связи) КОМПЬЮТЕР 2 г КОМПЬЮТЕР 3 « процесс » И31 'процесс - ИЗ о I I ? { '• процесс * ИЗ, Рис. 13.7.3. Базовая конфигурация распределения процессов для "классной доски" и источников знаний (вариант 3)
13.6. Реализация модели "классной доски" с помощью глобальных объектов 493 Обработка ошибок и исключительных ситуаций, возникающих при выполнении нескольких процессов на различных компьютерах, расположенных в различных сетях (при использование разных протоколов). Обработка ошибок и исключительных ситуаций, возникающих при выполнении нескольких процессов на различных компьютерах, но расположенных 8 одной сети. Обработка ошибок и исключительных ситуации возникающих при вь!полнени(4 нескольких процессов на одном компьютере. Обработка ошибсйс-й' йошющтелйшШт^ацШ^ 4ознЙ1#юШШ«р^ШоайШйш^ функций щноШ про]цёбс&(в однод;адр^^ <^;^да,^,.,,^.<., ,■,,,<■■ Обработка, ошибок,. возникающих, а одном • гютош. ;в различных гшшаС Рис. 13.8. Уровни сложности при обработке ошибок и исключений Если источники знаний реализованы в отдельных потоках одного и того же процесса, то обработка возможных ошибок или исключительных ситуаций в этом случае относится к уровню сложности 2. Эту степень сложности необходимо учитывать еще на этапах проектирования и разработки программы, особенно в случае, если она требует параллельного программирования. Простейшее архитектурное решение, использующее модель "классной доски", состоит в реализации "классной доски" в виде глобального объекта, а источников знаний — в виде потоков. Рассмотрим фрагмент объявления класса blackboard. // Листинг 13.10. Фрагмент объявления класса blackboard, разработанного для многопоточной среды // class blackboard{ Protected: set<long> set<long> set<long> set<long> set<long> set<long> mutex Mutex[10] //.. . Public: blackboard (void) ; -blackboard (void) SuggestionForMajor; SuggestionForMinor; SuggestionForGeneral; SuggestionForElective; Schedule; DegreePlan;
494 Глава 13. Реализация технологии "классной доски"... void suggestionsForMajor(set<long> &X); void suggestionsForMinor(set<long> &X); void suggestionsForGeneral(set<long> &X) ; void suggestionsForElectives(set<long> &X); set<long> currentDegreePlan(void); set<long> suggestedSchedule(void); }; Класс blackboard предназначен для реализации в качестве глобального объекта, к которому смогут получать доступ все потоки в программе. Обратите внимание на то, что класс blackboard в листинге 13.10 включает массив мьютексов. Эти мьютексы используются для защиты критических разделов "классной доски". При реализации источников знаний практически нет необходимости беспокоиться о синхронизации доступа к критическим разделам, поскольку код синхронизации инкапсулирован в классе blackboard. 13.7. Активизация источников знаний с помощью потоков В этом разделе рассматривается реализация источников знаний в отдельных потоках. Потоки создаются здесь при выполнении конструктора класса "классной доски" (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() ■ Этот вызов заставляет конструктор ожидать завершения работы всех четырех потоков. Эти потоки могут активизироваться и с помощью других функций-членов класса
13.8. Резюме 495 blackboard. Но те действия, которые выполняют источники знаний "в рамках" конструктора, представляют своего рода предварительную инициализацию "классной доски", поэтому весьма уместно не продолжать работу по созданию объекта "классной доски" Д° тех поР' пока эти потоки не доведут до конца свою работу. Такой подход к созданию потоков в конструкторе заставляет задуматься об обработке ошибок и исключительных ситуаций. Что произойдет, если по какой-то причине при выполнении потоков случится сбой? Поскольку конструкторы не возвращают никаких значений, то здесь просто необходимо позаботиться об обработке исключительных ситуаций. Каждый поток связывается со "своей" функцией. void *suggestionForMajor(void *X); void *suggestionForMinor(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, вполне естествен для систем, реализующих модель "классной доски", поскольку "классная доска" часто делится на разделы, относящиеся к определенным частям задачи или подзадачи. Обычно одной проблемной области соответствует один источник знаний, поэтому параллельный доступ к этим разделам вполне уместен. 13.8. Резюме Модель "классной доски" поддерживает параллелизм, который присутствует как в структуре "классной доски", так и в отношениях между "классной доской" и источниками знаний, а также между самими источниками знаний. Модель "классной доски" — это модель решения некоторой задачи. Общая задача делится на части, соответст- вующие конкретным областям знаний. Каждой области назначается источник знаний, или решатель задач. Источники знаний обычно отличаются самодостаточностью (.автономностью) и не требуют интенсивного общения с другими источниками зна- ии. Необходимое взаимодействие осуществляется через "классную доску". Следова- ельно, источники знаний позволяют организовать обработку данных в рамках программы по модульному принципу. Такие своеобразные модули могут работать отдельно параллельно, не требуя сложной синхронизации. "Классную доску" можно реали- °вать в виде CORBA-объектов. В этом случае источники знаний могут быть распре-
496 Глава 13. Реализация технологии "классной доски"... "классная деекАп Источник знаний 'Общие требования' запись Источник знаний "Факультативы" запись чтение чтение чтение Частные' решения Список курсов Основные курсы Второстепенные ¥$рСЫ Ц Общие требования Факультативы : Дипломный >план | Выбранные курсы запись Источник знаний "Основные курсы" запись I Источник знаний "Второстепенные курсы" чтение чтение чтение чтение Рис. 13.9. Четыре источника знаний могут параллельно считывать информацию из соответствующих разделов "классной доски" и записывать ее туда делены в сетях intranet или Internet. "Классная доска" действует как разновидность общей распределенной памяти для задач, выполняемых в среде PVM-типа. В модель "классной доски" легко вписываются модели MPMD (MIMD) и SPMD (SIMD). Концепция "классной доски" побуждает разработчика разделить работу, которую должна выполнить программа, на области знаний. После проведения декомпозиции работ "классная доска" должна содержать модели ПО предметной области и пространства решений. Эти модели ПО позволяют проектировщику и разработчику вскрыть параллелизм, который необходимо реализовать в программе. После классической модели распределенного программирования "клиент-сервер" модель "классной доски" является одной из мощных моделей, доступных как для распределенного, так и для параллельного программирования. Источники знаний, или решатели задач, в модели "классной доски" зачастую реализуются как агенты.
Приложение Зто приложение представляет собой краткий справочник UML-диаграмм, используемых в этой книге. Универсальный язык моделирования (Unified Modeling Language— UML) предлагает графические обозначения, используемые для проектирования, визуализации, моделирования и документирования артефактов системы программного обеспечения. Этот язык является стандартом "де-факто" для моделирования объектно-ориентированных систем. В нем используются символы и обозначения для представления артефактов системы ПО с различных точек зрения. И хотя в книге используются и другие обозначения, это приложение позволит читателю быстро ознакомиться с основными элементами и символами языка UML, которые могут понадобиться ему при составлении Документации на разрабатываемые системы ПО. А.1. Диаграммы классов и объектов Диаграммы классов и объектов — самые распространенные диаграммы, используемые в моделировании объектно-ориентированных систем. Диаграммы классов Используются для представления классов любого типа, в том числе шаблонных интерфейсных классов. Эти диаграммы могут содержать члены класса (атрибуты °перации). В диаграммах классов и объектов отображаются типы данных, значе- я переменных и типы значений, возвращаемых функциями. В диаграммах объек- в можно отобразить имя объекта. В диаграммах обоих типов можно указать коли- ТВо классов или объектов, используемых в системе, а также отношения между Яссами и объектами.
498 Приложение А ПРЕДСТАВЛЕНИЕ КЛАССА Имя класса Имя класса атрибут: тип = начальное значение операция (список аргументов): тип возвращаемого значения Имя класса Атрибуты - private # protected + public Операции - private # protected + public Обязанности класса Имя активного класса Имя активного класса Атрибут: тип = начальное значение Операция (список аргументов): тип возвращаемого значения ПРЕДСТАВЛЕНИЕ ОБЪЕКТА Имя объекта: имя класса Имя активного объекта: имя класса Рис.А.1. Различные способы представления класса или объекта. Для классов можно отобразить атрибуты, операции и их область видимости. При обозначении активных классов или объектов используется более жирная линия ПРЕДСТАВЛЕНИЕ МНОЖЕСТВА ЭКЗЕМПЛЯРОВ Имя класса Несколько классов Имя объекта: имя класса Несколько объектов Имя класса 1-7 Атрибуты - private # protected + public Операции - private # protected + public Обязанности класса 1 ..7 Обозначение множественности Рис. А.2. Различные способы представления множества классов или объектов. Множество экземпляров можно отобразить графически или с помощью обозначения множественности
A.2. Диаграммы взаимодействия 499 а) несвязанный класс Шаблонный класс Шаблонный класс б) связанный класс Шаблонный класс параметризованные типы > _ параметризованные « I типы | Шаблонный класс J параметризованные | ТИПЫ > | | «связать» (реальные параметры) Шаблонный класс Шаблонный объект Рис. А.З. Способы представления связанных и несвязанных шаблонов или параметризованных классов А.2. Диаграммы взаимодействия Диаграммы взаимодействия предназначены для отображения взаимодействия между объектами. Такие диаграммы состоят из множества объектов, отношений и сообщений, которыми обмениваются объекты. Диаграммы взаимодействия включают диаграммы сотрудничества, последовательностей и видов деятельности. А.2.1. Диаграммы сотрудничества Диаграммы сотрудничества используются для отображения объектов, работающих вместе с целью выполнения некоторой общей работы. Под сотрудничеством в системе понимается временная кооперация множества объектов. Диаграммы этого типа могут отображать организацию или структуру сотрудничества. Это подразумевает отображение всех объектов данного множества, связей между ними, а также отправляемых и получаемых ими сообщений. А.2.2. Диаграммы последовательностей Диаграммы последовательностей предназначены для отображения временного упорядочения сообщений, отправляемых и получаемых объектами в системе.
500 Приложение А ПРЕДСТАВЛЕНИЕ ИНТЕРФЕЙСНОГО КЛАССА О А. Использование символа "леденец на палочке" Интерфейсный класс «интерфейс» Класс Атрибуты Операции Б. Использование стереотипа «интерфейс» «интерфейс» Класс Атрибуты Операции В. Реализация интерфейсного класса и Реализованный класс Реализованный класс Атрибуты Операции Г. Реализация класса с использованием символа "леденец на палочке" о Интерфейсный класс Рис. А.4. Способы представления интерфейсного класса. Интерфейсный класс можно отобразить с помощью символа "леденец на палочке" или стереотипа «интерфейс». Можно также отобразить отношения между интерфейсным классом и классом реализации
A.2. Диаграммы взаимодействия 501 ЕДИНИЧНОЕ НАСЛЕДОВАНИЕ МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ Базовый класс л Базовый класс Базовый класс л «наследует» д «наследует» Производный класс Производный класс Базовый класс Базовый класс Объединенный стиль д Производный класс Производный класс д Производный класс Разъединенный стиль Производный класс Рис. А.5. Способы представления единичного и множественного наследования. Существует два стиля, которые можно использовать при участии нескольких классов в отношении: объединенный и разъединенный. При использовании объединенного стиля несколько классов привязываются к единому символу наследования, который указывает на целевой класс. При использовании разъединенного стиля каждый класс имеет собственный символ наследования А.2.3. Диаграммы видов деятельности Диаграммы видов деятельности отображают передачу управления от одного вида Деятельности другому. Под деятельностью подразумеваются действия, выполняемые объектами. Действия включают обработку операций ввода-вывода, создание или разрушение объектов либо выполнение вычислений. Диаграммы видов деятельности подобны блок-схемам.
502 Приложение А Агрегирование Включение Класс А К Класс В Зависимость: класс В зависит от класса А. Целевой класс Функциональное имя Исходный класс Способность к переходу Отношения множественности Класс В Класс А Класс А всегда связан с одним классом В. Класс В Класс А Класс А всегда связан с некоторым количеством классов В, которое больше или равно нулю. Класс В 0..1 Класс А Класс А всегда связан с некоторым количеством классов В, которое может быть равно нулю или единице. Класс В Класс А Класс А всегда связан с некоторым количеством классов В, которое может лежать в диапазоне от m до п. Рис. А.6. Примеры различных отношений, которые можно отобразить на диаграммах классов. Для отображения количества экземпляров, участвующих в отношениях, можно использовать обозначение множественности
A.2. Диаграммы взаимодействия 503 ОРГАНИЗАЦИЯ ВАРИАНТОВ СОТРУДНИЧЕСТВА •Сотрудничество^ .Сотрудничество' >, Сотрудничество' А В С СТРуЮуРНЫЕАСПЕ1аЫ_СрТРУГДНИЧ_ЕСТВА Класс А I >| Класс В к': 1 Класс Е КлассС I КлассD -о IcIassF Рис. А.7. Диаграмма сотрудничества, отображающая организацию сотрудничества в системе и структурные отношения объектов в этом сотрудничестве :объект А Вызов собственного метода « создать » (Асинхронная связь) юбъект В Сообщение ЕГ (Синхронная связь) | Возвращение значения j? Саморазрушение объекта Рис. А.8. Диаграмма последовательностей используется для отображения временного упорядочения сообщений, передаваемых между объектами. Активные объекты размещаются в верхней части диаграммы (по оси х). Сообщения, передаваемые между объектами, располагаются по оси у. На диаграмме можно отображать синхронные и асинхронные сообщения. Временное упорядочение сообщений демонстрируется путем чтения сообщений сверху вниз вдоль оси у
504 Приложение А "ПЛАВАТЕЛЬНАЯ ДОРОЖКА Г Объект А О НАЧАЛЬНОЕ СОСТОЯНИЕ ПРИНЯТИЕ РЕШЕНИЯ. [Условие истинно] РАЗВЕТВЛЕНИЕ. [Условие ложно] Ввод данных 1 Ввод данных 2 . СОЕДИНЕНИЕ БЛОК ОБЪЕДИНЕНИЯ —О Этап обработки 3 КОНЕЧНОЕ (@) СОСТОЯНИЕ ^^ "ПЛАВАТЕЛЬНАЯ ДОРОЖКА 2" Объект В о Этап "Л обработки 1 БЕЗУСЛОВНЫЙ ПЕРЕХОД ПОЛОСА СИНХРОНИЗАЦИИ Этап обработки 2 Рис. А.9. Диаграммы видов деятельности отображают действия объектов с точки зрения передачи управления от одного объекта другому. Диаграмма этого типа с помощью полосы синхронизации позволяет отобразить разветвление программы на несколько потоков управления (параллельность) и их слияние. Чтобы было понятно, какой объект выполняет соответствующее действие, здесь используется принцип "плавательных дорожек". Эти "плавательные дорожки" могут пересекаться переходами. "Плавательные дорожки" также могут пересекаться полосами синхронизации, что означает, что несколько потоков управления, распределенные по различным объектам, выполняют действия параллельно
А.З. Диаграммы состояний 505 А.З. Диаграммы состояний Диаграмма состояний используется для отображения последовательности изменения состояния объектов. Состояние — это условие, при котором объект занимает ту или иную позицию на своей "линии жизни". Объект за время своего существования может многократно изменять свое состояние. Объекты переходят в новое состояние, если создаются определенные условия, выполняется некоторое действие или происходит соответствующее событие. Начальное состояние Переход Состояние А Состояние Состояние В -> Q Конечное состояние РАСШИРЕННЫЕ СОСТОЯНИЯ И ПЕРЕХОДЫ Действие входа ■ Деятельность - Внутренний переход - Действие выхода ' Название состояния -> entry / действие или функция -> do / деятельность -> name / действие или функция "^ exit / действие или функция СЛОЖНОЕ СОСТОЯНИЕ Параллельные состояния Состояние В Подсостояние В2 Состояние с Рис. А. 10. Диаграммы состояний отображают состояния объектов и их переходы из одного состояния в другое за время их существования. Диаграмма этого вида содержит начальное и конечное состояние объекта. Состояние может включать несколько стадий (частей). Оно может также быть представлено совокупностью других состояний или даже других диаграмм состояний. Подсостояния, которые существуют параллельно в рамках одного объекта, называются параллельными подсостояниями
506 Приложение А А.4. Диаграммы пакетов Диаграммы пакетов используются для организации элементов системы по группа! Пакет ОТНОШЕНИЯ МЕЖДУ ( 1 1 :mctei\ I I « система » Система ЛАМИ « система » Система 2 4 « подсисте | Подси Подсистема А1 1 1 1 ' ■ ► I « подсистема » Подсистема ма » стема А ч « система » Система 1 Система 2 включает подсистему А, подсистема А содержит подсистему А1 I Система 1 зависит от подсистемы А Рис. А.11. Диаграммы пакетов могут служить для отображения организации элементов системы. При этом можно использовать стереотипы «система» или «подсистема». Если пакет содержит другие элементы, во вкладке (расположенной слева) можно указать имя пакета
Приложение Л Имя posix_spawn, posix__spawnp— функции порождения процессов (ADVANCED REALTIME) Синопсис SPN #include <spawn.h> int posix_spawn ( pid_t *restrict pid, const char *restrict path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr__t *restrict attrp, char *const argv[restrict], char *const envp[restrict]); int posix_spawnp ( pid_t *restrict pid, const char *restrict file, const posix__spawn_file_actions_t *file_actions, const posix_spawnattr_t ^restrict attrp, char *const argv[restrict], char * const envp[restrict]);
508 Приложение Б Описание функции posix__spawn () и posix_spawnp () предназначены для создания нового (сыновнего) процесса из заданного образа процесса. Новый образ процесса создается на основе обычного выполняемого файла, именуемого файлом образа нового процесса. Если в качестве результата этого вызова выполняется С-программа, то она должна быть представлена как функция языка С следующим образом: int main (int argc, char *argv[]); Здесь argc— количество аргументов, a argv— массив символьных указателей на аргументы функции. Кроме того, следующая переменная extern char **environ; должна быть инициализирована как указатель на массив символьных указателей на строки описания конфигурации среды. Аргумент argv представляет собой массив символьных указателей на строки с завершающим нулем. Последний член этого массива (он не учитывается аргументом argc) должен быть нулевым указателем. Эти строки составляют список аргументов, доступных для образа нового процесса. Значение элемента argv[0] должно указывать на имя файла, который связан с образом процесса, запускаемого функцией posix_spawn() miHposix_spawnp(). Аргумент envp представляет собой массив символьных указателей на строки с завершающим нулем. Эти строки составляют среду для образа нового процесса. Массив среды завершается нулевым указателем. Количество байтов, допустимых для обобщенного аргумента сыновнего процесса и списков строк описания конфигурации среды, составляет {ARG_MAX}. В системной документации конкретной реализации (см. том Base Definitions стандарта IEEE Std 1003.1- 2001, Chapter 2, Conformance) должно быть указано, включаются ли в это значение такие служебные данные, как символы конца строки, указатели или байты выравнивания. Аргумент path, передаваемый функции posix__spawn(), содержит путевое имя, которое идентифицирует файл образа нового процесса. Параметр file, передаваемый функции posix_spawnp (), используется для формирования путевого имени, которое идентифицирует файл образа нового процесса. Если параметр file содержит символ "косая черта", то параметр file следует рассматривать как путевое имя файла образа нового процесса. В противном случае префикс пути для этого файла должен быть получен путем поиска в каталогах, указанных с помощью переменной среды PATH (см. том Base Definitions стандарта IEEE Std 1003.1-2001, Chapter 8, Environment Variables). Если эта переменная среды не определена, результаты поиска определяются конкретной реализацией. Если параметр file_actions является нулевым указателем, то файловые дескрипторы, открытые в вызывающем процессе, останутся открытыми и в сыновнем, за исключением тех из них, для которых установлен флаг "закрытия после выполнения FD_CLOEXEC (см. описание функции fcntlO). Для оставшихся открытыми файловых дескрипторов все атрибуты соответствующих описаний открытых файлов, включая блокировки файлов (см. описание функции f cntl ()), останутся без изменений. Если параметр file_actions не содержит значение NULL, то файловые ДеС^~ рипторы, открытые в сыновнем процессе, должны соответствовать открытым файловым дескрипторам вызывающего процесса, но с учетом модификации, проведенной в соответствии с содержимым объекта действий, адресуемого параметром
Приложение Б 509 file_actions, и флагом FD__CLOEXEC каждого из оставшихся открытыми (после выполнения действий над файлами) файловых дескрипторов. Порядок выполнения действий над файлами должен быть таким. 1. Множество открытых файловых дескрипторов для сыновнего процесса должно сначала совпадать со множеством открытых файловых дескрипторов для вызывающего процесса. Все атрибуты соответствующих описаний открытых файлов, включая блокировки файлов (см. описание функции f cntl ()), останутся без изменений. 2. Маска сигнала, стандартные действия сигналов, а также идентификационные номера эффективного пользователя и группы для сыновнего процесса должны измениться в соответствии со значениями, заданными в объекте атрибутов, адресуемом параметром a t trp. 3. Действия над файлами, заданные объектом действий для порождаемого процесса, должны быть выполнены в порядке их добавления в этот объект. 4. Любой файловый дескриптор, у которого установлен флаг FD_CLOEXEC (см. описание функции f cntl ()), должен быть закрыт. Тип объекта атрибутов posix__spawnattr_t определяется в заголовке <spawn. h>. По меньшей мере он должен содержать атрибуты, определенные ниже. Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX_SPAWN_SETPGROUP, а атрибут spawn -pgroup того же объекта не равен нулю, то группа сыновних процессов должна быть задана этим (ненулевым) атрибутом объекта. В качестве специального случая, если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX_SPAWN_SETPGROUP, а атрибут spawn-pgroup того же объекта равен нулю, то порождаемый сыновний процесс будет входить в новую группу процессов, идентификатор (ID) которой будет равен значению ID его процесса. Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, флаг POSIX__SPAWN_SETPGROUP не установлен, то новый сыновний процесс наследует группу родительского процесса. PS Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX_SPAWN_SETSCHEDPARAM, но флаг POSIX_SPAWN_SETSCHEDULER не установлен, то образ нового процесса будет изначально обладать стратегией планирования вызывающего процесса с параметрами планирования, заданными в атрибуте spawn-schedparamобъекта, адресуемого параметром attrp. Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX_SPAWN_SETSCHEDULER (независимо от установки флага POSIX_SPAWN_SETSCHEDPARAM), то образ нового процесса будет изначально обладать стратегией планирования, заданной атрибутом spawn-schedpoliсу объекта, адресуемого параметром attrp, и параметрами планирования, заданными в атрибуте spawn-schedparamтого же объекта. Флаг POSIX_SPAWN_RESETIDS в атрибуте spawn-flags объекта, адресуемого параметром a t trp, обусловливает значение ID эффективного пользователя сыновнего процесса. Если этот флаг не установлен, сыновний процесс наследует ID эффективно- ° пользователя родительского процесса. Если этот флаг установлен, ID эффективно- ° пользователя сыновнего процесса должен быть установлен равным значению ID реального пользователя родительского процесса. В любом случае, если установлен
510 Приложение Б бит режима "set-user-ID" для файла образа нового процесса, ID эффективного пользователя сыновнего процесса примет значение, равное значению ID владельца этого файла до того, как начнет выполняться образ нового процесса. Флаг POSIX__SPAWN_RESETIDS в атрибуте spawn-flags объекта, адресуемого параметром attrp, также обусловливает значение ID эффективной группы сыновнего процесса. Если этот флаг не установлен, сыновний процесс наследует ID эффективной группы родительского процесса. Если этот флаг установлен, ID эффективной группы сыновнего процесса должен быть установлен равным значению ID реальной группы родительского процесса. В любом случае, если установлен бит режима "set-group-ID" для файла образа нового процесса, ID эффективной группы сыновнего процесса примет значение, равное значению ID группы этого файла до того, как начнет выполняться образ нового процесса. Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX__SPAWN_SETSIGMASK, то сыновний процесс изначально будет иметь маску сигнала, заданную в атрибуте spawn-sigmaskобъекта, адресуемого параметром attrp. Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX__SPAWN_SETSIGDEF, то сигналы, заданные в атрибуте spawn- sigdefaul t того же объекта, будут установлены равными их действиям по умолчанию в сыновнем процессе. Сигналы, установленные равными действиям по умолчанию в родительском процессе, должны быть установлены равными действиям по умолчанию в сыновнем процессе. Сигналы, установленные для перехвата вызывающим процессом, должны быть установлены равными действиям по умолчанию в сыновнем процессе. За исключением сигнала SIGCHLD, сигналы, которые должны игнорироваться образом вызывающего процесса, должны игнорироваться сыновним процессом, если не определено иное посредством флага POSIX__SPAWN_SETSIGDEF, установленного в атрибуте spawn-flags объекта, адресуемого параметром attrp, и сигнала SIGCHLD, обозначенного в атрибуте spawn-sigdefaul t того же объекта. Если сигнал SIGCHLD установлен как игнорируемый вызывающим процессом, точно не установлено, должен ли сигнал SIGCHLD игнорироваться сыновним процессом или он будет установлен равным действию по умолчанию в сыновнем процессе, если не определено иное посредством флага POSIX__SPAWN__SETSIGDEF, установленного в атрибуте spawn-flags объекта, адресуемого параметром attrp, и сигнала SIGCHLD, обозначенного в атрибуте spawn__slgdefaul t того же объекта. Если указатель a t trp содержит значение NULL, используются значения по умолчанию. Все атрибуты процесса, на которые не было оказано влияния со стороны атрибутов, установленных в объекте, адресуемом параметром attrp (как было описано выше), или вследствие манипуляций с файловыми дескрипторами, заданных в параметре file_actions, должны присутствовать в образе нового процесса в таком виде, как будто была вызвана функция fork() для создания сыновнего процесса, а затем член семейства функций exec был вызван сыновним процессом для выполнения образа нового процесса. THR Запускаются ли обработчики разветвлений при вызове функций posix_spawn () или posix__spawnp (), определяется конкретной реализацией. Возвращаемые значения При успешном выполнении функция posix_spawn() (и функция posix__spawnp ()) должна возвратить родительскому процессу идентификационный
Приложение Б 511 номер (ID) сыновнего процесса в переменной, адресуемой аргументом pid (если его значение не равно NULL), и нуль в качестве значения, возвращаемого функцией. В противном случае сыновний процесс не создается, значение, сохраненное в переменной, адресуемой аргументом pid (если его значение не равно NULL), не определяется, а в качестве значения, возвращаемого функцией, передается код ошибки, обозначающий ее характер. Если аргумент pid содержит нулевой указатель, значение ID сыновнего процесса инициатору вызова не возвращается. Ошибки Вызовы функций posix_spawn() и posix__spawnp() могут оказаться неудачными, если: [EINVAL] значение, заданное параметром file__actions или параметром attrp, недействительно. Если ошибка возникла после того, как вызывающий процесс успешно вернулся из функции posix_spawn() или posix__spawnp(), то сыновний процесс может завершиться со статусом выхода (exit status), равным значению 127. Если неудачный исход функции posix_spawn () или posix_spawnp () вызван одной из причин, которые бы привели к отказу функции fork () или одной из функций семейства exec, то возвращаемое значение ошибки будет соответствовать описанию для функций fork () и exec соответственно (или, если ошибка возникнет после того, как вызывающий процесс успешно вернется, сыновний процесс завершится со статусом выхода, равным значению 127). Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX_SPAWN_SETPGROUP, а функция posix_spawn() или posix_spawnp() потерпела неудачу при изменении группы сыновнего процесса, то возвращаемое значение ошибки будет соответствовать описанию для функции setpgid () (или, если ошибка возникнет после того, как вызывающий процесс успешно вернется, сыновний процесс завершится со статусом выхода, равным значению 127). PS Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX_SPAWN__SETSCHEDPARAM, а флаг POSIX_SPAWN_SETSCHEDULER не установлен, то, если неудачный исход функции posix_spawn() или posix_spawnp() вызван одной из причин, которые бы привели к отказу функции sched_setparam (), возвращаемое значение ошибки будет соответствовать описанию для функции sched_setparam() (или, если ошибка возникнет после того, как вызывающий процесс успешно вернется, сыновний процесс завершится со статусом выхода, равным значению 127). Если в атрибуте spawn-flags объекта, адресуемого параметром attrp, установлен флаг POSIX__SPAWN_SETSCHEDULER, и если неудачный исход функции posix_spawn() или posix_spawnp() вызван одной из причин, которые бы привели к отказу функции sched_setscheduler (), возвращаемое значение ошибки будет соответствовать описанию для функции sched_setscheduler () (или, если ошибка возникнет после того, как вызывающий процесс успешно вернется, сыновний процесс завершится со статусом выхода, равным значению 127).
512 Приложение Б Если аргумент file_actions не равен значению NULL и определяет для выполнения любое из действий close, dup2 или open, и если неудачный исход функции posix_spawn() или posix_spawnp () вызван одной из причин, которые бы привели к отказу функций close(), dup2 () или ореп(), возвращаемое значение ошибки будет соответствовать описанию для функций close (), dup2 () и ореп() соответственно (или, если ошибка возникнет после того, как вызывающий процесс успешно вернется, сыновний процесс завершится со статусом выхода, равным значению 127). Действие, связанное с открытием файла, может само по себе выразиться в любой из ошибок, описанных для функций close () или dup2 (), помимо тех, что описаны для функции open (). Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции 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, при использовании которых между созданием сыновнего процесса
Приложение Б 513 и выполнением образа нового процесса разрешаются любые определенные пользователем операции; ведь любая попытка достичь такого уровня потребовала бы параметрического задания используемого языка программирования. Поэтому функции posix_spawn () и posix_spawnp () представляют собой базовые операции создания процессов, подобные процедурам Start_Process и Start_Process_Search из пакета POSIX_Process_Primitives в языке программирования Ada, а также другим операциям, предусмотренным во многих операционных системах (но не UNIX), оснащенных POSIX-расширениями. Функции posix_spawn() и posix_spawnp() обеспечивают управление шестью типами наследования: файловыми дескрипторами, идентификационным номером (ID) группы процессов, ID пользователя и группы, маской сигналов, стратегией планирования, а также управление сигналами (будет ли каждый сигнал, игнорируемый в родительском процессе, игнорироваться и в сыновнем, или же он будет установлен равным действию по умолчанию). Возможность управления файловыми дескрипторами позволяет независимо записанному образу сыновнего процесса получить доступ к потокам данных, открытым (или даже сгенерированным) либо читаемым родительским процессом, без специального программирования средств, с помощью которых можно было бы определить, какие файлы (файловые дескрипторы) используются в родительском процессе. Возможность управления идентификационным номером группы процессов позволяет установить, как механизм управления заданиями в сыновнем процессе связан с аналогичным механизмом в родительском процессе. Управления маской сигналов и установкой сигналов по умолчанию вполне достаточно для поддержки реализации функции system(). Несмотря на то что поддержка функции system О не является одной из явных целей для функций posix_spawn() и posix_spawnp (), все же эта поддержка составляет не менее 50% от общей "суммы целей". Намерение состоит в том, что обычное наследование файлового дескриптора через функцию fork (), последующий результат заданных действий над файлами и обычное наследование файлового дескриптора через одну из функций семейства exec должно полностью определять наследование открытых файлов. Реализации не нужно принимать никаких решений относительно набора открытых дескрипторов файлов в начале выполнения образа сыновнего процесса, эти решения уже были приняты инициатором вызова функции и выражены в виде набора открытых дескрипторов файлов и их флагов FD_CLOEXEC в момент вызова, а также объекта действий над файлами, заданного в этом вызове. Мы убеждены, что в случаях, когда POSIX-примитивы языка Ada (Start_Process) реализованы в библиотеке, этот метод управления наследованием файловых дескрипторов может быть реализован очень легко. Мы можем идентифицировать ряд проблем, связанных с использованием функций posix__spawn() и posix_spawnp (), но нам неизвестно решение с меньшим количеством проблем. Модификация среды для атрибутов сыновнего процесса, которая не определяется с помощью аргументов attrp или file_actions1 должна быть выполнена в родительском процессе, а поскольку' родительский процесс обычно стремится сохранить свой контекст, это более затратно, чем аналогичное поведение, достигаемое с помощью функций fork () /exec. Кроме того, сложно модифицировать на время среду многопоточного процесса, поскольку для безопасного изменения среды все Потоки должны быть согласованы. Однако на эти затраты еще можно было бы пойти, применяя вызовы тех функций posix_spawn () и posix_spawnp (), которые используют дополнительные возможности. А поскольку расширенные модификации— это
514 Приложение Б исключение, а не правило, и они особенно непригодны в критическом ко времени полнения коде, сохранение большинства "рычагов управления" средой вне функци~ posix_spawn () и posix_spawnp () возлагается на соответствующее проектирование функции 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 инкапсулируют действия функций fo . и exec практически так же, как это делают функции posix^spa^711 Hposix_spawnp (). Первоначально, придерживаясь цели более простого под* разработчики стандарта ограничили возможности функций posix_spaWri ^ Hposix_spawnp () подмножеством возможностей, присущих процеДУР
Приложение Б 515 qtart_Process и Start__Process__Search, отказавшись от поддержки конкретных нестандартных средств. Но на основе пожеланий группы приема стандарта усовершенствовать отображение дескрипторов файлов или совсем отказаться от них, а так- же по рекомендации членов рабочей группы Ada Language Bindings разработчики стандарта решили, что функции posix_spawn() и posix_spawnp() должны быть в достаточной степени эффективными для реализации возможностей процедур Start_J?rocess и 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 (). Группа процессов Поле наследования группы процессов можно использовать для присоединения сыновнего процесса к существующей группе процессов. Если атрибуту spawn-рдгоир объекта, адресуемого параметром attrp, присвоить нулевое значение, то механизм выполнения функции setpgidO обеспечит присоединение сыновнего процесса к группе нового процесса. Потоки В системах, в которых отсутствует трансляция адресов, для представления абстракции параллелизма можно использовать потоки, так сказать, "в обход" функций Posix_spawn () и posix_spawnp (). Во многих случаях создания потоков для достижения параллельности вполне достаточно, но это не всегда является достойной заме- ои. Использование функций posix_spawn() и posix_spawnp() считается более ерьезным" вариантом, чем создание потоков. Процессы имеют ряд важных атрибу- °в, которые отсутствуют у потоков. Даже без трансляции адресов процесс может обдать определенной защитой памяти. Каждый процесс имеет среду, включающую риоуты защиты и характеристики файлов, а также атрибуты планирования. Про- ссы абстрагируют поведение множества процессоров с архитектурой неоднород- памяти лучше, чем потоки, и их удобнее использовать для отражения слабо свя- bix (в функциональном смысле) ветвей параллелизма. >нкции posix_spawn() и posix_spawnp () могут оказаться полезными не для ^/ОДои к°нфигурации. Ведь для поддержки функционирования множества процессов си СТаточно ограничиться только их созданием. В некоторых условиях общие затраты Мных ресурсов на поддержку "боеспособности" нескольких процессов могут быть
516 Приложение Б довольно высокими. Существующая практика показывает, что необходимость в поддержке множества процессов для систем с "малым ядром" скорее является исключением, чем правилом, а "правило" как раз образуют потоки. Поэтому для операционных систем с одним процессом рассматриваемые функции не представляют интерес. Асинхронное уведомление об ошибках Библиотечная реализация функций posix_spawn () или posix_spawnp () не позволяет выявить все возможные ошибки до создания сыновнего процесса. Стандарт IEEE Std 1003.1-2001 обеспечивает возможность индикации ошибок, возвращаемых из сыновнего процесса, которому не удалось успешно завершить операцию создания, с помощью специального статуса выхода, который можно обнаружить, используя значение статуса, возвращаемое функциями wait () и waitpid (). Интерфейс stat_val и макрос, используемый для его интерпретации, не совсем подходят для цели возврата API-ошибок, но они являются единственным способом, доступным для библиотечной реализации. Таким образом, реализация может заставить сыновний процесс завершиться со статусом выхода 127 в случае любой ошибки, выявленной во время порождения процесса после успешного завершения функции posix_spawn () или posix_spawnp (). Разработчики стандарта для интерпретации значения stat_val предложили использовать два дополнительных макроса. Первый, WIFSPAWNFAIL, предназначен для выявления статуса, который свидетельствует о завершении сыновнего процесса по причине ошибки, обнаруженной во время выполнения операции posix_spawn() или posix_spawnp (), а не во время реального выполнения образа сыновнего процесса; второй макрос, WSPAWNERRNO, должен выделить значение ошибки, если макрос WIFSPAWNFAIL обнаружит сбой. К сожалению, группа приема стандарта резко возражала против этого дополнения, поскольку оно поставило бы библиотечную реализацию функции posix_spawn () или posix_spawnp () в зависимость от модификации функции waitpidO, способной встраивать специальную информацию в значение stat_val для индикации сбоя при порождении процесса. Восьми бит статуса выхода сыновнего процесса, доступность которых для ожидающего родительского процесса гарантирована стандартом IEEE Std 1003.1-2001, недостаточно для устранения неоднозначности ошибок порождения процесса, которые может возвратить образ любого процесса. Требуется, чтобы в значении stat_val никакие другие биты статуса выхода не были видимы, поэтому упомянутые выше макросы не поддаются строгой реализации на библиотечном уровне. Резервирование значения статуса выхода 127 для таких ошибок порождения процессов согласуется с использованием этого значения функциями system () и рореп() при пропадании сигналов в этих операциях, которые возникают после завершения функции, но перед тем, как системная оболочка сможет их отработать. Статус выхода 127 уникальным образом не идентифицирует этот класс ошибок и не предоставляет никакой детальной информации о природе сбоя. Обратите внимание на то, что разрешается (и даже поощряется) "ядерная" реализация функций posix_spawn() и posix_spawnp () с обеспечением возврата любых возможных ошибок в виде значений, возвращаемых этой функцией, тем самым предоставляя для родительского процесса более детальную информацию о происшедших сбоях. Таким образом, для выделения асинхронных ошибок при выполнении функции posix_spawn () или posix_spawnp () упомянутые выше макросы не используются. О возможных ошибках, обнаруженных в контексте сыновнего процесса до того, как выполнится образ нового процесса, уведомление происходит путем установки статуса
Приложение Б 517 выхода сыновнего процесса равным значению 127. Вызывающий процесс для выявления сбоев при порождении процессов может использовать макросы WIFEXITED hWEXITSTATUS и значение stat_val, сохраненное функциями wait () или waitpidO, в тех случаях, когда другие значения статуса, с которыми может завершиться образ сыновнего процесса (до того, как родительский процесс сможет окончательно определить, что образ сыновнего процесса начал выполняться), отличаются от статуса выхода, равного числу 127. Будущие направления Отсутствуют. Смотри также alarm О , chmod(), close (), dup(), exec, exit (), fcntl (), fork(), kill (), open (),posix_spawn_file_actions_addclose(), posix_spawn_f ile_actions_adddup2 (), posix_spawn_f ile_actions_addopen (), posix_spawn_f ile_actions_destroy (), <REFERENCE UNDEFINED> (posix_spawn_f ile_actions_init), posix_spawnattr_destroy (), posix_spawnattr_init(), posix_spawnattr_getsigdefault(), posix_spawnattr_getflags(),posix_spawnattr_getpgroup(), posix_spawnattr_getschedparam(), posix_spawnattr_getschedpolicy (), posix_spawnattr_getsigmask (), posix_spawnattr_setsigde fault (), posix_spawnattr_setflags(), posix_spawnattr_setpgroup(), posix_spawnattr_setschedparam(), posix_spawnattr_setschedpolicy (), posix_spawnattr_setsigmask (), sched_setparam (), sched_setscheduler (), setpgid (), setuid (), stat (), times (), wait (), том Base Definitions стандарта IEEEStd 1003.1-2001, <spawn.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEEStd 1003.1d-1999. Применяется интерпретация IEEE PASC Interpretation 1003.1 #103, которая указывает, что в пункте 2 действия, соответствующие установкам сигналов по умолчанию, изменены так же, как маска сигналов. Применяется интерпретация IEEE PASC Interpretation 1003.1 #132.
518 Приложение Б Имя posix_spawn_f ile__actions_addclose, posix_spawn_f ile_actions_addopen— функции внесения в объект действий над файлами действия "закрыть файл" (или "открыть файл") (ADVANCED REALTIME) Синопсис SPN #include <spawn.h> int posix_spawn_file_actions_addclose ( posix_spawn_f ile_actions__t *file_actions, int fildes); int posix_spawn_file_actions_addopen ( posix_spawn__file_actions_t *restrict file__actions, int fildes, const char *restrict path, int о flag, mode_t mode) ; Описание Эти функции добавляют в объект действий над файлами действие "закрыть файл" (или "открыть файл") или удаляют соответствующее действие из этого объекта. Объект действий над файлами имеет тип posix_spawn_file_actions_t (который определен в заголовке <spawn.h>) и используется для задания последовательности действий, подлежащих выполнению функциями posix__spawn() или posix_spawnp() с целью получения для сыновнего процесса множества открытых файловых дескрипторов в соответствии с множеством открытых файловых дескрипторов родительского процесса. Стандарт IEEE Std 1003.1-2001 не определяет для типа posix_spawn_f ile_actions_t операторы сравнения или присваивания. Объект действий над файлами, передаваемый функции posix__spawn() или posix_spawnp (), определяет, как множество открытых файловых дескрипторов вызывающего процесса должно быть трансформировано во множество потенциально открытых файловых дескрипторов для порождаемого процесса. Эта трансформация должна выглядеть так, как если бы однократно была выполнена заданная последовательность действий в контексте порожденного процесса (до выполнения образа нового процесса), причем в порядке, в котором эти действия были добавлены в объект. Кроме того, при выполнении образа нового процесса любой файловый дескриптор (из этого нового множества), у которого установлен флаг FD_CLOEXEC, должен быть закрыт (см. описание функции posix_spawn ()). Функция posix_spawn_f ile_actions_addclose () добавляет в объект, адресуемый параметром file_actions, действие по закрытию файлов close, в результате чего при порождении нового процесса с использованием объекта действий файловый дескриптор, заданный параметром fildes, будет закрыт (как если бы была вызвана функция close (fildes)). Функция posix_spawn_f ile_actions_addopen () добавляет в объект, адресуемый параметром file_actions, действие по открытию файлов open, в результате чего при
Приложение Б 519 порождении нового процесса с использованием объекта действий файл, имя которого задано параметром path, будет открыт (как если бы была вызвана функция open (path, о£1&9> mode), и возвращенному ею файловому дескриптору, если он не равен значению fildes, будет присвоено значение fildes). Если дескриптор fildes относится к уже открытому файлу, этот файл будет закрыт перед открытием нового файла Строка, адресуемая параметром path, копируется функцией posix_spawn_file_actions_addopen(). Возвращаемые значения При успешном завершении эти функции возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Эти функции завершатся неудачно, если: [EBADF] значение, заданное параметром fildes, отрицательно либо больше или равно значению {OPEN_MAX}. Выполнение этих функций может завершиться неудачно, если: [EINVAL] значение, заданное параметром file_actions, недопустимо; [ENOMEM] для расширения содержимого объекта действий над файлами недостаточно существующей памяти. Не считается ошибкой, если в качестве значения аргумента fildes, передаваемого этим функциям, указан файловый дескриптор, для которого заданная операция не может быть выполнена во время вызова. Любая подобная ошибка будет обнаружена, когда соответствующий объект действий над файлами позже будет использован при выполнении функции posix_spawn () или posix_spawnp (). Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не представлены во всех реализациях. Логическое обоснование Объект действий над файлами можно инициализировать с помощью упорядоченной последовательности операций close (), dup2 {) и open (), предназначенной для использования функциями posix_spawn () или posix_spawnp () с целью получения множества открытых файловых дескрипторов, унаследованных порожденным процессом от своего родителя, имеющего собственное множество открытых файловых Дескрипторов в момент вызова функции posix_spawn () или posix_spawnp (). Для реорганизации файловых дескрипторов было предложено ограничиться только операциями close () и dup2 (), а для того, чтобы порожденный процесс получил "в наследство" открытые файлы, можно поступить двумя способами: либо заставить вызы-
520 Приложение Б вающий процесс открывать их перед вызовом функции posix_spawn() или posix_spawnp() (и закрывать их потом), либо передавать имена нужных файлов в порожденный процесс (в аргументе argv), чтобы он мог их открыть самостоятельно. Разработчики стандарта рекомендовали использовать на практике один из этих двух способов, поскольку детализированный статус ошибки в случае сбоя при выполнении операции открытия файла всегда доступен для приложения. Но разработчики стандарта по следующим причинам все же считают возможным разрешить включение в объект действий над файлами операцию open. 1. Это согласуется с эквивалентной функциональностью библиотеки POSIX.5 (Ada). 2. Это поддерживает парадигму перенаправления потоков ввода-вывода, часто применяемую POSIX-программами, предназначенными для вызова из оболочки. Если такая программа является сыновним процессом, ее можно сориентировать на самостоятельное открытие файлов. 3. Это позволяет сыновнему процессу открывать файлы, которые не должен открывать родительский процесс, поскольку операция по открытию файлов в этом случае может оказаться неудачной или нарушить права доступа к файлам (или права собственности). Относительно приведенного выше п. 2 заметим, что действие "открыть файл" создает для функций posix_spawn () и posix_spawnp () те же возможности, что и операторы перенаправления для функции system (), но только без промежуточного выполнения оболочки. Например, так: system ("myprog <filel 3<file2"); Относительно приведенного выше п. 3 заметим, что если вызывающему процессу нужно открыть один или несколько файлов для доступа к ним порожденного процесса, но он обладает недостаточными запасами файловых дескрипторов, то выполнение действия open () необходимо позволить в контексте сыновнего процесса после того, как другие файловые дескрипторы (которые должны оставаться открытыми в родительском процессе) будут закрыты. Кроме того, если родительский процесс выполняется из файла, для которого установлен бит режима "set-user-id" (идентификационный номер пользователя установлен) и в атрибутах порожденного процесса установлен флаг POSIX_SPAWN_RESETIDS, то файл, созданный в родительском процессе, получит (возможно, некорректно) в качестве владельца родительский ID эффективного пользователя, в то время как файл, созданный действием ореп() при выполнении функции posix_spawn() или posix_spawnp(), получит в качестве владельца реальный ID родительского процесса; при этом операция open, выполненная родительским процессом, может успешно открыть файл, к которому реальный пользователь не должен иметь доступ, или неудачно открыть (т.е. не открыть) файл, к которому реальный пользователь должен иметь доступ. Преобразование файловых дескрипторов Разработчики стандарта первоначально предлагали использовать массив, который бы определял преобразование файловых дескрипторов сыновнего процесса в обратном направлении, т.е. при переходе к родительскому процессу. Группа приема стандарта обратила внимание на то, что невозможно произвольно перетасовывать файловые дескрипторы в библиотечной реализации функции posix_spawn() или
Приложение Б 521 posix__spawnp (), не имея запаса файловых дескрипторов (которого попросту может не быть). Такой массив требует, чтобы реализация обладала сложной стратегией достижения нужного преобразования, которая бы исключала случайное закрытие "не того" файлового дескриптора в самое неподходящее время. Одним из членов рабочей группы Ada Language Bindings было отмечено, что принятое в языке Ada семейство POSIX-примитивов Start__Process использует множество действий (задаваемое инициатором вызова функции), чтобы изменить общепринятую семантику функций fork () /exec с целью наследования файловых дескрипторов довольно гибким способом, и пока никаких проблем не возникало, поскольку все бремя определения, как достичь конечного преобразования файловых дескрипторов, полностью лежит на приложении. Более того, хотя интерфейс, связанный с действиями над файлами, выглядит устрашающе сложным, в действительности он довольно прост для реализации либо в библиотеке, либо в ядре. Будущие направления Отсутствуют. Смотри также close (), dup (), open (), posix__spawn (), posix_spawn_f ile_actions_adddup2 (), posix_spawn_f ile_actions_destroy (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, < spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.Ы-1999. Применяется интерпретация IEEE PASC Interpretation 1003.1 #105, согласно которой в раздел "Описание" было внесено дополнение о том, что строка, адресуемая параметром path, копируется функцией posix_spawn_f ile_actions_addopen ().
522 Приложение Б Имя posix_spawn_f ile_actions_adddup2 — функция внесения в объект действий над файлами действия dup2 (ADVANCED REALTIME). Синопсис SPN #include <spawn.h> int posix_spawn_file_actions_adddup2 ( posix_spawn_file_actions_t * file__actions, int Hides, int newfildes) ; Описание Функция posix_spawn_f ile_actions_adddup2 () добавляет в объект, адресуемый параметром file_actions, действие dup2 (), в результате чего при порождении нового процесса с использованием объекта действий файловый дескриптор fildes дублируется в параметр newfildes (как если бы была вызвана функция dup2 (fi l des, newfi 1 des)). Объект действий над файлами порожденного процесса определяется в описании функции posix_spawn_f ile_actions_addclose (). Возвращаемое значение При успешном завершении функция posix_spawn_f ile_actions_adddup2 () возвращает нулевое значение; в противном случае— код ошибки, обозначающий ее характер. Ошибки Функция posix_spawn_f ile_actions_adddup2 () завершится неудачно, если: [EBADF] значение, заданное параметром fildes или newfildes, отрицательно либо больше или равно значению {OPEN_MAX}; [ENOMEM] для расширения содержимого объекта действий над файлами недостаточно существующей памяти. Выполнение функции posix_spawn_f ile_actions_adddup2 () может завершиться неудачно, если: [EINVAL] значение, заданное параметром file_actions, недопустимо. Не считается ошибкой, если в качестве значения аргумента fildes, передаваемого функции posix_spawn_f ile_actions_adddup2 (), указан файловый дескриптор, для которого заданная операция не может быть выполнена во время вызова. Любая подобная ошибка будет обнаружена, когда соответствующий объект действий над файлами позже будет использован при выполнении функции posix_spawn () или posix_spawnp ().
Приложение Б 523 Примеры Отсутствуют. Замечания по использованию Эта функция является частью опции Spawn и может быть не представлена во всех реализациях. Логическое обоснование Смотрите раздел "Логическое обоснование" в описании функции posix_spawn_file_actions_addclose(). Будущие направления Отсутствуют. Смотри также dup (), posix__spawn (), posix_spawn_f ile_actions_addclose (), posix_spawn_f ile_actions_destroy (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, <spawn. h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999. Применяется интерпретация IEEE PASC Interpretation 1003.1 #104, в которой указывается, что ошибка [EBADF], помимо аргумента fildes, может быть применима к аргументу newfildes.
524 Приложение Б Имя posix_spawn_f ile_actions_destroy, posix_spawn__f ile_actions__init — функции разрушения и инициализации объекта действий над файлами для порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include <spawn.h> int posix_spawn_f ile_actions_destroy ( posix_spawn_file_actions_t *file_actions); int posix_spawn_file_actions_init ( posix_spawn_f ile_actions_t * file__actions) ; Описание Функция posix_spawn_f ile_actions_destroy () предназначена для разрушения объекта, адресуемого параметром file_actions; после ее применения объект становится неинициализированным. В конкретной реализации функция posix_spawn_f ile_actions_destroy () может устанавливать объект, адресуемый параметром file_actions, равным недействительному значению. Разрушенный объект действий над файлами можно снова инициализировать с помощью функции posix_spawn_f ile__actions_init (); результаты ссылки на этот объект после его разрушения не определены. Функция posix_spawn_f ile_actions_init () используется для инициализации объекта, адресуемого параметром file_actions\ после ее применения объект не будет содержать никаких действий, предназначенных для выполнения над файлами при вызове функций posix_spawn () или posix_spawnp (). Объект действий над файлами порожденного процесса определяется в описании функции posix_spawn_f ile_actions_addclose (). Результат инициализации уже инициализированного объекта действий над файлами не определен. Возвращаемые значения При успешном завершении эти функции возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция posix_spawn_f ile_actions_init () завершится неудачно, если: [ENOMEM] для инициализации объекта действий над файлами недостаточно существующей памяти. Функция posix_spawn_f ile_actions_destroy () может завершиться неудачно, если: [EINVAL] значение, заданное параметром file_actionsy недопустимо.
Приложение Б 525 Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не представлены во всех реализациях. Логическое обоснование Смотрите раздел "Логическое обоснование" в описании функции posix_spawn_file_actions_addclose(). Будущие направления Отсутствуют. Смотри также posix_spawn (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, < spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003. ld-1999. В разделе "Синопсис" включение заголовка <sys/ types . h> больше не требуется.
526 Приложение Б Имя posix_spawnattr_destroy, posix_spawnattr_init— функции разрушения и инициализации объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include <spawn.h> 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 () не определен, если заданный параметром a t tr объект атрибутов уже инициализирован. Объект атрибутов порожденного процесса имеет тип posix_spawnattr_t (определен в заголовке <spawn.h>) и используется для задания наследования атрибутов процесса при выполнении операции порождения процесса. Для типа posix__spawnattr_t операторы сравнения и присваивания стандарт IEEE Std 1003.1-2001 не определяет. Для каждой реализации должны быть описаны отдельные атрибуты, которые она использует, и их стандартные значения, если они не определены стандартом IEEE Std 1003.1-2001. Атрибуты, не определенные стандартом IEEE Std 1003.1-2001, их стандартные значения и имена соответствующих функций чтения и записи этих атрибутов определяются конкретной реализацией. Результирующий объект атрибутов порожденного процесса (возможно, модифицированный путем установки значений отдельных атрибутов) используется для модификации поведения функций posix_spawn () или posix_spawnp (). После того как объект атрибутов был использован для порождения процесса путем вызова функции posix_spawn () или posix_spawnp(), любая функция, способная изменить объект атрибутов (включая функцию разрушения), не может повлиять на процесс, созданный таким способом. Возвращаемые значения При успешном завершении функции posix_spawnattr_destroy() и posix_spawnattr_init () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер.
Приложение Б 527 Ошибки Функция posix_spawnattr_init () завершится неудачно, если: [ ENOMEM ] для инициализации объекта атрибутов недостаточно существующей памяти, функция posix_spawnattr_destroy () может завершиться неудачно, если: [EINVAL] значение, заданное параметром a t tr, недопустимо. Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не представлены во всех реализациях. Логическое обоснование Исходный интерфейс, предложенный в стандарте IEEE Std 1003.1-2001, определял атрибуты, наследуемые при выполнении операции порождения процесса, в виде структуры. Чтобы иметь возможность выделить некоторые необязательные атрибуты в отдельные опции (например, атрибуты spawn- schedparam и spawn-schedpol icy относятся к опции Process Scheduling), а также с целью расширяемости и совместимости с более новыми POSIX-интерфейсами, для интерфейса атрибутов был изменен тип данных. Этот интерфейс в настоящее время состоит из типа posix_spawnattr_t, представляющего объект атрибутов порожденного процесса, и соответствующих функций, которые позволяют инициализировать или разрушить этот объект атрибутов, а также установить или получить значение каждого отдельного атрибута. Несмотря на то что новый объектно-ориентированный интерфейс более сложен, чем исходная структура, его проще использовать, легче наращивать и реализовывать. Будущие направления Отсутствуют. Смотри также posix_spawn (), posix_spawnattr_getsigdefault (), Posix__spawnattr_getflags(),posix_spawnattr_getpgroup(), pos ix__spawnattr_get schedpar am (), posix_spawnattr_getschedpolicy (), posix__spawnattr_getsigmask (), posix_spawnattr_setsigdef ault (), posix_spawnattr_setflags(),posix_spawnattr_setpgroup(), Posix_spawnattr_setsigmask(), posix_spawnattr_setschedpolicy(), Posix_spawnattr_setschedparam (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, <spawn.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999. Применяется интерпретация IEEE PASC Interpretation 1003.1 #106, в которой отмечается, 410 результат инициализации уже инициализированного объекта атрибутов не определен.
528 Приложение Б Имя posix_spawnattr_getflags, posix_spawnattr_setflags — функции считывания и установки атрибута spawn-flags из объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include <spawn.h> int posix_spawnattr_getflags ( const posix_spawnattr_t ^restrict attr, short ^restrict flags); int posix_spawnattr_setflags (posix_spawnattr_t *attr, short flags); Описание Функция posix_spawnattr_get flags () предназначена для получения значения атрибута spawn-flags из объекта атрибутов, адресуемого параметром attr. Функция posix_spawnattr_set flags () предназначена для установки значения атрибута spawn-flags в инициализированном объекте атрибутов, адресуемом параметром a t tr. Атрибут spawn-flags используется для обозначения того, какие атрибуты процесса должны быть изменены в образе нового процесса при вызове функции posix_spawn() или posix_spawnp(). Этот атрибут представляет собой результат применения поразрядной операции включающего ИЛИ к некоторому числу (которое может быть нулевым) следующих флагов: POSIX_SPAWN_RESETIDS POSIX_SPAWN_SETPGROUP POSIX_SPAWN_SETSIGDEF POSIX_SPAWN_SETSIGMASK PS POSIX_SPAWN_SETSCHEDPARAM POSIX_SPAWN_SETSCHEDULER Эти флаги определены в заголовке <spawn.h>. Значение, действующее по умолчанию для этого атрибута, должно соответствовать ситуации, при которой ни один флаг не установлен. Возвращаемые значения При успешном выполнении функция posix_spawnattr_get flags () возвращает нулевое значение и сохраняет значение атрибута spawn-flags из объекта атрибутов, адресуемого параметром attr, в объекте, адресуемом параметром flags; в противном случае возвращается код ошибки, обозначающий ее характер. При успешном выполнении функция posix_spawnattr_set flags () возвращает нулевое значение, в противном случае — код ошибки, обозначающий ее характер.
Приложение Б 529 Ошибки Эти функции могут завершиться неудачно, если: [EINVAL] значение, заданное параметром a t fcr, недопустимо. функция posix_spawnattr_set flags () может завершиться неудачно, если: [ 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_setsigdef ault (), posix_spawnattr_setpgroup (), posix_spawnattr_setschedparam(), posix_spawnattr_setschedpolicy (), posix_spawnattr_setsigmask (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, <spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999.
530 Приложение Б Имя posix_spawnattr_getpgroup, posix_spawnattr_setpgroup— функции считывания и установки атрибута spawn-pgroup из объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include <spawn.h> int posix_spawnattr_getpgroup ( const posix_spawnattr_t *restrict attr, pid__t *restrict pgroup); int posix_spawnattr_setpgroup (posix_spawnattr_t *attr, pid_t pgroup) ; Описание Функция posix_spawnattr__getpgroup() предназначена для получения значения атрибута spawn-pgroup из объекта атрибутов, адресуемого параметром attr. Функция posix_spawnattr__setpgroup () позволяет установить атрибут spawn- pgroup в инициализированном объекте атрибутов, адресуемом параметром attr. Атрибут spawn-pgroup представляет группу процессов, к которой при выполнении операции порождения процесса присоединяется новый процесс (если в атрибуте spawn-flags установлен флаг POSIX_SPAWN_SETPGROUP). По умолчанию значение этого атрибута равно нулю. Возвращаемые значения При успешном выполнении функция posix_spawnattr_getpgroup () возвращает нулевое значение и сохраняет значение атрибута spawn-pgroup из объекта атрибутов, адресуемого параметром attr, в объекте, адресуемом параметром pgroup] в противном случае возвращается код ошибки, обозначающий ее характер. При успешном выполнении функция posix_spawnattr_setgroup () возвращает нулевое значение, в противном случае — код ошибки, обозначающий ее характер. Ошибки Выполнение этих функций может завершиться неудачно, если: [EINVAL] значение, заданное параметром a t tr, недопустимо. Функция posix_spawnattr_setgroup () может завершиться неудачно, если: [ EINVAL ] устанавливаемое значение атрибута недопустимо. Примеры Отсутствуют.
Приложение Б 531 Замечания по использованию Эти функции являются частью опции Spawn и могут быть не представлены во всех реализациях. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn (), posix__spawnattr_destroy (), posix_spawnattr_init (), posix_spawnattr_getsigdef ault (), posix_spawnattr_get flags (), posix_spawnattr_getschedparam(), posix_spawnattr_getschedpolicy(), posix__spawnattr_getsigmask (), posix_spawnattr_setsigdef ault (), posix__spawnattr_set flags (), posix_spawnattr_setschedparam(), posix_spawnattr_setschedpolicy (), posix_spawnattr__setsigmask (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, <spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1d-1999.
532 Приложение Б Имя posix_spawnattr_getschedparam, posix_spawnattr_setschedparam — функции считывания и установки атрибута spawn-schedparam из объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPNPS #include <spawn.h> #include <sched.h> int posix_spawnattr_getschedparam ( const posix_spawnattr_t *restrict attr, struct sched_param *restrict schedparam); int posix_spawnattr_setschedparam ( posix_spawnattr_t *restrict attr, const struct sched_param *restrict schedparam); Описание Функция posix_spawnattr_getschedparam() предназначена для получения значения атрибута spawn-schedparam из объекта атрибутов, адресуемого параметром a t tr. Функция posix_spawnattr_setschedparam() позволяет установить атрибут spawn-schedparam в инициализированном объекте атрибутов, адресуемом параметром a t tr. Атрибут spawn-schedparam представляет параметры стратегии планирования, присваиваемые образу нового процесса при выполнении операции порождения процесса (если в атрибуте spawn-flags установлен флаг POSIX_SPAWN_SETSCHEDULER или флаг POSIX_SPAWN_SETSCHEDPARAM). По умолчанию значение этого атрибута не задано. Возвращаемые значения При успешном выполнении функция posix_spawnattr_getschedparam() возвращает нулевое значение и сохраняет значение атрибута spawn-schedparam из объекта атрибутов, адресуемого параметром attr, в объекте, адресуемом параметром schedparam, в противном случае возвращается код ошибки, обозначающий ее характер. При успешном выполнении функция posix_spawnattr_setschedparam() возвращает нулевое значение, в противном случае — код ошибки, обозначающий ее характер. Ошибки Выполнение этих функций может завершиться неудачно, если: [ EINVAL] значение, заданное параметром a t tr, недопустимо. Функция posix_spawnattr_setschedparam () может завершиться неудачно, если: [ ЕINVAL ] устанавливаемое значение атрибута недопустимо.
Приложение Б 533 Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опций 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 стандарта IEEE Std 1003.1-2001, <sched.h>, < spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std ЮОЗ.Ы-1999.
534 Приложение Б Имя posix_spawnattr_getschedpolicy, posix_spawnattr_setschedpolicy— функции считывания и установки атрибута spawn -schedpol icy из объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include <spawn.h> #include <sched.h> int posix__spawnattr_getschedpolicy ( const posix_spawnattr_t *restrict attr, int *restrict schedpolicy); int posix_spawnattr_setschedpolicy ( posix_spawnattr_t *attr, int schedpolicy) ; Описание Функция pos ix_spawnattr_get schedpol icy () предназначена для получения значения атрибута spawn-schedpol icy из объекта атрибутов, адресуемого параметром a t tr. Функция posix_spawnattr_set schedpol icy () позволяет установить атрибут spawn-schedpol icy в инициализированном объекте атрибутов, адресуемом параметром a t tr. Атрибут spawn-schedpol icy представляет стратегию планирования, назначаемую образу нового процесса при выполнении операции порождения процесса (если в атрибуте spawn-flags установлен флаг POSIX_SPAWN_SETSCHEDULER). По умолчанию значение этого атрибута не задано. Возвращаемые значения При успешном выполнении функция posix spawnattr, „get schedpol icy () возвращает нулевое значение и сохраняет значение атрибута spawn-schedpol icy из объекта атрибутов, адресуемого параметром attr, в объекте, адресуемом параметром schedpolicy в противном случае возвращается код ошибки, обозначающий ее характер. При успешном выполнении функция posix_spawnat tr_set schedpol icy () возвращает нулевое значение, в противном случае — код ошибки, обозначающий ее характер. Ошибки Выполнение этих функций может завершиться неудачно, если: [EINVAL] значение, заданное параметром a t tr, недопустимо. Функция pos ix_spavmattr_set schedpol icy () может завершиться неудачно, если: [ EINVAL] устанавливаемое значение атрибута недопустимо.
Приложение Б 535 Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опций 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_setsigdef ault (), posix_spawnattr_setflags(), posix_spawnattr_setpgroup(), posix_spawnattr_setschedparam(), posix_spawnattr_setsigmask(), posix__spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, <sched.h>, <spawn.h> Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003. ld-1999.
536 Приложение Б Имя posix__spawnattr_getsigdefault, posix_spawnattr_setsigdefault — функции считывания и установки атрибута spawn-sigdefault из объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include <signal.h> #include <spawn.h> int posix_spawnattr_getsigdefault ( const posix_spawnattr_t *restrict attr, sigset_t *restrict sigdefault); int posix_spawnattr_setsigdefault ( posix_spawnattr_t *restrict attr, const sigset_t *restrict sigdefault); Описание Функция posix_spawnattr_getsigdefault () предназначена для получения значения атрибута spawn-sigde fault из объекта атрибутов, адресуемого параметром attr. Функция posix_spawnattr_setsigdefault () позволяет установить атрибут spawn-sigde fault в инициализированном объекте атрибутов, адресуемом параметром attr. Атрибут spawn-sigde fault представляет множество сигналов, которые должны быть подвергнуты обработке по умолчанию в образе нового процесса (если в атрибуте spawn-flags установлен флаг POSIX_SPAWN_SETSIGDEF) при выполнении операции порождения процесса. По умолчанию значение этого атрибута представляет собой пустое множество сигналов. Возвращаемые значения При успешном выполнении функция posix_spawnattr_getsigdefault О возвращает нулевое значение и сохраняет значение атрибута spawn-sigdefaul t из объекта атрибутов, адресуемого параметром attr, в объекте, адресуемом параметром sigdefault: в противном случае возвращается код ошибки, обозначающий ее характер. При успешном выполнении функция posix_spawnattr_setsigdef ault () возвращает нулевое значение, в противном случае — код ошибки, обозначающий ее характер. Ошибки Выполнение этих функций может завершиться неудачно, если: [EINVAL] значение, заданное параметром attr, недопустимо.
Приложение Б 537 Функция posix_spawnattr_setsigdef ault () может завершиться неудачно, если: [ ЕINVAL ] устанавливаемое значение атрибута недопустимо. Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции 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_set flags (), posix_spawnattr_setpgroup (), posix_spawnattr_setschedparam(), posix_spawnattr_setschedpolicy (), posix_spawnattr_setsigmask (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, <signal. h>, < spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.Ы-1999.
538 Приложение Б Имя posix_spawnattr_get sigmask, posix_spawnattr_setsigmask— функции считывания и установки атрибута spawn-sigmask из объекта атрибутов порожденного процесса (ADVANCED REALTIME). Синопсис SPN #include <signal.h> #include <spawn.h> int posix_spawnattr_getsigmask ( const posix_spawnattr_t *restrict attr, sigset_t *restrict sigmask); int posix_spawnattr_setsigmask ( posix_spawnattr_t *restrict attr, const sigset_t *restrict sigmask) ; Описание Функция posix_spawnattr_getsigmask() предназначена для получения значения атрибута spawn-sigmask из объекта атрибутов, адресуемого параметром attr. Функция posix_spawnattr_setsigmask() позволяет установить атрибут spawn- si gmasк в инициализированном объекте атрибутов, адресуемом параметром attr. Атрибут spawn-sigmask представляет маску сигналов, предназначенную для использования в образе нового процесса при выполнении операции порождения процесса (если в атрибуте spawn-flags установлен флаг POSIX_SPAWN_SETSIGMASK). По умолчанию значение этого атрибута не определено. Возвращаемые значения При успешном выполнении функция posix_spawnattr_getsigmask() возвращает нулевое значение и сохраняет значение атрибута spawn-sigmask из объекта атрибутов, адресуемого параметром attr, в объекте, адресуемом параметром sigmask; в противном случае возвращается код ошибки, обозначающий ее характер. При успешном выполнении функция posix_spawnattr_setsigmask() возвращает нулевое значение, в противном случае — код ошибки, обозначающий ее характер. Ошибки Выполнение этих функций может завершиться неудачно, если: [EINVAL] значение, заданное параметром a t tr, недопустимо. Функция posix_spawnattr_setsigmask () может завершиться неудачно, если: [EINVAL] устанавливаемое значение атрибута недопустимо.
Приложение Б 539 Примеры Отсутствуют. Замечания по использованию Эти функции являются частью опции Spawn и могут быть не представлены во всех реализациях. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также posix_spawn (), posix_spawnattr_destroy (), posix_spawnattr__init (), posix_spawnattr_getsigdef ault (), posix_spawnattr_get flags (), posix_spawnattr_getpgroup (), posix_spawnattr_getschedparam(), posix_spawnattr_getschedpolicy (), posix_spawnattr_setsigdefault (), posix_spawnattr_set flags (), posix_spawnattr_setpgroup (), posix_spawnattr_setschedparam(), posix_spawnattr_setschedpolicy (), posix_spawnp (), том Base Definitions стандарта IEEE Std 1003.1-2001, <signal. h>, < spawn. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003. ld-1999.
540 Приложение Б Имя pthread_attr_destroy, pthread_attr_init — функции разрушения и инициализации объекта атрибутов потока. Синопсис THR #include <pthread.h> int pthread_attr_destroy (pthread_attr__t *attr) ; int pthread_attr_init (pthread_attr_t *attr); Описание Функция pthread_attr_destroy () предназначена для разрушения объекта атрибутов потока. В конкретной реализации функция pthread_attr_destroy () может устанавливать параметр attr равным некоторому недействительному значению, определяемому реализацией. Разрушенный объект атрибутов, адресуемый параметром attr, можно инициализировать повторно с помощью функции pthread_attr_init (); результаты ссылки на этот объект после его разрушения не определены. Функция pthread_attr_init () позволяет инициализировать объект атрибутов потока, адресуемый параметром attr, значением, действующим по умолчанию для всех отдельных атрибутов, используемых в данной реализации. Результирующий объект атрибутов (возможно, модифицированный путем установки значений отдельных атрибутов) при выполнении функции pthread_create () определяет атрибуты создаваемого потока. В нескольких одновременных вызовах функции pthread_create () можно использовать один объект атрибутов. Результат вызова функции pthread_attr_init () не определен, если заданный параметром a t tr объект атрибутов уже инициализирован. Возвращаемые значения При успешном завершении эти функции возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_attr_init () завершится неудачно, если: [ENOMEM] для инициализации объекта атрибутов потока недостаточно существующей памяти. Эти функции не возвращают код ошибки в виде значения [EINTR]. Примеры Отсутствуют.
Приложение Б 541 Замечания по использованию Отсутствует. Логическое обоснование Объекты атрибутов используются для потоков, мьютексов и условных переменных в качестве будущего механизма поддержки стандартизации в этих областях, не требующего изменения самих функций. Объекты атрибутов обеспечивают четкую автономность реконфигурируемых аспектов потоков. Например, важным атрибутом потока является "размер стека", который при переносе многопоточной программы с одного компьютера на другой часто приходится корректировать. Использование объектов атрибутов позволит вносить необходимые изменения в одном месте программы, а не в разных местах, "разбросанных" по всем экземплярам потоков. Объекты атрибутов можно использовать для создания классов потоков с аналогичными атрибутами; например, "потоков с большими стеками и высоким приоритетом" или "потоков с минимальными стеками". Эти классы можно определить в одном месте программы, а затем их использовать, когда понадобится создать поток. В результате значительно упростится процесс изменения "классовых" решений потоков, и не придется подробно анализировать каждый вызов функции pthread_create (). Объекты атрибутов с целью потенциальной расширяемости определяются как "закрытые" типы. Если бы они были определены как "прозрачные" структуры, то при добавлении новых атрибутов (т.е. при расширении объектов атрибутов) пришлось бы перекомпилировать все многопоточные программы, что не всегда возможно, например, если различные программные компоненты приобретены у различных изготовителей. Кроме того, "непрозрачные" объекты атрибутов предоставляют возможность для повышения быстродействия. Достоверность атрибутов можно проверить один раз при их установке, а не при каждом создании потока. Ведь реализации зачастую требуют кэширования объектов ядра, создание которых считается "дорогим удовольствием". Именно "непрозрачные" объекты атрибутов позволяют вовремя определить, в какой момент кэшированные объекты становятся недействительными из-за изменения атрибутов. Поскольку оператор присваивания необязательно должен быть определен для каждого "непрозрачного" типа, значения, определяемые конкретной реализацией по умолчанию, невозможно назначать без ущерба для переносимости. Для решения этой проблемы можно позволить динамическую инициализацию объектов атрибутов с помощью соответствующих функций инициализации, и тогда значения, действующие по умолчанию, реализация сможет назначать автоматически. В качестве предполагаемой альтернативы поддержки атрибутов были представлены следующие предложения. 1. Поддерживается стиль передачи функциям инициализации (pthread_create (), pthread_mutex_init (), pthread_cond_init ()) параметра, формируемого путем применения поразрядной операции включающего ИЛИ к флагам. Содержащий эти флаги параметр (в расчете на расширяемость в будущем) должен иметь "непрозрачный" тип. Если в этом параметре флаги не установлены, то объекты создаются с использованием характеристик, действующих по умолчанию. Реализация самостоятельно может задавать значения флагов и соответствующее им поведение.
542 Приложение Б 2. Если необходима дальнейшая специализация мьютексов и условных переменных, в реализациях могут быть определены дополнительные процедуры, предназначенные для выполнения действий над объектами типа pthread_mutex__t и pthread_cond_t (а не над объектами атрибутов). При внедрении этого решения возможны следующие трудности. 1. Побитовая маска не будет считаться "закрытой", если биты должны быть установлены в векторных объектах атрибутов с использованием явно закодированных поразрядных операций включающего ИЛИ. Если количество опций превышает размер типа int, прикладные программисты должны знать местоположение каждого бита. Если биты устанавливаются или считываются путем средств инкапсуляции (т.е. с помощью функций считывания и установки), то побитовая маска будет представлять собой всего лишь реализацию объектов атрибутов без свободного доступа для программиста. 2. Многие атрибуты имеют тип, отличный от булевого, или представляют собой малые целые значения. Например, для задания стратегии планирования можно выделить 3 или 4 бит, но для приоритета потребуется 5 или больше бит, следовательно по меньшей мере 8 из 16 возможных бит (для компьютеров с 16-разрядными целочисленными значениями) уже "занято". Поэтому побитовая маска может корректно управлять только атрибутами булевого типа ("установлен" или нет) и не может служить в качестве хранилища для значений иного типа. Такие значения необходимо задавать или в качестве параметров функций (которые не относятся к числу наращиваемых), или путем установки полей структуры (которые не являются "закрытыми"), или с помощью функций доступа, т.е. функций считывания и записи (которые делают побитовую маску излишним дополнением к объектам атрибутов). Размер стека определяется как необязательный атрибут, поскольку само понятие стека зависит от конкретного компьютера. Например, в одних реализациях невозможно изменить размер стека, а в других в этом вообще нет необходимости, поскольку страницы стека могут быть несмежными и выделяться (и освобождаться) по требованию. Механизм атрибутов разработан по большей мере ради расширяемости. Будущие дополнения к механизму атрибутов или любому объекту атрибутов, определенному в этом томе (разделе) стандарта IEEE Std 1003.1-2001, необходимо вносить с чрезвычайной тщательностью, чтобы они не отразились на совместимости на уровне машинных кодов. Объекты атрибутов, даже создаваемые с помощью таких функций динамического распределения памяти, как mallocO, во время компиляции могут иметь фиксированный размер. Это означает, например, что функция pthread_create () в реализации с дополнениями для использования типа pthread_attr_t не сможет "видеть" за пределами области, которую двоичное приложение считает допустимой. Это говорит о том, что реализации должны поддерживать в объекте атрибутов поле размера, а также информацию о версии, если дополнения приходится использовать в различных направлениях (или объединять продукты различных изготовителей).
Приложение Б 543 Будущие направления Отсутствуют. Смотри также pthread_attr_getstackaddr(), pthread__attr_getstacksize(), pthread__attr_getdetachstate (), thread_create (), том Base Definitions стандарта IEEEStd 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_attr_destroy() и pthread_attr_init () отмечены как часть опции Threads. Применяется интерпретация IEEE PASC Interpretation 1003.1 #107, в которой ука зывается, что результат инициализации уже инициализированного объекта атрибутов потока не определен.
544 Приложение Б Имя pthread_attr_getdetachstate, pthread_attr__setdetachstate — функции считывания и записи атрибута detachstate. Синопсис THR #include <pthread.h> int pthread_attr__getdetachstate ( const pthread__attr_t *attr, int *detachstate); int pthread_attr_setdetachstate (pthread__attr_t *attr, int detachstate); Описание Атрибут detachstate управляет тем, создается ли поток в открепленном (отсоединенном) состоянии. Если поток создается открепленным, то использование его идентификационного номера (ID) функциями pthread_detach () или pthread_j о in () является ошибкой. Функции pthread_attr_getdetachstate() и pthread_attr_setde- tachstateO считывают и устанавливают соответственно атрибут detachstate в объекте атрибутов, адресуемом параметром a t fcr. С помощью функции pthread_attr_setdetachstate() приложение может установить атрибут detachstate равным либо значению PTHREAD_CREATE_DETACHED, либо значению PTHREAD_CREATE_JOINABLE. С помощью функции pthread_attr_getdetachstate () считывается значение атрибута detachstate, которое может быть равным либо PTHREAD_ CREATE_DETACHED, либо PTHREAD_CREATE_JOINABLE. Значение PTHREAD_CREATE_DETACHED используется для перевода всех потоков, создаваемых с помощью объекта, адресуемого параметром at fcr, в открепленное состояние, в то время как значение PTHREAD_CREATE_JOINABLE применяется для перевода всех потоков, создаваемых с помощью объекта, адресуемого параметром a fc fcr, в присоединенное состояние. По умолчанию атрибут detachstate устанавливается равным значению PTHREAD_CREATE_JOINABLE. Возвращаемые значения При успешном завершении функции pthread_attr_getdetachstate () Hpthread_attr_setdetachstate() возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Функция pthread_attr_getdetachstate () при успешном выполнении сохраняет значение атрибута detachstate в параметре detachstate.
Приложение Б 545 Ошибки функция pthread_attr_setdetachstate () завершится неудачно, если: [EINVAL] значение, заданное параметром detachstate, недействительно. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствует. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy(),pthread_attr_getstackaddr(), pthread_attr_getstacksize (), pthread_create (), том Base Definitions стандарта IEEEStd 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_attr_getdetachstate() и pthread_attr_setdetachs- tate () отмечены как часть опции Threads. Раздел "Описание" был отредактирован с целью исключить из него слово "must" ("должен").
546 Приложение Б Имя pthread_attr_getguardsize, pthread_attr_setguardsize— функции считывания и установки значения потокового атрибута guards ize. Синопсис XSI #include <pthread.h> int pthread_attr_getguardsize ( const pthread_attr_t *restrict attr, size__t *restrict guardsize) ; int pthread__attr_setguardsize (pthread_attr__t *attr, size_t guardsize); Описание Функция pthread_attr_getguardsize () используется для считывания атрибута guardsize из объекта атрибутов, адресуемого параметром attr. Этот атрибут возвращается в параметре guardsize. Функция pthread_attr_setguardsize () позволяет установить атрибут guardsize в объекте атрибутов, адресуемом параметром a t tr. Новое значение этого атрибута записывается из параметра guardsize. Если значение параметра guardsize равно нулю, то для потоков, создаваемых с использованием атрибута attr, защищенная область не предоставляется. Если значение параметра guardsize больше нуля, то для каждого потока, создаваемого с использованием атрибута attr, предоставляется защищенная область, размер которой составляет не менее guardsize байтов. Атрибут guardsize позволяет управлять размером защищенной области, выделяемой для стека создаваемого потока. Атрибут guardsize обеспечивает защиту от адресного переполнения указателя стека. Если стек создается с использованием такой защиты, реализация выделяет дополнительную память в конце области переполнения стека, которая служит специальным буфером. Если приложение переполняет стек, входя в этот буфер, формируется сигнал ошибки (возможно, в сигнале SIGSEGV, передаваемом потоку). Реализация может округлить значение, содержащееся в атрибуте guardsize, до числа, кратного значению реконфигурируемой системной переменной {PAGESIZE} (см. заголовок <sys/mman.h>). Если реализация округлит значение атрибута guardsize до числа, кратного значению переменной {PAGESIZE}, то при вызове функции pthread_attr_get guards ize () с заданным параметром at tr в параметре guardsize сохранится размер защитной области, установленный в результате предварительного вызова функции pthread_attr_setguardsize (). По умолчанию значение атрибута guardsize равно {PAGESIZE} байтам. Реальное значение переменной {PAGESIZE} определяется реализацией. Если предварительно был установлен атрибут stackaddr или stack (т.е. инициатор вызова самостоятельно выделяет память для стеков потока и управляет этой памятью), то атрибут guardsize игнорируется, и никакая защита от переполнения стека потока реализацией не обеспечивается. Вся ответственность в этом случае за управление памятью стека лежит на приложении.
Приложение Б 547 Возвращаемые значения При успешном завершении функции рthread_attr_getguards ize() Hpthread__attr_setguardsize() возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_attr_getguardsize () завершится неудачно, если: [EINVAL] значение, заданное параметром guardsize, недействительно; [ЕINVAL] значение параметра attr недействительно. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствует. Логическое обоснование Атрибут guardsize предлагается для использования приложением по двум причинам. 1. На защиту от переполнения могут потенциально затрачиваться существенные системные ресурсы. Для приложения, в котором создается большое количество потоков и существует уверенность в том, что при выполнении потоков их стеки никогда не будут переполнены, можно сэкономить системные ресурсы, отключив выделение областей защиты. 2. Если потоки размещают в стеке большие структуры данных, то для обнаружения факта переполнения стека могут понадобиться области защиты большого объема. Будущие направления Отсутствуют. Смотри также Том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread. h> , < sys /mman. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Из раздела "Ошибки" было удалено третье условие возникновения ошибки [EINVAL], поскольку оно включается во второе условие. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_attr_getguardsize () было добавлено ключевое слово restrict
548 Приложение Б Имя pthread_attr_getinheritsched, pthread_attr_setinheritsched— функции считывания и установки атрибута inheri tsched (REALTIME THREADS). Синопсис THRTPS #include <pthread.h> int pthread_attr__getinheritsched ( const pthread__attr__t *restrict attr, int *restrict inheritsched); int pthread__attr__setinheritsched (pthread_attr_t *attr, int inheritsched) ; Описание Функции pthread_attr_getinheritsched() и pthread_attr_setinheri tsched () используются для считывания и установки соответственно атрибута inheri tsched в объекте, заданном параметром a t tr. Если при вызове функции pthread__create () используются объекты атрибутов, то атрибут inheritsched определяет, как будут устанавливаться другие атрибуты планирования создаваемого потока. Значение PTHREAD_INHERIT_SCHED говорит о том, что атрибуты планирования потоков наследуются от создающего потока, а атрибуты планирования, содержащиеся в объекте, заданном параметром attr, игнорируются. Значение PTHREAD_EXPLICIT_SCHED подразумевает, что атрибуты планирования потоков устанавливаются равными соответствующим значениям, содержащимся в объекте атрибутов, заданном параметром a t tr. Значения PTHREAD_INHERIT_SCHED и PTHREAD_EXPLICIT_SCHED определяются в заголовке <рthread. h>. От значения атрибута inheri tsched зависят следующие атрибуты планирования потоков, определенные стандартом IEEE Std 1003.1-2001: стратегия планирования (schedpolicy), параметры планирования (schedparam) и область конкуренции (con ten ti on scope). Возвращаемые значения При успешном завершении функции pthread_attr_get inheri tsched О и pthread_attr_set inheri tsched () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_attr_setinheritsched () может завершиться неудачно, если: [EINVAL] значение, заданное параметром inheri tsched, недействительно;
Приложение Б 549 [ENOTSUP] была сделана попытка установить атрибут равным значению, которое не поддерживается реализацией. Эти функции не возвращают код ошибки [ EINTR]. Примеры Отсутствуют. Замечания по использованию После установки этих атрибутов поток можно создать путем вызова функции pthread_create () с заданными атрибутами. Использование этих функций не оказывает влияния на поток, выполняемый в данный момент. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy (), pthread_attr_getscope (), pthread_attr_getschedpolicy(), pthread_attr_getschedparam(), pthread_create (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>, <sched.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_attr_getinheritsched() и pthread_attr_setinneri- tsched () отмечены как часть опций Threads и Thread Execution Scheduling. Условие ошибки [ENOSYS] было удалено, поскольку в заглушках нет необходимости, если реализация не поддерживает опцию Thread Execution Scheduling. Ь целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread__attr_getinheritsched () было добавлено ключевое слово restrict.
550 Приложение Б Имя pthread_attr_getschedparam, pthread_attr_setschedparam— функции считывания и установки атрибута schedparam. Синопсис THR #include <pthread.h> 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() используются для считывания и установки соответственно атрибутов параметров планирования в объекте, заданном параметром a t fcr. Содержимое структуры param определено в заголовке <sched.h>. Для установки стратегий планирования SCHED_FIFO и SCHED_RR единственным обязательным членом структуры param является sched_jpriority. * ^" Для установки стратегии планирования SCHED__SP0RADIC необходимо установить следующие члены структуры param: sched__priority, sched_ss_low_priority, sched__ss__repl_per±od, sched__ss_init__badget и sched_ss_max_repl. Для успешного выполнения функции установки необходимо, чтобы заданное значение члена sched_ss_repl__period было больше или равно значению заданного члена sched__ss_ini t_ budget; в противном случае функция завершится неудачно. Для успешного выполнения функции установки также необходимо, чтобы значение члена sched_ss_max__repl находилось в пределах включающего диапазона [1. {SS_REPL_MAX} ]; в противном случае функция завершится неудачно. Возвращаемые значения При успешном завершении функции pthread_attr_getschedparam() Hpthread_attr_setschedparam() возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_attr_setschedparam () может завершиться неудачно, если: [EINVAL] значение, заданное параметром param, недействительно;
Приложение Б 551 [ ENOTSUP ] была сделана попытка установить атрибут равным значению, которое не поддерживается реализацией. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию После установки этих атрибутов поток можно создать путем вызова функции pthread_create () с использованием объекта атрибутов. Применение этих функций не оказывает влияния на поток, выполняемый в данный момент Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy (), pthread_attr_getscope (), pthread_attr_getinheritsched(), pthread_attr_getschedpolicy (), pthread_create (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>, <sched.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_attr_getschedparam () и pthread_attr_setschedparam () отмечены как часть опции Threads. В целях согласования со стандартом IEEE Std 1003. Id-1999 была добавлена стратегия планирования SCHED_SPORADIC. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототипы функций Pthread_attr_getschedparam () и pthread_attr_setschedparam() было добавлено ключевое слово restrict.
552 Приложение Б Имя pthread_attr_getschedpolicy, pthread_attr_setschedpolicy— функции считывания и установки атрибута schedpolicy (REALTIME THREADS). Синопсис THRTPS #include <pthread.h> int pthread_attr_getschedpolicy ( const pthread_attr_t *restrict attr, int *restrict policy); int pthread__attr__setschedpolicy ( pthread_attr_t *attr, int policy) ; Описание Функции pthread_attr_getschedpolicy () и pthread_attr_setschedpolicy () используются для считывания и установки соответственно атрибута schedpolicy в объекте атрибутов, адресуемом параметром a. t tr. Для обозначения стратегии планирования поддерживаются значения SCHED_FIFO, SCHED_RR и SCHED_OTHER, которые определены в заголовке <sched. h>. TSP Когда потоки, выполняющиеся с использованием стратегий планирования SCHED__FIFO, SCHED__RR или SCHED_SPORADIC, ожидают освобождения мьютекса, то его получение (после разблокировки) происходит согласно приоритетам. Возвращаемые значения При успешном завершении функции pthread_attr_getschedpolicy () иpthread_attr_setschedpolicy () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_attr_setschedpolicy () может завершиться неудачно, если: [EINVAL] значение, заданное параметром poliсу, недействительно; [ENOTSUP] была сделана попытка установить атрибут равным значению, которое не поддерживается реализацией. Эти функции не возвращают код ошибки [EINTR].
Приложение Б 553 Примеры Отсутствуют. Замечания по использованию После установки этих атрибутов поток можно создать путем вызова функции pthread_create () с использованием объекта атрибутов. Применение этих функций не оказывает влияния на поток, выполняемый в данный момент. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy(), pthread_attr_getscope(), pthread_attr__getinheritsched (), pthread_attr_getschedparam(), pthread_create (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>, <sched.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_attr_getschedpolicy() и pthread_attr_setsched- policy () отмечены как часть опций Threads и Thread Execution Scheduling. Условие ошибки [ENOSYS] было удалено, поскольку в заглушках нет необходимости, если реализация не поддерживает опцию Thread Execution Scheduling. В целях согласования со стандартом IEEE Std 1003.1d-1999 была добавлена стратегия планирования SCHED_SPORADIC. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_attr_getschedpolicy () было добавлено ключевое слово restrict.
554 Приложение Б Имя pthread_cancel — функция отмены выполнения потока. Синопсис THR #include <pthread.h> int pthread_cancel (pthread_t thread); Описание Функция pthread_cancel () создает запрос на отмену потока. Когда именно отмена вступит в силу, зависит от текущего состояния потока, заданного параметром thread, и его типа. При отмене потока должны быть вызваны обработчики, выполняющие подготовительные действия, связанные с отменой потока. По завершении последнего обработчика должны быть вызваны деструкторы объектных данных, используемых отменяемым потоком. По завершении последнего деструктора поток, заданный параметром thread, должен завершиться. Действия, связанные с отменой заданного потока, выполняются асинхронно по отношению к потоку, вызывающему функцию pthread_cancel (). Возвращаемое значение При успешном завершении функция pthread_cancel () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_cancel () может завершиться неудачно, если: [ESRCH] не удалось найти поток, идентификационный номер (ID) которого соответствовал бы заданному. Функция pthread_cancel () не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Для отправки потоку уведомления об отмене рассматривалось два альтернативных варианта. Для одного предполагалось определить новый сигнал SIGCANCEL с соот-
Приложение Б 555 ветствующей семантикой отмены, а для другого— новую функцию pthread__cancel (), которая бы приводила в действие процедуру отмены потока. Преимущество варианта, предусматривающего создание нового сигнала, состояло в том, что критерии его выдачи были бы во многом идентичны тем, которые использовались при попытке выдать любой другой сигнал, поэтому сигнальный механизм уведомления об отмене казался унифицированным. И в самом деле, во многих реализациях отмена потоков осуществляется посредством специального сигнала. Однако до сих пор не существовало ни одной сигнальной функции (за исключением функции pthread_kill ()), которую можно было бы использовать совместно с этим новым сигналом, поскольку поведение выдаваемого сигнала отмены должно было отличаться от поведения любого из уже определенных сигналов. К достоинству варианта создания специальной функции можно отнести осознание того, что уведомление об отмене потока было бы в этом случае четко определенным. Кроме того, механизм выдачи уведомления об отмене не требует реализации в виде сигнала. Ведь если такой механизм заметно ближе к сигналам, то ему свойственны аналогии с языковым механизмом исключительных ситуаций, которые потенциально не видны. В конечном счете, поскольку необходимость обеспечивать обработку большого числа исключительных ситуаций при использовании нового сигнала с существующими сигнальными функциями может неоправданно усложнить (даже запутать) процесс отмены потока, было решено сделать выбор в пользу специальной функции, которая устраняет эту проблему. Такая функция была тщательно разработана, причем так, что любая реализация могла бы обеспечить "безоговорочное" выполнение процедуры отмены "поверх" каких бы то ни было сигналов. Наличие специальной функции отмены потока также означает, что реализации не обязаны обеспечивать процедуру отмены с помощью сигналов. Будущие направления Отсутствуют. Смотри также pthread_exit(), pthread_cond_timedwait(), pthread_join(), pthread__setcancelstate (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширением POSIX Threads Extension. Issue 6 Функция pthread_cancel () отмечена как часть опции Threads.
556 Приложение Б Имя pthread_cleanup_pop, pthread_cleanup__push — функции создания обработчиков запроса об отмене потоков. Синопсис THR #include <pthread.h> void pthread_cleanup_pop (int execute); void pthread_cleanup_push (void (* routine) (void*), void *arg); Описание Функция pthread_cleanup_pop () используется для извлечения функции, расположенной в вершине стека вызывающего потока, предназначенного для выполнения подготовительных действий по аннулированию потока, и ее вызова (если параметр execute не равен нулю). Функция pthread_cleanup_push() позволяет поместить в стек вызывающего потока заданную функцию обработчика rou tine, предназначенного для выполнения подготовительных действий по аннулированию потока. Этот обработчик будет извлечен из соответствующего стека и вызван с аргументом агд при наличии следующих условий: • поток существует (т.е. он вызывает функцию pthread_exit ()); • поток действует в соответствии с запросом отмены; • поток вызывает функцию pthread_cleanup_pop() с ненулевым значением аргумента execute. Эти функции можно реализовать как макросы. Приложение должно гарантировать, что они имеют форму инструкций и используются попарно в пределах одного и того же лексического контекста (чтобы макрос pthread_cleanup__push () раскрывался в список лексем, начинающийся лексемой '{\ а макрос pthread_cleanup_pop () раскрывался в список лексем, завершающийся соответствующей лексемой '}'). Результат вызова функции longjmpO или siglongjmp() не определен, если имели место обращения к функции pthread_cleanup_push() или pthread_cleanup__pop() без соответствующего "парного" вызова по причине заполнения буфера переходов. Результат вызова функции long jmp () или siglongjmpO из обработчика, предназначенного для выполнения подготовительных действий по аннулированию потока, также не определен. Возвращаемые значения Функции pthread_cleanup_push () и hread_cleanup__pop() не возвращают никакого значения.
Приложение Б 557 Ошибки Ошибки не определены. Эти функции не возвращают код ошибки [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() * выполняет здесь функцию waiting_reader_cleanup(). */ pthread_cleanup_pop(1); void release_read_JLock (rwlock *1) { pthread_mutex_lock (&l->lock); if (—l->lock_count == 0) pthread_cond_signal (&l->wcond); pthread_mutex_unlock (1);
558 Приложение Б void waiting_writer_cleanup (void *arg) { rwlock *1; 1 = (rwlock *) arg; if ((—l->waiting__writers == 0) && (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 ! = 0) pthread_cond_wait (&l->wcond/ &l->lock); l->lock_count = -1; /* * Обратите внимание на то, что функция pthread_cleanup_pop() * выполняет здесь функцию waiting_writer_cleanup(). */ pthread_cleanup_pop (l); } void release_write_lock (rwlock *1) { pthread_jmitex_lock (&l->lock) ; l->lock_count = 0; if (l->waiting_writers == 0) 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 = 0; l->waiting_writers = 0;
\ \ Приложение Б 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, block); /* * Поток устанавливает блокировку для записи. */ pthread_cleanup_pop (l) ; } Замечания по использованию Две описываемые здесь функции, pthread_cleanup__push() и pthread_clea- nup_pop (), которые помещают и извлекают из стека обработчики запроса на отмену потока, можно сравнить с левой и правой круглыми скобками. Их нужно всегда использовать "в паре**. Логическое обоснование Ограничение, налагаемое на две функции, pthread_cleanup__push() npthread__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); \ Возможна даже более "смелая" реализация этих функций, которая позволит компилятору "считать" обработчик запроса на отмену константой, значение которой можно
560 Приложение Б "встраивать" в код. В данном томе стандарта IEEE Std 1003.1-2001 пока оставлен неопределенным результат вызова функции longjmp () из обработчика сигнала, выполняемого в функции библиотеки POSIX System Interfaces. Если в какой-то реализации потребуется разрешить этот вызов и придать ему надлежащее поведение, функция longjmp () должна в этом случае вызвать все обработчики запроса на отмену, которые были помещены в стек (но еще не извлечены из него) с момента вызова функции set jmp (). Рассмотрим многопоточную функцию, вызываемую одним потоком, который использует сигналы. Если бы сигнал был выдан обработчику сигналов во время операции qsort (), и этому обработчику пришлось бы вызвать функцию longjmp () (которая в свою очередь не вызывала бы обработчики запроса на отмену), то вспомогательные потоки, создаваемые функцией qsort (), не были бы аннулированы. Они бы продолжали выполняться и осуществляли запись в массив аргументов даже в том случае, если этот массив был к тому времени извлечен из стека. Обратите внимание на то, что такой механизм обработки запросов на отмену особенно тесно связан с языком С, и, несмотря на требование независимости языка, предъявляемое к любому унифицированному механизму выполнения "очистительно- восстановительных работ", подобный механизм, выраженный в других языках, может быть совершенно иным. Кроме того, необходимость этого механизма в действительности связана только с отсутствием реального механизма обработки исключительных ситуаций в языке С, который был бы идеальным решением. Здесь отсутствуют замечания о функции безопасной отмены потока. Если приложение в своих обработчиках сигналов не имеет точек отмены, блокирует любой сигнал, обработчик которого может иметь точки отмены (несмотря на вызов асинхронно-опасных функций), или запрещает отмену (несмотря на вызов асинхронно- опасных функций), все функции можно безопасно вызывать из функций обработки запросов на отмену потоков. Будущие направления Отсутствуют. Смотри также pthread_cancel (), pthread_setcancelstate (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_cleanup_pop () и pthread_cleanup_push() отмечены как часть опции Threads. Добавлен раздел "Замечания по использованию" (APPLICATION USAGE). Раздел "Описание" был отредактирован с целью исключить из него слово 'must" Г'должен").
Приложение Б 561 Имя pthread_cond_broadcast, pthread_cond_signal — функции разблокировки потоков, заблокированных с помощью переменной условия. Синопсис THR #include <pthread.h> int pthread_cond_broadcast (pthread_cond_t *cond) ; int pthread_cond_signal (pthread_cond_t *cond) ; Описание Эти функции используются для разблокировки потоков, заблокированных с помощью переменной условия. Функция pthread_cond_.broadcast () позволяет разблокировать все потоки, заблокированные в данный момент с использованием переменной условия, заданной параметром cond. Функция pthread_cond__signal () используется для разблокировки по крайней мере одного из потоков, заблокированных с использованием условной переменной, заданной параметром cond (если таковые существуют). Если с использованием этой переменной условия заблокировано несколько потоков, то порядок разблокировки будет определен в соответствии с их стратегией планирования. Когда каждый поток, разблокированный в результате вызова функции рthread_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() не будут иметь результата, если в данный момент не существует потоков, заблокированных с использованием условной переменной, заданной параметром cond. Возвращаемые значения При успешном завершении функции pthread_cond_broadcast () и pthread_ cond_signal () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер.
562 Приложение Б Ошибки Функции pthread_cond_broadcast () и pthread_cond_signal () могут завершиться неудачно, если: [EINVAL] значение, заданное параметром cond, не ссылается на инициализированную условную переменную. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Функция pthread_cond_broadcast () используется при изменении состояния общей переменной в ситуации, когда выполняется сразу несколько потоков. Рассмотрим задачу с участием одного "изготовителя" и нескольких "потребителей", в которой "изготовитель" может вставить в список несколько элементов, к которым могут получать доступ "потребители" (по одному элементу за раз). Путем вызова функции pthread_cond_broadcast () "изготовитель" уведомляет о своем действии всех "потребителей", которые, возможно, находятся в состоянии ожидания, и, таким образом, при использовании мультипроцессора приложение может достичь более высокой пропускной способности. Кроме того, функция pthread__cond_broadcast () позволяет упростить реализацию блокировки чтения-записи. Функция рthread_cond_.broadcast () весьма полезна, когда записывающий поток освобождает блокировку, и нужно "запустить" все "читающие" потоки, находящиеся в состоянии ожидания. Наконец, эту широковещательную функцию можно использовать в двухфазном алгоритме фиксации для уведомления всех клиентов о предстоящей фиксации транзакции. Функцию pthread_cond_signal () небезопасно использовать в обработчике сигналов, который вызывается асинхронно. Даже если это было бы безопасно, имела бы место "гонка" данных между проверками булевой функции pthread_cond_ wait (), которую невозможно эффективно устранить. Следовательно, мьютексы и переменные условий не подходят для освобождения ожидающего потока путем сигнализации из кода обработчика сигналов. Логическое обоснование Несколько запусков по условному сигналу Для мультипроцессора, скорее всего, невозможно применить функцию pthread_cond_signal (), чтобы избежать разблокировки нескольких потоков, заблокированных с использованием условной переменной. Рассмотрим, например, следующую частичную реализацию функций pthread_cond_wait () и pthread_cond__ signal (), выполняемых потоками в заданном порядке. Один поток пытается "дождаться" нужного значения условной переменной, другой при этом выполняет функцию pthread_cond_signal (), в то время как третий поток уже находится в состоянии ожидания.
Приложение Б 563 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 в прямой форме отмечена возможность возникновения "фиктивных запусков". Будущие направления Отсутствуют.
564 Приложение Б Смотри также pthread_cond__destroy (), pthread_cond_timedwait (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread__cond_broadcast() и pthread_cond_signal() отмечены как часть опции Threads. Добавлен раздел "Замечания по использованию" (APPLICATION USAGE).
Приложение Б 565 Имя pthread_cond_destroy, pthread_cond_init — функции разрушения и инициализации условных переменных. Синопсис THR #include <pthread.h> 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 () используется для разрушения условной переменной, заданной параметром cond, в результате чего объект становится неинициализированным. В конкретной реализации функция pthread_cond_destroy () может устанавливать объект, адресуемый параметром cond, равным недействительному значению. Разрушенный объект условной переменной можно снова инициализировать с помощью функции pthread_cond_init (); результаты ссылки на этот объект после его разрушения не определены. Нет никакой опасности в разрушении инициализированной условной переменной, по которой не заблокирован в данный момент ни один поток. Попытка же разрушить условную переменную, по которой заблокированы в данный момент другие потоки, может привести к неопределенному поведению. Функция pthread_cond_init () используется для инициализации условной переменной, адресуемой параметром cond, объектом атрибутов, адресуемым параметром attr. Если параметр attr содержит значение NULL, для инициализации применяются атрибуты условной переменной, действующие по умолчанию, т.е. результат в этом случае равносилен передаче адреса объекта, содержащего стандартные атрибуты условной переменной. После успешной инициализации условная переменная становится инициализированной. Для осуществления синхронизации используется только сама условная переменная cond. Результат ссылки на копии переменной cond в обращениях к функциям pthread_cond_wait(), pthread_cond_timedwait(), pthread_cond_signal(), Pthread__cond_broadcast () и pthread_cond_destroy () неопределен. Попытка инициализировать уже инициализированную условную переменную приведет к неопределенному поведению. Если атрибуты условной переменной, действующие по умолчанию, заранее определены, для инициализации условных переменных, которые создаются статически, можно использовать макрос PTHREAD_COND_INITIALIZER. Результат в этом случае эквивалентен динамической инициализации путем вызова функции Pthread__cond_init () с параметром attr, равным значению NULL, но без проверки на наличие ошибок.
566 Приложение Б Возвращаемые значения При успешном завершении функции pthread_cond__destroy () Hpthread_cond_init () возвращают нулевое значение; в противном случае— код ошибки, обозначающий ее характер. Проверка на наличие ошибок с кодами [EBUSY] и [EINVAL] реализована так (если реализована вообще), как будто она выполняется в самом начале работы каждой функции, и код ошибки в случае ее обнаружения возвращается до модификации состояния условной переменной, заданной параметром cond. Ошибки Функция pthread_cond_destroy () может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку разрушить объект, адресуемый параметром cond, который относится к другому потоку (например, при использовании в функциях pthread_cond_wait () или pthread_cond_timedwait ()); [EINVAL] значение, заданное параметром cond, недействительно. Функция pthread_cond_init () завершится неудачно, если: [EAGAIN] система испытывает недостаток в ресурсах (не имеется в виду память), необходимых для инициализации еще одной условной переменной; [ENOMEM] для инициализации условной переменной недостаточно существующей памяти. Функция pthread_cond_init () может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку повторно инициализировать объект условной переменной, адресуемый параметром cond, которой был ранее инициализирован, но еще не разрушен; [ ЕINVAL ] значение, заданное параметром a t fcr, недействительно. Примеры Условную переменную можно разрушить сразу после того, как будут запущены все потоки, заблокированные по ней. Рассмотрим, например, следующий код. 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);
Приложение Б 567 while ((ер = find_elt (1, к) ! = NULL) && ep->busy) pthread_cond_wait (&ep->notbusy, &lp->lm); if (ep != NULL) ep->busy = 1; pthread_mutex_unlock (&lp->lm); return (ep); 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 (ep) ; } В этом примере условную переменную и ее элемент списка можно освободить (строка В) сразу после того, как все потоки, ожидающие соответствующего значения условной переменной, будут "разбужены" (строка А), поскольку мьютекс и этот код гарантируют, что никакой другой поток не сможет ссылаться на удаляемый элемент. Замечания по использованию Отсутствуют. Логическое обоснование См. раздел "Логическое обоснование" в описании функции pthread_mutex_init (). Будущие направления Отсутствуют. Смотри также pthread_cond_broadcast (), pthread_cond_signal (), pthread_cond_timedwait (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_cond_destroy() и pthread_cond_init () отмечены как часть опции Threads. Раздел "Описание" был отредактирован путем применения интерпретации IEEE pASC Interpretation 1003.1c #34. В целях согласования со стандартом ISO/ТЕС 9899: 1999 в прототип функции Pthread—cond_init ^) было добавлено ключевое слово restrict
568 Приложение Б Имя pthread_cond_timedwait, pthread_cond_wait — функции ожидания условия. Синопсис THR #include <pthread.h> int pthread_cond_timedwait ( pthread__cond_t *restrict cond, pthread__mutex_t *restrict mutex, const struct timespec *restrict abstime); int pthread_cond_wait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); Описание Функции pthread_cond_timedwait () и pthread_cond_wait () используются для блокирования потоков по условной переменной. Они вызываются с использованием мьютекса mutex, блокируемого вызывающим потоком; в противном случае результирующее поведение не определено. Эти функции автоматически освобождают мьютекс mutex и обеспечивают блокирование вызывающего потока по условной переменной cond; "автоматически" здесь означает "автоматический доступ к мьютексу со стороны другого потока с последующим доступом к условной переменной". Другими словами, если какой-то другой поток может получить мьютекс после его освобождения вызывающим потоком, то результат последующего вызова функции pthread_cond_broadcast () или pthread_cond_signal () в этом (другом) потоке будет таким, как если бы он имел место после блокирования вызывающего потока. При успешном выполнении мьютекс будет заблокирован, а владеть им будет вызывающий поток. При использовании условных переменных всегда существует булев предикат, совместно используемый этими переменными, которые связаны с каждым ожидаемым условием. Это условие становится истинным, если поток должен продолжать выполнение. При использовании функций pthread_cond_timedwait () или pthread_cond_wait () возможны фиктивные запуски. Поскольку возврат из этих функций не подразумевает ничего, кроме оценки значения упомянутого выше предиката, он должен вычисляться после каждого такого выхода из функции. Результат использования нескольких мьютексов для параллельно выполняемых операций pthread_cond_timedwait () или pthread_cond_wait () по одной и той же условной переменной не определен; другими словами, условная переменная связывается с уникальным мьютексом, когда поток ожидает заданного значения условной переменной, и это (динамическое) связывание завершится вместе с завершением ожидания. Ожидание условия (синхронизированное или нет) представляет собой "точку отмены". Если статус возможности анулирования для потока соответствует значению PTHREAD_CANCEL_DEFERRED, побочным эффектом действий, выполняемых по запросу на аннулирование во время ожидания условия будет повторный захват мьютек-
Приложение Б 569 са перед вызовом первого обработчика запроса на отмену. Другими словами, результат будет выглядеть так, как если бы поток был разблокирован и получил возможность выполниться до точки выхода из вызова функции pthread_cond__timedwait () или pthread_cond_wait (), но в этой точке "обнаружил" запрос на отмену и вместо возврата к инициатору вызова функции pthread_cond_timedwait () или pthread_cond_wait () приступил к выполнению действий по аннулированию, которые включают вызов обработчиков этого запроса. Поток, который был разблокирован по причине отмены в то время, пока он был заблокирован в вызове функции pthread_cond__timedwait () или pthread_cond_wait (), не будет использовать условный сигнал, который можно направить параллельно на условную переменную, если существуют другие потоки, заблокированные по этой условной переменной. Функция pthread_cond_timedwait () эквивалентна функции pthre- ad_cond_wait (), за исключением того, что она возвращает код ошибки, если абсолютное время, заданное параметром abstime, наступит (т.е. системное время станет равным или превысит значение abstime) до того, как будет передано (с помощью сигнала) условие cond, или если абсолютное время, заданное параметром abstime, уже наступило в момент вызова. CS Если поддерживается опция Clock Selection, условная переменная будет иметь атрибут часов, определяющий механизм, который предназначен для измерения времени, заданного параметром abstime. По истечении заданного времени функция pthread_cond_timedwait () освободит и снова захватит мьютекс, адресуемый параметром mutex. Функция pthread_cond_timedwait () также представляет собой точку отмены. Если потоку, ожидающему значения условной переменной, передается сигнал, то при возврате из обработчика сигнала поток возобновит ожидание этой условной переменной (как будто не было никакого прерывания на обработку сигнала) или возвратит нуль вследствие фиктивного запуска. Возвращаемые значения За исключением кода ошибки [ETIMEDOUT], все проверки на наличие ошибок реализованы так, как если бы они были выполнены в самом начале работы каждой функции, и код ошибки в случае ее обнаружения возвращается до модификации состояния мьютекса, заданного параметром mutex, или условной переменной, заданной параметром cond. При успешном завершении возвращается нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_cond_timedwait () завершится неудачно, если: IMEDOUT] время, заданное параметром abstime, наступило. Функции pthread__cond_timedwait () и pthread_cond_wait () могут заверяться неудачно, если:
570 Приложение Б [EINVAL] значение, заданное хотя бы одним из параметров cond, mutex или abstime, недействительно; [EINVAL] для выполнения параллельных операций pthread_cond_timedwait () или pthread_cond_wait () по одной и той же условной переменной были задействованы различные мьютексы; [ EPERM ] во время вызова любой из функций мьютексом не владел текущий поток. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Семантика ожидания по условию Важно отметить, что, когда функции pthread_cond_wait () и pthread_cond_timedwait () завершаются без ошибки, соответствующий предикат может все еще иметь ложное значение. Аналогично, когда функция pthread_cond_timedwait () возвращается с ошибкой истечения времени ожидания, соответствующий предикат может иметь истинное значение из-за неизбежной "гонки" между истечением периода ожидания и изменением состояния предиката. В некоторых реализациях, в частности мультипроцессорных, иногда возможно пробуждение сразу нескольких потоков, если сигнал об изменении состояния условной переменной генерируется одновременно на различных процессорах. В общем случае при каждом завершении ожидания по условию поток должен оценивать значение предиката, связанного с ожиданием по условию, чтобы узнать, может ли он безопасно продолжать выполнение, ожидать или объявить тайм-аут. Возврат из состояния ожидания не означает, что соответствующий предикат имеет конкретное значение (ЛОЖЬ или ИСТИНА). Поэтому рекомендуется ожидание по условию выражать в коде, эквивалентном циклу "while", который выполняет проверку предиката. Семантика ожидания по времени Абсолютное время было выбрано для задания параметра лимита времени по двум причинам. Во-первых, несмотря на то, что измерение относительного времени нетруД" но реализовать в начале функции, для которой задается абсолютное время, с заданием абсолютного времени в начале функции, которая определяет относительное время, связано условие "гонок". Предположим, например, что функция clock_gettime () возвращает текущее время, а функция cond_relative_timed_wait () использует относительное время.
Приложение Б 571 clock_gettime (CLOCK_REALTIME, &now) reltime = sleep_til_this_absolute_time -now; cond_relative_timed_wait (C/ m/ &reltime) ; Если поток выгружается между первой и последней инструкциями, поток блокируется слишком надолго. Однако блокирование несущественно, если используется абсолютное время. Кроме того, абсолютное время не нужно пересчитывать, если оно используется в цикле несколько раз. Для случаев, когда системные часы работают дискретно, можно предполагать, что реализации обработают любые ожидания по времени, истекающие в промежутке между дискретными состояниями, так, как если бы нужное время уже наступило. Аннулирование потока и ожидание по условию Ожидание по условию, синхронизированное или нет, является точкой отмены (аннулирования) потока. Другими словами, функции pthread_cond_wait () или pthread_cond_timedwait () представляют собой точки, в которых обнаружен необработанный запрос на отмену. Дело в том, что в этих точках возможно бесконечное ожидание, т.е. какое бы событие ни ожидалось, даже при совершенно корректной программе оно может никогда не произойти; например, входные данные, получения которых ожидает программа, могут быть никогда не отправлены. Сделав же ожидание по условию точкой отмены, поток можно безопасно аннулировать и выполнить соответствующие обработчики даже в случае, если программа "увязнет" в бесконечном ожидании. Побочный эффект обработки запроса на отмену потока в случае, когда он заблокирован по условной переменной, состоит в повторном захвате мьютекса до вызова любого из обработчиков. Это позволяет гарантировать, что обработчик запроса на отмену выполняется в таком же статусе, который имеет критический код, расположенный до и после вызова функции ожидания по условию. Это правило также требуется соблюдать при взаимодействии с POSIX-потоками, написанными на таких языках программирования, как Ada или C++, причем здесь можно организовать отмену потоков с использованием встроенного в язык механизма исключительных ситуаций. Это правило гарантирует, что каждый обработчик исключения, защищающий критический раздел, всегда может безопасно отталкиваться от следующего факта: связанный мьютекс уже заблокирован, независимо от того, в каком именно месте критического раздела было сгенерировано исключение. Без этого правила обработчики исключительных ситуаций не могли бы единообразно выполнять свою работу в отношении блокировки, и поэтому кодирование стало бы весьма громоздким. Следовательно, поскольку в случае, когда запрос на отмену приходит во время ожидания, в отношении состояния блокировки должна быть выполнена определенная инструкция, при этом должно быть выбрано такое определение, которое сделает кодирование приложения наиболее удобным и свободным от ошибок. При выполнении действий, связанных с получением запроса на отмену потока в то время, когда он заблокирован по условной переменной, реализация требует гарантии, что поток не будет использовать ни один из условных сигналов, направленных на условную переменную, если существуют другие потоки, ожидающие сигнала по этой условной переменной. Соблюдение этого правила позволяет избежать условий взаимоблокировки, которые могут возникнуть в случае, если два независимых запроса (один действует в потоке, а другой связан с условной переменной) не были обработаны независимо.
572 Приложение Б Быстродействие мьютексов и условных переменных Предполагается, что мьютексы должны блокироваться только для нескольких инструкций. Такая практика почти автоматически вытекает из желания программистов избегать длинных последовательностей программных инструкций (которые способны снизить общую эффективность параллелизма). При использовании мьютексов и условных переменных всегда пытаются обеспечить последовательность, которая считается обычным случаем: заблокировать мьютекс, получить доступ к общим данным и разблокировать мьютекс. Ожидание по условной переменной — относительно редкая ситуация. Например, при реализации блокировки чтения-записи коду, который получает блокировку чтения, обычно нужно лишь инкрементировать счетчик считывающих потоков (при взаимном исключении доступа). Вызывающий поток будет реально ожидать по условной переменной только тогда, когда уже существует активный записывающий поток. Поэтому эффективность операции синхронизации связана с "ценой" блокировки- разблокировки мьютекса, а не с ожиданием по условию. Обратите внимание на то, что в обычном случае переключения контекста не происходит. Из вышесказанного отнюдь не следует, что эффективность ожидания по условию не важна. Поскольку существует потребность по крайней мере в одном переключении контекста на рандеву (взаимодействие между параллельными процессами), то эффективность ожидания по условию также важна. Цена ожидания по условной переменной должна быть намного меньше минимальной цены одного переключения контекста и времени, затрачиваемого на разблокировку и блокировку мьютекса. Особенности мьютексов и условных переменных Было предложено отделить захват и освобождение мьютекса от ожидания по условию. Но это предложение было отклонено, по причине "сборной природы" этой операции, которая в действительности упрощает реализации реального времени. Такие реализации могут незаметно перемещать высокоприоритетный поток между условной переменной и мьютексом, тем самым предотвращая излишние переключения контекстов и обеспечивая более детерминированное владение мьютексом при получении сигнала ожидающим потоком. Таким образом, вопросы равнодоступности и приоритетности могут быть решены непосредственно самой дисциплиной планирования. К тому же, широко распространенная операция ожидания по условию соответствует существующей практике. Планирование поведения мьютексов и условных переменных Примитивы (базовые элементы) синхронизации, которые могут противоречить используемой стратегии планирования путем установки "своего" правила упорядочения, считаются нежелательными. Выбор среди потоков, ожидающих освобождения мьютексов и условных переменных, происходит в порядке, который зависит именно от стратегии планирования, а не от какой-то другой дисциплины, устанавливающей некий фиксированный порядок (имеется в виду, например, FIFO-дисциплина или учет приоритетов). Таким образом, только стратегия планирования определяет, какой поток (потоки) будет запущен для продолжения работы.
Приложение Б 573 Синхронизированное ожидание по условию функция pthread_cond_timedwait () позволяет приложению прервать ожидание наступления конкретного условия после истечения заданного интервала времени. Рассмотрим следующий пример. (void) pthread_inutex_lock (&t. mn) ; t.waiters++; clock_gettime (CLOCK_REALTIME, &ts); ts.tv_sec += 5; re = 0; while (! mypredicate (&t) && re == 0) re = pthread_cond_timedwait (&t.cond, &t.mn, &ts) ; t.waiters--; if (re == 0) setmystate (&t); (void) pthread_mutex_unlock (&t.mn); Абсолютный параметр времени ожидания позволяет не пересчитывать его значение каждый раз, когда программа проверяет значение предиката блокирования. Если бы время ожидания было задано относительной величиной, соответствующий пересчет пришлось бы делать перед каждым вызовом функции. Это было бы особенно трудно сделать, поскольку такому коду пришлось бы учитывать возможность дополнительных запусков вследствие дополнительной сигнализации по условной переменной, которые могут происходить до того, как предикат станет истинным или истечет время ожидания. Будущие направления Отсутствуют. Смотри также pthread_cond_signal (), pthread_cond_broadcast (), том Base Definitions стандарта IEEEStd 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_cond_timedwait () и pthread_cond_wait () отмечены как часть опции Threads. К описанию прототипа функции pthread_cond_wait () был приложен список опечаток Open Group Corrigendum U021/9. Для согласования со стандартом IEEE Std 1003.1J-2000 раздел "Описание" был отредактирован путем добавления семантики для опции Clock Selection. В раздел "Ошибки" внесен еще один код ошибки [EPERM] в ответ на включение интерпретации IEEE PASC Interpretation 1003.1c #28. В Целях согласования со стандартом ISO/IEC 9899: 1999 в прототипы функций Ptnread__cond_timedwait () и pthread_cond_wait () было добавлено ключевое слово restrict.
574 Приложение Б Имя pthread_condattr_destroy, pthread_condattr_init — функции разрушения и инициализации объекта атрибутов условной переменной. Синопсис THR #include <pthread.h> int pthread_condattr_destroy (pthread_condattr_t *attr); int pthread_condattr_init (pthread_condattr_t *attr); Описание Функция pthread_condattr_destroy () используется для разрушения объекта атрибутов условной переменной, в результате чего он становится неинициализированным. В конкретной реализации функция pthread_condattr_destroy () может устанавливать объект, адресуемый параметром a t tr, равным недействительному значению. Разрушенный объект атрибутов attr можно снова инициализировать с помощью функции pthread_condattr_init (); результаты ссылки на этот объект после его разрушения не определены. Функция pthread_condattr_init () предназначена для инициализации объекта атрибутов условной переменной attr значением, действующим по умолчанию для всех атрибутов, определенных конкретной реализацией. Если функция pthread_condattr_init () вызывается для уже инициализированного объекта атрибутов attr, то результаты вызова этой функции не определены. После того как объект атрибутов условной переменной уже был использован для инициализации одной или нескольких условных переменных, любая функция, которая оказывает влияние на объект атрибутов (включая деструктор), никак не отразится на ранее инициализированных условных переменных. Этот том стандарта IEEE Std 1003.1-2001 требует наличия двух атрибутов: clock и process-shared. Дополнительные атрибуты, их значения по умолчанию и имена соответствующих функций доступа, которые считывают и устанавливают эти значения атрибутов, определяются конкретной реализацией. Возвращаемые значения При успешном завершении функции pthread_condattr_destroy О Hpthread_condattr_init () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_condattr_destroy () может завершиться неудачно, если: [ EINVAL] значение, заданное параметром a t tr, недействительно.
Приложение Б 575 Функция pthread_condattr_init () завершится неудачно, если: [ENOMEM] для инициализации объекта атрибутов условной переменной недостаточно существующей памяти. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование См. описание функций pthread_attr_init () и pthread_mutex_init (). Атрибут process-shared был определен для условных переменных по той же причине, что и для мьютексов. Будущие направления Отсутствуют. Смотри также pthread_attr_destroy (), pthread_cond_destroy (), pthread_condattr_getpshared(), pthread_create(), pthread_mutex_destroy (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread__condattr_destroy() и pthread_condattr_init () отмечены как часть опции Threads.
576 Приложение Б Имя pthread_condattr_getpshared, pthread_condattr_setpshared — функции считывания и установки атрибута условной переменной process-shared. Синопсис THRTSH #include <pthread.h> int pthread_condattr_getpshared ( const pthread_condattr_t *restrict attr, int *restrict pshared); int pthread_condattr_setpshared ( pthread_condattr__t *attr, int pshared) ; Описание Функция pthread_condattr_getpshared () используется для получения значения атрибута process -shared из объекта атрибутов, адресуемого параметром attr. Функция pthread__condattr_setpshared () позволяет установить атрибут process-shared в инициализированном объекте атрибутов, адресуемом параметром attr. Атрибут process-shared устанавливается равным значению PTHREAD_PROCESS__ SHARED, чтобы разрешить использование условной переменной любым потоком, имеющим доступ к области памяти, в которой она размещена, даже если эта область памяти разделяется несколькими процессами. Если же атрибут process-shared равен значению PTHREAD_PROCESS_PRIVATE, условная переменная должна использоваться только потоками, созданными в одном процессе с потоком, который ее инициализировал; если с этой условной переменной попытаются работать потоки из различных процессов, поведение такой программы не определено. По умолчанию для этого атрибута устанавливается значение PTHREAD_PROCESS__PRIVATE. Возвращаемые значения При успешном завершении функция pthread_condattr_setpshared() возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. При успешном завершении функция pthread_condattr_getpshared() возвращает нулевое значение и сохраняет считанное значение атрибута process-shared объекта attr в объекте, адресуемом параметром pshared; в противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функции pthread_condattr__getpshared () и pthread_condattr_setpshared () могут завершиться неудачно, если: [EINVAL] значение, заданное параметром attr, недействительно.
Приложение Б 577 функция pthread_condattr_setpshared () может завершиться неудачно, если: [EINVAL] новое значение, заданное для атрибута, не попадает в диапазон значений, действительных для этого атрибута. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_create (), pthread_cond_destroy (), pthread_condattr_destroy (), pthread__mutex_destroy (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_condattr_getpshared () и pthread_condattr_setpshared () отмечены как часть опций Threads и Thread Process-Shared Synchronization. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread__condattr_getpshared () было добавлено ключевое слово restrict.
578 Приложение Б Имя pthread_create — функция создания потока. Синопсис THR #include <pthread.h> int pthread_create (pthread_t *restrict thread, const pthread_attr_t *restrict attr, void * (*start_routine) (void*), void *restrict arg); Описание Функция pthread_create () используется для создания в процессе нового потока с атрибутами, заданными параметром attr. Если значение параметра attr равно NULL, используются атрибуты, действующие по умолчанию. Если атрибуты, заданные параметром attr, будут модифицироваться позже, то на атрибуты уже созданного потока это не повлияет. При успешном завершении функция pthread_create () сохраняет ID- значение созданного потока в области памяти, адресуемой параметром thread. При создании потока выполняется функция s tart_rou tine, которая вызывается с единственным аргументом агд. Если функция start_rou tine выполнится до конца, то результат будет таким, как если бы было сделано явное обращение к функции pthread_exit (), использующей в качестве состояния выхода (exit status) значение, возвращаемое функцией start_rou tine. Обратите внимание на то, что поток, в котором изначально вызывалась функция main(), отличается от функции s tart_rou tine. При выходе из функции main () результат будет таким, как если бы было сделано явное обращение к функции exit (), использующей в качестве состояния выхода значение, возвращаемое функцией main (). Статус сигналов для нового потока будет инициализирован следующим образом: • маска сигналов будет унаследована от создающего потока; • множество необработанных сигналов для нового потока будет пустым. Среда обработки данных с плавающей точкой будет унаследована от создающего потока. При неудачном выполнении функции pthread_create () поток не создается, а содержимое области, адресуемое параметром thread, остается неопределенным. ТСТ Если определено значение _POSIX_THREAD_CPUTIME, новый поток получит доступ к таймеру центрального процессора (CPU-time clock), и начальное значение для этих часов будет установлено равным нулю. Возвращаемое значение При успешном завершении функция pthread_create () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер.
Приложение Б 579 Ошибки функция pthread_create () завершится неудачно, если: [EAGAIN] в системе недостаточно ресурсов, необходимых для создания еще одного потока, или был превышен предел ({PTHREAD_THREADS_MAX}), установленный в системе для общего количества потоков в процессе; [EINVAL] значение, заданное параметром a t tr, недействительно; [EPERM] инициатор вызова не имеет соответствующего разрешения на установку требуемых параметров планирования или стратегии планирования. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование В качестве альтернативного решения для функции pthread_create () предлагалось определить две отдельные операции: "создать" и "запустить". Для некоторых приложений такое поведение было бы более естественным. В среде Ada, в частности, отделено "создание" задачи от ее "активизации". Разбиение этой операции на две части разработчиками стандарта было отклонено по нескольким причинам. • Количество вызовов, требуемых для запуска потока, в этом случае возросло бы от одного до двух, что, таким образом, возложило бы излишние расходы на приложения, которым не нужна дополнительная синхронизация. Однако второго вызова можно было бы избежать за счет усложнения атрибута состояния запуска. • Для потока пришлось бы вводить дополнительное состояние, которое можно определить как "созданный, но не активизированный". Это потребовало бы введения стандарта для определения поведения операций потока в случае, когда поток еще не начал выполняться. • Для приложений, которым подходит именно такое поведение, можно сымитировать два отдельных действия с использованием существующих средств. Функцию start_routine () можно синхронизировать путем организации ожидания по условной переменной, сигнализируемой операцией активизации потока. При реализации Ada-приложений можно создавать потоки в любой из двух точек ^da-программы: при создании объекта задачи или при ее активизации. В случае принятия первого варианта функции start_routine () пришлось бы ожидать по условной переменной получения "приказа" начать активизацию. Второй вариант не требу- ет использования условной переменной или дополнительной синхронизации. В лю-
580 Приложение Б бом случае при создании объекта задачи потребовалось бы создание отдельного блока управления Ada-задачей, чтобы поддерживать рандеву-очереди. Расширение упомянутой модели позволило бы модифицировать состояние потока между созданием и активизацией, и, следовательно, удалить объект атрибутов потока. Это предложение было отвергнуто по таким причинам. • Должна существовать возможность установки любого состояния в объекте атрибутов потока. Это потребовало бы определения функций для модификации атрибутов потока, что не уменьшило бы количество вызовов, необходимых для установки потока. На самом деле для приложения, которое создает все потоки с использованием идентичных атрибутов, количество вызовов функций, необходимых для установки потоков, резко бы возросло. Использование объектов атрибутов потока позволяет приложению создать один набор вызовов функций установки атрибутов. В противном случае набор вызовов функций установки атрибутов пришлось бы делать для создания каждого потока. • В зависимости от архитектурного решения функции установки состояния потока потребовали бы вызовов функций ядра системы или (по каким-то иным причинам) не могли быть реализованы как макросы, что увеличило бы расходы ресурсов на создание потока. • Была бы утеряна возможность "классовой" организации потоков для приложений. Предлагалась еще одна альтернатива, в которой рассматривалось использование модели, аналогичной созданию процессов, — "разветвление потока". Семантика разветвления обеспечивала бы большую гибкость, и функцию создания можно было реализовать в виде простого разветвления потока, за которым немедленно следовал вызов требуемой "запускающей" функции. Этот вариант имел такие недостатки. • Для многих реализаций внутренний стек вызывающего потока пришлось бы дублировать, поскольку во многих архитектурах нет возможности определить размер вызывающего фрейма. • Эффективность снизилась бы, поскольку пришлось бы копировать по крайней мере некоторую часть стека, несмотря на то, что в большинстве случаев после вызова нужной "запускающей" функции потоку уже не требуется скопированный контекст. Будущие направления Отсутствуют. Смотри также fork (), pthread_exit () , pthread_join (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширением POSIX Threads Extension.
Приложение Б 581 Issue 6 Функция pthread_create () отмечена как часть опции Threads. В результате согласования со спецификацией Single UNIX Specification был добавлен обязательный код ошибки [EPERM]. С целью согласования со стандартом IEEE Std 1003.1d-1999 для потока была добавлена семантика таймера центрального процессора. Для согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_create () было добавлено ключевое слово restrict. В раздел "Описание" внесено явное утверждение о том, что среда обработки данных с плавающей точкой наследуется от создающего потока.
582 Приложение Б Имя pthread_detach — функция отсоединения потока. Синопсис THR #include <pthread.h> int pthread_detach (pthread_t thread); Описание Функция pthread_detach () уведомляет реализацию о том, что область памяти для потока thread может быть восстановлена, когда он завершит выполнение. Если поток не завершается, функция pthread_detach () не служит причиной для его завершения. Результат нескольких вызовов функции pthread_detach () для одного и того же потока не определен. Возвращаемое значение При успешном завершении функция pthread_detach () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_detach () завершится неудачно, если: [EINVAL] реализация обнаружила, что значение, заданное параметром thread, не относится к присоединенному потоку; [ESRCH] не был найден ни один поток, соответствующий заданному идентификационному номеру потока ID. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Функции pthread_join() или pthread_detach () должны вызываться для каждого потока, который создается, чтобы можно было снова использовать область памяти, связанную с потоком.
Приложение Б 583 Высказывалось мнение о необязательности использования функции pthread_detach (): поскольку поток никогда динамически не отсоединяется, то достаточно использовать атрибут создания потока detachstate. Однако необходимость в этой функции возникает по крайней мере в двух случаях. 1. В обработчике запроса на отмену для функции присоединения потока (pthread_join()) важно иметь функцию pthread_detach(), чтобы отсоединить поток. Без нее обработчик вынужден был бы выполнить еще раз функцию pthread_j oin (), чтобы попытаться отсоединить поток, который не только задерживает процедуру отмены в течение неограниченного времени, но и вносит новый вызов функции рthread_join(). В этом случае есть смысл говорить о динамическом отсоединении. 2. Чтобы отсоединить "исходный поток" (это может понадобиться в процессах, которые создают потоки сервера). Будущие направления Отсутствуют. Смотри также pthread_j oin (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread. h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширением POSIX Threads Extension. Issue 6 Функция pthread_detach () отмечена как часть опции Threads.
584 Приложение Б Имя pthread_exit — функция завершения потока. Синопсис THR #include <pthread.h> void pthread_exit (void *value_ptr); Описание Функция pthread_exit () завершает вызывающий поток и делает значение value__ptr доступным для успешного присоединения к завершающему потоку. Любые обработчики отмены, которые были помещены в стек, но еще не извлечены из него, будут извлечены в порядке, обратном тому, в котором они помещались туда, а затем выполнены. Если потоку принадлежат данные, то после выполнения всех обработчиков отмены будут вызваны соответствующие функции деструкторов (в неопределенном порядке). При завершении потока ресурсы процесса, включая мьютексы и дескрипторы файлов, не освобождаются, и не выполняются какие бы то ни было "восстановительные" действия уровня процесса, включая вызовы любых функций at exit (), какие только могут существовать. Когда из функции запуска возвращается поток, отличный от того, в котором была изначально вызвана функция main (), делается неявное обращение к функции pthread_exit (). Значение, возвращаемое этой функцией, служит в качестве состояния выхода этого потока. Поведение функции pthread_exit () не определено, если она вызвана из обработчика запроса на отмену потока или функции деструктора, к которой было сделано обращение в результате явного или неявного вызова функции pthread_exit (). После завершения потока результат доступа к локальным переменным потока не определен. Таким образом, ссылки на локальные переменные существующего потока не следует использовать для функции pthread_exit () в качестве значения параметра \ralue_ptr. После завершения процесс будет иметь состояние выхода, равное нулю, после того, как завершится его последний поток. Поведение при этом будет таким, как если бы во время завершения потока была вызвана функция exit () с нулевым аргументом. Возвращаемое значение Функция pthread_exit () не возвращается к инициатору ее вызова. Ошибки Ошибки не определены.
Приложение Б 585 Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Нормальный механизм завершения потока состоит в возвращении из функции, которая была задана в вызове функции pthread_create (). Функция pthread_exit () обеспечивает возможность завершения потока без обязательного выхода из стартовой функции этого потока и, следовательно, служит аналогом функции exit (). Независимо от метода завершения потока любые обработчики отмены, которые были помещены в стек, но еще не извлечены из него, будут выполнены, а также вызваны деструкторы для любых существующих данных потока. Этот том стандарта IEEE Std 1003.1-2001 требует, чтобы обработчики отмены извлекались из стека и выполнялись по порядку. После выполнения всех обработчиков отмены для каждого элемента потоковых данных вызываются соответствующие функции деструкторов (в неопределенном порядке). Такая последовательность действий обязательна, поскольку обработчики отмены могут использовать данные потока. Поскольку значение состояния выхода определяется приложением (за исключением случаев, когда поток был отменен, т.е. в случаях отмены используется значение PTHREAD_CANCELED), реализации не известно, что следует понимать под недействительным значением состояния, поэтому проверка на наличие ошибок не выполняется. Будущие направления Отсутствуют. Смотри также exit (), pthread_create (), pthread_j oin (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширением POSIX Threads Extension. Issue 6 Фу нкция pthread_exit () отмечена как часть опции Threads.
586 Приложение Б Имя pthread_getconcurrency, pthread_setconcurrency — функции считывания и установки уровня параллелизма. Синопсис XSI #include <pthread.h> int pthread__getconcurrency (void) ; int pthread_setconcurrency (int new_level); Описание Несвязанные потоки в процессе выполняются (или не выполняются) одновременно. По умолчанию реализация потоков гарантирует активность достаточного количества потоков для того, чтобы процесс мог успешно продолжать выполнение. И хотя такой подход сохраняет системные ресурсы, он может не обеспечить наиболее эффективный уровень параллелизма. Функция pthread_setconcurrency () позволяет приложению с помощью параметра new_level информировать реализацию потоков о желаемом уровне параллелизма. Реальный же уровень параллелизма, обеспечиваемый реализацией в результате вызова этой функции, не определен. Если значение параметра new_level равно нулю, это означает, что реализация должна поддерживать уровень параллелизма таким, как если бы функция 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 () возвращает нуль.
Приложение Б 587 Ошибки Функция pthread_setconcurrency () завершится неудачно, если: [EINVAL] значение, заданное параметром new_level, отрицательно; [EAGAIN] значение, заданное параметром new_level, приводит к перерасходу системных ресурсов. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Использование этих функций изменяет состояние базового уровня параллелизма, от которого зависит работа приложения. Разработчикам библиотек рекомендуется не использовать функции pthread_getconcurrency() и pthread_setconcurrency(), поскольку это может привести к конфликту с их использованием в приложении. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также Том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5.
588 Приложение Б Имя pthread_getschedparam, pthread_setschedparam — функции динамического доступа к параметрам стратегии планирования потока (REALTIME THREADS). Синопсис THRTPS #include <pthread.h> int pthread_getschedparam ( pthread_t thread, int *restrict policy, struct sched_param ^restrict param),- int pthread_setschedparam ( pthread_t thread, int policy, const struct sched_param *param); Описание Функции pthread_getschedparain() и pthread_setschedparam() используются для считывания и установки соответственно значений стратегии планирования и параметров отдельных потоков многопоточного процесса. Для значений стратегии планирования SCHED_FIFO и SCHEDJRR в структуре sched_param должен быть установлен только один ее член sched__priority (уровень приоритета). Для значения SCHED_OTHER параметры планирования определяются реализацией. Функция pthread_getschedparain() предназначена для считывания значения стратегии планирования и параметров планирования для потока, идентификационный номер (ID) которого задан параметром thread. Считанные значения сохраняются в параметрах policy и рагат. Функция pthread_getschedparam () возвращает значение приоритета, установленное в результате самого последнего вызова функций pthread_setschedparam(), pthread_setschedprio() или pthread_create () для данного потока. Она не отражает никаких временных корректировок, вносимых в значение приоритета в результате выполнения других функций. Функция pthread_setschedparam() устанавливает для потока, ID которого задан параметром thread, стратегию планирования и соответствующие параметры планирования равными значениям параметров policy и рагат соответственно. Параметр policy может иметь значения SCHED_OTHER, SCHED_FIFO или SCHED_RR- Параметры планирования для стратегии планирования, заданной значением SCHED_OTHER, определяются реализацией. Для стратегии планирования, задаваемой значениями SCHED_FIFO и SCHED_RR, используется только один параметр priori ty. TSP Если определено значение _POSIX_THREAD_SPORADIC_SERVER, аргумент policy может иметь значение SCHEDJSPORADIC (за исключением функции pthread_setschedparam()). Если стратегия планирования в момент вызова этой функции не соответствовала значению SCHED_SPORADIC, то поддержка этого значения определяется реализацией, т.е. реализация может не
Приложение Б 589 позволить приложению динамически изменять стратегию планирования, устанавливая ее равной значению SCHED_SPORADIC. Для стратегии планирования, определяемой значением SCHED_SPORADIC, устанавливаются такие параметры: sched_ss_low_priority, sched_ss__repl__period, sched_ss_init_budget, sched_priority и sched_ss_max_repl. Для успешного выполнения функции установки параметров значение параметра sched_ss_repl__period должно быть больше или равно значению sched_ss_init_budget; в противном случае функция завершится неудачно. Кроме того, для успешного выполнения этой функции значение параметра sched_ss_max_repl должно попадать во включающий диапазон [ 1 / SS_REPL_MAX]; в противном случае функция завершится неудачно. При неудачном завершении функции pthread_setschedparain() параметры планирования для заданного потока изменены не будут. Возвращаемые значения При успешном завершении функции pthread_getschedparam() Hpthread_setschedparam() возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_getschedparam () может завершиться неудачно, если: [ESRCH] значение, заданное параметром thread, не относится ни к одному из существующих потоков. Функция pthread_setschedparam() может завершиться неудачно, если: [EINVAL] значение, заданное параметром policy, или значение одного из параметров планирования, связанных со значением стратегии планирования policy, недействительно; [ENOTSUP] была сделана попытка установить для стратегии планирования или ее параметров неподдерживаемые значения; 1ЬР [ENOTSUP] была сделана попытка динамически изменить стратегию планирования, установив для нее значение SCHED_SPORADIC, при том, что реализация не поддерживает такое изменение; [EPERM] инициатор вызова не имеет соответствующего разрешения устанавливать параметры планирования или стратегию планирования для заданного потока; [EPERM] реализация не позволяет приложению модифицировать один из параметров в соответствии с заданным значением; [ESRCH] значение, заданное параметром thread, не относится ни к одному из существующих потоков. эти функции не возвращают код ошибки [EINTR].
590 Приложение Б Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_setschedprio (), sched_getparam(), sched_getscheduler (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>, <sched.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_getschedparam() и pthread_setschedparam() отмечены как часть опций Threads и Thread Execution Scheduling. Код ошибки [ENOSYS] был исключен, поскольку его нет смысла учитывать, если реализация не поддерживает опцию Thread Execution Scheduling. К описанию прототипа функции pthread_setschedparam() был приложен список опечаток Open Group Corrigendum U026/2, чтобы второй аргумент этой функции имел тип int. Для согласования со стандартом IEEE Std 1003.1d-1999 было добавлено значение стратегии планирования SCHED_SPORADIC. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_getschedparam () было добавлено ключевое слово restrict. Был добавлен список опечаток Open Group Corrigendum U047/1. Была добавлена интерпретация IEEE PASC Interpretation 1003.1 #96, отмечающая, что значения приоритета также можно установить путем вызова функции pthread_setschedprio().
Приложение Б 591 Имя pthread_j о in — функция ожидания завершения потока. Синопсис THR #include <pthread.h> int pthread_join (pthread_t thread, void **value_ptr); Описание Функция pthread_join() приостанавливает выполнение вызывающего потока до тех пор, пока не завершится заданный поток (если он еще не завершился). Если после удачного возвращения из функции pthread_j oin () параметр value__ptr не равен значению NULL, значение, передаваемое функции pthread_exit () завершающимся потоком, будет доступным в области памяти, адресуемой параметром value_ptr. Успешное выполнение функции pthread_join () означает, что заданный поток завершился. Результаты нескольких одновременных обращений к функции pthread_join (), в параметрах которых задается один и тот же поток, не определены. Если поток, вызывающий функцию pthread_join(), отменен, то заданный поток не будет выгружен. Не определено, учитывается ли в значении {PTHREAD_THREADS_MAX} поток, который завершился, но остался отсоединенным. Возвращаемые значения При успешном завершении функция pthread_join () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_j oin () завершится неудачно, если: [EINVAL] реализация обнаружила, что значение, заданное параметром thread, не относится ни к одному из присоединенных потоков; LESRCH] не найден ни один поток, идентификационный номер которого (ID) соответствовал бы заданному потоку. Функция pthread_j oin () может завершиться неудачно, если: IEDEADLK] была обнаружена взаимоблокировка или значение параметра thread соответствует вызывающему потоку. Функция pthread_j oin () не возвращает код ошибки [EINTR]. Примеры Ниже приведен пример создания потока и его удаления.
592 Приложение Б typedef struct { int *ar; long n; } subarray; void * incer (void *arg) { long i; for (i =0; i < ((subarray *)arg) ->n; i++) ((subarray *) arg) ->ar[i]++; } int main (void) { int ar[1000000]; pthread_t thl, th2; subarray sbl, sb2; sbl.ar = &ar[0]; 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_j о in () в этот том стандарта IEEE Std 1003.1-2001 считается весьма полезным. Функция pthread_join() обеспечивает простой механизм, позволяющий приложению ожидать завершения потока. После того как поток завершится, приложение может приступать к освобождению ресурсов, которые использовались этим потоком.
Приложение Б 593 Например, после возвращения функции pthread_j oin () может быть восстановлена любая область памяти, предоставленная приложением под стек. функции pthread_join () или pthread_detach () должны в конце концов быть вызваны для каждого потока, который создается с атрибутом detachstate, равным значению PTHREAD_CREATE_JOINABLE, чтобы можно было восстановить память, связанную с потоком. Взаимодействие между функцией pthread_j oin () и механизмом отмены потока хорошо определено по следующим причинам: • функция pthread_j oin (), как и все остальные неасинхронные функции безопасной отмены потоков, можно вызывать только при возможности отложенного типа отмены. • отмена потока не может происходить в состоянии запрещения отмены. Таким образом, имеет смысл рассматривать только стандартное состояние возможности отмены. Итак, вызов функции pthread_join() либо отменяется, либо успешно завершается. Для приложения это различие очевидно, поскольку либо выполняется обработчик запроса на отмену, либо возвращается функция pthread_join (). В этом случае условия "гонок" не возникают, поскольку функция pthread_join() вызывается в состоянии отложенного запроса на отмену. Будущие направления Отсутствуют. Смотри также pthread_create (), wait (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширением POSIX Threads Extension. Issue 6 Функция pthread_j oin () отмечена как часть опции Threads.
594 Приложение Б Имя pthread_mutex_destroy, pthread_mutex_init — функции разрушения и инициализации мьютекса. Синопсис THR #include <pthread.h> int pthread_mutex_destroy (pthread_mutex__t *mutex) ; int pthread_mutex_init ( pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) ; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; Описание Функция pthread_mutex_destroy () используется для разрушения объекта мьютекса, адресуемого параметром mutex, в результате чего этот объект мьютекса становится неинициализированным. В конкретной реализации функция pthread_mutex_destroy () может устанавливать объект, адресуемый параметром mutex, равным недействительному значению. Разрушенный объект мьютекса можно снова инициализировать с помощью функции pthread_mutex_init (); результаты ссылки на этот объект после его разрушения не определены. Нет никакой опасности в разрушении инициализированного объекта мьютекса, по которому не заблокирован в данный момент ни один поток. Попытка же разрушить заблокированный мьютекс может привести к неопределенному поведению. Функция pthread_mutex_init () используется для инициализации мьютекса, адресуемого параметром mutex, объектом атрибутов, адресуемым параметром attr. Если параметр attr содержит значение NULL, для инициализации применяются атрибуты мьютекса, действующие по умолчанию, т.е. результат в этом случае равносилен передаче адреса объекта, содержащего стандартные атрибуты мьютекса. После успешной инициализации мьютекс становится инициализированным и разблокированным. Для осуществления синхронизации используется только сам объект, адресуемый параметром mutex. Результат ссылки на копии объекта mutex в обращениях к функциям pthread_mutex_lock(), pthread_mutex_trylock(), pthread_mutex_unlock() и pthread_mutex_destroy () не определен. Попытка инициализировать уже инициализированный объект мьютекса приведет к неопределенному поведению. В случаях, когда атрибуты мьютекса, действующие по умолчанию, заранее определены, для инициализации мьютексов, которые создаются статически, можно использовать макрос PTHREAD_MUTEX_INITIALIZER. Результат в этом случае эквивалентен динамической инициализации путем вызова функции pthread_mutex_init () с параметром attr, равным значению NULL, но без выполнения проверки на наличие ошибок.
Приложение Б 595 Возвращаемые значения При успешном завершении функции pthread_mutex_destroy () Hpthread_ mutex_init () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Проверка на наличие ошибок с кодами [EBUSY] и [EINVAL] реализована так (если реализована вообще), как будто она выполняется в самом начале работы каждой функции, и код ошибки в случае ее обнаружения возвращается до модификации состояния мьютекса, заданного параметром ти tex. Ошибки функция pthread_mutex_destroy () может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку разрушить объект, адресуемый параметром mutex, который относится к другому потоку (например, при использовании в функциях pthread_mutex_wait () или pthread_mutex_timedwait ()), или указанный объект заблокирован; [EINVAL] значение, заданное параметром ти tex, недействительно. Функция pthread_mutex_init () завершится неудачно, если: [EAGAIN] система испытывает недостаток ресурсов (не имеется в виду память), необходимых для инициализации еще одного мьютекса; [ENOMEM] для инициализации мьютекса недостаточно существующей памяти; [EPERM] инициатор вызова функции не имеет привилегий для выполнения этой операции. Функция pthread_mutex_init () может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку повторно инициализировать объект мьютекса, адресуемый параметром mtztex, которой был ранее инициализирован, но еще не разрушен; [EINVAL] значение, заданное параметром at tr, недействительно. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. •Логическое обоснование возможность альтернативных реализаций Данный том стандарта IEEE Std 1003.1-2001 поддерживает несколько альтернатив- Нь1Х реализаций мьютексов. Реализация может сохранять блокировку непосредствен-
596 Приложение Б но в объекте типа pthread_mutex_t. Возможно также хранение блокировки в "куче" а указателя, дескриптора или уникального ID — в объекте мьютекса. Каждая реализация обладает различными достоинствами в зависимости от определенных конфигураций оборудования. Поэтому, чтобы написать код, который не нужно будет изменять в зависимости от выбранной реализации, в данном томе стандарта IEEE Std 1003.1- 2001 жестко не определяется тип хранения блокировки и термин "инициализировать" используется для усиления утверждения о том, что блокировка может в действительности располагаться в самом объекте мьютекса. Обратите внимание на то, что это устраняет избыточность определения типа мьютекса или условной переменной. В реализации разрешается, чтобы при выполнении функции pthread_mutex_ destroy () в мьютексе хранилось недействительное значение. Это позволит выявить ошибочные программы, которые пытаются заблокировать уже разрушенный мьютекс (или по крайней мере сослаться на него). Компромисс между контролем за ошибками и производительностью Существует множество случаев, когда можно обойтись без проверки на наличие ошибок ради достижения более высокой производительности. Полнота применения контроля за ошибками должна соответствовать нуждам конкретных приложений и возможностям сред выполнения. В общем случае об ошибках или ошибочных условиях, вызванных системными причинами (например, недостаточностью памяти), необходимо уведомлять всегда, но необязательно сообщать об ошибках, связанных с некорректностью кода приложения (например, при неудачной попытке обеспечить адекватную синхронизацию, используемую при защите мьютекса от удаления). Таким образом, возможен широкий диапазон реализаций. Например, реализация, предназначенная для отладки приложений, может включать все возможные проверки ошибок, в то время как реализация, выполняющая на встроенном компьютере одно- единственное уже отлаженное приложение при очень строгих требованиях к производительности, может содержать лишь минимальный набор проверок на наличие ошибок. Более того, реализация может быть представлена даже в двух версиях подобно опциям, предоставляемым компиляторами: в версии с полным объемом проверок ошибок (но более медленной) и в версии с ограниченным объемом проверок ошибок (но более быстрой). Запретить возможность необязательности контроля за ошибками значило бы оказать пользователю медвежью услугу. Предусмотрительно ограничивая использование понятия "неопределенное поведение" только случаями ошибочных действий самого приложения (по причине недостаточно продуманного кода) и обязательно определяя ошибки, связанные с недоступностью системных ресурсов, данный том стандарта IEEE Std 1003.1-2001 гарантирует, что любое корректно написанное приложение переносимо в полном диапазоне реализаций, но не обязывает все реализации нести дополнительные затраты на проверку многочисленных условий, которые корректно написанная программа никогда не создаст. Почему не определяются предельные значения Определение символьных значений для использования в качестве максимального числа мьютексов и условных переменных рассматривалось, но было отвергнуто, поскольку количество этих объектов может изменяться динамически. Более того, мно-
Приложение Б 597 гие реализации размещают эти объекты в памяти приложения, следовательно, говорить о необходимости явного определения максимума нет никакого смысла. Статические инициализаторы для мьютексов и условных переменных Обеспечение статической инициализации статически размещаемых в памяти объектов синхронизации позволяет в модулях, содержащих закрытые статические переменные синхронизации, избежать тестирования и соответствующих затрат, связанных с динамической инициализацией. Более того, это упрощает кодирование модулей самоинициализации. Такие модули широко используются в С-библиотеках, в которых по различным причинам вместо явного вызова функций инициализации используется самоинициализация. Ниже приводится пример использования статической инициализации. Без применения статической инициализации функция самоинициализации f оо () может иметь следующий вид. 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) ; } С применением статической инициализации ту же функцию самоинициализации f оо () можно было бы закодировать таким образом. static pthread_mutex_t foo_mutex = PTHREAD_MUTEX_INITIALIZER; void foo() { pthread__mutex_lock (&f oo_mutex) ; /* Выполнение действий. */ pthread__mutex_unlock (&f oo_mutex) ; } Обратите внимание на то, что статическая инициализация устраняет необходимость в тестировании, проводимом в функции pthread_once (), и получении значения адреса &foo_mutex, передаваемого функции pthread_mutex_lock() или Pthread__mutex_unlock (). Таким образом, С-код, написанный для инициализации статических объектов, проще во всех системах и работает быстрее на большом классе систем, в которых объект (внутренней) синхронизации можно хранить в памяти приложения. До сих пор вопрос о быстродействии блокировок поднимался для машин, которые требовали, чтобы для мьютексов выделялась специальная память. В действительности в таких машинах мьютексы и, возможно, условные переменные должны были содержать Указатели на реальные аппаратные средства защиты. Для того чтобы на таких машинах работала статическая инициализация, функция pthread_mutex_lock () также должна проверять, выделена ли память для указателя на реальный объект блокировки. Если не вЬ1делена, функция pthread_mutex_lock (), прежде чем его использовать, должна его
598 Приложение Б инициализировать. Резервирование таких ресурсов можно выполнить при загрузке программы, и поэтому для мьютексов и условных переменных не были введены дополнительные коды ошибок, означающие неудачное выполнение инициализации. Такое динамическое тестирование в функции pthread_mutex_lock(), которое позволяет узнать, был ли инициализирован указатель, могло показаться на первый взгляд лишним. На большинстве компьютеров это было бы реализовано в виде считывания его значения, сравнения с нулем и использования по назначению при условии получения нужного результата сравнения. Несмотря на то что это тестирование кажется лишним, дополнительные затраты (на тестирование содержимого регистра) обычно незначительны, поскольку в действительности никакие дополнительные ссылки на память не делаются. Так как все больше и больше компьютеров оснащаются кэш-памятью (быстродействующей буферной памятью большой емкости), то реальные издержки представляют собой отработку ссылок, а не выполнение инструкций. В качестве альтернативного варианта (в зависимости от архитектуры компьютера) можно в наиболее важных случаях ликвидировать все расходы системных ресурсов на операции блокировки, которые выполняются после инициализации средств блокировки. Это можно сделать путем перехода от более затратных к редко выполняемым операциям, т.е. перенести весь "груз расходов" на однократно выполняемую инициализацию. Поскольку "внешняя" (т.е. выполняемая вне основной программы) инициализация мьютекса также означает, что для получения реальной блокировки адрес должен быть разыменовывай, один из широко применяемых методов при статической инициализации состоит в сохранении фиктивного значения для этого адреса; в частности, адреса, который вызывает сбой в работе компьютера. При возникновении такого сбоя во время первой попытки заблокировать мьютекс можно сделать проверку достоверности, а затем для реальной блокировки использовать корректный адрес. Последующие операции, связанные с блокировкой, не будут сопряжены с дополнительными расходами, поскольку они уже не являются "сбойными". Это — всего лишь метод, который можно использовать для поддержки статической инициализации, несмотря на то, что он неблагоприятно отражается на скорости захвата блокировки. Безусловно, существуют и другие методы, которые в высокой степени зависят от архитектуры компьютера. Расходы на блокировку для компьютеров, выполняющих "внешнюю" инициализацию мьютекса, сравнимы с расходами для модулей, инициализируемых неявным образом (имеются в виду те из них, где достигнута "внутренняя" инициализация мьютексов). Безусловно, "внутренняя" инициализация выполняется гораздо быстрее, но "внешняя" ненамного хуже. Помимо вопроса быстродействия блокировки, нас беспокоит то, что потоки могут соперничать за блокировки при попытке завершить инициализацию статически размещаемых в памяти мьютексов. (Такое завершение обычно включает захват внутренней блокировки, выделение памяти для структуры, сохранение указателя на эту структуру вмьютексе и освобождение внутуренней блокировки.) Во-первых, многие реализации могут сократить эту последовательность действий путем хэширования по адресу мьютекса. Во-вторых, количество таких "сериалов" может быть весьма ограниченным. В частности, их может быть столько, сколько создается статически размещаемых объектов синхронизации. Динамически же создаваемые объекты по-прежнему инициализируется с помощью функций pthread_mutex_init () или pthread_cond_init (). Наконец, если ни один из описанных выше методов оптимизации для "внешнего размещения объектов синхронизации не позволяет достичь нужной производительно-
Приложение Б 599 сти приложения при использовании определенной реализации, приложение может избежать статической инициализации, явным образом инициализируя все объекты синхронизации с помощью соответствующих функций pthread_*_init (), которые поддерживаются всеми реализациями. В документации на реализацию также могут быть описаны компромиссные решения и рекомендации относительно того, какие методы инициализации являются наиболее эффективными для данной конкретной реализации. Разрушение мьютексов Мьютекс можно разрушить сразу после разблокировки. Например, рассмотрим следующий код. struct obj { рthread_mut ex_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 (C) 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, <pthread. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с Расширением POSIX Threads Extension. Issue 6 Функции pthread_mutex_destroy() и pthread_mutex_init() отмечены как часть опции Threads. В целях согласования со стандартом IEEE Std 1003.1d-1999 в раздел "Смотри также" была добавлена функция pthread_mutex_timedlock ().
600 Приложение Б Раздел "Описание" был отредактирован путем применения интерпретации IEEE PASC Interpretation 1003.1c #34. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_mutex_init () было добавлено ключевое слово restrict.
Приложение Б 601 Имя pthread_mutex_getprioceiling, pthread_mutex__setprioceiling — функции считывания и установки предельного значения приоритета мьютекса (REALTIME THREADS). Синопсис THR #include <pthread.h> Т™ int pthread__mutex_getprioceiling ( const pthread__mutex__t ^restrict mutex, int ^restrict prioceiling) ; int pthread_mutex_setprioceiling ( pthread_mutex__t *restrict mutex, int prioceiling, int ^restrict old_ceiling); Описание Функция pthread_mutex_getprioceiling() используется для считывания текущего значения предельного приоритета мьютекса. Функция pthread_mutex_setprioceiling() сначала блокирует мьютекс, если он разблокирован, или надежно удерживает его в заблокированном состоянии, а затем изменяет значение предельного приоритета мьютекса и после этого освобождает его. При успешном изменении приоритета его предыдущее значение возвращается с помощью параметра old__ceiling. В процессе блокирования мьютекса нет необходимости привязываться к протоколу защиты приоритета. При неудачном выполнении функции pthread_mutex_setprioceiling() предельное значение приоритета мьютекса не будет изменено. Возвращаемые значения При успешном завершении функции pthread_mutex_getprioceiling() Hpthread_mutex_setprioceiling() возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функции pthread_mutex_getprioceiling () и Pthread__mutex_setprioceiling () могут завершиться неудачно, если: NVAL] приоритет, заданный параметром prioceiling, не попадает в нужный диапазон: [EINVAL] [EPERM] значение, заданное параметром mutex, не относится ни к одному из существующих мьютексов; инициатор вызова не имеет привилегий для выполнения этой операции.
602 Приложение Б Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_mutex_destroy(),pthread_mutex_lock(), pthread_mutex_timedlock (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске 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.
Приложение Б 603 Имя pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex__unlock — функции блокировки и разблокировки мьютекса. Синопсис THR #include <pthread.h> int pthread_mutex_lock (pthread_mutex_t *mutex); int pthread_mutex_trylock (pthread_mutex_t *mutex); int pthread_mutex_unlock (pthread_mutex_t *mutex); Описание Объект мьютекса, адресуемый параметром mutex, блокируется путем вызова функции pthread_mutex_lock(). Если мьютекс уже заблокирован, вызывающий поток блокируется до тех пор, пока мьютекс не станет доступным. При завершении этой операции объект мьютекса, адресуемый параметром mutex, находится в состоянии блокировки, а вызывающий поток является его владельцем. XSI Если мьютекс имеет тип PTHREAD_MUTEX_NORMAL, обнаружение взаимоблокировок не обеспечивается. К взаимоблокировке может привести попытка заблокировать мьютекс повторно. Если поток попытается разблокировать мьютекс, который не заблокирован, дальнейшее его поведение не определено. Для мьютексов типа PTHREAD_MUTEX_ERRORCHECK предусмотрена проверка на наличие ошибок. Если поток попытается заблокировать мьютекс, который уже заблокирован, возвращается ошибка. Если поток попытается разблокировать мьютекс, который не заблокирован, возвращается ошибка. Если мьютекс имеет тип PTHREAD_MUTEX_RECURSIVE, мьютекс должен поддерживать концепцию подсчета блокировок. При первом успешном блокировании мьютекса счетчик блокировок устанавливается равным единице. При каждом очередном блокировании этого мьютекса счетчик блокировок инкрементируется, а при каждом разблокировании — декрементируется. Когда счетчик блокировок достигает нулевого значения, мьютекс становится доступным для других потоков. Если поток попытается разблокировать мьютекс, который не заблокирован, возвращается ошибка. Если мьютекс имеет тип PTHREAD_MUTEX_DEFAULT, попытка рекурсивно заблокировать мьютекс приводит к неопределенному поведению. Попытка разблокировать мьютекс, который не был заблокирован (любым потоком, включая вызывающий), приводит к неопределенному поведению. Функция pthread_mutex_trylock () эквивалентна функции nread__mutex_lock (), за исключением того, что если объект мьютекса, адресуемый раметром mutex, в данный момент заблокирован (любым потоком, включая теку- ^ и), эта функция немедленно завершится. Если мьютекс имеет тип
604 Приложение Б PTHREAD_MUTEX_RECURSIVE, и в данный момент мьютексом владеет вызывающий поток счетчик блокировок этого мьютекса инкрементируется, а функция pthread_mutex_trylock () немедленно возвращает признак успешного завершения. Функция pthread_mutex_unlock () освобождает объект мьютекса адресуемый параметром mutex. XSI Способ освобождения зависит от атрибута типа мьютекса. Если при вызове функции pthread_mutex_unlock (), в результате которого мьютекс стал доступным, существуют потоки, заблокированные по объекту мьютекса, адресуемому параметром мьютекс, то поток- владелец этого мьютекса будет установлен стратегией планирования. XSI (Для мьютексов типа PTHREAD_MUTEXJRECURSIVE: мьютекс становится доступным, когда счетчик блокировок достигает нуля, и вызывающий поток больше не имеет никаких блокировок по этому мьютексу.) Если к потоку, ожидающему освобождения мьютекса, поступает сигнал, то после выполнения обработчика этого сигнала поток снова перейдет в состояние ожидания, как если бы он и не прерывался на обработку сигнала. Возвращаемые значения При успешном завершении функции pthread_mutex__lock () и pthread_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] значение, заданное параметром mutex, не относится к инициализированному объекту мьютекса; XSI [EAGAIN] мьютекс остался недоступным, поскольку было превышено максимальное количество рекурсивных блокировок для мьютекса, заданного параметром mutex.
Приложение Б 605 функция pthread_mutex_lock () может завершиться неудачно, если: [ EDEADLK ] текущий поток уже владеет мьютексом. функция pthread_mutex_unlock () может завершиться неудачно, если: [ EPERM ] текущий поток не владеет мьютексом. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Объекты мьютексов служат в качестве базовых элементов низкого уровня, на основе которых можно построить другие функции синхронизации потоков. Поэтому реализация мьютексов должна быть максимально эффективной. Функции управления мьютексами и, в частности, устанавливаемые по умолчанию значения атрибутов мьютексов позволяют по желанию организовать быстродействующие встроенные реализации блокировок и разблокировок мьютексов. Например, тупиковая ситуация при двойной блокировке— это явным образом разрешенное поведение, которое позволяет избежать внесения в базовый механизм больших затрат. (Более "дружественные" мьютексы, которые обнаруживают взаимоблокировку или позволяют множественное блокирование одним и тем же потоком, пользователь может легко создать с помощью других механизмов. Например, для регистрации владельцев мьютекса можно использовать функцию pthread_self ().) Реализации путем использования специальных атрибутов мьютексов также могут предоставлять дополнительные возможности в виде опций. Поскольку большинство атрибутов проверяется перед тем, как поток должен быть заблокирован, их использование не замедляет процесс блокирования мьютекса. Более того, несмотря на возможность выделить идентификационный номер (ID) владельца мьютекса, это потребовало бы сохранения текущего ID потока при каждом блокировании мьютекса, что связано с неприемлемым уровнем затрат. Аналогичные аргументы применимы и к операции mutex_tryunlock. будущие направления Отсутствуют. Смотри также Pthread_mutex_destroy (), pthread_mutex_timedlock (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>.
606 Приложение Б Последовательность внесения изменений функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_mutex_lock (), pthread_rautex_trylock () и pthread_mutex__ unlock () отмечены как часть опции Threads. В результате согласования со спецификацией Single UNIX Specification было определено поведение при попытке повторно заблокировать мьютекс. В целях согласования со стандартом IEEE Std 1003.1d-1999 в раздел "Смотри также" была добавлена функция pthread_mutex_t imedlock ().
Приложение Б 607 Имя pthread_mutex_timedlock — функция блокировки мьютекса (ADVANCED REALTIME). Синопсис THR #include <pthread.h> ТМО #include <time.h> int pthread_mutex_timedlock ( pthread_mutex_t *restrict mutex, const struct timespec ^restrict abs__timeout) ; Описание Функция pthread_mutex_timedlock() используется для блокирования объекта мьютекса, адресуемого параметром mutex. Если этот мьютекс уже заблокирован, блокируется вызывающий поток до тех пор, пока мьютекс не станет доступным (как при использовании функции pthread_mutex_lock ()). Если мьютекс нельзя заблокировать без ожидания, пока другой поток его разблокирует, это ожидание будет прервано, когда истечет заданный интервал времени. Заданный интервал времени истекает, когда наступит абсолютное время, заданное параметром abs_timeout (т.е. когда значение системных часов станет равным или превысит значение abs_timeout) или если в момент вызова функции абсолютное время, заданное параметром abs__timeouty уже наступило. TMR Если поддерживается опция Timers, отсчет интервала времени происходит с использованием часов CLOCK_JREALTIME; в противном случае — с использованием системных часов, значение которых возвращает функция time(). Разрешение для интервала времени определяется разрешением часов, которые используются для его отсчета. Тип данных timespec определяется в заголовке <time. h>. Ни при каких условиях эта функция не завершится неудачно, если мьютекс может быть заблокирован немедленно. В проверке достоверности параметра abs_timeout Нет никакой необходимости, если мьютекс может быть заблокирован немедленно. У правил наследования приоритета (для мьютексов, инициализированных с использованием протокола PRIO_INHERIT) есть следствие: если ожидание мьютекса, Действующего с ограничением по времени, завершается по причине исчерпания заданного интервала времени, то приоритет владельца мьютекса будет откорректиро- ан таким образом, чтобы отражать факт того, что данный поток больше не относит- Ся к числу потоков, ожидающих заданный мьютекс. Возвращаемое значение *фи успешном завершении функция pthread_mutex_timedlock() возвращает Нулевое значение; в противном случае — код ошибки, обозначающий ее характер.
608 Приложение Б Ошибки функция pthread_mutex_timedlock () завершится неудачно, если: [ EINVAL ] мьютекс был создан с использованием атрибута pro tocol, имеющего значение PTHREAD_PRIO_PROTECT, а приоритет вызывающего потока выше текущего значения предельного приоритета мьютекса; [EINVAL] процесс или поток заблокирован, а параметр abs_timeout в поле наносекунд имеет значение, которое меньше нуля либо больше или равно 1000 млн; [ETIMEDOUT] мьютекс не удалось заблокировать до истечения заданного интервала времени. Функция pthread_mutex_timedlock () может завершиться неудачно, если: [EINVAL] значение, заданное параметром mufcex, не относится к инициализированному объекту мьютекса; XSI [EAGAIN] мьютекс остался недоступным, поскольку было превышено максимальное количество рекурсивных блокировок для мьютекса, заданного параметром mutex; [ EDEADLK] текущий поток уже владеет мьютексом. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Функция pthread_jnutex_timedlock() является частью опций Threads и Timeouts и может быть не представлена во всех реализациях. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_mutex_destroy (), pthread_mutex_lock (), pthread_mutex_trylock ()» time (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread. h>, <time. h>- Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 6, основанием послужил станД^Р IEEE Std 1003.1d-1999.
Приложение Б 609 Имя pthread_mutexattr_destroy, pthread_mutexattr__init — функции разрушения и инициализации объекта атрибутов мьютекса. Синопсис THR #include <pthread.h> int pthread_mutexattr_destroy ( pthread__mutexattr__t *atfcr) ; int pthread_jmitexattr_init (pthread_mutexattr__t *attr) ; Описание Функция pthread_mutexattr_destroy () используется для разрушения объекта атрибутов мьютекса, в результате чего этот объект становится неинициализированным. В конкретной реализации функция pthread_mutexattr_destroy () может устанавливать объект, адресуемый параметром attr, равным недействительному значению. Разрушенный объект атрибутов можно снова инициализировать с помощью функции pthread_mutexattr_init (); результаты ссылки на этот объект после его разрушения не определены. Результаты не определены, если функция pthread_mutexattr__init () вызывается, ссылаясь на уже инициализированный объект атрибутов attr. После того как объект атрибутов мьютекса был использован для инициализации одного или нескольких мьютексов, любая функция, которая оказывает влияние на объект атрибутов (включая деструктор), никак не отразится на ранее инициализированных мьютексах. Возвращаемые значения При успешном завершении функции pthread_mutexattr__destroy () HPthread_mutexattr_init () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_mutexattr_destroy () может завершиться неудачно, если: EINVAL ] значение, заданное параметром a t fcr, недействительно. Функция pthread_mutexattr__init () завершится неудачно, если: IEN0MEM] длЯ инициализации объекта атрибутов мьютекса недостаточно существующей памяти. Эти функции не возвращают код ошибки [EINTR].
610 Приложение Б Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Для получения общих разъяснений назначения атрибутов см. описание функции pthread_attr_init (). Объекты атрибутов позволяют реализациям экспериментировать с полезными расширениями и разрешают использовать расширение этого тома стандарта IEEE Std 1003.1-2001, не изменяя существующих функций. Таким образом, они обеспечивают возможности для будущего расширения этого тома стандарта IEEE Std 1003.1-2001 и уменьшают соблазн преждевременно стандартизировать семантику, которая еще широко не реализована или не до конца понята. Рассматривалась возможность использования таких дополнительных атрибутов мьютексов, как spin__only4 limited spin, no_spin, recursive и metered. (Считаем необходимым разъяснить назначение таких атрибутов, как recursive и metered: рекурсивные мьютексы позволяют выполнение нескольких повторных блокировок со стороны текущего владельца; мьютексы с регистрирацией фиксируют длину очереди, время ожидания и т.д.) Поскольку еще нет достаточных данных о том, насколько полезны эти атрибуты, в данном томе стандарта IEEE Std 1003.1-2001 они не определены. Однако объекты атрибутов мьютексов позволяют проверить эти идеи на предмет возможной их стандартизации в будущем. Атрибуты мьютекса и производительность Необходимо позаботиться о том, чтобы действующие по умолчанию значения атрибутов мьютекса были определены таким образом, чтобы мьютексы, инициализированные этими значениями, имели достаточно простую семантику, согласно которой блокирование и разблокирование можно было бы выполнить с помощью инструкций, эквивалентных операциям тестирования и установки значений (и, возможно, еще некоторых других базовых инструкций). Существует по крайней мере один метод реализации, который можно использовать для сокращения расходов в период блокирования на проверку того, имеет ли мьютекс нестандартные атрибуты. Один такой метод заключается в том, чтобы предварительно заблокировать любые мьютексы, которые инициализированы нестандартными атрибутами. Любая попытка позже заблокировать такой мьютекс заставит реализацию перейти на "медленный путь", как если бы мьютекс был недоступен; затем реализация могла бы "по-настоящему" заблокировать "нестандартный" мьютекс. Базовая операция разблокировки более сложна, поскольку реализация никогда в действительности не желает освобождать мьютекс, который был предварительно заблокирован. Это показывает, что (в зависимости от оборудования) существует необходимость применения оптимизаций для более эффективной обработки часто используемых атрибутов мьютекса.
Приложение Б 611 Использование общей памяти и синхронизация процессов Существование функций распределения памяти в этом томе стандарта IEEE Std 1003.1-2001 дает приложению возможность выделять память объектам синхронизации из того раздела, который доступен многим процессам (а следовательно, и потокам многих процессов). Чтобы реализовать такую возможность при эффективной поддержке обычного (т.е. однопроцессорного) случая, был определен атрибут process -shared. Если реализация поддерживает опцию _POSIX_THREAD_PROCESS_SHARED, то атрибут process -shared можно использовать для индикации того, что к мьютексам или условным переменным могут получать доступ потоки сразу нескольких процессов. Для того чтобы объекты синхронизации по умолчанию создавались в самой эффективной форме, для атрибута process-shared в качестве стандартного было выбрано значение PTHREAD_PROCESS_PRIVATE. Переменные синхронизации, которые инициализированы значением PTHREAD_PROCESS__PRIVATE атрибута process- shared, могут обрабатываться потоками только в том процессе, в котором была выполнена инициализации этих переменных. Переменные синхронизации, которые инициализированы значением PTHREAD_PROCESS_SHARED атрибута process- shared, могут обрабатываться любым потоком в любом процессе, который имеет к ним доступ. В частности, эти процессы могут существовать независимо от процесса инициализации. Например, следующий код реализует простой семафор-счетчик в общедоступном файле, который может быть использован многими процессами. /* 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 <sys/types.h> #include <sys/stat.h> ^include <sys/mman.h> ^include <fcntl.h> ^include <pthread.h> #include "sem.h" semaphore_t * SemaPhore_create (char *semaphore_name) t int fd; semaphore_t * semap; pthread_mutexattr_t psharedm;
612 Приложение Б pthread_condattr_t psharedc; fd = open(semaphore_name, 0_RDWR | 0_CREAT | 0__EXCL, 0666); if (fd <0) return (NULL); (void) ftruncate (fd, sizeof (semaphore__t) ) ; (void) pthread_mutexattr__init (&psharedm) ; (void) pthread_mutexattr__setpshared (Scpsharedm, PTHREAD_PROCESS_SHARED) ; (void) pthread_condattr_init (Scpsharedc); (void) pthread__condattr_setpshared (Scpsharedc, PTHREAD_PROCESS_SHARED); semap = (semaphore__t *) mmap (NULL, sizeof (semaphore_t) , PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close (fd); (void) pthread_jmitex__init (&semap->lock, Scpsharedm) ; (void) pthread_cond_init (5csemap->nonzero, Scpsharedc); semap->count = 0; return (semap); } semaphore_t * semaphore__open (char *semaphore__name) { int fd; semaphore_t *semap; fd = open (semaphore_name, 0_RDWR, 0666); if (fd <0) return (NULL); semap = (semaphored *) mmap (NULL, sizeof (semaphore_t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 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 (5csemap->lock) ; } void semaphore_wai t (semaphore_t * semap) {
Приложение Б 613 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.с */ #include "pthread.h" #include "sem.h" int main () { semaphore_t * semap; semap = semaphore_create("/tmp/semaphore"); if (semap == NULL) exit(1); 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"
614 Приложение Б #include "sem.h" int main () { semaphore__t * semap ; semap = semaphore_open ("/tmp/semaphore"); 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,<pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_mutexattr_destroy () и pthread_mutexattr_init () отмечены как часть опции Threads. Раздел "Ошибки" был отредактирован путем применения интерпретации IEEE PASC Interpretation 1003.1c #27.
Приложение Б 615 Имя pthread_mutexattr_getprioceiling, pthread_mutexattr_setprioceiling — функции считывания и установки атрибута prioceiling в объекте атрибутов мьютекса (REALTIME THREADS). Синопсис THR #include <pthread.h> Ti^ int pthread_mutexattr_getprioceiling ( const pthread_mutexattr_t *restrict attr, int *restrict prioceiling); int pthread_mutexattr_setprioceiling ( pthread_mutexattr__t *attr, int prioceiling); Описание Функции pthread_mutexattr_getprioceiling () и pthread__mute- xattr_setprioceiling () используются для считывания и установки соответственно атрибута prioceiling в объекте атрибутов мьютекса, адресуемого параметром attr, который был ранее создан с помощью функции pthread_mutexattr_init (). Атрибут prioceiling содержит предельное значение приоритета инициализированных мьютексов. Значения атрибута prioceiling ограничены границами диапазона приоритетов, определенного для стратегии планирования, соответствующей значению SCHED_FIFO. Значение атрибута prioceiling — это минимальный уровень приоритета, на котором еще выполняется критический раздел, защищаемый мьютексом. Чтобы избежать инверсии приоритетов, предельное значение приоритета мьютекса устанавливается выше самого высокого приоритета всех потоков, которые могут блокировать этот мьютекс, или равным ему. Возвращаемые значения При успешном завершении функции pthread_mutexattr_getprioceiling() Hpthread_mutexattr_setprioceiling() возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функции pthread_mutexattr_getprioceiling() и pthread_mutexattr_setp- гЮсеiling () могут завершиться неудачно, если: LEINVAL] значение, заданное параметром attr, или параметром prioceiling, недействительно; IEPERMJ инициатор вызова не обладает привелегиями для выполнения этой операции. Эти функции не возвращают код ошибки [EINTR].
61 б Приложение Б Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_cond_destroy (), pthread_create (), pthread_mutex_destroy (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_mutexattr_getprioceiling() и pthread_mutexattr_setp- rioceiling () отмечены как часть опций Threads и Thread Priority Protection. Код ошибки [ENOSYS] был исключен, поскольку его нет смысла учитывать, если реализация не поддерживает опцию Thread Priority Protection. Код ошибки [ENOTSUP] был исключен, поскольку эти функции не имеют аргумента protocol. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_mutexattr_getprioceiling () было добавлено ключевое слово restrict.
Приложение Б 617 Имя pthread_mutexattr_getprotocol, pthread_mutexattr_setprotocol — функции считывания и установки атрибута protocol в объекте атрибутов мьютекса (REALTIME THREADS). Синопсис THR #include <pthread.h> tppitpi int pthread_mutexattr_getprotocol ( const pthread_mutexattr__t *restrict attr, int ^restrict protocol); int pthread_mutexattr_setprotocol ( pthread_jnutexattr_t * attr, int protocol) ; Описание Функции pthread_mutexattr_getprotocol() и pthread_mutexattr_setp- rotocol () используются для считывания и установки соответственно атрибута protocol в объекте атрибутов мьютекса, адресуемого параметром attr, который был ранее создан с помощью функции pthread__mutexattr_init (). Атрибут protocol определяет протокол, которому необходимо следовать при использовании мьютексов. Этот атрибут может иметь следующие значения (которые определены в заголовке <рthread. h>): PTHREAD_PRIO_NONE TPI PTHREAD_PRIO_INHERIT TPP PTHREAD_PRIO_PROTECT Если поток владеет мьютексом с использованием значения PTHREAD_PRIO_NONE Для атрибута protocol, то факт обладания мьютексом не отражается на значении его приоритета и стратегии планирования. TPI Если поток блокирует потоки с более высоким приоритетом благодаря тому, что он владеет одним или несколькими мьютексами, у которых атрибут protocol имеет значение PTHREAD_PRIO_INHERIT, то он будет выполняться с наивысшим из приоритетов потоков, ожидающих освобождения любого из мьютексов. ™Р Если поток владеет одним или несколькими мьютексами, у которых атрибут protocol имеет значение PTHREAD_PRIO_PROTECT, то он будет выполняться с самым высоким из предельных приоритетов всех мьютексов, принадлежащих этому потоку и инициализированных с этим атрибутом, независимо от того, заблокированы другие потоки по любому из этих мьютексов или нет.
618 Приложение Б Пока поток удерживает мьютекс, у которого атрибут protocol был инициализирован значением PTHREAD_PRIO_INHERIT или PTHREAD_PRIO_PROTECT, он не будет претендентом для перемещения в конец очереди планируемых заданий в результате изменения его исходного приоритета, например, после вызова функции sched__setparam (). Аналогично, если поток разблокирует мьютекс, у которого атрибут protocol был инициализирован значением PTHREAD_PRIO_INHERIT или PTHREAD_PRIO_PROTECT, он не будет претендентом для перемещения в конец очереди планируемых заданий в результате изменения его исходного приоритета. Если поток одновременно владеет несколькими мьютексами, инициализированными в соответствии с различными протоколами, он будет выполняться с самым высоким из приоритетов, полученных по каждому из протоколов. TPI Если поток обращается к функции pthread_mutex_lock (), а атрибут protocol задаваемого мьютекса был инициализирован значением PTHREAD_PRIO__INHERIT, и если вызывающий поток блокируется из-за того, что мьютекс принадлежит другому потоку, то этот поток — владелец мьютекса — наследует уровень приоритета вызывающего потока, причем до тех пор, пока он продолжает удерживать мьютекс. Реализация устанавливает приоритет выполнения согласно максимальному значению (выбранного из заданного и всех унаследованных приоритетов). Более того, если этот поток— владелец мьютекса сам блокируется по другому мьютексу, такой же эффект наследования приоритетов рекурсивно распространяется и на этого владельца. Возвращаемые значения При успешном завершении функции pthread_mutexattr_getprotocol () и pthread_mutexattr_setprotocol () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_mutexattr_setprotocol () завершится неудачно, если: [ ENOTSUP ] значение, заданное параметром pro tocol, не поддерживается. Функции pthread__mutexattr_getprotocol() и pthread_mutexattr_setp- rotocol () могут завершиться неудачно, если: [EINVAL] значение, заданное параметром attr, или параметром protocol, недействительно; [EPERM] инициатор вызова не обладает привилегиями для выполнения этой операции. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют.
Приложение Б 619 Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_cond_destroy (), pthread_create (), pthread_mutex_destroy (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Отмечены как часть группы Realtime Threads Feature Group. Issue 6 Функции pthread_mutexattr_getprotocol() и pthread_mutexattr__setp- rotocol () отмечены как часть опции Threads и одной из опций Thread Priority Protection или Thread Priority Inheritance. Код ошибки [ENOSYS] был исключен, поскольку его нет смысла учитывать, если реализация не поддерживает опции Thread Priority Protection или Thread Priority Inheritance. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread__mutexattr_getprotocol () было добавлено ключевое слово restrict.
620 Приложение Б Имя pthread_mutexattr_getpshared, pthread_mutexattr_setpshared — функции считывания и установки атрибута process -shared. Синопсис THR #include <pthread.h> TSH int pthread_mutexattr_getpshared ( const pthread_mutexattr_t *restrict attr, int *restrict pshared); int pthread_mutexattr_setpshared( pthread_mutexattr_t *attr, int pshared); Описание Функция pthread_mutexattr_getpshared () используется для получения значения атрибута process -shared из объекта атрибутов, адресуемого параметром attr. Функция pthread_mutexattr_setpshared() позволяет установить атрибут process- shared в инициализированном объекте атрибутов, адресуемом параметром a t tr. Атрибут process-shared устанавливается равным значению PTHREAD_PROCESS_SHARED, чтобы позволить обработку мьютекса любым другим потоком, который имеет доступ к памяти, в которой размещен этот мьютекс, даже если он размещен в памяти, совместно используемой несколькими процессами. Если атрибут process-shared установлен равным значению PTHREAD_PROCESS_PRIVATE, мьютекс будет обрабатываться только теми потоками, созданными в одном процессе с потоком, который инициализировал этот мьютекс; если потоки из различных процессов попытаются работать с таким мьютексом, то дальнейшее их поведение не определено. По умолчанию этот атрибут устанавливается равным значению PTHREAD_PROCES S_PRI VATE. Возвращаемые значения При успешном завершении функция pthread_mutexattr_setpshared() возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. При успешном завершении функция pthread_mutexattr_getpshared() возвращает нулевое значение и сохраняет считанное значение атрибута process- shared объекта a t tr в объекте, адресуемом параметром pshared; в противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функции pthread_mutexattr_getpshared() и pthread_mutexattr__setp- shared () могут завершиться неудачно, если:
Приложение Б 621 [ EINVAL] значение, заданное параметром a t tr, недействительно. Функция pthread_mutexat tr_setpshared () может завершиться неудачно, если: [EINVAL] новое значение, заданное для атрибута, попадает вне диапазона значений, действительных для этого атрибута. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_cond_destroy (), pthread_create (), pthread__mutex__destroy (), pthread_mutexattr_destroy (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_mutexattr_getpshared() и pthread_mutexattr_setp- shared () отмечены как часть опций Threads и Thread Process-Shared Synchronization. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_mutexattr_getpshared () было добавлено ключевое слово restrict.
622 Приложение Б Имя pthread_mutexattr_gettype, pthread_mutexattr__settype — функции считывания и установки атрибута type. Синопсис XSI #include <pthread.h> 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. По умолчанию атрибут type устанавливается равным значению PTHREAD_MUTEX_DEFAULT. Атрибут type содержит тип мьютекса. Допустимыми значениями атрибута type могут быть следующие: PTHREAD_MUTEX_NORMAL Мьютекс этого типа не обнаруживает взаимоблокировки. Поток, пытаясь перезаблокировать такой мьютекс без первоначального его разблокирования, попадает во взаимоблокировку. Попытка разблокировать мьютекс, заблокированный другим потоком, приводит к неопределенному поведению. Попытка разблокировать незабло- кированный мьютекс также приводит к неопределенному поведению. PTHREAD_MUTEX_ERRORCHECK Мьютекс этого типа выполняет проверку на наличие ошибок. Поток, пытаясь перезаблокировать такой мьютекс без первоначального его разблокирования, генерирует код ошибки. При попытке разблокировать мьютекс, заблокированный другим потоком, генерируется код ошибки. При попытке разблокировать незаблокирован- ный мьютекс также генерируется код ошибки. PTHREAD_MUTEX_RECURSIVE Поток, пытаясь перезаблокировать такой мьютекс без первоначального его разблокирования, успешно его блокирует. Взаимоблокировка, возникающая в результате переблокирования мьютексов типа PTHREAD_MUTEX_NORMAL, не может произойти с мьютексами этого типа. Множественное блокирование такого мьютекса потребует такого же количества раблокировок, которые полностью освободят мьютекс, прежДе чем другой поток сможет его захватить. При попытке разблокировать мьютекс, заблокированный другим потоком, генерируется код ошибки. При попытке разблокировать незаблокированный мьютекс также генерируется код ошибки.
Приложение Б 623 PTHREAD_MUTEX_DEFAULT Попытка рекурсивного блокирования мьютекса этого типа приводит к неопределенному поведению. Попытка разблокировать мьютекс, не заблокированный вызывающим потоком, приводит к неопределенному поведению. Попытка разблокировать незаблокированный мьютекс также приводит к неопределенному поведению. Реализация может преобразовать мьютекс этого типа в один из других типов мьютексов. Возвращаемые значения При успешном завершении функция pthread__mutexattr_gettype () возвращает нулевое значение и сохраняет значение атрибута type, считанное из объекта a t tr, в объекте, адресуемом параметром type; в противном случае она возвращает код ошибки, обозначающий ее характер. При успешном завершении функция pthread_mutexattr_settype () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_mutexattr_set type () завершится неудачно, если: [EINVAL] значение, заданное параметром type, недействительно. Функции pthread_mutexattr_gettype () и pthread_mutexattr_settype () могут завершиться неудачно, если: [EINVAL] значение, заданное параметром attr, недействительно. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию В приложениях предлагалось не использовать мьютекс типа PTHREAD_MU- TEX_RECURSIVE с условными переменными, поскольку неявная блокировка, выполненная для функций pthread_cond__timedwait () или pthread_cond_wait (), может в действительности не освободить мьютекс (если он был заблокирован многократно). Если это произойдет, никакой другой поток не сможет удовлетворить усло- вию предиката. «Логическое обоснование Отсутствует. Будущие направления Отсутствуют.
624 Приложение Б Смотри также pthread_cond_timedwait (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Приложен список опечаток Open Group Corrigendum U033/3. Был отредактирован раздел "Синопсис" для функции pthread_mutexattr_gettype (), в результате чего первый аргумент получил тип const pthread_mutexattr__t *. В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread__mutexattr_gettype было добавлено ключевое слово restrict.
Приложение Б 625 Имя pthreacLonce — функция динамической инициализации пакетов. Синопсис THR #include <pthread.h> int pthread_once (pthread__once_t *once_control, void (*init__routine) (void)); pthread_once_t once_control = PTHREAD_ONCE_INIT; Описание При первом обращении к функции pthread_once () любым потоком процесса с заданным параметром once__control будет вызвана функция ini t_routine без аргументов. Последующие обращения к функции pthread__once () с тем же параметром once_control не вызывают функцию ini t__routine. Возвращение из функции pthread_once () означает, что функция ini t__rou tine выполнена. Параметр once^control определяет, вызывалась ли соответствующая функция инициализации. Функция pthread_once () не является точкой отмены. Но если функция init^routine является точкой отмены, и отмена таки происходит, то ее воздействие на параметр once__control будет таким, как если бы функция pthread__once () никогда не вызывалась. Константа PTHREAD_ONCE_INIT определяется в заголовке <pthread. h>. Поведение функции pthread_once () будет неопределенным, если параметр once^control имеет автоматический класс памяти (объекты этого класса размещаются в стеке и инициализируются всякий раз при входе в блок, где они объявлены, и разрушаются при выходе из этого блока) или не инициализирован константой PTHREAD_ONCE_INIT. Возвращаемое значение При успешном завершении функция pthread_once () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_once () может завершиться неудачно, если: NVAL] значения, заданные параметрами once__control или ini t__routine, недействительны. Функция pthread__once () не возвращает код ошибки [EINTR]. Примеры Отсутствуют.
626 Приложение Б Замечания по использованию Отсутствуют. Логическое обоснование Некоторые библиотеки С разработаны для динамической инициализации. Это означает, что глобальная инициализация для такой библиотеки выполняется при вызове первой библиотечной процедуры. В однопоточной программе это обычно реализуется с использованием статической переменной, значение которой проверяется при входе в функцию, например: 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 <pthread.h> 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) ; ... /* Операции, выполняемые после инициализации. */ } Обратите внимание на то, что тип pthread_once_t не может быть массивом, поскольку для некоторых компиляторов конструкция &<array_name> неприемлема.
Приложение Б 627 Будущие направления Отсутствуют. Смотри также Том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread. h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функция pthread_once () отмечена как часть опции Threads. Был добавлен код ошибки [EINVAL], возвращаемый при неудачном завершении функции в случае, если хотя бы один из аргументов недействителен.
628 Приложение Б Имя pthread_rwlock_destroy, pthread_rwlock_init — функции разрушения и инициализации объекта блокировки для чтения и записи. Синопсис THR #include <pthread.h> int pthread_rwlock_destroy(pthread_rwlock_t *rwlock) ; int pthread__rwlock_init ( pthread__rwlock_t *restrict rwlock, const pthread__rwlockattr__t *restrict attr) ; Описание Функция pthread_rwlock_destroy () используется для разрушения объекта блокировки чтения и записи, адресуемого параметром rwlock, и освобождения любых ресурсов, задействованных этой блокировкой. Результат последующего использования этой блокировки не определен до тех пор, пока объект не будет инициализирован повторно посредством еще одного обращения к функции pthread_rwlock_init (). В конкретной реализации функция pthread_rwlock_destroy () может устанавливать объект, адресуемый параметром rwlock, равным недействительному значению. Результаты не определены, если функция pthread_rwlock_destroy () вызывается в то время, когда какой-нибудь поток удерживает объект блокировки, адресуемый параметром rwlock. Попытка разрушить неинициализированный объект блокировки для чтения и записи приводит к неопределенному поведению. Функция pthread_rwlock_init () выделяет любые ресурсы, необходимые для использования объекта блокировки для чтения и записи, адресуемого параметром rwlock, и инициализирует его (он переходит в незаблокированное состояние) с использованием объекта атрибутов, адресуемого параметром attr. Если параметр attr содержит значение NULL, для блокировки чтения и записи будут использованы атрибуты, действующие по умолчанию; т.е. результат в этом случае равносилен передаче адреса объекта, содержащего стандартные атрибуты блокировки для чтения и записи. После первой инициализации объект блокировки можно использовать любое число раз без повторной инициализации. Результаты не определены, если функция pthread_rwlock_init () вызвана с заданием уже инициализированного объекта блокировки. Результаты не определены, если объект блокировки для чтения и записи используется без предварительной инициализации. При неудачном выполнении функции pthread_rwlock_init () объект, адресуемый параметром rwlock, остается неинициализированным, а содержимое параметра rwl ock — неопределенным. Для выполнения синхронизации можно использовать только объект, адресуемый параметром rwlock. Результат ссылки на копии этого объекта в вызовах функци pthread_rwlock_destroy(), pthread_rwlock_rdlock(), pthread_rwlock_ti' medrdlock(), pthread_rwlock_timedwrlock(), pthread_rwlock_tryrdlock(J» pthread_rwlock_trywrlock (), pthread_rwlock_unlock () или pthread_rwloc*-~ wrlock () не определен.
Приложение Б 629 Возвращаемые значения При успешном завершении функции pthread_rwlock_destroy () и pthread_rwlock_init () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Проверка на наличие ошибок с кодами [EBUSY] и [EINVAL] реализована (если реализована вообще) так, как будто она выполняется в самом начале работы каждой функции, и код ошибки в случае ее обнаружения возвращается до модификации состояния объекта блокировки чтения и записи, заданного параметром rwlock. Ошибки Функция pthread_rwlock_destroy () может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку разрушить заблокированный объект, адресуемый параметром rwlock; [EINVAL] значение, заданное параметром rwlock, недействительно. Функция pthread_rwlock_init () завершится неудачно, если: [EAGAIN] система испытывает недостаток в ресурсах (не имеется в виду память), необходимых для инициализации еще одного объекта блокировки для чтения и записи; [ENOMEM] для инициализации объекта блокировки для чтения и записи недостаточно существующей памяти; [EPERM] инициатор вызова не обладает привилегиями для выполнения этой операции. Функция pthread_rwlock__init () может завершиться неудачно, если: [EBUSY] реализация обнаружила попытку повторно инициализировать объект блокировки, адресуемый параметром rwlock, которой был ранее инициализирован, но еще не разрушен; [EINVAL] значение, заданное параметром a t tr, недействительно. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует.
630 Приложение Б Будущие направления Отсутствуют. Смотри также 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, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1J-2000 были внесены следующие изменения. • В разделе "Синопсис" изменена метка. Новая метка THR обозначает, что рассматриваемые функции теперь являются частью опции Threads (ранее они относились к опции Read-Write Locks стандарта IEEE Std 1003.1J-2000, а также считались частью дополнения XSI). В раздел "Синопсис" также не входит макрос инициализации. • Раздел "Описание" отредактирован следующим образом: — явно отмечено выделение ресурсов при инициализации объекта блокировки для чтения и записи; — добавлен абзац, в котором указывается, что копии объекта блокировки для чтения и записи использовать нельзя. • В раздел "Ошибки" добавлен код ошибки [EINVAL], означающий, что при вызове функции pthread_rwlock_init () значение, заданное параметром attr, было недействительно. • Отредактирован раздел "Смотри также". В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_rwlock_init () было добавлено ключевое слово restrict.
Приложение Б 631 Имя pthread_rwlock_rdlock, pthread_rwlock_tryrdlock— функции блокирования объекта блокировки чтения-записи для обеспечения чтения. Синопсис THR #include <pthread.h> int pthread__rwlock_rdlock (pthread_rwlock_t *rwlock) ; int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock) ; Описание Функция pthread_rwlock_rdlock() применяет блокировку для обеспечения чтения к объекту блокировки чтения-записи, адресуемому параметром rwlock. Вызывающий поток получает блокировку для чтения, если никакой записывающий поток не удерживает этот объект блокировки и не существует никаких других записывающих потоков, заблокированных по этому объекту. TPS Если поддерживается опция Thread Execution Scheduling и потоки, участвующие в данной блокировке, выполняются с использованием стратегий планирования SCHED__FIFO или SCHED_RR, то вызывающий поток не получит эту блокировку, если ее удерживает записывающий поток или если по этому объекту блокировки заблокированы записывающие потоки такого же или более высокого приоритета; в противном случае вызывающий поток получит блокировку. юг Если поддерживается опция Thread Execution Scheduling и потоки, участвующие в данной блокировке, выполняются с использованием стратегии планирования SCHED_SPORADIC, то вызывающий поток не получит эту блокировку, если ее удерживает записывающий поток или если по этому объекту блокировки заблокированы записывающие потоки такого же или более высокого приоритета; в противном случае вызывающий поток получит блокировку. Если опция Thread Execution Scheduling не поддерживается, то только конкретная реализация определяет, получит ли вызывающий поток эту блокировку, если никакой записывающий поток не удерживает этот объект блокировки и существуют другие записывающие потоки, заблокированные по этому объекту. Если записывающий поток удерживает этот объект блокировки, вызывающий поток не получит блокировку для чтения. Если блокировка для чтения не предоставлена, вызывающий поток блокируется до тех пор, пока он не получит блокировку. Вызывающий поток может попасть в ловушку взаимоблокировки, если во время вызова он удерживает блокировку для обеспечения записи. Поток может удерживать несколько параллельных блокировок для чтения по объ- е*ту rwlock (т.е. функция pthread_rwlock_rdlock () может быть успешно вызвана
632 Приложение Б п раз). В этом случае приложение должно гарантировать, что поток выполнит соответствующие действия по разблокировке объекта rwlock (т.е. он п раз вызовет функцию pthread_rwlock_unlock ()). Максимальное количество одновременных (и гарантированно успешных) блокировок для чтения, которое может быть применено к объекту блокировки чтения- записи, определяется конкретной реализацией. В случае превышения этого максимума функция pthread_rwlock_rdlock () может завершиться неудачно. Функция pthread_rwlock_tryrdlock () применяет блокировку для обеспечения чтения подобно функции pthread_rwlock_rdlock (), за исключением того, что эта функция завершится неудачно, если эквивалентный вызов функции pthread_rwlock_rdlock() заблокировал вызывающий поток. Ни в каких случаях функция pthread_rwlock_tryrdlock() не блокирует потоки; она всегда либо добивается блокировки, либо немедленно завершается с неудачным результатом. Результаты выполнения этих функций не определены, если любая из них вызывается с неинициализированным объектом блокировки чтения-записи. Если потоку, ожидающему освобождения блокировки чтения-записи для обеспечения блокировки чтения передается сигнал, то после его обработки поток возобновит ожидание освобождения блокировки, как если бы оно и не прерывалось. Возвращаемые значения При успешном завершении функция pthread_rwlock_rdlock() возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Функция pthread_rwlock_tryrdlock () возвращает нулевое значение, если блокировка для чтения по объекту блокировки чтения-записи, адресуемому параметром rwloc, предоставлена. В противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функция pthread_rwlock_tryrdlock () завершится неудачно, если: [EBUSY] блокировка чтения-записи не могла быть предоставлена для чтения, поскольку удерживает блокировку записывающий поток, или по этому объекту заблокирован записывающий поток с соответствующим приоритетом. Функции pthread_rwlock_rdlock() и pthread_rwlock_tryrdlock() могут завершиться неудачно, если: [EINVAL] значение, заданное параметром rwlock, не относится к инициализированному объекту блокировки чтения-записи; [EAGAIN] блокировка не могла быть предоставлена для чтения, поскольку пре" вышено максимальное число блокировок чтения по объекту, адресуй" мому параметром rwlock. Функция pthread_rwlock_rdlock () может завершиться неудачно, если: [EDEADLK] текущий поток уже удерживает объект блокировки чтения-записи JU1 обеспечения записи. Эти функции не возвращают код ошибки [EINTR].
Приложение Б 633 Примеры Отсутствуют. Замечания по использованию Как упоминалось в томе Base Definitions стандарта IEEE Std 1003.1-2001 (Section 3.285, Priority Inversion), приложения, которые используют эти функции, могут подвергнуться инверсии приоритетов. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy (), pthread_rwlock_timedrdlock (), pthread_rwlock_timedwrlock (), pthread_rwlock__trywrlock (), pthread_rwlock_unlock (), pthread_rwlock_wrlock (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1J-2000 были внесены следующие изменения. • В разделе "Синопсис" была изменена метка. Новая метка THR означает, что рассматриваемые функции теперь являются частью опции Threads (ранее они относились к опции Read-Write Locks стандарта IEEE Std 1003.1J-2000, а также считались частью дополнения XSI). • Раздел "Описание" был отредактирован следующим образом: - заданы условия, при которых записывающие потоки имеют преимущество перед считывающими; - разъяснена возможная причина неудачного завершения функции pthread_rwlock_tryrdlock(); - добавлен абзац, в котором говорится о применении максимального количество блокировок для обеспечения чтения. • Был модифицирован раздел "Ошибки", посвященный описанию кода ошибки [EBUSY]: теперь предлагается принять во внимание приоритет записывающих потоков. Удален абзац, посвященный описанию кода ошибки [EDEADLK], возвращаемому функцией pthread_rwlock_tryrdlock (). • Был отредактирован раздел "Смотри также".
634 Приложение Б Имя pthread_rwlock_timedrdlock— функция, блокирующая объект блокировки чтения-записи для обеспечения чтения. Синопсис THR #include <pthread.h> ТМО #include <time.h> int pthread_rwlock_timedrdlock ( pthread_rwlock_t *restrict rwlock, const struct timespec ^restrict abs_timeout); Описание Функция pthread__rwlock_timedrdlock () применяет блокировку для обеспечения чтения к объекту блокировки чтения-записи, адресуемому параметром rwlock, подобно функции pthread_rwlock_rdlock (). Однако, если блокировка не может быть предоставлена без ожидания, пока другие потоки не освободят ее, это ожидание будет прервано, когда истечет заданный интервал времени. Интервал времени истекает, когда наступит абсолютное время, заданное параметром abs_timeout (т.е. когда показания времени на используемых в системе часах станут равными или превысят значение abs_timeout), или если абсолютное время, заданное параметром abs_ timeou t, уже наступило в момент вызова. TMR Если поддерживается опция Timers, отсчет интервала времени происходит с использованием часов CLOCK__REALTIME. Если опция Timers не поддерживается, отсчет интервала времени происходит с использованием системных часов, значение которых возвращает функция time (). Разрешение для интервала времени определяется разрешением часов, которые используются для его отсчета. Тип данных timespec определяется в заголовке <time .h>. Ни при каких условиях эта функция не завершится неудачно, если блокировка может быть предоставлена немедленно. В проверке достоверности параметра abs_timeout нет никакой необходимости, если блокировка может быть предоставлена немедленно. Если потоку, заблокированному по объекту блокировки чтения-записи при вызове функции pthread__rwlock_timedrdlock(), передается сигнал, то после его обработки поток возобновит ожидание освобождения блокировки, как если бы оно и не прерывалось. Вызывающий поток может попасть в ловушку взаимоблокировки, если во время вызова он удерживает блокировку для обеспечения записи по объекту, адресуемом)' параметром rwlock. Результаты не определены, если эта функция вызывается с неинициализированным объектом блокировки чтения-записи.
Приложение Б 635 Возвращаемое значение Функция pthread__rwlock_timedrdlock() возвращает нулевое значение, если блокировка для чтения по объекту блокировки чтения-записи, адресуемому параметром rwloc, предоставлена. В противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функция pthread_rwlock_timedrdlock () завершится неудачно, если: [ETIMEDOUT] блокировка не могла быть предоставлена до истечения заданного интервала времени. Функция pthread__rwlock_timedrdlock () может завершиться неудачно, если: [EAGAIN] блокировка для чтения не могла быть предоставлена, поскольку превышено максимальное число блокировок чтения по объекту, адресуемому параметром rwlock; [EDEADLK] вызывающий поток уже удерживает объект блокировки для обеспечения записи по объекту, адресуемому параметром rwlockr, [EINVAL] значение, заданное параметром rwlock, не относится к инициализированному объекту блокировки чтения-записи, или значение abs_timeout, выраженное в наносекундах, меньше нуля либо больше или равно 1000 миллионам. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Как упоминалось в томе Base Definitions стандарта IEEE Std 1003.1-2001 (Section 3.285, Priority Inversion), приложения, которые используют эту функцию, могут подвергнуться инверсии приоритетов. Функция pthread_rwlock__timedrdlock() является частью опций Threads и Timeouts и может быть не предоставлена во всех реализациях. Логическое обоснование Отсутствует. будущие направления Отсутствуют.
636 Приложение Б Смотри также pthread_rwlock_destroy(), pthread_rwlock_rdlock(), pthread_rwlock_timedwrlock (), pthread_rwlock__tryrdlock (), pthread_rwlock_trywrlock (), pthread_rwlock_unlock (), pthread_rwlock_wrlock (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>, <time.h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.1J-2000.
Приложение Б 637 Имя pthread_rwlock_timedwrlock— функция, блокирующая объект блокировки чтения-записи для обеспечения записи Синопсис THRTMO #include <pthread.h> #include <time.h> int pthread_rwlock_timedwrlock ( pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout) ; Описание Функция pthread_rwlock_timedwrlock () применяет блокировку для обеспечения записи к объекту блокировки чтения-записи, адресуемому параметром rwlock, подобно функции pthread_rwlock_wrlock (). Однако, если блокировка не может быть предоставлена без ожидания, пока другие потоки не освободят ее, это ожидание будет прервано, когда истечет заданный интервал времени. Интервал времени истекает, когда наступит абсолютное время, заданное параметром abs_timeout (т.е. когда показания времени на используемых в системе часах станут равными или превысят значение abs_timeout), или если абсолютное время, заданное параметром abs_timeout, уже наступило в момент вызова. TMR Если поддерживается опция Timers, отсчет интервала времени происходит с использованием часов CLOCK_REALTIME. Если опция Timers не поддерживается, отсчет интервала времени происходит с использованием системных часов, значение которых возвращает функция time (). Разрешение для интервала времени определяется разрешением часов, которые используются для его отсчета. Тип данных timespec определяется в заголовке <time.h>. Ни при каких условиях эта функция не завершится неудачно, если блокировка может быть предоставлена немедленно. В проверке достоверности параметра abs_timeout нет никакой необходимости, если блокировка может быть предоставлена немедленно. Если потоку, заблокированному по объекту блокировки чтения-записи при вызове функции pthread_rwlock_timedwrlock(), передается сигнал, то после его обработки поток возобновит ожидание освобождения блокировки, как если бы оно и не прерывалось. Вызывающий поток может попасть в ловушку взаимоблокировки, если во время вызова он удерживает блокировку чтения-записи по объекту, адресуемому параметром rwlock. Результаты не определены, если эта функция вызывается с неинициализированным объектом блокировки чтения-записи.
638 Приложение Б Возвращаемое значение функция pthread_rwlock_timedwrlock () возвращает нулевое значение, если блокировка для записи по объекту блокировки чтения-записи, адресуемому параметром rwloc, предоставлена. В противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функция pthread_rwlock_timedwrlock () завершится неудачно, если: [ETIMEDOUT] блокировка не могла быть предоставлена до истечения заданного интервала времени. Функция pthread_rwlock_timedwrlock () может завершиться неудачно, если: [EDEADLK] вызывающий поток уже удерживает объект блокировки по объекту, адресуемому параметром rwlock; [EINVAL] значение, заданное параметром rwlock, не относится к инициализированному объекту блокировки чтения-записи, или значение abs_timeout, выраженное в наносекундах, меньше нуля либо больше или равно 1000 миллионам. Эта функция не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Как упоминалось в томе 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 (),
Приложение Б 639 pthread_rwlock_trywrlock (), pthread_rwlock_unlock (), pthread_rwlock_wrlock (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread. h>, <t ime. h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6, основанием послужил стандарт IEEE Std 1003.lj-2000.
640 Приложение Б Имя pthread__rwlock_trywrlock, pthread_rwlock_wrlock— функции, блокирующие объект блокировки чтения-записи для обеспечения записи. Синопсис THR #include <pthread.h> int pthread_2rwlock_trywrlock (pthread_rwlock_t *rwlock) ; int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock) ; Описание Функция pthread_rwlock_trywrlock () применяет блокировку для обеспечения записи подобно функции pthread_rwlock_wrlock(), за исключением того, что эта функция завершится неудачно, если какой-нибудь поток в данный момент удерживает блокировку по объекту, адресуемому параметром rwlock (для чтения или записи). Функция pthread_rwlock_wrlock () применяет блокировку для обеспечения записи к объекту блокировки чтения-записи, адресуемому параметром rwlock. Вызывающий поток получает блокировку для записи, если никакой другой поток (записывающий или считывающий) не удерживает этот объект блокировки. В противном случае этот поток будет заблокирован до тех пор, пока он не сможет получить блокировку. Вызывающий поток может попасть в ловушку взаимоблокировки, если во время вызова он удерживает блокировку чтения-записи (либо для записи, либо для чтения) по объекту, адресуемому параметром rwlock Реализации могут благоприятствовать записывающим потокам перед считывающими, чтобы избежать зависания записывающего потока. Результаты не определены, если любая из этих функций вызывается с неинициализированным объектом блокировки чтения-записи. Если потоку, ожидающему блокировки для обеспечения записи, передается сигнал, то после его обработки поток возобновит ожидание освобождения блокировки, как если бы оно и не прерывалось. Возвращаемые значения Функция pthread_rwlock_trywrlock() возвращает нулевое значение, если блокировка для записи по объекту блокировки чтения-записи, адресуемому параметром rwloc, предоставлена. В противном случае возвращается код ошибки, обозначающий ее характер. При успешном завершении функция pthread_rwlock_wrlock () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_rwlock_trywrlock () завершится неудачно, если: [EBUSY] блокировка чтения-записи не могла быть предоставлена для записи, поскольку заданный объект блокировки уже заблокирован для чтения или записи.
Приложение Б 641 Функции pthread_rwlock_wrlock () и pthread__rwlock__trywrlock() могут завершиться неудачно, если: [EINVAL] значение, заданное параметром rwlock, не относится к инициализированному объекту блокировки чтения-записи. Функция pthread__rwlock_wrlock () может завершиться неудачно, если: [EDEADLK] текущий поток уже удерживает объект блокировки чтения-записи для обеспечения записи или чтения. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Как упоминалось в томе Base Definitions стандарта IEEE Std 1003.1-2001 (Section 3.285, Priority Inversion), приложения, которые используют эти функции, могут подвергнуться инверсии приоритетов. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread__rwlock_destroy (), pthread__rwlock_rdlock (), pthread_rwlock_timedrdlock (), pthread_rwlock__timedwrlock (), pthread_rwlock_tryrdlock(),pthread_rwlock_unlock(), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1J-2000 были внесены следующие изменения: • В разделе "Синопсис" была изменена метка. Новая метка THR означает, что рассматриваемые функции теперь являются частью опции Threads (ранее они от-
642 П ри ложен ие Б носились к опции Read-Write Locks стандарта IEEE Std 1003.1J-2000, а также считались частью дополнения XSI). • Из раздела "Ошибки" удален абзац, посвященный описанию кода ошибки [EDEADLK], возвращаемому функцией pthread__rwlock__trywrlock (). • Был отредактирован раздел "Смотри также".
Приложение Б 643 Имя pthread__rwlock__unlock— функция разблокирования объекта блокировки чтения-записи. Синопсис THR #include <pthread.h> int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); Описание Функция pthread_rwlock_unlock() используется для освобождения блокировки, удерживаемой с помощью объекта блокировки чтения-записи, адресуемого параметром rwlock. Результаты не определены, если объект блокировки чтения-записи, адресуемый параметром rwlock, не удерживается вызывающим потоком. Если эта функция вызывается, чтобы освободить блокировку для обеспечения чтения, и существуют другие блокировки чтения, удерживаемые в данный момент по этому объекту блокировки чтения-записи, то он (объект) останется в состоянии блокирования для обеспечения чтения. Если с помощью этой функции освобождается последняя блокировка для чтения по заданному объекту блокировки чтения-записи, то этот объект перейдет в разблокированное состояние и, соответственно, не будет иметь владельцев. Если эта функция вызывается, чтобы освободить блокировку для обеспечения записи по заданному объекту блокировки чтения-записи, то этот объект перейдет в разблокированное состояние. Если существуют потоки, заблокированные по этому объекту блокировки, то при его освобождении именно стратегия планирования определяет, какой поток (потоки) получит блокировку. TPS Если потоки, ожидающие освобождения блокировки, выполняются в соответствии со стратегиями планирования SCHED_FIFO, SCHED_RR или SCHED_SPORADIC, то при поддержке опции Thread Execution Scheduling после освобождения этой блокировки потоки получат блокировку в порядке следования их приоритетов. Для потоков с одинаковыми приоритетами блокировки для записи имеют преимущество перед блокировками для чтения. Если опция Thread Execution Scheduling не поддерживается, то будут ли блокировки для записи иметь преимущество перед блокировками для чтения, определяется конкретной реализацией. Результаты не определены, если эта функция вызывается с неинициализирован ным объектом блокировки чтения-записи.
644 Приложение Б Возвращаемое значение При успешном завершении функция pthread_rwlock_unlock () возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки функция pthread_rwlock_unlock () может завершиться неудачно, если: [EINVAL] значение, заданное параметром rwlock, не относится к инициализированному объекту блокировки чтения-записи [EPERM] текущий поток не удерживает объект блокировки чтения-записи для обеспечения записи или чтения. Функция pthread_rwlock_unlock () не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock__destroy (), pthread_rwlock_rdlock (), pthread_rwlock_timedrdlock (), pthread_rwlock_timedwrlock (), pthread_rwlock_tryrdlock(), pthread_rwlock_trywrlock(), pthread_rwlock_wrlock (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1J-2000 были внесены следующие изменения. • В разделе "Синопсис" была изменена метка. Новая метка THR означает, что рассматриваемые функции теперь являются частью опции Threads (ранее они
Приложение Б 645 относились к опции Read-Write Locks стандарта IEEE Std 1003.1J-2000, а также считались частью дополнения XSI). • Раздел "Описание" был отредактирован следующим образом: — заданы условия, при которых записывающие потоки имеют преимущество перед считывающими; — удалена концепция владельца блокировки чтения-записи. • Был отредактирован раздел "Смотри также".
646 Приложение Б Имя pthread_rwlockattr_destroy, pthread_rwlockattr_init — функции разрушения и инициализации объекта атрибутов для блокировки чтения-записи. Синопсис THR #include <pthread.h> 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 () Hpthread_rwlockattr_init () возвращают нулевое значение; в противном случае — код ошибки, обозначающий ее характер. Ошибки Функция pthread_rwlockattr_destroy () может завершиться неудачно, если: [ EINVAL] значение, заданное параметром a t tr, недействительно. Функция pthread_rwlockattr_init () завершится неудачно, если: [ENOMEM] для инициализации объекта атрибутов блокировки чтения-записи недостаточно существующей памяти. Эти функции не возвращают код ошибки [EINTR].
Приложение Б 647 Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock__destroy (), pthread_rwlockattr_getpshared (), pthread_rwlockattr_setpshared (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1J-2000 были внесены следующие изменения. • В разделе "Синопсис" была изменена метка. Новая метка THR означает, что рассматриваемые функции теперь являются частью опции Threads (ранее они относились к опции Read-Write Locks стандарта IEEE Std 1003.1J-2000, а также считались частью дополнения XSI). • Был отредактирован раздел "Смотри также".
648 П ри ложен ие Б Имя pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared — функции считывания и установки атрибута process-shared в объекте атрибутов блокировки чтения-записи. Синопсис THRTSH #include <pthread.h> int pthread_rwlockattr_getpshared( const pthread_rwlockattr_t *restrict attr, int *restrict pshared); int pthread_rwlockattr__setpshared ( pthread_rwlockattr_t * attr, int pshared); Описание Функция pthread_rwlockattr_getpshared() используется для получения значения атрибута process-shared из инициализированного объекта атрибутов, адресуемого параметром attr. Функция pthread_rwlockattr_setpshared () позволяет установить атрибут process-shared в инициализированном объекте атрибутов, адресуемом параметром a t tr. Атрибут pro cess -shared устанавливается равным значению PTHREAD_PROCESS_ SHARED, чтобы разрешить использование объекта блокировки чтения-записи любым потоком, имеющим доступ к области памяти, в которой он размещен, даже если эта область памяти разделяется несколькими процессами. Если же атрибут process- shared равен значению PTHREAD_PROCESS_PRIVATE, объект блокировки чтения- записи должен использоваться только потоками, созданными в одном процессе с потоком, который его инициализировал; если с этим объектом блокировки чтения- записи попытаются работать потоки из различных процессов, поведение такой программы не определено. По умолчанию для этого атрибута устанавливается значение PTHREAD_PROCESS_PRIVATE. Дополнительные атрибуты, их значения по умолчанию и имена соответствующих функций считывания и установки значений этих атрибутов определяются конкретной реализацией. Возвращаемые значения При успешном завершении функция рthread_rwlockattr_getpshared () возвращает нулевое значение и сохраняет считанное значение атрибута process- shared объекта attr ъ объекте, адресуемом параметром pshared; в противном случае возвращается код ошибки, обозначающий ее характер. При успешном завершении функция pthread_rwlockattr_setpshared() возвращает нулевое значение; в противном случае — код ошибки, обозначающий ее характер.
Приложение Б 649 Ошибки Функции pthread_rwlockattr_getpshared() и pthread_rwlockattr__ setpshared () могут завершиться неудачно, если: [ EINVAL ] значение, заданное параметром a t tr, недействительно. Функция pthread_rwlockattr_setpshared () может завершиться неудачно, если: [EINVAL] новое значение, заданное для атрибута, попадает вне диапазона значений, действительных для этого атрибута. Эти функции не возвращают код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Отсутствует. Будущие направления Отсутствуют. Смотри также pthread_rwlock_destroy(), pthread_rwlockattr_destroy(), pthread_rwlockattr_init (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Issue 6 Для согласования со стандартом IEEE Std 1003.1J-2000 были внесены следующие изменения. • В разделе "Синопсис" была изменена метка. Новая метка THR означает, что рассматриваемые функции теперь являются частью опции Threads (ранее они относились к опции Read-Write Locks стандарта IEEE Std 1003.1J-2000, а также считались частью дополнения XSI).
650 Приложение Б • В разделе "Описание" отмечено, что дополнительные атрибуты определяются конкретной реализацией. • Был отредактирован раздел "Смотри также". В целях согласования со стандартом ISO/IEC 9899: 1999 в прототип функции pthread_rwlockattr_getpshared () было добавлено ключевое слово restrict.
Приложение Б 651 Имя pthread_self — функция получения идентификационного номера (ID) вызывающего потока. Синопсис THR #include <pthread.h> pthread__t pthread_self (void) ; Описание Функция pthread_self () возвращает идентификационный номер (ID) вызывающего потока. Возвращаемое значение См. раздел "Описание". Ошибки Коды ошибок не определены. Функция pthread_self () не возвращает код ошибки [EINTR]. Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Функция pthread_self О обеспечивает возможность, аналогичную функции getpid () для процессов, поэтому и логическое обоснование у нее такое же: при вызове функции создания потока идентификационный номер (ID) созданному потоку автоматически не предоставляется. Будущие направления Отсутствуют.
652 Приложение Б Смотри также pthread_create (), pthread_equal (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 5. Включена для согласования с расширением POSIX Threads Extension. Issue 6 Функция pthread_self () отмечена как часть опции Threads.
Приложение Б 653 Имя pthread_setcancelstate, pthread_setcanceltype, pthread_testcancel— функции установки состояния отмены (аннулирования) потока. Синопсис THR #include <pthread.h> int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldtype); void pthread_testcancel(void); Описание Функция pthread_setcancelstate () одновременно устанавливает состояние отмены вызывающего потока равным значению, заданному параметром state, и возвращает значение предыдущего состояния отмены в переменной, адресуемой параметром oldstate. Допустимыми значениями для параметра state являются PTHREAD_CANCEL_ENABLE и PTHREAD_CANCEL_DISABLE. Функция pthread_setcanceltype () одновременно устанавливает тип отмены вызывающего потока равным значению, заданному параметром type, и возвращает значение предыдущего типа отмены в переменной, адресуемой параметром о Id type. Допустимыми значениями для параметра type являются PTHREAD__CANCEL__ DEFERRED и PTHREAD_CANCEL_ASYNCHRONOUS. Состояние и тип отмены любых создаваемых потоков, включая поток, в котором впервые вызывается функция main(), устанавливаются равными значениям PTHREAD_CANCEL_ENABLE и PTHREAD_CANCEL_DEFERRED соответственно. Функция pthread_testcancel () предназначена для создания точки отмены в вызывающем потоке. Функция pthread_testcancel () не имеет эффекта, если отмена потока запрещена. Возвращаемые значения При успешном завершении функции pthread_setcancelstate() и pthread_ setcanceltype () возвращают нулевое значение; в противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функция pthread_setcancelstate () может завершиться неудачно, если: [EINVAL] заданный параметр state не содержит ни значения PTHREAD_ CANCEL_ENABLE, НИ значения PTHREAD_CANCEL_DISABLE. Функция pthread_setcancel type () может завершиться неудачно, если: [EINVAL] заданный параметр type не содержит ни значения PTHREAD_ CANCEL_DEFERRED, ни значения PTHREAD_CANCEL_ASYNCHRONOUS. Эти функции не возвращают код ошибки [EINTR].
654 Приложение Б Примеры Отсутствуют. Замечания по использованию Отсутствуют. Логическое обоснование Функции pthread_set cancel state () и pthread_setcanceltype () позволяют управлять точками, в которых поток можно асинхронно отменить. Для того чтобы управление отменой потоков можно было осуществлять в соответствии с модульными принципами, необходимо следовать следующим правилам. Объект можно рассматривать как обобщение некоторой процедуры. Вернее, он представляет собой множество процедур и глобальных переменных, организованных в виде одного модуля, вызываемого клиентами, не известными для этого объекта, причем одни объекты могут зависеть от других. Во-первых, на входе в объект возможность отмены должна быть запрещена (никогда явно не разрешена). На выходе из объекта состояние отмены должно быть всегда восстановлено до значения, которое оно имело на входе в этот объект. Это следует из принципа модульности: если клиент объекта (или клиент объекта, использующего данный объект) запретил возможность отмены, это означает, что клиент не желает проведения очистительно-восстановительных операций в случае, если поток будет отменен во время выполнения некоторой важной последовательности действий. Если объект вызывается в таком состоянии и предоставляет возможность отмены, а запрос на отмену задерживается для этого потока, то такой поток отменяется вопреки желанию клиента (т.е. вопреки запрету на отмену). Во-вторых, на входе в объект тип отмены можно установить явным образом (равным либо "отложенному", либо "асинхронному" значению). Но, как и для состояния отмены, на выходе из объекта тип отмены должен быть всегда восстановлен до значения, которое он имел на входе в этот объект. Наконец, из потока, который позволяет асинхронную отмену, можно вызывать только безопасные (с точки зрения отмены) функции. Будущие направления Отсутствуют. Смотри также pthread_cancel (), том Base Definitions стандарта IEEE Std 1003.1-2001, <pthread.h>. Последовательность внесения изменений Функции впервые реализованы в выпуске Issue 5. Включены для согласования с расширением POSIX Threads Extension. Issue 6 Функции pthread_set cancel state (), pthread_set cancel type () и pthread___ testcancel () отмечены как часть опции Threads.
Приложение Б 655 Имя pthread_setschedprio— функция динамического доступа к параметрам планирования потока (REALTIME THREADS). Синопсис THRTPS #include <pthread.h> int pthread_setschedprio(pthread_t thread, int prio); Описание Функция pthread_setschedprio () используется для установки приоритета планирования равным значению, заданному параметром prio, для потока, идентификационный номер (ID) которого задан параметром thread. В случае неудачного завершения функции pthread_setschedprio () приоритет планирования заданного потока останется без изменения. Возвращаемое значение При успешном завершении функция pthread_setschedprio () возвращает нулевое значение; в противном случае возвращается код ошибки, обозначающий ее характер. Ошибки Функция pthread_setschedprio () может завершиться неудачно, если: [EINVAL] значение параметра prio не действительно для стратегии планирования заданного потока; [ENOTSUP] была сделана попытка установить приоритет равным значению, которое не поддерживается; [EPERM] инициатор вызова не имеет соответствующего разрешения на установку параметров стратегии планирования заданного потока; [EPERM] реализация не позволяет приложению модифицировать приоритет, устанавливая его равным заданному значению; [ESRCH] значение, заданное параметром thread, не относится к существующему потоку. Функция pthread_setschedprio () не возвращает код ошибки [EINTR]. Примеры Отсутствуют.
656 Приложение Б Замечания по использованию Отсутствуют. Логическое обоснование Функция pthread_setschedprio() обеспечивает для приложения возможность временного увеличения приоритета с последующим его понижением без нежелательного побочного эффекта, выражаемого в установке другими потоками такого же приоритета. Это нужно, если приложение должно реализовать такие собственные стратегии для ограничения инверсии приоритетов, как наследование приоритетов или использование предельных значений приоритетов. Эта возможность особенно важна, если реализация не поддерживает опции Thread Priority Protection или Thread Priority Inheritance, но даже в случае их поддержки эту возможность необходимо использовать, если приложение обязано привязывать наследование приоритетов к использованию таких ресурсов, как семафоры. Несмотря на то что, возможно, предпочтительнее было бы решить эту проблему, модифицируя спецификацию функции pthread_.set schedparam(), было слишком поздно вносить такое изменение, поскольку уже существовали реализации, которые пришлось бы в этом случае изменять. Поэтому данная функция и была введена. Будущие направления Отсутствуют. Смотри также pthread_getschedparam(), том Base Definitions стандарта IEEE Std 1003.1-2001, <р thread. h>. Последовательность внесения изменений Функция впервые реализована в выпуске Issue 6. Включена в качестве реакции на интерпретацию IEEE PASC Interpretation 1003.1 #96.
СПИСОК ЛИТЕРАТУРЫ 1. Audi, Robert. Action, Intention, and Reason. Ithaca, N. Y.: Cornell University Press, 1993. 2. Axford, Tom. Concurrent Programming: Fundamental Techniques for Real-Time and Parallel Software Design, Chichester, U. K.: John Wiley, 1989. 3. Baase, Sarah. Computer Algorithms: Introduction to Design and Analysis. 2nd ed. Reading, Mass.: Addison-Wesley, 1988. 4. Barfield, Woodrow, and Thomas A. Furnell III. Virtual Environments and Advanced Interface Design. New York: Oxford University Press, 1995. 5. Binkley, Robert, Bronaugh, Richard, and Ausonio Marras. Agent, Action, and Reason. Toronto: University of Toronto Press, 1971. 6. Booch, Grady, James Rumbaugh, and Ivar Jacobson. The Unified Modeling Language User Guide. Boston: Addison-Wesley, 1999. 7. Bowan, Howard, and John Derrick. Formal Methods for Distributed Processing: A Survey of Object-Oriented Approaches. New York: 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 Coding Reusable C++. Reading, Mass.: Addison-Wesley, 1995. 10. Cassell, Justine, Joseph Sullivan, Scott Prevost, and Elizabeth Churchill. Embodied Conversational Agents. Cambridge, Mass.: MIT Press, 2000. 11. Chellas, Brian F. Modal Logic: An Introduction. New York: Cambridge University Press, 1980. 12. Coplien, James O. Multi-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. Blackboard Systems. Wokingham, England: Addison-Wesley, 1988. 15. Garg, Vijay K. Principles of Distributed Systems. Norwell, Mass.: Kluwer Academic, 1996.
658 Список литературы 16. Geist, Al, Adam Beguelin, Jack Dongarra, Weicheng Jiang, Robert Manchek, and Vaidy Sinderman. PVM: Parallel Virtual Machine. London, England: MIT Press, 1994. 17. Goodheart, Berny, and James Cox. The Magic Garden Explained: The Internals of Unix System VRelease 4. New York: Prentice Hall, 1994. 18. Gropp, William, Steven Huss-Lederman, Andrew Lumsdaine, Ewing Lusk, Bill Nitzberg, William Saphir, and Marc Snir. MPI: The Complete 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 of Epistemology and the Epistemology of Logic. Amsterdam: Kluwer Academic, 1989. 22. Horty, John F. Agency and Deontic Logic. New York: Oxford University Press, 2001. 23. Hughes, Cameron, and Tracey Hughes. Mastering the Standard C++ Classes. New York: John Wiley, 1990. 24. Hughes, Cameron, and Tracey Hughes. Object-Oriented Multithreading Using C++. New York: John Wiley, 1997. 25. Hughes, Cameron, and Tracey Hughes. Linux Rapid Application Development. Foster City, Calif.: M 8c T Books, 2000. 26. International Standard Organization. Information Technology: Portable 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, С 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, Larry Rosenstein, and Kraus, Sarit. Strategic Negotiation in Multitangent Environments. London: MIT Press, 2001. 31. Luger, George F. Artificial Intelligence. 4th ed. England: Addison-Wesley, 2002. 32. Mandrioli Dino, and Carlo Ghezzi. Theoretical Foundations of Computer Science. New York: John Wiley, 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 Synthesis of Intelligent Agents. 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.
Список литературы 659 37. Robbins, Kay A., and Steven Robbins. Practical Unix Programming, Upper Saddle River, N. J.: Prentice Hall, 1996. 38. Schmucker, Kurt, Ander Weinand, and John M. Vlissides. Object-Oriented Application Frameworks. Greenwich, Conn.: Manning Publications, 1995. 39. Singh, Harry. Progressing to Distributed Multiprocessing. Upper Saddle River, N.J.: Prentice Hall, 1999. 40. Skillicorn, David. Foundations of Parallel Programming. New York: Cambridge University Press, 1994. 41. Soukup, Jiri. Taming C++: Pattern Classes and Persistence for Large Projects. Reading, Mass.: Addison-Wesley, 1994. 42. Sterling, Thomas L., John Salmon, Donald J. Becker, and Daniel F. Savarese. How to Build a Bewoulf: A Guide to Implementation and Application of PC Clusters. London: MIT Press, 1999. 43. Stevens, Richard W. UNIX Network Programming: Interprocess Communications. Vol. 2, 2nd ed. Upper Saddle River: Prentice Hall, 1999. 44. Stroustrup, Bjarne. The Design and Evolution of C++. Reading, Mass.: Addison-Wesley, 1994. 45. Subrahmanian, V.S., Piero Bonatti, Jurgen Dix, Thomas Eiter, Sarit Kraus, Fatma Ozcan, and Robert Ross. Heterogeneous Agent Systems. Cambridge, Mass.: МГГ Press, 2000. 46. Tel, Gerard. Introduction to Distributed Algorithms. 2nd ed. New York: Cambridge University Press, 2000. 47. Thompson, William J. Computing for Scientists and Engineers. New York: John Wiley, 1992. 48. Tomas, Gerald, and Christoph W. Uebeerhuber. Visualization of Scientific Parallel Programming. New York: Springer-Verlag, 1994. 49. Tracy, Kim W. and Peter Bouthoorn. Object-Oriented: Artificial Intelligence Using C++. New York: Computer Science Press, 1997. 50. Weiss, Gerhard. Multitagent Systems. Cambridge, Mass.: МГГ Press, 1999. 51. Wooldridge, Michael. Reasoning About Rational Agents. London: MIT Press, 2000.
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ A Ada, 513; 514; 520 ADT, ^35 AI, 17 American National Standards Institute, 18 ANSI, 18 AOP, 435 API, i9 Artificial intelligence, 17 в Basic object adapter, 293 BDI, 43G; 450 BFS, 440 BOA, 292; 303 ВОА-адаптер, 293 Breadth First Search, 440 Bug, 248 С catch, 259 CDS, 443; 444; 450 class, 271 Class diagram, 335 Clipboard, 35 Cognitive Data Structures, 443 Common Object Request Broker Architecture, 17 CORBA, 17; 35; 37; 38; 274; 435; 466 CR (Concurrent Read), 343 CRCW, 29, 170; 343; 437 CREW, 29, 170; 203; 437; 466 crontab, 460 CW (Concurrent Write), 343 D Daemon, 74 Data race, 47 DCS, 443 Decomposition, 271 Depth First Search, 440 deque, 351 DFS, 440, 442; 454 E Eiffel, 269 Encapsulation, 271 ER (Exclusive Read), 343 ERCW, 29, 170,437 EREW, 29, 170, 343; 437; 466 Error, 248 EW (Exclusive Write), 343 Extranet, 269
Предметный указатель F Failure, 248 Fault, 248 FIFO, 19, 67; 415; 435 FIPA, 429 FTP, протокол, 280 H HTTP, 289, 306 I IDL, 282 IDL-компилятор, 292 IEEE, 19 HOP, 39 ПОР, протокол, 274 Implementation Repository, 298 imr, утилита, 305 Indefinite postponement, 48 interface, 283 Interface Definition Language, 282 Interface Repository, 298 International Organization for Standardization, 18 Internet Protocol, 17 Interoperable Object Reference, 277 Inter-Process Communication, 50, 393 Inter-Thread Communication, 393 Intranet, 269 IOR, 277 IPC, 50t 60, 393; 412; 420, 435; 488 ird, 306 ISO, 36 ITC, 393 J Java, 269 L LIFO, 438 list, 351 M MAF, 461 Main thread, 356 Man pages, 461 Massively Parallel Processing, 325 Massively Parallel Processors, 28 Message Passing Interface, 17; 26; 314 MICO, 17; 39, 274 MIMD, 30, 213; 466 MIOR-ссылка, 294 MIWCO, 294 MMU, 512; 514 Mobile IOR, 294 MPI, 17; 26; 37; 314; 435 MPICH, 19, 37 MPMD, 30,134 MPP, 28; 38; 212; 325 MTMD, 134 Multi-agent facility, 461 Multiple-Instruction, Multiple-Data, 30 Multiple-Programs, Multiple-Data, 30 Mutex, 165 N Name binding, 294 Naming context, 294 Naming graph, 295 NFS, 289 Nice value, 71 NLG, 375 NLI, 375 NLP, 223; 375 О Object Management Group, 338 Object-to-Object Communicaton, 393 OMG, 338 ORB, 51; 278; 461 ORB-брокер, 484 OTOC, 393 P Parallel Random Access Machine, 29 Parallel Virtual Machine, 26 PCB, 59 Peer-to-peer, 32
662 Предметный указатель PID, 60 РОА, 303 РОА-адаптер, 293 РОА-менеджер, 304 Portable object adapter, 293 POSIX, 19,36 Postcondition, 185 PPID, 60 PRAM, 29, 30; 189 Precondition, 185 Process, 26; 58 Prolog, 480 ps, утилита, 69 pstree, утилита, 77 Pthread, 134 Pthreads, 19 PVM, 17; 26; 37; 212; 435 PVM-демон, 233 R Race condition, 47 Read lock, 388 RMI, протокол, 274 RPC, 488 RR, 67 Runtime polymorphism, 324 s Secure Shell, 307 Secure Socket Layer, 307 Semantic network, 297 SIMD, 3ft 213 Single UNIX Specifications Standard, 19 Single-Instruction, Multiple-Data, 30 Single-Program, Multiple-Data, 30 Smalltalk, 269 SMP, 38; 325 SPMD, 3ft 133; 213 SSH, 307 SSL, 307 stack, класс, 351 Standard Template Library, 351 STL, 351 STMD, 134 Symmetrical Multiprocessing, 325 T TCP/IP, 17 telnet, 306 template, 346 Thread, 26 main, 356 throw, 259 Trading service, 307 Transmission Control Protocol, 17 try, 260 TTS-engine, 223 и UML, 2ft 55; 337; 497 Unified Modeling Language, 2ft 337; 497 v vector, 357 virtual, 324 w WAP WDP, 294 WBM, 272 WBR, 53 WBS, 33; 129, 432; 464 WCORBA, 294 Wireless Mico, 294 X XDR, стандарт, 236 XPVM, 218 A Абдукция, 433 Агент, З^ 429 реализация в C++, 444 стратегии логического вывода, 452 цикл активизации, 451 Агентно-ориентированная система, 423 Агрегирование, 348 Адаптер, 350 базовый объектный, 292 переносимый объектный, 303
Предметный указатель 663 Алгоритм сору(), 413; 417 CRCW, 191 CREW, 191 ERCW, 191 EREW, 191 find(), 456 open(), 417 transform(), 414 Архитектура:, 388 системы, 373 Ассоциация, 347 Б Бесконечная отсрочка, 48 Библиотека iostream, 171 libpthread, 194 libpvm, 214 libpvm3.a, 217 MPICH, 37 POSIX, 36 POSIX Threads, 36 Pthread, 134 Pthreads, 37 pvmd, 233 шаблонов, 351 Бинарная связь, 347 Блокировка считывания, 388 Брокер объектных запросов, 278 БУП, 59 В Взаимоблокировка, 48 г Гонка данных, 47 Д Дедукция, 433 Декомпозиция, 271 работ, 33 Дефект, 248 Диаграмма видов деятельности, 360; 501 взаимодействия, 499 класса, 338 классов, 497 объектов, 497 пакетов, 506 последовательностей, 359, 499 развертывания, 371 событий, 265 состояний, 364; 505 сотрудничества, 354; 499 ДСС, 33 3 Зависимость, 347 и Именной граф, 295 контекст, 294 Индукция, 433 Инкапсуляция, 271 Интерфейс передачи сообщений, 314 Информационный голод, 128 Исключение, 248 Исключительная ситуация, 248 Источник знаний, 465; 470 Итератор istream_iterator, 408 ostream_iterator, 408 К Канал, 401 именованный, 416 Квант времени, 64 Класс, 338 agent_framework, 424 basic_filebuf, 404 basic_streambuf, 404 basic_stringbuf, 404 domain_error, 261 exception, 261 invalid_argument, 261 ios, 394 istream, 394 istream_iterator, 409 length_error, 261 logic_error, 261; 262
664 Предметный указатель map, 346 mutex, 381; 397 ostream, 394 ostream_iterator, 409 out_of_range, 261 overflow_error, 261 pthread_cond_t, 205 pthread_mutex_t, 166; 194; 381 pvm_stream, 397 range_error, 261 runtime_error, 261 stack, 351 underflow_error, 261 vector, 346 абстрактный, 379 адаптерный, 380 активный, 356 базовый, 347; 379 доменный, 379 интерфейсный, 350; 379 каркасный, 379, 421 конкретный, 379 контейнерный, 379 синхронизации, 379 составной, 379 сыновний, 347 узловой, 379 шаблонный, 346; 379 Классная доска, 388 Классы исключений, 260 Кластер, 40 Коммуникатор, 317 Конечные автоматы, 364 Контейнер deque, 351 list, 351 vector, 351 м Маклер, 308 Маклерская служба, 307 Методология, 16 Механизмы обработки исключений, 259 Многозадачность, 101 Многопоточность, 101 Множественность, 340 Моделирование, 337 Модель BDI, 430; 450 CRCW, 170 CREW, 170,389 ERCW, 170 EREW, 170 MIMD, 213; 472 MPMD, 134; 315; 322; 327; 472 MTMD, 134 PRAM, 189, 343 SIMD, 213; 314; 484 SPMD, 134; 213; 314; 321; 472 STMD, 134 декомпозиции работ, 272 делегирования, 130, 173 конвейер, 130 конвейера, 132; 177 равноправных узлов, 32; 130, 132; 177 системы, 337 Модель:, 32; 130, 133; 178; 286; 465 ПО, 271 МПВ-технология, 50 Мультиагентная система, 423; 461 Мультиагентные распределенные системы, 32 Мьютекс, 165; 380, 392 н Надежность ПО, 247 о Обобщение, 347 Обработчик исключений, 260 Образ процесса, 61 Объект активный, 356 распределенный, 371 Объектная ссылка, 277 Оператор вставки («), 395 извлечения данных (»), 396 Оплошность, 248 Отказоустойчивая система, 248 Отказоустойчивость, 248 Отношения между классами, 347 Отношения синхронизации старт-старт, 186
Предметный указатель 665 старт-финиш, 188 финиш-старт, 187 финиш-финиш, 188 Ошибка, 248 п Пакет, 374 Перегрузка операторов, 395 Подкласс, 347 Подсостояние, 369 Поиск в ширину, 440 вглубь, 440 Полиморфизм, 257; 317; 322 вертикальный, 327 горизонтальный, 327 динамического связывания, 327 параметрический, 327 Постусловие, 185 Поток, 26 определение, 113 Предусловие, 185 Прецедент, 359 Протокол FTP, 280 HTTP, 289, 306 ПОР, 39, 274 RMI, 274 TCP/IP, 274; 290 telnet, 306 защищенных сокетов, 307 передачи файлов, 280 Процесс, 26; 58; 356 ink, 74 образ, 61 создание, 74 Р Ранг, 315 Распределенные объекты, 269 Реализатор, 352 Реализация, 351 Решатель задач, 465 С Сбой, 248 Связывание по имени, 294 Семантическая сеть, 297 Семафор, 193; 392 мьютексный, 194 условная переменная, 205 Сервер баз данных, 310 логический, 310 приложений, 310 транзакций, 310 файловый, 310 Синхронизация, 185 доступа к данным, 189 потоков, 185 Служба имен, 294 Состояние гонок, 47 действия, 360 деятельности, 362 подсостояние, 369 составное, 369 элемента, 364 Сотрудничество, 353 объектов, 354 Стековый фрейм, 154 Стереотип, 347 Стратегия планирования, 67 FIFO, 67; 126 RR, 67; 126 Суперкласс, 347 Суперсостояние, 369 у Условная переменная, 205 Утилита crontab, 460 imr, 305 ird, 306 Ф Фактор уступчивости, 71 Формат In Place, 237 Raw, 237 XDR, 237 Функция abort(), 90 exec, 78 execl(), 79
666 Предметный указатель execle(), 79 execlpO, 79 execv(), 80, 81 execve(), 80, 81 execvp(), 80, 81 exit(), 90 fclose(), 90 fork(), 75 getenv(), S2i -/72 getpid(), 89, 141 getppid(), 89 getpriorityO, 72 getrlimit(), 96 getrusage(), 96 kill(), 91 pathconf(), 403 pipe(), 403 posix_spawn, 507 posix_spawn(), 83 posix_spawn_file_actions_addclose(), 518 posix_spawn_file_actions_adddup2(), 522 posix_spawn_file_actions_addopen, 518 posix_spawn_file_actions_destroy(), 524 posix_spawn_file_actions_init (), 524 posix_spawnattr_destroy(), 526 posix_spawnattr__getflags(), 528 posix_spawnattr__getpgroup(), 530 posix_spawnattr_getschedparam (), 532 posix_spawnattr_getschedpolicy(), 534 posix_spawnattr_getsigdefault (), 536 posix_spawnattr_getsigmask(), 538 posix_spawnattr_init (), 526 posix_spawnattr_setflags(), 528 posix_spawnattr_setpgroup(), 530 posix_spawnattr_setschedparam (), 532 posix_spawnattr_setschedpolicy(), 534 posix_spawnattr_setsigdefault(), 536 posix_spawnattr_setsigmask(), 538 posix_spawnp, 507 pth read_attr_destroy (), 143 pth read_attr__getde tachstate (), 144 pthread_attr_getscope(), 162 pthread_attr__getstack(), 156 pthread_attr_getstackaddr(), 156 pthread_attr__getstacksize (), 7 55 pthread_attr_init(), 143 pthread_attr_setdetachstate(), 144 pthread_attr_setinheritsched (), 158 pthread_attr_setschedparam(), 159 pthread_attr_setschedpolicy (), 158 pthread_attr_setscope(), 162 pthread_attr_setstack(), 756 pthread_attr_setstackaddr(), 756 pthread_attr_setstacksize(), 755 pthread_cancel(), 746 pthread_cleanup_pop(), 753 pthread_cleanup_push(), 753 pthread_cond_destroy(), 565 pthread_cond_init, 565 pthread_cond_timedwait(), 568 pthread_cond_wait(), 568 pthread_condattr_destroy(), 574 Pthread_condattr__getpshared(), 5 76 pthread_condattr_init(), 574 pthread_condattr_setpshared(), 5 76 pthread_create, 578 pthread_create(), 739 pthread_detach(), 142; 582 pthread_exit(), 146; 584 pthread__getconcurrency(), 586 pthread_getschedparam(), 767; 588 pthread join(), 747; 189, 591 pthread_mutex_destroy(), 766; 381; 594 pthread_mutex__getprioceiling(), 601 pthread_mutex_init(), 196; 381; 594 pthread_mutex_lock(), 766; 381; 603 pthread_mutex_setprioceiling(), 601 pthread_mutex_timedlock(), 381; 607 pthread_mutex_trylock(), 381; 603 pthread_mutex_unlock(), 766; 381; 603 pthread_mutexattr_destroy(), 609 pthread_mutexattr_getprioceiling(), 675 pthread_mutexattr__getprotocol (), 67 7 pthread_mutexattr_getpshared (), 620 pthread_mutexattr__gettype(), 622 pthread_mutexattr_init(), 609 pthread_mutexattr_setprioceiling(), 675 pthread_mutexattr_setprotocol(), 67 7 pthread_mutexattr_setpshared (), 620 pthread_mutexattr_settype (), 622 pthread_once, 625 pthread_rwlock_destroy(), 628 pthread_rwlock_init(), 628 pthread_rwlock_rdlock(), 637 pthread_rwlock_timedrdlock(), 634 pthread_rwlock_timedwiiock(), 637 Функция pthread_rwlock_tryrdlock(), 637 pthread_rwlock_trywrlock(), 640 pthread_rwlock_unlock(), 643
pthread_rwlock_wrlock(), 640 pthread_rwlockattr_destroy(), 646 pthread_rwlockattr__getpshared(), 648 pthread_rwlockattr_init(), 646 pthread_rwlockattr_setpshared(), 648 pthread_self(), 141; 651 pthread_setcancelstate(), 147; 653 pthread_setcanceltype(), 147; 653 pthread_setconcurrency(), 586 pthread_setschedparam(), 161; 588 pthread_setschedprio, 655 pthread_setschedprio(), 162 pthread_testcancel(), 148; 653 pvm_addhosts(), 235 pvm_barrier(), 481 pvm_delhosts(), 235 pvm_exit(), 216; 235 pvm_freebuf(), 242 pvm__getrbuf(), 241 pvm__getsbuf(), 241 pvm_halt(), 236 pvm_initsend(), 216; 237 pvm_joingroup(), 481 pvm_kill(), 235 pvm_mcast(), 238 pvm_mkbuf(), 242 pvm_mytid(), 216 pvm_nrecv(), 240 pvm_pkbyte, 236 pvm_pkcplx, 236 pvm_pkdcplx, 236 pvm_pkdouble, 236 pvm_pkdouble(), 394 pvm_pkfloat, 237 pvm_pkfloat(), 394 pvm_pkint, 237 pvm_pkint(), 394 pvm_pklong, 237 pvm_pkshort, 237 pvm_pkstr, 237 pvm_pkstr(), 216 pvm_precv(), 240 pvm_probe(), 241 pvm_psend(), 238 pvm_recv(), 216; 239 Предметный указатель 667 pvm_send(), 216; 238 pvm_setopt(), 243 pvm_setrbuf(), 242 pvm_setsbuf(), 241 pvm_spawn(), 216; 232; 235 pvm_trecv(), 240 pvm_upkfloat(), 216 sched_gei_priority__max(), 159 sched_get_priority__min(), 159 sem_close(), 391 sem__destroy(), 391 sem_getvalue(), 391 sem_init(), 391 sem__open(), 391 sem_post(), 391 sem__trywait(), 391 sem_unlink(), 391 sem_wait(), 391 setenv(), 83; 412 setpriorityO, 72 sysconf(), 81; 162; 403 system(), 83; 513 unsetenv(), 83 wait(), 99; 188 waitpid(), 100 X Хранилище интерфейсов, 298; 306 реализаций, 298; 305 Хрон, 460 ш Шаблон, 346 э Экстрасеть, 269 я Язык UML, 337
C++ Параллельное и распределенное программирование с использованием В книге Параллельное и распределенное программирование с использованием C++ отражен свежий взгляд на построение программ, ориентированных на использование преимуществ мультипроцессорных компьютеров. Здесь представлены простые методы программирования параллельных виртуальных машин и основы разработки кластерных приложений Эта книга не только научит создавать программные компоненты, предназначенные для совместной работы в сетевой среде, но и послужит надежным "путеводителем" по стандартам для программистов которые занимаются многозадачными и многопоточными приложениями. Данная книга, представляющая архитектурный подход к параллельному программированию, адресована программистам, проектировщикам и разработчикам программных продуктов, а также научным работникам преподавателям и студентам, которых интересует введение в параллельное и распределенное программирование с использованием языка C++. • Возможность использования агентов и технологии "классной доски' для уг программирования • Объектно-ориентированные подходы к многозадачности и многопоточноа • Использование средств языка UML в разработке проектов, требующих при] распределенного программирования. • Новый стандарт POSIX/UNIX IEEE для библиотеки Pthreads. Об авторах КАМЕРОН ХЬЮЗ — старший инженер-программист компании CTEST Laboratories и штатный программист- аналитик государственного университета в г. Янгстаун (Youngstown State University) Он участвовал в разработках программных продуктов различного уровня, а в настоящее время занимается кластерным программированием для системы PROTEUS V, в которой используется обработка данных на естественном языке (NLP) и моделирование представления знаний. ТРЕЙСИ ХЬЮЗ — инженер-программист компании CTEST Laboratories. Она занимается программированием машинной графики и имитационным моделированием на C++, успешно разрабатывает библиотеки классов для обработки изображений и создает виртуальные миры КАМЕРОН и ТРЕЙСИ ХЬЮЗ — соавторы многочисленных книг, включая Linux Rapid Application Development и Object-Oriented Multithreading Using C++. к Посетите Издательский дом "Вильяме" в Internet по адресу: http://www.williamspublishing.com /▼Addison-Wesley Pearson Education www. awprof essional. com ISBN 5-8459-0686-5 0413 1 9"785845M906861 ]