/
Tags: компьютерные технологии программирование язык программирования c++
ISBN: 5-8459-0686-5
Year: 2004
Text
Данный файл был взят с сайта: -FVQHBook.IlGfe Данный файл представлен исключительно в ознакомительных целях. Уважаемый читатель! Если вы скопируете его, Вы должны незамедлительно удалить его сразу после ознакомления с содержанием. Копируя и сохраняя его Вы принимаете на себя всю ответственность, согласно действующему международному законодательству. Все авторские права на данный файл сохраняются за правообладателем. Любое коммерческое и иное использование кроме предварительного ознакомления запрещено. Публикация данного документа не преследует за собой никакой коммерческой выгоды. Но такие документы способствуют быстрейшему профессиональному и духовному росту читателей и являются рекламой бумажных изданий таких документов. Все авторские права сохраняются за правообладателем. Если Вы являетесь автором данного документа и хотите дополнить его или изменить, уточнить реквизиты автора или опубликовать другие документы, пожалуйста, свяжитесь с нами по e-mail - мы будем рады услышать ваши пожелания. ProgBook.net - библиотека программиста. В нашей библиотеке Вы найдете книги и статьи практически по любому языку программирования.
параллельное и распределенное программирование с использованием Камерон Хьюз Трейси Хьюз
Параллельное и распределенное программирование с использованием
Parallel and Distributed Programming Using Cameron Hughes • Tracey Hughes Л Addison-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 (pyc.) ISBN 0-13-101376-9 (англ.) © Издательский дом “Вильямс”, 2004 © Pearson Education, Inc., 2004
ОГЛАВЛЕНИЕ Введение 16 Глава 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 1.2.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. Использование семейства системных функций ехес 78 3.6.4. Использование функции system() для порождения процессов 83 3.6.5. Использование posix-функций для порождения процессов 83
Содержание $ 3.6.6. Идентификация родительских и сыновних процессов с помощью функций управления процессами 8С 3.7. Завершение процесса 82 3.7.1. Функции exit(), kill() и abort() 9С 3.8. Ресурсы процессов 91 3.8.1. Типы ресурсов 93 3.8.2. Posix-функции для установки ограничений доступа к ресурсам 94 3.9. Асинхронные и синхронные процессы 97 3.9.1. Создание синхронных и асинхронных процессов с помощью функций fork(), ехес(), system() и posix_spawn() 99 3.9.2. Функция wait() 99 3.10. Разбиение программы на задачи 101 3.10.1. Линии видимого контура 108 3.11. Резюме 109 Глава 4. Разбиение С++-программ на множество потоков 111 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 211 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. Анатомия MPI-задачи 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
Эта книга посвящена всем программистам, “безвредным”хакерам, инженерам-полуночникам и бесчисленным добровольцам, которые без устали и сожаления отдают свой талант, мастерство, опыт и время, чтобы сделать открытые программные продукты реальностью и совершить революцию в 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. Эти компоненты можно использовать для обеспечения различных Intemet/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) версии 3 — совместный труд Института инженеров по электротехнике и электронике (Institute of Electrical and Electronics Engineers — IEEE2) и организации Open Group — был выпущен в декабре 2001 года. Новый единый стандарт специфика- ций UNIX реализует стандарты POSIX и способствует повышению уровня переноси- мости программных продуктов. Его основное назначение — дать разработчикам про- граммного обеспечения единый набор API-функций (Application Programming Interface — интерфейс прикладного программирования, т.е. набор функций, предос- тавляемый для использования в прикладных программах), поддерживаемых каждой UNIX-системой. Этот документ обеспечивает надежный “путеводитель” по стандар- там для программистов, которые занимаются многозадачными и многопоточными приложениями. В этой книге, рассматривая темы создания процессов, управления процессами, использования библиотеки Pthreads, новых процедур posix_spawn (), POSIX-семафоров и FIFO-очередей (/irst-m, yirst-out — “первым поступил, первым об- служен”), мы опираемся исключительно на новый единый стандарт спецификаций UNIX. В приложении Б представлены выдержки из этого стандарта, которые могут быть использованы в качестве справочника для изложенного нами материала. Для кого написана эта книга Эта книга предназначена для проектировщиков и разработчиков программного обеспечения, прикладных программистов и научных работников, преподавателей и студентов, которых интересует введение в параллельное и распределенное про- граммирование с использованием языка C++. Для освоения материала этой книги чи- тателю необходимо иметь базовые знания языка C++ и стандартной С++-библиотеки классов, поскольку учебный курс по программированию на C++ и по объектно-1 ориентированному программированию здесь не предусмотрен. Предполагается, что читатель должен иметь общее представление о таких принципах объектно-| 1 POSIX— Portable Operating System Interface for computer environments— интерфейс переносимой one\ рационной системы (набор стандартов IEEE, описывающих интерфейсы ОС для UNIX). 2 IEEE— профессиональное объединение, выпускающие свои собственные стандарты; членами /£££| являются ANSI и ISO.
20 Введение ориентированного программирования, как инкапсуляция, наследование и полимор- физм. В настоящей книге излагаются основы параллельного и распределенного про- граммирования в контексте C++. Среды разработки Примеры и программы, представленные в этой книге, разработаны и протестиро- ваны в Linux- и UNIX-средах, а именно — под управлением Solaris 8, Aix и Linux (SuSE, Red Hat). MPI- и PVM-код разработан и протестирован на 32-узловом Linux-ориенти- рованном кластере. Многие программы протестированы на серверах семейства Sun Enterprise 450. Мы использовали Sun C++ Workshop (С++-компилятор компании Port- land 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 Е-250 и Е-450. Мы также признательны Мэри Энн Джонсон (Mary Ann Johnson) и Джеффри Тримблу (Jeffrey Trimble) из YSU MAAG за помощь в получении справочной информации; Клавдию М. Стэнзиоло (Claudio М. 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) за то, что он указал нужное направление при составлении категорий структур данных, которые можно использовать в PYM (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. Резюме
“Я допускаю, что параллелизм лучше всего поддерживать с помощью библиотеки, причем такую библиотеку можно реализовать без существенных расширений самого языка программирования.” — Бьерн Страуструп, создатель языка 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. Рис.Д/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-модель об- ладает как параллельными, так и исключающими ал- горитмами считывания данных. Параллельные алго- Рис. 1.2. Простая модель PRAM ритмы считывания данных позволяют одновременно обращаться к одной и той же области памяти без ис- кажения (порчи) данных. Исключающие алгоритмы считывания данных использу- ются в случае, когда необходима гарантия того, что никакие два процесса никогда не будут считывать данные из одной и той же области памяти одновременно. PRAM- модель также обладает параллельными и исключающими алгоритмами записи дан- ных. Параллельные алгоритмы позволяют нескольким процессам одновременно записывать данные в одну и ту же область памяти, в то время как исключающие ал- горитмы гарантируют, что никакие два процесса не будут записывать данные в одну и ту же область памяти одновременно. Четыре основных алгоритма считывания и записи данных перечислены в табл. 1.1. Таблица 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). И в самом деле, эти схемы и модели ус- пешно используются для реализации практических мелко- и среднемасштабных приложений и вполне могут вас устраивать до тех пор, пока вы не подготовитесь к параллельному программированию более высокой степени организации. 1 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.8 компонент (А + В) можно вычислить одновременно с компонентом (Q _ о) • Этот вид параллелизма обычно поддерживается директивами компилятора и не попадает под управление С++-программиста. X = (А + В ) * ( С - D ) Х1 = а + в х2 s с -d Параллельное выполнение 1J..... Синхронизация 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- Data— одна программа, несколько потоков данных) и 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 multi- processor (симметричный мультипроцессор).
1.6. Отсутствие языковой поддержки параллелизма в C++ 39 /oeer-to-peer). В мультиагентных системах работа может быть организована параллельно. Это одна из областей, в которых параллельное и распределенное программирование пе- пекрываются. Несмотря на то что агенты выполняются на различных компьютерах, это происходит в течение одного и того же промежутка времени, т.е. агенты совместно ра- ботают над общей проблемой. Стандарт CORBA обеспечивает открытую, независимую от изготовителя архитектуру и инфраструктуру, которую компьютерные приложения используют для совместного функционирования в сети. Используя стандартный прото- кол ПОР (Internet InterORB Protocol — протокол, определяющий передачу сообщений между сетевыми объектами по TCP/IP), CORBA-ориентированная программа (созданная любым производителем на любом языке программирования, выполняемая практически на любом компьютере под управлением любой операционной системы в любой сети) может взаимодействовать с другой CORBA-ориентированной програм- мой (созданной тем же или другим производителем на любом другом языке програм- мирования, выполняемой практически на любом компьютере под управлением любой операционной системы в любой сети). В этой книге мы используем М1СО-реализацию стандарта 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 и POSIXThreads с C++ Библиотека Использование с C++ i 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. Резюме
“Стремление обозначать точные значения любой физической величины (температура, плотность, напряженность потенциального поля или что-либо еще...) есть не что иное как смелая экстраполяция.” — Эрвин Шредингер (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 есть программа поддержки электронного банка, которая позволяет снимать деньги со счета и класть их на депозит. Допустим, что эта программа разделена на три задачи (обозначим их А, В и С), которые могут выполняться параллельно. Параллелизм на уровне инструкций Эти компоненты инструкции могут выполняться параллельно. X Рис. 2.1. Три уровня параллелизма, которые возможны в одной компьютерной программе Задача А получает запросы от задачи В на выполнение операций снятия денег со счета. Задача А также получает запросы от задачи С положить деньги на депозит. За- дача А принимает запросы и обрабатывает их по принципу “первым пришел — пер- вым обслужен”. Предположим, на счете имеется 1000 долл., при этом задача С требует Положить на депозит 100 долл., а задача В желает снять со счета 1100 долл. Что про- изойдет, если обе задачи В и С попытаются обновить один и тот же счет одновременно?
46 Глава 2. Проблемы параллельного и распределенного программирования Параллелизм Однопроцессорный компьютер Рис. 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, а задачи В и С “скованы” взаимоблокировкой, то те другие задачи будут ожидать условия, которое никогда не выполнится. При координации парал- лельно выполняемых задач необходимо помнить, что взаимоблокировка и бесконечная отсрочка — это самые опасные преграды, которые нужно предусмотреть и избежать. Рис. 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. Отказы оборудования и поведение ПО При совместной работе множества процессоров над решением некоторой задачи Возможен отказ одного или нескольких процессоров. Каким в этом случае должно быть поведение ПО? Программа должна остановиться или возможно перераспреде- ление работы? Что случится, если при использовании мультикомпьютерной системы Канал связи между несколькими компьютерами временно выйдет из строя? Что про- изойдет, если поток данных будет настолько медленным, что процессы на каждом
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. Распространенные архитектуры ПО, используемые для поддержки параллельного и распределенного программирования Модель Архитектура Распределенное программирование Параллельное программирование Модель ведущего узла, именуемая также: • главный/подчиненный; • управляющий/рабочий; • клиент/сервер Модель равноправных узлов Главный узел управляет задачами, т.е. контроли- рует их выполнение и передает работу подчи- ненным задачам Все задачи, в основ- ном, имеют одинако- вый ранг, и работа ме- жду ними распределя- ется равномерно Векторная или конвейер- ная (поточная)обработка Один исполнительный узел соответствует каждому элементу массива (вектора) или шагу конвейера Дерево с родительскими и дочерними элементами Динамически генерируе- мые исполнители в отношении типа “родитель/потомок”. Этот тип архитектуры полезно использовать в алгоритмах следующих типов: • рекурсия; • “разделяй и властвуй”; • И/ИЛИ • древовидная обработка 2.6. Различные методы тестирования и отладки При тестировании последовательной программы разработчик может отследить ее логику в пошаговом режиме. Если он будет начинать тестирование с одних и тех же данных при условии, что система каждый раз будет пребывать в одном и том же со- стоянии, то результаты выполнения программы или ее логические цепочки будут вполне предсказуемыми. Программист может отыскать ошибки в программе, исполь- зуя соответствующие входные данные и исходное состояние программы, путем про- верки ее логики в пошаговом режиме. Тестирование и отладка в последовательной модели зависят от степени предсказуемости начального и текущего состояний про- граммы, определяемых заданными входными данными. С параллельным и распределенным программированием все обстоит иначе. Здесь трудно воспроизвести точный контекст параллельных или распределенных задач из- за разных стратегий планирования, применяемых в операционной системе, дина- мически меняющейся рабочей нагрузки, квантов процессорного времени, приори- тетов процессов и потоков, временных задержек при их взаимодействии и собственно
2.7. Связь между параллельным и распределенным проектами 55 выполнении, а также различных случайных изменений ситуаций, характерных для параллельных или распределенных контекстов. Чтобы воспроизвести точное состоя- ние в котором находилась среда при тестировании и отладке, необходимо воссоздать каждую задачу, выполнением которой была занята операционная система. При этом должен быть известен режим планирования процессорного времени и точно воспро- изведены состояние виртуальной памяти и переключение контекстов. Кроме того, следует воссоздать условия возникновения прерываний и формирования сигналов, а в некоторых случаях — даже рабочую нагрузку7 сети. При этом нужно понимать, что и сами средства тестирования и отладки оказывают немалое влияние на состояние среды. Это означает, что создание одинаковой последовательности событий для тес- тирования и отладки зачастую невозможно. Необходимость воссоздания всех пере- численных выше условий обусловлено тем, что они позволяют определить, какие процессы или потоки следует выполнять и на каких именно процессорах. Смешанное выполнение процессов и потоков (в некоторой неудачной “пропорции”) часто явля- ется причиной возникновения взаимоблокировок, бесконечных отсрочек, “гонки” данных и других проблем. И хотя некоторые из этих проблем встречаются и в после- довательном программировании, они не в силах зачеркнуть допущения, сделанные при построении последовательной модели. Тот уровень предсказуемости, который имеет место в последовательной модели, недоступен для параллельного программи- рования. Это заставляет разработчика овладевать новыми тактическими приемами для тестирования и отладки параллельных и распределенных программ, а также тре- бует от него поиска новых способов доказательства корректности его программ. 2.7. Связь между параллельным и распределенным проектами При создании документации на проектирование параллельного или распределен- ного ПО необходимо описать декомпозицию работ и их синхронизацию, а также взаимодействие между задачами, объектами, процессами и потоками. При этом про- ектировщики должны тесно контактировать с разработчиками, а разработчики — с теми, кто будет поддерживать систему7 и заниматься ее администрированием. В идеале это взаимодействие должно осуществляться по действующим стандартам. Однако найти единый язык, понятный всем сторонам и позволяющий четко представить мультипара- Дигматическую природу всех этих систем, — труднодостижимая цель. Мы остановили свой выбор на языке UML (Unified Modeling Language — унифицированный язык моде- лирования). В табл. 2.3 перечислено семь UML-диаграмм, которые часто используются При создании многопоточных, параллельных или распределенных программ. Семь диаграмм, перечисленных в табл. 2.3, представляют собой лишь подмноже- ство диаграмм, которые предусмотрены языком UML, но они наиболее всего подхо- дят к тому’, что мы хотим подчеркнуть в наших проектах параллельного ПО. В частно- СТи- UML-диаграммы деятельности, развертывания и состояний весьма полезны для °Писания взаимодействующего поведения параллельной и распределенной подсис- тем обработки данных. Поскольку UML— это фактический стандарт, используемый ПРИ создании взаимодействующих объектно-ориентированных и агентно- °рИентированных проектов, при изложении материала в этой книге мы опираемся Именно на него. Описание обозначений и символов, используемых в перечисленных вЬнпе диаграммах, содержится в приложении А.
56 Глава 2. Проблемы параллельного и распределенного программирования Таблица 2.3. UML-диаграммы, используемые при создании многопоточных, параллельных или распределенных программ UML-диаграммы Описание Диаграмма (видов) деятельности Диаграмма взаимодействия Диаграмма (параллел ьных) состояний Диаграмма последователь- ностей Диаграмма сотрудничества Диаграмма развертывания (внедрения) Диаграмма компонентов Разновидность диаграммы состояний, в которой большинство со- стояний (или все) представляют виды деятельности, а большинство переходов (или все) активизируются при выполнении некоторого действия в исходных состояниях Тип диаграммы, которая отображает взаимодействие между объек- тами. Взаимодействия описываются в виде сообщений, которыми они обмениваются. К диаграммам взаимодействия относятся диа- граммы сотрудничества, диаграммы последовательностей и диа- граммы (видов)деятельности Диаграмма, которая показывает последовательность преобразований объекта в процессе его реакции на события. При использовании диа- граммы параллельных состояний эти преобразования могут проис- ходить в течение одного и того же интервала времени Диаграмма взаимодействия, в которой отображается организация структуры объектов, принимающих или отправляющих сообщения (с акцентом на упорядочении сообщений по времени) Диаграмма взаимодействия, в которой отображается организация структуры объектов, принимающих или отправляющих сообщения (с акцентом на структурной организации) Диаграмма, которая показывает динамическую конфигурацию узлов обработки, аппаратных средств и программных компонентов в системе Диаграмма взаимодействия, в которой отображается организация физических модулей программного кода (пакетов) в системе и зави- симости между ними 2.8. Резюме При создании параллельного и распределенного ПО разработчиков ожидает мно- жество проблем. Поэтому при проектировании ПО им необходимо искать новые архи- тектурные подходы и технологии. Многие фундаментальные допущения, которых при- держивались разработчики при построении последовательных моделей программиро- вания, совершенно неприемлемы в области создания параллельного и распределенного ПО. В программах, включающих элементы параллелизма, программисты чаще всего сталкиваются со следующими четырьмя проблемами координации: “гонка” данных, бесконечная отсрочка, взаимоблокировка и проблемы синхронизации при взаимо- действии задач. Наличие параллелизма и распределения оказывает огромное влияние на все аспекты жизненного цикла разработки ПО: начиная эскизным проектом и за- канчивая тестированием готовой системы и подготовкой документации. В этой книге мы представляем архитектурные подходы к решению многих упомянутых проблем, используя преимущества мультипарадигматических средств языка C++, которые по- зволяют справиться со сложностью параллельных и распределенных программ.
РАЗБИЕНИЕ С++- ПРОГРАММ НА МНОЖЕСТВО ЗАДАЧ В этой главе... 3.1. Определение процесса 3.2. Анатомия процесса 3.3. Состояния процессов 3.4. Планирование процессов 3.5. Переключение контекста 3.6. Создание процесса 3.7. Завершение процесса 3.8. Ресурсы процессов 3.9. Асинхронные и синхронные процессы 3.10. Разбиение программы на задачи 3.11. Резюме
Коль выполнение параллельных процессов возможно на более низком (нейронном) уровне, то на символическом уровне мышление человека с принципиальной точки зрения можно рассматривать как последовательную машину, которая использует временно создаваемые последовательности процессов, выполнение которых длится сотни миллисекунд. — Герберт Саймон (Herbert A. Simon), The Machine As Mind Параллельность в С++-программе достигается путем ее (программы) разложения на несколько процессов или потоков. Несмотря на существование различных вариантов организации логики С++-программы (например, с помощью объек- тов, функций или обобщенных шаблонов), под параллелизмом все же понимается ис- пользование множества процессов и потоков. Прочитав эту главу, вы поймете, что та- кое процесс и как С++-программы можно разделить на несколько процессов. 3.1. Определение процесса Процесс (process) — это некоторая часть (единица) работы, создаваемая операци- онной системой. Важно отметить, что процессы и программы — необязательно экви- валентные понятия. Программа может состоять из нескольких процессов. В некото- рых ситуациях процесс может быть не связан с конкретной программой. Процессы — это артефакты операционной системы, а программы — это артефакты разработчика. Такие операционные системы, как UNIX/Linux позволяют управлять сотнями или даже тысячами параллельно загружаемых процессов.
3.1. Определение процесса 59 Чтобы некоторую часть работы можно было назвать процессом, она должна иметь адресное пространство, назначаемое операционной системой, и идентификатор, или идентификационный номер (id процесса). Процесс должен обладать определенным статусом и иметь свой элемент в таблице процессов. В соответствии со стандартом pOSlX он должен содержать один или несколько потоков управления, выполняющих- ся в рамках его адресного пространства, и использовать системные ресурсы, требуе- мые для этих потоков. Процесс состоит из множества выполняющихся инструкций, размещенных в адресном пространстве этого процесса. Адресное пространство про- цесса распределяется между инструкциями, данными, принадлежащими процессу, и стеками, обеспечивающими вызовы функций и хранение локальных переменных. 3.1.1. Два вида процессов При выполнении процесса операционная система назначает ему некоторый процес- сор. Процесс выполняет свои инструкции в течение некоторого периода времени. За- тем он выгружается, освобождая процессор для другого процесса. Планировщик опера- ционной системы переключается с кода одного процесса на код другого, предоставляя каждому процессу шанс выполнить свои инструкции. Различают пользовательские про- цессы и системные. Процессы, которые выполняют системный код, называются систем- ными и применяются к системе в целом. Они занимаются выполнением таких служеб- ных задач, как распределение памяти, обмен страницами между внутренним и вспомо- гательным запоминающими устройствами, контроль устройств и т.п. Они также выполняют некоторые задачи “по поручению” пользовательских процессов, например, делают запросы на ввод-вывод данных, выделяют память и т.д. Пользовательские про- цессы выполняют собственный код и иногда обращаются к системным функциям. Вы- полняя собственный код, пользовательский процесс пребывает в пользовательском режи- ме (user mode). В пользовательском режиме процесс не может выполнять определенные привилегированные машинные команды. При вызове системных функций (например read(), write () или open ()) пользовательский процесс выполняет инструкции опе- рационной системы. При этом пользовательский процесс “удерживает” процессор до тех пор, пока не будет выполнен системный вызов. Для выполнения системного вызова процессор обращается к ядру операционной системы. В это время о пользователь- ском процессе говорят, что он пребывает в привилегированном режиме, или режиме ядра (kernel mode), и не может быть выгружен никаким другим пользовательским процессом. 3.1.2. Блок управления процессами Процессы имеют характеристики, используемые для идентификации и определе- ния их поведения. Ядро поддерживает необходимые структуры данных и предостав- ляет системные функции, которые дают возможность пользователю получить доступ к этой информации. Некоторые данные хранятся в блоках управления процессами (process control block — РСВ), или БУЛ. Данные, хранимые в БУП-блоках, описывают Процесс с точки зрения потребностей операционной системы. С помощью этой ин- формации операционная система может управлять каждым процессом. Когда операци- онная система переключается с одного процесса на другой, она сохраняет текущее со- ст°яние выполняющегося процесса и его контекст в области сохранения БУП-блока, Чтобы надлежащим образом возобновить выполнение этого процесса в следующий раз,
60 Глава 3. Разбиение С++-программ на множество задач когда ему снова будет выделен центральный процессор (ЦП). БУП-блок считывается и обновляется различными модулями операционной системы. Модули “отвечают” за контроль производительности операционной системы, планирование, распределе- ние ресурсов и доступ к механизму обработки прерываний и/или модифицируют БУП-блок. Блок БУИ содержит следующую информацию: • текущее состояние и приоритет процесса; • идентификатор процесса, а также идентификаторы родительского и сыновне- го процессов; • указатели на выделенные ресурсы; • указатели на область памяти процесса; • указатели на родительский и сыновний процесс; • процессор, занятый процессом; • регистры управления и состояния; • стековые указатели. Среди данных, содержащихся в БУП-блоке, есть такие, которые “отвечают” за управление процессом, т.е. отражают его текущее состояние и приоритет, указывают на БУП-блоки родительского и сыновнего процессов, а также выделенные ресурсы и па- мять. Кроме того, этот блок включает информацию, связанную с планированием, привилегиями процессов, флагами, сообщениями и сигналами, которыми обменива- ются процессы (имеется в виду межпроцессное взаимодействие — znter/?rocess communication, или IPC). С помощью информации, связанной с управлением процес- сами, операционная система может координировать параллельно выполняемые про- цессы. Стековые указатели и содержимое регистров пользователя, управления и со- стояния содержат информацию, связанную с состоянием процессора. При выполнении процесса соответствующая информация размещается в регистрах ЦП. При переклю- чении операционной системы с одного процесса на другой вся информация из этих регистров сохраняется. Когда процесс снова получает ЦП во “временное пользова- ние”, ранее сохраненная информация может быть восстановлена. Есть еще один вид информации, который связан с идентификацией процесса. Имеется в виду идентифика- тор процесса (id), или PID, и идентификатор родительского процесса (PPID). Эти идентификационные номера (которые представлены положительными целочислен- ными значениями) уникальны для каждого процесса. 3.2. Анатомия процесса Адресное пространство процесса делится на три логических раздела: текстовый (для кода программы), информационный (для данных программы) и стековый (для стеков про- граммы). Логическая структура процесса показана на рис. 3.1. Текстовый раздел (расположенный в нижней части адресного пространства) содержит подлежащие вы- полнению инструкции, которые называются программным кодом. Раздел данных (расположенный над текстовым разделом) содержит инициализированные глобальные, внешние и статические переменные процесса. Раздел стеков содержит локально созда- ваемые переменные и параметры, передаваемые функциям. Поскольку7 процесс может
3.2. Анатомия процесса 61 вызывать как системные функции, так и функции, определенные пользователем, в стеко- вом разделе поддерживаются два стека: стек пользователя и стек ядра. При вызове функции создается стековый фрейм функции, который помещается в стек пользователя или стек ядра в зависимости от того, в каком режиме пребывает процесс в данный момент: в пользова- тельском или привилегированном (режиме ядра). Стековый раздел имеет тенденцию расти в направлении раздела данных. При выходе из функции ее стековый фрейм из- влекается из стека. Разделы кода, данных и стеков, а также блок управления процессом образуют часть того, из чего складывается образ процесса (process image). ИДЕНТИФИКАЦИЯ ПРОЦЕССА ИНФОРМАЦИЯ О СОСТОЯНИИ ПРОЦЕССА - БУП ИНФОРМАЦИЯ ОБ УПРАВЛЕНИИ ПРОЦЕССОМ РАЗДЕЛ СТЕКОВ Стек ядра ОБРАЗ ПРОЦЕССА Стек пользователя РАЗДЕЛ ДАННЫХ • инициализированные глобальные переменные * внешние переменные « статические переменные РАЗДЕЛ КОДА • код программы Рис. 3.1. Адресное пространство процесса делится на три логических раздела: текстовый, информационный и стековый. Так выглядит логическая структура процесса Адресное пространство процесса виртуально. Применение виртуальной памяти по- зволяет отделить адреса, используемые в текущем процессе, от адресов, реально дос- тупных во внутренней памяти. Тем самым значительно увеличивается задействованное пространство адресов памяти по сравнению с реально доступными адресами. Разделы виртуального адресного пространства процесса представляют собой смежные блоки па- мяти. Каждый такой раздел и физическое адресное пространство разделены на участки памяти, именуемые страницами. У каждой страницы есть уникальный номер страничного блока (page frame number). В качестве индекса для входа в таблицы страничных блоков (page frame table) используется номер виртуального страничного блока. Каждый элемент таб- лицы страничных блоков содержит номер физического страничного блока, что позволяет Установить соответствие между виртуальными и физическими страничными блоками. Это соответствие отображено на рис. 3.2. Как видите, виртуальное адресное пространство Непрерывно, но устанавливаемое с его помощью соответствие физическим страницам Не является упорядоченным. Другими словами, при последовательных виртуальных ад- ресах соответствующие им физические страницы не будут последовательными.
62 Глава 3. Разбиение С++-программ на множество задач Несмотря на то что виртуальное адресное пространство каждого процесса защи- щено, т.е. приняты меры по предотвращению доступа к нему со стороны другого про- цесса, текстовый раздел процесса может совместно использоваться несколькими процессами. На рис. 3.2 также показано, как два процесса могут разделять один и тот же программный код. При этом в элементах таблиц страничных блоков обоих про- цессов хранится один и тот же номер физического страничного блока. Как показано на рис. 3.2, виртуальный страничный блок с номером 0 процесса А соответствует фи- зическому страничному блоку с номером 5, что также справедливо и для виртуального страничного блока с номером 2 процесса В. ВИРТУАЛЬНОЕ АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В - РАЗДЕЛ СТЕКОВ - РАЗДЕЛ ДАННЫХ - РАЗДЕЛ КОДА ВИРТУАЛЬНОЕ АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А Рис. 3.2. Соответствие последовательных виртуальных страничных блоков страницам физической памяти (НСБ — номер страничного блока; НВСБ— номер виртуального страничного блока)
3.3. Состояния процессов 63 Чтобы операционная система могла управлять всеми процессами, хранимыми во внутренней памяти, она создает и поддерживает таблицы процессов (process table). В дей- ствительности операционная система содержит отдельные таблицы для всех объектов, которыми она управляет. Следует иметь в виду, что операционная система управляет не только процессами, но и всеми ресурсами компьютера, т.е. устройствами ввода-вывода, памятью и файлами. Часть памяти, устройств и файлов управляется от имени пользова- тельских процессов. Эта информация отмечена в БУП-блоках как ресурсы, выделенные процессу. Таблица процессов должна иметь соответствующую структуру для каждого об- раза процесса в памяти. Каждая такая структура содержит идентификаторы (id) самого процесса и родительского процесса, идентификаторы реального и эффективного пользо- вателей, идентификатор группы, список подвешенных сигналов, местоположение тексто- вого, информационного и стекового разделов, а также текущее состояние процесса. Если операционной системе нужен доступ к определенному процессу, в таблице процессов ра- зыскивается информация о нем, а затем в памяти размещается его образ (рис. 3.3). ТАБЛИЦЫ ПРОЦЕССОВ Рис. 3.3. Операционная система управляет таблицами. Каждая структура в массиве таблиц процессов представляет процесс в системе З.з. Состояния процессов Во время выполнения процесса его состояние изменяется. Под состоянием процес- са подразумевается его текущий режим, или статус. В среде UNIX процесс может пре- бывать в одном из следующих состояний: • выполнения; • работоспособности (готовности);
64 Глава 3. Разбиение С++-программ на множество задач • “зомби”; • ожидания (блокирования); • останова. Состояние процесса меняется при определенных обстоятельствах, создаваемых существованием процесса или операционной системы. Под сменой состояний, или пе- реходом из одного состояния в другое, понимают обстоятельства, которые заставля- ют процесс изменить свое состояние. На рис. 3.4 отображена диаграмма состояний для среды UNIX. Диаграмма состояний содержит узлы и направленные ребра, соеди- няющие эти узлы. Каждый узел представляет состояние процесса, а направленные ребра между узлами — переходы из одного состояния в другое. Возможные смены со- стояний (с их кратким описанием) перечислены в табл. 3.1. На рис. 3.4 и в табл. 3.1 показано, что между состояниями разрешены только определенные переходы. На- пример, между состояниями готовности и выполнения существует переход (ребро диаграммы), а между состояниями ожидания и выполнения — нет. Это означает, что возможны обстоятельства, заставляющие процесс перейти из состояния готовности в состояние выполнения, но нет обстоятельств, которые могут заставить процесс пе- рейти в состояние выполнения из состояния ожидания. Рис. 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 сведен в очередь с более высоким приоритетом. Кроме того, процесс, который монополизирует процессор, может получить более низкий приоритет, или же другие онессы могут получить более высокий приоритет, чем процесс-монополист. В сре- дах UNIX/Linux для уровней приоритетов предусмотрен диапазон от -20 до 19. Чем выше значение уровня, тем ниже приоритет процесса. При назначении приоритета пользовательскому процессу следует учитывать, на что именно этот процесс тратит большую часть времени. Одни процессы отличаются по- вышенной интенсивностью использования процессорного времени (они используют процессор в течение всего кванта процессорного времени). У других же большая часть времени уходит на ожидание выполнения операций ввода-вывода или наступления не- которых иных событий. Если такой процесс готов к использованию процессора, ему следует немедленно предоставить процессор, чтобы он мог сделать следующий запрос к устройствам ввода-вывода. Процессы, которые взаимодействуют между собой, могут требовать довольно высокий приоритет, чтобы рассчитывать на приличное время ре- акции. Системные процессы имеют более высокий приоритет, чем пользовательские. Рис. 3.5. Многоуровневая приоритетная очередь (массив распределения), каждый элемент которой указывает на очередь готовых процессов с одинаковым уровнем приоритета 3-4.1. Стратегия планирования Процессы размещаются в приоритетных очередях в соответствии со стратегией Планирования. В системах UNIX/Linux используются две стратегии планирования: А*О (сокр. от First In First Out, т.е. первым прибыл, первым обслужен) и RR (сокр. от
68 Глава 3. Разбиение С++-программ на множество задач rtmnd-robin, т.е. циклическая). Схема действия стратегии FIFO показана на рис. 3.6, а. При использовании стратегии FIFO процессы назначаются процессору в соответст- вии со временем поступления в очередь. После истечения кванта времени процесс помещается в начало (головную часть) своей приоритетной очереди. Когда ждущий процесс становится работоспособным (готовым к выполнению), он помещается в ко- нец своей приоритетной очереди. Процесс может вызвать системную функцию и от- казаться от процессора в пользу другого процесса с таким же уровнем приоритета. Такой процесс также будет помещен в конец своей приоритетной очереди. а) FIFO-планирование ОЧЕРЕДЬ ГОТОВЫХ ПРОЦЕССОВ ОЧЕРЕДЬ ЖДУЩИХ ПРОЦЕССОВ завершена б) RR-планирование ОЧЕРЕДЬ ГОТОВЫХ ПРОЦЕССОВ Рис. 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) Все процессы, включая процессы остальных пользователей r (Linux) Только выполняющиеся процессы Таблица 3.2. Общие заголовки, используемые для утилиты ps в средах Solaris/Linux Заголовки Описание Пользовательское имя владельца процесса ID процесса ID родительского процесса ID лидирующего процесса в группе ID лидера сеанса USER, UID PID PPID PGID SID
70 Глава 3. Разбиение С++-программ на множество задач Окончание табл. 3.2 Заголовки Описание %CPU Коэффициент использования времени ЦП (в процентах) процессом в течение последней минуты RSS %МЕМ Объем реального ОЗУ, занимаемый процессом в данный момент (в Кбайт) Коэффициент использования реального ОЗУ процессом в течение по- следней минуты SZ Размер виртуальной памяти, занимаемой данными и стеком процесса (в Кбайт или страницах) WCHAN Адрес события, в ожидании которого процесс пребывает в состоянии ожидания COMMAND CMD ТТ, TTY S, STAT TIME Имя команды и аргументы Управляющий терминал процесса Текущее состояние процесса Общее время ЦП, используемое процессом (HH:MM:SS) STIME, START NI PRI C, CP Время или дата старта процесса Фактор уступчивости процесса Приоритет процесса Коэффициент краткосрочного использования ЦП для вычисления пла- нировщиком значения PRI ADDR Адрес памяти, выделенной процессу LWP ID потока NLWP Количество потоков Синопсис (Linux) ps -[опции в стиле Unix98] [опции в стиле BSD] - -[GNU-опции в длинном формате] (Solaris) ps [-aAdeflcjLPy][-о format][-t termlist][-u userlist] [-G grouplist] [-p proclist] [-g pgrplist] [-s sidlist] В следующий список включены командные опции, которые используются для управления отображаемой информацией о процессах: - f полные распечатки - 1 в длинном формате - j в формате задания Приведем пример использования утилиты ps в средах Solaris/Linux: ps -f По этой команде будет отображена полная информация о процессах, которая выводится по умолчанию в каждой среде. На рис. 3.7 показан результат выполнения этой команды
3.4. Планирование процессов 71 еде Solaris. Командные опции можно использовать тандемом (одна за другой). На рис 3 7 также показан результат совместного использования опций -1 и - f в среде Solaris: ps Командная опция 1 позволяет отобразить дополнительные заголовки: F, S, С, PRI, NI, дрШ и 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 F s UID PID PPID C PRI NI 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 -IfB среде Solaris На рис. 3.8 показан результат выполнения утилиты ps с использованием командных опций Тих в среде Linux. Данные, выводимые с помощью заголовков %CPU, %МЕМ и 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 [tdhughesOcolony]$ ps Tux USER PID %CPU %MEM vsz RSS TTY STAT START TIME COMMAND tdhughes 19259 0.0 0.1 2448 1356 pts/4 S 20:29 0:00 -bash tdhughes 19334 0.0 0.0 1732 860 pts/4 s 20:33 0:00 /homeItdhughes/pv tdhughes 19336 0.0 0.0 1928 780 pts/4 s 20:33 0:00 /home1tdhughes/pv tdhughes 19337 18.0 2.4 26872 24856 pts/4 R 20:33 0:47 /home/tdhughes/pv tdhughes 19338 18.0 2.3 26872 24696 pts/4 R 20:33 0:47 /home1tdhughes/pv tdhughes 19341 17.9 2.3 26872 24556 pts/4 R 20:33 0:47 /home1tdhughes/pv tdhughes 19400 0.0 0.0 2544 692 pts/4 R 20:38 0:00 ps Tux tdhughes 19401 0.0 0.1 2448 1356 pts/4 R 20:38 0:00 -bash Рис. 3.8. Результат выполнения команды ps Tux в среде Linux Синопсис #include <unistd.h> int nice(int incr); Чем ниже фактор уступчивости, тем выше уровень приоритета процесса. Параметр 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){ r = setpriority(which,pid,value); зге ь } //••• В листинге 3.1 возвращается и устанавливается приоритет вызывающего процесса. Если фактор уступчивости вызывающего процесса оказывается меньше 10, он уста- навливается равным 10. Процесс задается значениями, хранимыми в параметрах which и who (см. соответствующий синопсис). Параметр which может определять процесс, группу процессов или пользователя и иметь следующие значения. PRIO__PROCESS Означает процесс PRIO_PGRP Означает группу процессов PRIO—USER Означает пользователя В зависимости от значения параметра 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. Переключение контекста Переключение контекста происходит в момент, когда процессор переключается с одного процесса на другой. При переключении контекста система сохраняет кон- текст текущего процесса и восстанавливает контекст следующего процесса, выбран- ного для использования процессора. БУП-блок прерванного процесса при этом об- новляется, а также изменяется значение поля состояния процесса (т.е. признак со- стояния выполнения заменяется признаком другого состояния: готовности, блокирования или “зомби”). Сохраняется и обновляется содержимое регистров про- цессора, состояние стека, данные об идентификации (и привилегиях) пользователя и процесса, а также о стратегии планирования и учетная информация. Система должна отслеживать статус устройств ввода-вывода процесса и других ре- сурсов, а также состояние всех структур данных, связанных с управлением памятью. Ь1груженный (прерванный) процесс помещается в соответствующую очередь.
74 Глава 3. Разбиение С++-программ на множество задач Переключение контекста происходит в случаях, когда: • процесс выгружается; • процесс добровольно отказывается от процессора; • процесс делает запрос к устройству ввода-вывода или должен ожидать наступ- ления события; • процесс переходит из пользовательского режима в режим ядра. Когда выгруженный процесс снова выбирается для использования процессора, его контекст восстанавливается, и выполнение продолжается с точки, на которой он был прерван в предыдущем сеансе. 3.6. Создание процесса Чтобы выполнить любую программу, операционная система должна сначала создать процесс. При создании нового процесса в главной таблице процессов создается новая структура. Создается и инициализируется новый блок БУП, и в его раздел идентифика- ции процесса записывается уникальный идентификационный номер процесса (id) и id родительского процесса. Программный счетчик устанавливается указателем на входную точку программы, а указатели системных стеков устанавливаются таким образом, чтобы определить стековые границы для процесса. Процесс инициализируется любыми тре- буемыми атрибутами. Если процессу не присвоено значение приоритета, то по умолча- нию ему присваивается самое низкое значение. Изначально процесс не обладает ника- кими ресурсами, если нет явного запроса на ресурсы или если они не были унаследова- ны от процесса-создателя. Процесс “входит” в состояние выполнения и помещается в очередь готовых к выполнению процессов. Для него выделяется адресное пространство, размер которого определяется по умолчанию на основе типа процесса. Кроме того, размер можно установить по запросу от создателя процесса. Процесс-создатель может передать системе размер адресного пространства в момент создания процесса. 3.6.1. Отношения между родительскими и сыновними процессами Процесс, который создает, или порождает, другой процесс, является родительским (parent) процессом по отношению к порожденному, или сыновнему (child) процессу. Процесс init— родитель (или предок) всех пользовательских процессов — первый процесс, видимый системой UNIX после ее загрузки. Процесс init организует сис- тему, при необходимости выполняет другие программы и запускает демон-программы (daemon), т.е. сетевые программы, работающие в фоновом режиме. Идентификатор процесса init (PID) равен 1. Сыновний процесс имеет собственный уникальный идентификатор PID, БУП-блок и отдельную структуру в таблице процессов. Сынов- ний процесс также может породить новый процесс. Выполняющееся приложение может создать дерево процессов. Например, родительский процесс выполняет поиск накопителя на жестких дисках для заданного HTML-документа. Имя этого HTML- документа записано в глобальной структуре данных, подобной списку, который со- держит все запросы на документы. После успешного обнаружения документ удаляется из списка запросов, и его путь (маршрут в сети) записывается в другую глобальную структуру данных, которая содержит пути найденных документов. Чтобы обеспечить
3.6. Создание процесса 75 Приемлемую реакцию на пользовательские запросы, для процесса предусматривается ограничение в виде пяти необработанных запросов в списке. По достижении этого предела порождаются два новых процесса. Если порожденный процесс в свою оче- редь достигнет установленного предела, он создаст еще два новых процесса. Созда- ваемое таким способом дерево процессов показано на рис. 3.9. Любой процесс может иметь только один родительский, но множество сыновних процессов. процессов. При определенных условиях Рис. 3.9. Дерево процесс порождает два новых потомка Сыновний процесс может быть создан с собственным исполняемым образом или в виде дубликата родительского процесса. При создании в качестве дубликата предка сыновний процесс наследует множество его атрибутов, включая среду, приоритет, стра- тегию планирования, ограничения по ресурсам, открытые файлы и разделы общей па- мяти. Если сыновний процесс перемещает указатель текущей позиции в файле или за- крывает файл, то результаты этих действий будут видны родительскому процессу. Если родителю выделяются любые дополнительные ресурсы уже после создания процесса- потомка, то они не будут доступны потомку. В свою очередь, если сыновний процесс ис- пользует какие-либо ресурсы, они также будут недоступны для процесса-родителя. Некоторые атрибуты родителя не наследуются потомком. Как упоминалось выше, сЫновний процесс не наследует PID родителя и его БУП-блок. Потомок не наследует Никаких файловых блокировок, созданных родителем или необработанными сигна- ^аМи. Для сыновнего процесса используются собственные значения таких временных ^рактеристик, как коэффициент загрузки процессора и время создания. Несмотря на Чт° сыновние процессы связаны определенными отношениями с родителями, они
76 Глава 3. Разбиение С++-программ на множество задач все же функционируют как отдельные процессы. Их программные и стековые счет- чики действуют раздельно. Поскольку разделы данных копируются, а не используются совместно, процесс-потомок может изменять значения своих переменных, не оказы- вая влияния на родительскую копию данных. Родительский и сыновний процесс со- вместно используют раздел программного кода и выполняют инструкции, располо- женные непосредственно после вызова системной функции, создавшей сыновний процесс. Они не выполняют эти инструкции на этапе блокировки из-за соперничества за процессор со всеми остальными процессами, загруженными в память. После создания образ сыновнего процесса может быть заменен другим исполняе- мым образом. Разделы программного кода, данных и стеков, а также его “куча” памяти перезаписывается новым образом процесса. Новый процесс сохраняет свои иденти- фикационные номера (PID и PPID). Атрибуты, сохраняемые новым процессом после замены его исполняемого образа, перечислены в табл. 3.3. В ней также указаны сис- темные функции, которые возвращают эти атрибуты. Переменные среды также со- храняются, если во время замены исполняемого образа процесса не были заданы но- вые переменные среды. Файлы, которые были открыты до момента замены испол- няемого образа, остаются открытыми. Новый процесс будет создавать файлы с теми же файловыми разрешениями. Время ЦП при этом не сбрасывается. Таблица 3.3. Атрибуты, сохраняемые новым процессом после замены его исполняемого образа образом нового процесса Сохраняемые атрибуты Функция Идентификатор (ID) процесса getpid() ID родительского процесса getppid() ID группы процессов getpgidO Сеансовое членство getsid() Идентификатор эффективного пользователя getuid() Идентификатор эффективной группы getgid() Дополнительные ID групп getgroups() Время, оставшееся до сигнала тревоги alarm() Фактор уступчивости nice() Время, используемое до настоящего момента times () Маска сигналов процесса sigprocmask() Ожидающие сигналы sigpending() Предельный размер файла ulimit() Предельный объем ресурсов getrlimit() Маска создания файлового режима umask() Текущий рабочий каталог getcwd() Корневой каталог
3.6. Создание процесса 77 3.6.1.1. Утилита pstree Утилита pstree в среде Linux отображает дерево процессов (точнее, она отобра- жает выполняющиеся процессы в форме древовидной структуры). Корнем этого де- рева является процесс init. Синопсис pstree [-а] [-с] [-h | -Hpid] [-1] [-n] [-р] [-u] [-G] | -U] [pid | user] pstree -V При вызове этой утилиты можно использовать следующие опции. - а Отобразить аргументы командной строки. - h Выделить текущий процесс и его предков. - Н Аналогично опции -h, но выделению подлежит заданный процесс. - п Отсортировать процессы с одинаковым предком по значению PID, а не по имени. - р Отобразить значения PID. На рис. 3.10 показан результат выполнения команды pstree -h в среде Linux. ka:~ # pstree -h init-+-applix -atd -axmain -axnet -cron -gpm -inetd -9*[kdeinit] -kdeinit -+-kdeinit | -kdeinit----------bash----gimp script-fu '-kdeinit--bash -+-man----sh--sh less '-pstree -kdeinit---cat -kdm-+-X ' -kdm--------kde----ksmserver -kflushd -khubd -klogd -knotify -kswapd -kupdate -login---bash -Ipd -mdrecoveryd -5*[mingetty] -nscd----nscd--5* [nscd] -sshd -syslogd -usbmgr '-xconsole рИс. 3.10. Результат выполнения команды pstree -h в среде Linux
78 Глава 3. Разбиение С++-программ на множество задач 3.6.2. Использование системной функции fork() Системная функция (или системный вызов) fork() создает новый процесс, кото- рый представляет собой дубликат вызывающего процесса, т.е. его родителя. При ус- пешном выполнении функция fork() возвращает родительскому и сыновнему про- цессам два различных значения. Сыновнему возвращается число 0, а родительскому — значение PID сыновнего процесса. Родительский и сыновний процессы продолжают выполняться с инструкции, непосредственно следующей за функцией fork(). В слу- чае неудачного выполнения (оно выражается в том, что сыновний процесс не был создан) родительскому процессу возвращается число -1. Синопсис #include <unistd.h> pid t fork (void); Неудачный исход функции fork () возможен в случае, если система не обладает ре- сурсами для создания еще одного процесса. Это происходит при превышении ограни- чения (если оно существует) на количество сыновних процессов, которое может поро- ждать родитель, или на количество выполняющихся процессов в масштабе всей систе- мы. В этом случае устанавливается переменная ermo, которая означает наличие ошибки. 3.6.3. Использование семейства системных функций ехес Семейство функций ехес предназначено для замены образа вызывающего процесса образом нового процесса. При вызове функции fork () создается новый процесс, кото- рый является точной копией родительского процесса, а функция ехес () заменяет об- раз “скопированного” процесса образом копии. Образ нового процесса представляет собой обычный выполняемый файл, который немедленно запускается на выполнение. Этот файл можно задать с помощью имени и пути доступа к нему. Функции семейства ехес могут передать новому процессу аргументы командной строки, а также установить переменные среды. Если функция выполнилась успешно, она не возвращает никакого значения, поскольку образ процесса, который содержал обращение к функции ехес, уже перезаписан. В случае неудачи вызывающему процессу возвращается число -1. Все функции ехес () могут иметь неудачный исход при следующих условиях: • разрешения не признаны, разрешение на поиск отвергается для каталога выполняемых файлов; разрешение на выполнение отвергается для выполняемого файла; • файлы не существуют', выполняемый файл не существует; каталог не существует; • файл невозможно выполнить', файл невозможно выполнить, поскольку он открыт для записи другим процессом; файл не является выполняемым;
3.6. Создание процесса 79 с символическими ссылками', проблемы к исполняемому файлу символические ссылки образуют циклы; при анализе пути символические ссылки делают путь к исполняемому файлу слишком длинным. функции семейства ехес используются совместно с функцией fork(). Функция fork () создает и инициализирует сыновний процесс “по образу и подобию” роди- тельского. Образ сыновнего процесса затем заменяет образ своего предка посредст- вом вызова функции ехес (). Пример использования функций fork () и ехес () по- казан в листинге 3.2. // Листинг 3.2. Использование системных функций ,, fork() и ехес() RtValue = fork(); if(RtValue == 0){ execl("/path/direct","direct" } В листинге 3.2 демонстрируется вызов функции fork (). Значение, которое она воз- вращает, сохраняется в переменной RtValue. Если значение RtValue равно 0, зна- чит, это — сыновний процесс, и в нем вызывается функция execl () с параметрами. Первый параметр содержит путь к выполняемому модулю, второй — инструкцию для выполнения, а третий — аргумент. Второй параметр, direct, представляет собой имя утилиты, которая перечисляет все каталоги и подкаталоги из данного каталога. Всего существует шесть версий функций ехес, предназначенных для использования раз- личных соглашений о вызовах. 3.6.3.1. Функции execl () Функции execl (), execle () и execlp () передают аргументы командной строки в виде списка. Количество аргументов командной строки должно быть известно во время компиляции. • int execl(const char *path,const char *arg0,.../*, (char *)0 * /) ; Здесь path — путевое имя выполняемой программы. Его можно задать в виде полного составного имени либо относительного составного имени из текущего каталога. Последующие параметры представляют собой список аргументов ко- мандной строки, от arg0 до argn. Всего может быть п аргументов. Этот список завершается NULL-указателем. • int execle(const char *path,const char *arg0 (char *)0 *, char *const envp[]*/); Эта функция аналогична функции execl () с одним отличием: она имеет до- полнительный параметр, envp [ ]. Этот параметр указывает на новую среду для нового процесса, т.е. envp [ ] — это указатель на строковый массив с завершаю- щим нулевым символом. Каждая его строка, также завершающаяся нулевым символом, имеет следующую форму: name=value
80 Глава 3. Разбиение С++-программ на множество задач Здесь паше — имя переменной среды, 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[] = {"directNULL}; 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 execlefconst char *path,const char *arg0,.../*, (char *)0 *,char *const envp[]*/); int execlp(const char *file,const char *arg0,.../*, (char *)0 * /) ; int int execv(const char *path,char *const arg[]); execve(const char *path,char *const arg[], char *const envp[]); int execvpfconst 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}; Вот пример вызова этой функции: execvf’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. Определение ограничений для функций ехес () Существуют ограничения на размеры вектора argv [ ] и массива envp [ ], переда- ваемые функциям семейства ехес. Для определения максимального размера аргумен- тов командной строки и размера переменных среды при использовании ехес- функций (которые принимают параметр envp [ ]) можно использовать функцию sysconf (). Чтобы эта функция возвратила размер, ее параметру паше необходимо Передать значение _SC_ARG_MAX. Синопсис #include <unistd.h> Jlong sysconf (int name);______________________________________________ Еще одним ограничением при использовании функций семейства ехес и других Функций, применяемых для создания процессов, является максимальное количество одновременно выполняемых процессов, которое допустимо для одного пользователя, тобы функция 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() используется для определения факта установки заданной переменной. Интересующая вас переменная задается с по- мощью параметра паше. Если заданная переменная не установлена, функция возвра- щает значение 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. Использование функции systemO для порождения процессов Функция systemO используется для выполнения команды или запуска програм- мы. Функция system () выполняет функцию fork(), а затем сыновний процесс вы- зывает функцию ехес () с оболочкой, выполняя заданную команду или программу. Синопсис #include <stdlib.h> int system(const char *string); В качестве параметра string можно передать системную команду или имя выпол- няемого файла. При удачном исходе функция возвращает статус завершения команды или значение, возвращаемое программой (если таковое предусмотрено). Ошибки мо- гут возникнуть на нескольких уровнях, т.е. ошибка может произойти при выполнении функции fork () или ехес () либо заданная оболочка может оказаться неподходящей для выполнения команды или программы. Функция system () возвращает значение родительскому процессу. При неудачном исходе функции ехес () возвращается число 127, а при обнаружении других оши- бок — число -1. Эта функция не влияет на состояние ожидания сыновних процессов. 3.6.5. Использование POSIX-функций для порождения процессов Подобно созданию процессов с помощью функций systemO и fork-exec, функ- Чии posix_spawn() создают новые сыновние процессы из заданных образов про- весов. Однако функции posix_spawn () позволяют при этом реализовать более Многослойные “рычаги” управления, т.е. они управляют следующими атрибутами сы- новних процессов, унаследованных от родительского процесса: • Дескрипторы файлов; • стратегия планирования; • идентификатор группы процессов;
84 Глава 3. Разбиение С++-программ на множество задач • идентификатор пользователя и группы; • маска сигналов. Функции posix_spawn() позволяют управлять тем, будут ли сигналы, проигно- рированные родительским процессом, игнорироваться его потомком или устанавли- ваться для выполнения действий, заданных по умолчанию. Управление дескриптора- ми файлов позволяет сыновнему процессу получить самостоятельный доступ к потоку данных, независимо открытому родителем. Возможность установить для сыновнего процесса идентификатор группы повлияет на то, как управление сыновней задачей будет связано с управлением родителем. Наконец, стратегию планирования сыновне- го процесса можно установить отличной от стратегии планирования его родителя. Синопсис #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 argvfrestrict], 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_асtions—t: s t rue t po s ix_spawn—f i1e_ac t ions_t{ { int ___allocated; int ___used; struct ___spawn_action *actions; int ___pad [16] ; }; Структура posix— spawn_file_actions—t содержит информацию о действиях, выполняемых в новом процессе над дескрипторами файлов. Параметр file_actions используется для преобразования родительского набора дескрипто- ров открытых файлов в набор дескрипторов файлов для порожденного сыновнего процесса. Эта структура может содержать ряд файловых операций, предназначенных для выполнения в последовательности, в которой они были добавлены в объект деИ' ствий над файлами. Эти файловые операции выполняются над дескрипторами от- крытых файлов родительского процесса и позволяют копировать, добавлять, удалять
3.6. Создание процесса 85 или закрывать дескрипторы заданных файлов от имени сыновнего процесса даже до его создания. Если параметр f ile_actions содержит нулевой указатель, то дескрип- торы файлов, открытые родительским процессом, останутся открытыми для его по- томка без каких-либо модификаций. Функции, используемые для добавления действий над файлами в объект типа posix_spawn_f ile_actions, перечислены в табл. 3.4. int pos ix_spawn_f i le_ac t i ons_addc lose (pos ix_spawn_f i 1 e_ac t i ons_t *file_actions, int fildes); Таблица 3.4. Функции, используемые для добавления действий над файлами в объект типа posix„spawn_f ile_actions Функции Описание Добавляет действие close () в объект действий над файлами, заданный па- раметром f ile_actions. В результа- те при порождении нового процесса с помощью этого объекта будет закрыт файловый дескриптор fildes int pos ix_spawn_f i 1 e_ac t i ons_addopen (posix_spawn_f ile_actions__t *file_actions, int fildes, const char *restrict path, int oflag, mode_t mode); int pos ix_spawn_f i 1 e_ac t i ons_adddup2 (pos ix_spawn_f i 1 e_ac t i ons_t *file_actions, int fildes, int new fildes); int pos ix_spawn_f i le_ac t i ons_des troy (posix_spawn_fi1e_actions_t *file_actions); int Posix_spawn_f ile_actions_init (posix_spawn_file_actions_t *file_actions); Добавляет действие open () в объект действий над файлами, заданный па- раметром file_actions. В результа- те при порождении нового процесса с помощью этого объекта будет открыт файл, заданный параметром path, с использованием дескриптора fildes Добавляет действие dup2 () в объект действий над файлами, заданный па- раметром file_actions. В результа- те при порождении нового процесса с помощью этого объекта будет создан дубликат файлового дескриптора fildes с использованием файлового дескриптора newf ildes Разрушает объект, заданный парамет- ром f ile_ actions, что приводит к деинициализации этого объекта. За- тем его можно инициализировать по- вторно с помощью функции posix_spawn_ file_actions—ini t () Инициализирует объект, заданный параметром f ile_actions. После инициализации этот объект не будет содержать действий, предназначенных для выполнения над файлами Параметр attrp указывает на структуру pos ix_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 (const posix_spawnattr_t *restrict attr, short *restrict flags); int posix_spawnattr_setflags (posix_spawnattr_t *attr, short flags); Возвращает значение атрибута flags, хра- нимого в объекте, заданном параметром attr Устанавливает значение атрибута 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 pos ix_spawna 11r_s e t s chedparam (posix—spawnattr_t *attr, const struct sched_param *restrict schedparam) ; int pos ix_spawna t tr_ge t s chedpol icy (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 Posix—spawnattr_getsigmask (const posix—spawnattr_t Restrict attr, __ sigset_t Restrict sigmask); Описание Возвращает значение атрибута ___pgroup, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре pgroup Устанавливает значение атрибута_pgroup, хранимого в объекте, заданном параметром attr, равным параметру pgroup, если в атрибуте __flags установлен признак POSIX_SPAWNJSETPGROUP Возвращает значение атрибута_sp, храни- мого в объекте, заданном параметром attr, и сохраняет его в параметре schedparam Устанавливает значение атрибута_sp, хра- нимого в объекте, заданном параметром attr, равным параметру schedparam, если в атрибуте __flags установлен признак POSIX_SPAWN_SETSCHEDPARAM Возвращает значение атрибута ___policy, хранимого в объекте, заданном параметром attr, и сохраняет его в параметре schedpolicy Устанавливает значение атрибута_policy, хранимого в объекте, заданном параметром attr, равным параметру schedpolicy, если в атрибуте_flags установлен признак POSIX_SPAWN_SETSCHEDULER Возвращает значение атрибута_sd, храни- мого в объекте, заданном параметром attr, и сохраняет его в параметре sigdefault Устанавливает значение атрибута_sd, хра- нимого в объекте, заданном параметром attr, равным параметру sigdefault, если в атрибуте __flags установлен признак POSIX_SPAWN_SETSIGDEF Возвращает значение атрибута _ss, храни- мого в объекте, заданном параметром attr, и сохраняет его в параметре sigmask
88 Глава 3. Разбиение С++-программ на множество задач Окончание табл. 3.5 Функции Описание int posix_spawnattr_setsigmask (posix_spawnattr_t *restrict attr, const sigset_t *restrict sigmask); int posix_spawnattr_destroy (posix_spawnattr_t *attr); int posix_spawnattr_init (posix_spawnattr_t *attr); Устанавливает значение атрибута ss, хра- нимого в объекте, заданном параметром attr, равным параметру sigmask, если в ат- рибуте flags установлен признак POSIX_S PAWN-SETSIGMASK Разрушает объект, заданный параметром attr. Этот объект можно затем снова ини- циализировать с помощью функции posix_spawnattr_init() Инициализирует объект, заданный парамет- ром 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 () или ехес (). В этом случае они (ошибки) займут место значений, возвращаемых spawn-функциями. Если сынов- ний процесс генерирует ошибку, родительский процесс не получает “дурного извес- тия” автоматически. Для извещения родителя об ошибке сыновнего процесса необ- ходимо использовать другие механизмы, поскольку информация об этом не сохраня- ется в статусе завершения потомка. С этой целью можно использовать механизм межпроцессного взаимодействия либо специальный флаг, устанавливаемый сынов- ним процессом и видимый для его родителя. 3.6.6. Идентификация родительских и сыновних процессов с помощью функций управления процессами Существуют две функции, которые возвращают значение идентификатора (PID) вызы- вающего процесса и значение идентификатора (PPID) родительского процесса. Функция getpid () возвращает идентификатор вызывающего процесса, а функция getppid () — идентификатор процесса, который является родительским для вызывающего процесса. Эти функции всегда завершаются успешно, поэтому коды ошибок не определены. Синопсис ^include <unistd.h> Pid_t getpid(void); 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 < -1 Сигнал будет отослан всем процессам, у которых идентификатор груп- пы процессов равен абсолютному значению параметра pid, и для кото- рых вызывающий процесс имеет разрешение отправлять этот сигнал. Синопсис ^include <signal.h> jjit kill (pid t pid, int sig) ; 3.8. Ресурсы процессов При выполнении возложенной на процесс задачи часто приходится записывать Данные в файл, отправлять их на принтер или отображать полученные результаты на экране. Процессу могут понадобиться данные, вводимые пользователем с клавиатуры Или содержащиеся в файле. Кроме того, процессы в качестве ресурса могут использо- вать другие процессы, например, подпрограммы. Подпрограммы, файлы, семафоры, Мьютексы, клавиатуры и экраны дисплеев — все это примеры ресурсов, которые мо-
92 Глава 3. Разбиение С++-программ на множество задач жет затребовать процесс. Под ресурсом понимается все то, что использует процесс в любое заданное время в качестве источника данных, средств обработки, вычисле- ний или отображения информации. Чтобы процесс получил доступ к ресурсу, он должен сначала сделать запрос, обра- тившись с ним к операционной системе. Если ресурс свободен, операционная система позволит процессу его использовать. После использования ресурса процесс освобож- дает его, чтобы он стал доступным для других процессов. Если ресурс недоступен, за- прос отвергается, и процесс должен подождать его освобождения. Как только ресурс станет доступным, процесс активизируется. Таков базовый подход к распределению ресурсов между процессами. На рис. 3.11 показан граф распределения ресурсов, по которому можно понять, какие процессы удерживают ресурсы, а какие их ожидают. Так, процесс В делает запрос на ресурс 2, который удерживается процессом С. Про- цесс С делает запрос на ресурс 3, который удерживается процессом D. Рис. 3.11. Граф распределения ресурсов, который показывает, какие процессы удерживают ресурсы, а какие их запрашивают Если удовлетворяется сразу несколько запросов на получение доступа к ресурсу, этот ресурс является совместно используемым, или разделяемым (эта ситуация также ото- бражена на рис. 3.11). Процесс А разделяет ресурс R, с процессом D. Разделяемые ре- сурсы могут допускать параллельный доступ сразу нескольких процессов или разре- шать доступ только одному процессу в течение ограниченного промежутка времени, после чего аналогичным доступом сможет воспользоваться другой процесс. Приме- ром такого типа разделяемых ресурсов может служить процессор. Сначала процессор назначается одному процессу в течение короткого интервала времени, а затем про- цессор “получает” другой процесс. Если удовлетворяется только один запрос на полу- чение доступа к ресурсу, и это происходит после того, как ресурс освободит другой процесс, такой ресурс является неразделяемым, а о процессе говорят, что он имеет мо- нопольный доступ (exclusive access) к ресурсу. В многопроцессорной среде важно знать, какой доступ можно организовать к разделяемому ресурсу: параллельный или последовательный (передавая “эстафету” поочередно от ресурса к ресурсу). Это по- зволит избежать ловушек, присущих параллелизму. Одни ресурсы могут изменяться или модифицироваться процессами, а другие — нет. Поведение разделяемых модифицируемых или немодифицируемых ресурсов оп- ределяется типом ресурса.
3.8. Ресурсы процессов 93 §3 1 Граф распределения ресурсов Г афы распределения ресурсов — это направленные графы, которые показывают, как определяются ресурсы в системе. Такой граф состоит из множества вершин V и множества ребер Е. Множество вершин делится на две категории: Р={Р,Л- -Рп} R={R„R2. -.RJ Множество Р— это множество всех процессов, a R — это множество всех ресурсов в системе. Ребро, направленное от процесса к ресурсу, называется ребром запроса, а ребро, направленное от ресурса к процессу, называется ребром назначения. Направ- ленные ребра обозначаются следующим образом: р R Ребро запроса: процесс Р( запрашивает экземпляр типа ресурса R. r р. Ребро назначения: экземпляр типа ресурса R выделен процессу Pj Каждый процесс в графе распределения ресурсов отображается кругом, а каждый ресурс — прямоугольником. Поскольку может быть много экземпляров одного типа ре- сурса, то каждый из них представляется точкой внутри прямоугольника. Ребро запро- са указывает на периметр прямоугольника ресурса, а ребро назначения берет начало из точки и касается периметра круга процесса. Граф распределения ресурсов, показанный на рис. 3.11, отображает следующее. Множества Р, R и Е p={pa,p„,pc,pd} R={R„R2, R,} E={R,-»P„ R,->Pd, Pb->R2,R2^Pc,Pc^R,)R3-»Pd} 3.8.1. Типы ресурсов Существуют три основных типа ресурсов: аппаратные, информационные и про- граммные. Аппаратные ресурсы представляют собой физические устройства, подклю- ченные к компьютеру (например, процессоры, основная память и все устройства вво- да-вывода, включая принтеры, жесткий диск, накопитель на магнитной ленте, диско- вод с zip-архивом, мониторы, клавиатуры, звуковые, сетевые и графические карты, а также модемы. Все эти устройства могут совместно использовать несколько процессов. Некоторые аппаратные ресурсы прерываются, чтобы разрешить доступ к ним раз- личных процессов. Например, прерывания процессора позволяют различным про- цессам выполняться по очереди. Оперативное запоминающее устройство, или ОЗУ (RAM), — это еще один пример ресурса, разделяемого посредством прерываний. Ко- гда процесс не выполняется, некоторые страничные блоки, которые он занимает, мо- гут быть выгружены во вспомогательное запоминающее устройство, а на их место за- гружены данные, относящиеся к другому процессу. В любой момент времени весь Диапазон памяти может быть занят страничными блоками только одного процесса. римером разделяемого, но непрерываемого ресурса может служить принтер. При совместном использовании принтера задания, посылаемые на печать каждым процес- с°м, хранятся в очереди. Каждое задание печатается до конца, и только потом начи-
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; r1im_t r1im_max; }; Тип rlim_t — это целочисленный тип без знака. Член rlim_cur содержит значе- ние текущего, или мягкого предела, а член rlim_шах — значение максимума, или же- сткого предела. Членам rlim_cur и rlim_max можно присвоить любые значения, а также символические константы, определенные в заголовке <sys/resource. h>. RLIM_INFINITY Отсутствие ограничения. RLIM_SAVED—МАХ Непредставимый хранимый жесткий предел. RLIM_SAVED—CUR Непредставимый хранимый мягкий предел. Как жесткий, так и мягкий пределы можно установить равными значению RLIM__INFINITY, которое подразумевает, что ресурс неограничен. Таблица 3.6. Значения параметра resource Определение ресурса Описание RLIMIT—CORE Максимальный размер файла ядра в байтах, который может быть создан процессом RLIMIT—CPU Максимальный объем времени ЦП в секундах, которое может быть использовано процессом RLIMIT—DATA Максимальный размер раздела данных процесса в байтах RLIMIT—FSIZE Максимальный размер файла в байтах, который может быть соз- дан процессом RLIMIT-NOFILE Увеличенное на единицу максимальное значение, которое систе- ма может назначить вновь созданному дескриптору файла RLIMIT-STACK Максимальный размер стека процесса в байтах ^RLIMIT—as Максимальный размер доступной памяти процесса в байтах
gg Глава 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 = 2000; 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 rujnaxrss Максимальный размер, установленный для резидент- ной программы long ru_maxixrss Размер разделяемой памяти long ru_maxidrss Размер неразделяемой области данных long ru_maxisrss Размер неразделяемой области стеков long ru_minflt Количество запросов на страницы 1ong ru_maj f11 Количество ошибок из-за отсутствия страниц long ru_nswap Количество перекачек страниц long ru_inblock Блочные операции по вводу данных long ru_oublock Блочные операции операций по выводу данных long ru_msgsnd Количество отправленных сообщений long rujnsgrcv Количество полученных сообщений long ru_nsignaIs Количество полученных сигналов long ru_nvcsw Количество преднамеренных переключений контекста long ru_nivcsw Количество принудительных переключений контекста 3.9. Асинхронные и синхронные процессы Асинхронные процессы выполняются независимо один от другого. Это означает, что процесс А будет выполняться до конца безотносительно к процессу В. Между асинхронными процессами могут быть прямые родственные (“родитель-сын”) отно- шения, а могут и не быть. Если процесс А создает процесс В, они оба могут выпол- няться независимо, но в некоторый момент родитель должен получить статус завер- шения сыновнего процесса. Если между процессами нет прямых родственных отно- шений, у них может быть общий родитель. Асинхронные процессы могут выполняться последовательно, параллельно или с пе- рекрытием. Эти сценарии изображены на рис. 3.12. В ситуации 1 до самого конца вы- полняется процесс А, затем процесс В и процесс С выполняются до самого конца. Это и есть последовательное выполнение процессов. В ситуации 2 процессы выполняются одновременно. Процессы А и В — активные процессы. Во время выполнения процесса А процесс В находится в состоянии ожидания. В течение некоторого интервала времени а процесса пребывают в ждущем режиме. Затем процесс В “просыпается”, причем раньше процесса А, а через некоторое время “просыпается” и процесс А, и теперь оба процесса выполняются одновременно. Эта ситуация показывает, что асинхронные процессы могут выполняться одновременно только в течение определенных интерва- ле времени. В ситуации 3 выполнение процессов А и В перекрывается.
дя Глава 3. Разбиение С++-программ на множество задач {^Ситуация 1 Г} АСИНХРОННЫЕ ПРОЦЕССЫ ПРОЦЕСС А выполняется ПРОЦЕСС В ПРОЦЕСС С выполняется , I выполняется ПРОЦЕСС А ПРОЦЕСС В выполняется ожидает выполняется ожидает выполняется яш* мм*» «ли* >м(^^^^ИМВИИИНВМВНИНИВ1 ПРОЦЕСС А ПРОЦЕСС В выполняется ожидает выполняется jwwssa жжк ПРОЦЕСС А СИНХРОННЫЕ ПРОЦЕССЫ выполняется ожидает л» выполняется ПРОЦЕСС В fork() ' у выполняется • возвращает код выхода Рис. 3.12. Возможные сценарии асинхронных и синхронных процессов Асинхронные процессы могут совместно использовать такие ресурсы, как файлы или память. Это может потребовать (или не потребовать) синхронизации или взаи- модействия при разделении ресурсов. Если процессы выполняются последовательно (ситуация 1), то они не потребуют никакой синхронизации. Например, все три про- цесса, А, В и С, могут разделять некоторую глобальную переменную. Процесс А (перед тем как завершиться) записывает значение в эту переменную, затем процесс В во вре- мя своего выполнения считывает данные, хранимые в этой переменной и (перед тем как завершиться) записывает в нее “свое” значение. Затем во время своего выполне- ния процесс С считывает данные из этой переменной. Но в ситуациях 2 и 3 процессы могут попытаться одновременно модифицировать эту переменную, поэтому здесь не обойтись без синхронизации доступа к ней. Мы определяем синхронные процессы как процессы с перемежающимся выпол- нением, когда один процесс приостанавливает свое выполнение до тех пор, пока не
3.9. Асинхронные и синхронные процессы 99 завершится ДРУГОЙ- Например, процесс А, родительский, при выполнении создает оцесс В, сыновний. Процесс А приостанавливает свое выполнение до тех пор, пока не завершится процесс В. После завершения процесса В его выходной код по- мещается в таблицу процессов. Тем самым процесс А уведомляется о завершении процесса В. Процесс А может продолжить выполнение, а затем завершиться или за- вершиться немедленно. В этом случае выполнение процессов А и В является син- хронизированным. Сценарий синхронного выполнения процессов А и В (для срав- нения с асинхронным) также показан на рис. 3.12. 3.9.1. Создание синхронных и асинхронных процессов с помощью функций fork (), ехес (), system () и posix_spawn() Функции fork (), fork-exec и posix_spawn () позволяют создавать асинхронные процессы. При использовании функции fork() дублируется образ родительского процесса. После создания сыновнего процесса эта функция возвращает родителю (через параметр) идентификатор (PID) процесса-потомка и (обычным путем) число О, означающее, что создание процесса прошло успешно. При этом родительский про- цесс не приостанавливается; оба процесса продолжают выполняться независимо от инструкции, следующей непосредственно за вызовом функции fork (). При создании сыновнего процесса посредством f ork-exec-комбинации его образ инициализирует- ся с помощью образа нового процесса. Если функция ехес () выполнилась успешно (т.е. успешно прошла инициализация), она не возвращает родительскому процессу никакого значения. Функции posix_spawn () создают образы сыновних процессов и инициализируют их. Помимо идентификатора (PID), возвращаемого (через пара- метр) функцией posix_spawn() родительскому процессу, обычным путем возвраща- ется значение, служащее индикатором успешного порождения процесса. После вы- полнения функции posix_spawn() оба процесса выполняются одновременно. Функ- ция system О позволяет создавать синхронные процессы. При этом создается оболочка, которая выполняет системную команду или запускает выполняемый файл. В этом случае родительский процесс приостанавливается до тех пор, пока не завер- шится сыновний процесс и функция system () не возвратит значение. 3.9.2. Функция wait () Асинхронный процесс, вызвав функцию wait (), может приостановить выполне- ние до тех пор, пока не завершится сыновний процесс. После завершения сыновнего пР°Цесса ожидающий родительский процесс считывает статус завершения своего по- томка, чтобы не допустить создания процесса-“зомби”. Функция wait () получает ста- тус завершения из таблицы процессов. Параметр status указывает на ту область, ко- торая содержит статус завершения сыновнего процесса. Если родительский процесс имеет не один, а несколько сыновних процессов и некоторые из них уже заверши- ЛИсь’ функция wait () считывает из таблицы процессов статус завершения только для °Дного сыновнего процесса. Если информация о статусе окажется доступной еще до выполнения функции wait (), эта функция завершится немедленно. Если родитель- СКий процесс не имеет ни одного потомка, эта функция возвратит код ошибки.
100 Глава 3. Разбиение С++-программ на множество задач Функцию wait () можно использовать также в том случае, когда вызывающий про- цесс должен ожидать до тех пор, пока не получит сигнал, чтобы затем выполнить оц. ределенные действия по его обработке. Синопсис #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Тые (передними объектами) поверхности должны быть удалены. Шестая зада- наложить на поверхности изображения тень. СПОСОБ 1 Представление программы в виде родительского процесса, который создает некоторое количество сыновних процессов ПРОГРАММА СПОСОБ 2 Разделение программы на множество отдельных выполняемых файлов ПРОГРАММА СПОСОБ 3 Разделение программы на несколько процессов, отвечающих за создание других процессов только определенного типа рис. 3.13. Способы разбиения программы на отдельные задачи
104 Глава 3. Разбиение С++-программ на множество задач ЭТАПЫ ВИЗУАЛИЗАЦИИ № вершины х у z 1 1,40000 0,00000 2,30000 2 1,40000 -0,78400 2,30000 3 0,78400 -1,40000 2,30000 4 0,00000 -1,40000 2,30000 5 1,33750 0,00000 2,53125 6 1,33750 -0,75000 2,53125 7 0,74900 -1,33700 2,53125 8 0,00000 -1,33700 2,53125 9 1,43750 0,00000 2,53125 10 1,43750 -0,90500 2,53125 306 1,42500 -0,79800 0,00000 1. Представление трехмерного объекта в виде записей базы данных. 2. Построение многоугольной 3. Наложение тени на сеточной модели трехмерный объект, трехмерного объекта. Рис. 3.14. Этапы визуализации Решение каждой задачи представляется в виде отдельного выполняемого файла. Первые три задачи (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—file_actions—t FileActions; char *const argv4[] = {"Task4",...,NULL}; char *const argv5[] = {"Task5NULL}; char *const argv6[] = {"Task6NULL}; pid_t Pid; int stat; //. . . // Выполняем первые три задачи синхронно, system("Taskl ..."); system("Task2 ..."); system(”Task3 ...");
3.10. Разбиение программы на задачи 105 / инициализируем структуры. z/ ix__spawnattr_init(&Attr) ; ^osix spawn_file_actions—init (&FileActions) . выполняем последние три задачи асинхронно. ix spawn(&Pid,"Task4",&FileActions,&Attr,argv4,NULL) ^°^x""spawn (&Pid, "Task5", &FileActions, &Attr, argv5, NULL) P°sixZspawn(&Pid, "Taske”,&FileActions,&Attr,argv6,NULL) // Подобно хорошему родителю, ожидаем возвращения // своих "детей". wait (&stat); wait (fcstat); 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_file_actions_t FileActions; pid_t Pid; int stat;
3.10. Разбиение программы на задачи 107 system("Taskl ..."); // Выполняется безотносительно к // типу используемой визуализации. Определяем, какой нужен тип визуализации. Это можно . сделать, получив информацию от пользователя или // выполнив специальный анализ. // затем сообщаем о результате другим задачам с помощью // аргУментов• char *const argv4[] = {"TaskType4NULL}; char *const argv5[] = {"TaskType5NULL}; char *const argv6[] = {”TaskType6NULL}; system("TaskType2 ..."); system("TaskType3 11 Инициализируем структуры. posix_spawnattr_init (&Attr) ; posix_spawn_f ile_actions_init (&FileActions) ; posix_spawn (&Pid, " TaskType4", &FileActions, &Attr, argv4, NULL); posix_spawn (&Pid, "TaskType5", &FileActions, &Attr, argv5, NULL); if(Y){ posix—spawn(&Pid,"TaskType6",&FileActions,&Attr, argv6,NULL); } // Подобно хорошему родителю, ожидаем возвращения // своих "детей". wait(&stat); wait(&stat); wait(&stat); return(0) ; } // Bee TaskType-задачи должны быть аналогичными. И. . . int main(int argc, char *argv[]) int Rt; //. . . if(argv[l] == X){ /I Инициализируем структуры. //. . . posix_spawn(&Pid,"TaskTypeX",&FileActions,&Attr,..., j NULL); else{ // Инициализируем структуры.
108 Глава 3. Разбиение С++-программ на множество задач //.. • posix_spawn(&Pid,"TaskTypeY",&FileActions,&Attr, ...,NULL); } wait(&stat); exi t(0); } В листинге 3.6 тип каждой задачи (а следовательно, и тип порождаемого процесса) определяется на основе информации, передаваемой от родительского процесса или сценария оболочки. 3.10.1. Линии видимого контура Порождение процессов, как показано в листинге 3.7, возможно с помощью функ- ций, вызываемых из функции main (). // Листинг 3.7. Стержневая ветвь программы, из которой // вызывается функция, порождающая процесс int main(int argc, char *argv[]) { Rt = funcl(X, 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 11 листинг 3.8. Объявление объекта в процессе //••• my_object MyObject; //-•• // объявление и определение класса. class my_object ( public: //• • • int spawnProcess(int X) ; //... }; int my_object:: spawnProcess (int X) { //. . . // posix_spawn() или systemf) //. . . } Как показано в листинге 3.8, объект может создавать любое количество процессов из любого метода. 3.11. Резюме Параллелизм в С++-программе достигается за счет ее разложения на несколько процессов или несколько потоков. Процесс— это “единица работы”, создаваемая операционной системой. Если программа— это артефакт (продукт деятельности) разработчика, то процесс — это артефакт операционной системы. Приложение может состоять из нескольких процессов, которые могут быть не связаны с какой-то кон- кретной программой. Операционные системы способны управлять сотнями и даже тысячами параллельно загруженных процессов. Некоторые данные и атрибуты процесса хранятся в блоке управления процессами (process control block — РСВ), или БУП, используемом операционной системой для Идентификации процесса. С помощью этой информации операционная система Управляет процессами. Многозадачность (выполнение одновременно нескольких Процессов) реализуется путем переключения контекста. Текущее состояние выпол- няемого процесса и его контекст сохраняются в БУП-блоке, что позволяет успешно возобновить этот процесс в следующий раз, когда он будет назначен центральному Процессору. Занимая процессор, процесс пребывает в состоянии выполнения, а когда ожидает использования ЦП, — то в состоянии готовности (ожидания). Получить формацию о процессах, выполняющихся в системе, можно с помощью утилиты ps. 1 процессы, которые создают другие процессы, вступают с ними в “родственные” Цььи-дети) отношения. Создатель процесса называется родительским, а создан-
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. Резюме
Непрерывное усложнение компьютерных систем вселяет в нас надежду, что мы и в дальнейшем сможем успешно управлять этим видом абстракции. — Эндрю Кёниг и Барбара Му (Andrew Koening and Barbara Moo), Ruminations on C++ Работу любой последовательной программы можно разделить между нескольки- ми подпрограммами. Каждой подпрограмме назначается конкретная задача, и все эти задачи выполняются одна за другой. Вторая задача не может начаться до тех пор, пока не завершится первая, а третья — пока не закончится вторая и т.д. Описанная схема прекрасно работает до тех пор, пока не будут достигнуты границы производительности и сложности. В одних случаях единственное решение проблемы производительности — найти возможность выполнять одновременно более одной за- дачи. В других ситуациях работа подпрограмм в программе настолько сложна, что имеет смысл представить эти подпрограммы в виде мини-программ, которые выпол- няются параллельно внутри основной программы. В главе 3 были представлены мето- ды разбиения одной программы на несколько процессов, каждый из которых выпол- няет отдельную задачу. Такие методы позволяют приложению в каждый момент вре- мени выполнять сразу несколько действий. Однако в этом случае каждый процесс имеет собственные адресное пространство и ресурсы. Поскольку каждый процесс за- нимает отдельное адресное пространство, то взаимодействие между процессами пре- вращается в настоящую проблему. Для обеспечения связи между раздельно выпол- няемыми частями общей программы нужно реализовать такие средства межпроцесс- ного взаимодействия, как каналы, FIFO-очереди (с дисциплиной обслуживания по принципу “первым пришел — первым обслужен”) и переменные среды. Иногда нужно иметь одну программу (которая выполняет несколько задач одновременно), не разби- вая ее на множество мини-программ. В таких обстоятельствах можно использовать
4.1. Определение потока 113 потоки. Потоки позволяют одной программе состоять из параллельно выполняемых частей, причем все части имеют доступ к одним и тем же переменным, константам и ад- еному пространству в целом. Потоки можно рассматривать как мини-программы в ос- новной программе. Если программа разделена на несколько процессов, как было пока- зано в главе 3, то с выполнением каждого отдельного процесса связаны определенные затраты системных ресурсов. Для потоков требуется меньший объем затрат системных песурсов. Поэтому потоки можно рассматривать как облегченные процессы, т.е. они позво- ляют воспользоваться многими преимуществами процессов без больших затрат на орга- низацию взаимодействия между ними. Потоки обеспечивают средства разделения ос- новного “русла” программы на несколько параллельно выполняемых “ручейков”. 4.1. Определение потока Под потоком подразумевается часть выполняемого кода в UNIX- или Linux-процессе, которая может быть регламентирована определенным образом. Затраты вычислитель- ных ресурсов, связанные с созданием потока, его поддержкой и управлением, у опера- ционной системы значительно ниже по сравнению с аналогичными затратами для про- цессов, поскольку объем информации отдельного потока гораздо меньше, чем у процес- са. Каждый процесс имеет основной, или первичный, поток. Под основным потоком процесса понимается программный поток управления или поток выполнения. Процесс может иметь несколько потоков выполнения и, соответственно, столько же потоков управления. Каждый поток, имея собственную последовательность инструкций, выпол- няется независимо от других, а все они — параллельно друг другу. Процесс с нескольки- ми потоками, называется многопоточным. Многопоточный процесс, состоящий из не- скольких потоков, показан на рис. 4.1. ПОТОКИ ВЫПОЛНЕНИЯ ПРОЦЕССА Рис. 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. Потоки, управляющие другими потоками В то время как процессы могут управлять другими процессами, если между ними установлены отношения типа “родитель-потомок”, потоки одного процесса счита- ются равноправными и находятся на одном уровне, независимо от того, кто кого создал. Любой поток, имеющий доступ к идентификационному7 номеру (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. Простая схема взаимодействия между параллельно выполняющимися потоками Потоки не требуют специального механизма взаимодействия между7 подзадачами. По- токи могут напрямую передавать данные другим потокам и получать данные от них, что также способствует экономии системных ресурсов, которые при использовании несколь- ких процессов пришлось бы направлять на настройку и поддержку специальных механиз- мов взаимодействия. Потоки же используют общую память, выделяемую в адресном про- странстве процесса. Процессы также могут взаимодействовать через общую память, но они имеют раздельные адресные пространства, и поэтому такая общая память должна быть вне адресных пространств обоих взаимодействующих процессов. Этот подход уве- личит временные и пространственные расходы системы на поддержку и доступ к общей памяти. Схема взаимодействия между потоками и процессами показана на рис. 4.2. 4.1.3.4. Упрощение структуры программы Потоки можно использовать, чтобы упростить структуру приложения. Каждому потоку назначается подзадача или подпрограмма, за выполнение которой он отвеча- ет. Поток должен независимо управлять выполнением своей подзадачи. Каждому по- току можно присвоить приоритет, отражающий важность выполняемой им задачи Для приложения. Такой подход позволяет упростить поддержку программного кода. 4.1.4. Недостатки использования потоков Простота доступности потоков к памяти процесса имеет свои недостатки. • Потоки могут легко разрушить адресное пространство процесса. • Потоки необходимо синхронизировать при параллельном доступе (для чтения или записи) к памяти. • Один поток может ликвидировать целый процесс или программу7. • Потоки существуют только в рамках единого процесса и, следовательно, не яв- ляются многократно используемыми.
118 Глава 4. Разбиение С++-программ на множество потоков адресное пространство процесса а Рис. 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 показана схема процесса, который содержит два потока. Как показано на рис. 4.3. процесс содержит два потока А и В, и их стеки располо- жены в стековом разделе процесса. Потоки выполняют различные функции: поток А выполняет функцию fund (), а поток В — функцию func2 ().
120 Глава 4. Разбиение С++-программ на множество потоков 1 Атрибуты И 345 priority = 2 size ... [AttrObjB^^Hl । ThreadA scope = process stack size = 1000 priority = 2 joinable //... Атрибуты priority - 2 Регистры Регистры SPi< ФО АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА РАЗДЕЛ СТЕКОВ Стек потока ThreadB func2() Count - 100 PC size ... main() AttrObj РАЗДЕЛ КОДА pthread_attrJnit(&AttrObj); pthread_create(&ThreadA?&AttrObj.func1. pthread_create(&ThreadB,&AttrObj;func2,. Стек потока ThreadA fund () Count = 10 РАЗДЕЛ ДАННЫХ ThreadA ThreadB X pthreadj ThreadA; pthreadj ThreadB; int X; ' int Y; pthreacjattrJ AttrObj; main() void funci (...) Count- 10: void func2(.,.) Count ~ 100: Рис. 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 int pthread_attr_ setdetachstate (pthread_attr_t *attr, int detachstate); Атрибут' detachstate определяет, является ли новый поток откреплен- ным. Если это соответствует истине, то его нельзя объединить ни с каким другим потоком guardsize int pthread_attr_ setguardsize (pthread_attr_t *attr, size_t guardsize); Атрибут guardsize позволяет управлять размером защитной облас- ти стека нового потока. Он создает буферную зону размером guardsize на переполненяемом конце стека inheritsched int pthread_attr_ setinheritsched (pthread_attr_t *attr, int inheritsched); Атрибут inheritsched определяет, как будут установлены атрибуты пла- нирования для нового потока, т.е. бу- дут' ли они унаследованы от потока- создателя или установлены атрибут- ным объектом
4.3. Планирование потоков 123 Окончание табл. 4.3 Атрибуты Функции Описание param int pthread_attr_ setschedparam (pthread—attr_t *restrict attr, const struct sched_param *restrict param); Атрибут param— это структура, ко- торую можно использовать для уста- новки приоритета нового потока schedpolicy int pthread—attr_ setschedpolicy (pthread—attr_t *attr, int policy); Атрибут schedpolicy определяет стратегию планирования создаваемо- го потока contentionscope int pthread—attr_ setscope (pthread—attr_t *attr, int contentionscope); Атрибут contentionscope опреде- ляет, с каким множеством потоков будет соперничать создаваемый по- ток за использование процессорного времени. Область видимости процес- са означает, что поток будет состя- заться со множеством потоков одно- го процесса, а область видимости системы означает, что поток будет состязаться с потоками в масштабе всей системы (т.е. сюда входят пото- ки других процессов) stackaddr int pthread—attr_ Атрибуты stackaddr и stacksize stacksize setstack (pthread—attr_t *attr, void *stackaddr, size_t stacksize); определяют базовый адрес и мини- мальный размер (в байтах) стека, вы- деляемого для создаваемого потока, соответственно stackaddr int pthread—attr_ setstackaddr (pthread—attr_t *attr, void *stackaddr); Атрибут stackaddr определяет ба- зовый адрес стека, выделяемого для создаваемого потока stacksize int pthread—attr_ setstacksize (pthread—attr_t *attr, size_t stacksize); Атрибут stacksize определяет ми- нимальный размер стека в байтах, вы- деляемого для создаваемого потока 4.3. Планирование потоков Когда подходит время для выполнения процесса, процессор занимает один из его оков. Если процесс имеет только один поток, то именно он (т.е. основной поток) на- ается процессору. Если процесс содержит несколько потоков и в системе есть доста- Ое количество процессоров, то процессорам назначаются все потоки. Потоки сопер- акэт за процессор либо со всеми потоками из активного процесса системы, либо только
124 Глава 4. Разбиение С++-программ на множество потоков с потоками из одного процесса. Потоки помещаются в очереди готовых потоков, отсор- тированные по значению их приоритета. Потоки с одинаковым приоритетом назначают- ся процессорам в соответствии с некоторой стратегией планирования. Если система не содержит достаточного количества процессоров, поток с более высоким приоритетом может выгрузить поток, выполняющийся в данный момент. Если новый активный поток принадлежит тому же процессу, что и выгруженный, возникает переключение контек- ста потоков. Если же новый активный поток “родом” из другого процесса, то сначала происходит переключение контекста процессов, а затем — контекста потоков. 4.3.1. Состояния потоков Потоки имеют такие же состояния и переходы между ними (см. главу 3), как и про- цессы. Диаграмма состояний, показанная на рис. 4.4, — это копия диаграммы, изобра- женной на рис. 3.4 из главы 3. (Вспомним, что процесс может пребывать в одном из че- тырех состояний: готовности, выполнения, останова и ожидания, или блокирования.) Состояние потока — это режим или условия, в которых поток существует в данный мо- мент. Поток находится в состоянии готовности (работоспособности), когда он готов к выполнению. Все готовые к работе потоки помещаются в очереди готовности, причем в каждой такой очереди содержатся потоки с одинаковым приоритетом. Когда поток вы- бирается из очереди готовности и назначается процессору, он (поток) переходит в со- стояние выполнения. Поток снимается с процессора, если его квант времени истек, или если перешел в состояние готовности поток с более высоким приоритетом. Выгру- женный поток снова помещается в очередь готовых потоков. Поток пребывает в со- стоянии ожидания, если он ожидает наступления некоторого события или заверше- ния операции ввода-вывода. Поток прекращает выполнение, получив сигнал остано- ва, и остается в этом состоянии до тех пор, пока не получит сигнал продолжить работу. Рис. 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 из процесса А, поскольку эти потоки соперничают за процессор в рамках своих процессов. Потоки с системной областью конкуренции и более низким приоритетом не вытесняются ни одним из потоков из процессов А или В. Они соперничают за про- цессорное время только с потоками, имеющими системную область конкуренции. Рис. 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. Изменение приоритета потоков Приоритеты потоков следует менять, чтобы ускорить выполнение потоков, от кото- рых зависит выполнение других потоков. И, наоборот, этого не следует делать ради того, Ы какой-то конкретный поток получил больше процессорного времени. Это может
128 Глава 4. Разбиение С++-программ на множество потоков изменить общую производительность системы. Потоки с более высоким классом приорц. тета получают больше процессорного времени, чем потоки с более низким классом при- оритета, поскольку7 они выполняются чаще. Потоки с более высоким приоритетом прак- тически монополизируют процессор, не выделяя потокам с более низким приоритетом такое ценное процессорное время. Эта ситуация получила название информационного голода (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 Управляющий поток создает новый поток для каждого нового запроса. ПРОГРАММА А Модель делегирования 2 Управляющий поток создает пул потоков, которые обрабатывают все запросы. ПРОГРАММА В Ис- 4.6. Два подхода к реализации модели делегирования
132 Глава 4. Разбиение С++-программ на множество потоков 4.5.2. Модель с равноправными узлами Если в модели делегирования есть управляющий поток, который делегирует зада- чи рабочим потокам, то в модели с равноправными узлами все потоки имеют одинаковый рабочий статус. Несмотря на существование одного потока, который изначально созда- ет все потоки, необходимые для выполнения всех задач, этот поток считается рабочим потоком, но он не выполняет никаких функций по делегированию задач. В этой модели нет никакого централизованного потока, но на рабочие потоки возлагается большая от- ветственность. Все равноправные потоки могут обрабатывать запросы из одного вход- ного потока данных, либо каждый рабочий поток может иметь собственный входной поток данных, за который он отвечает. Входной поток данных может также хранить- ся в файле или базе данных. Рабочие потоки могут нуждаться во взаимодействии и разделении ресурсов. Модель равноправных потоков представлена на рис. 4.7. ПРОГРАММА А Поток входных данных, разделяемый потоками выполнения Рис. 4.7. Модель равноправных потоков (или модель с равноправными узлами) 4.5.3. Модель конвейера Модель конвейера подобна ленте сборочного конвейера в том, что она предполагает наличие потока элементов, которые обрабатываются поэтапно. На каждом этапе отдель- ный поток выполняет некоторые операции над определенной совокупностью входных данных. Когда эта совокупность данных пройдет все этапы, обработка всего входного
4.5. Модели создания и функционирования потоков 133 а данных будет завершена. Этот подход позволяет обрабатывать несколько вход- П° потоков одновременно. Каждый поток отвечает за получение промежуточных ре- НЫХтатов, делая их доступными для следующего этапа (или следующего потока) конвей- * Последний этап (или поток) генерирует результаты работы конвейера в целом. еРа^о мере того как входные данные проходят по конвейеру, не исключено, что неко- ie их порции придется буферизировать на определенных этапах, пока потоки еще занимаются обработкой предыдущих порций. Это может вызвать торможение конвейе- если окажется, что обработка данных на каком-то этапе происходит медленнее, чем на других. При этом образуется отставание в работе. Чтобы предотвратить отставание, можно для “слабого” этапа создать дополнительные потоки. Все этапы конвейера долж- ны быть уравновешены по времени, чтобы ни один этап не занимал больше времени, чем другие Для этого необходимо всю работу распределить по конвейеру равномерно. Чем больше этапов в конвейере, тем больше должно быть создано потоков обработки. Увеличение количества потоков также может способствовать предотвращению отста- ваний в работе. Модель конвейера представлена на рис. 4.8. ПРОГРАММА ЭТАП 1 Рис. 4.8. Модель конвейера ЭТАП 2 БУФЕР [входные данные 5 Поток 1 обрабатывает входные данные 2 Поток 3 обрабатывает^ ЭТАПЗ Обработка входных данных 1 завершена 4.5.4. Модель “изготовитель-потребитель” В модели “изготовитель-потребитель” существует поток-“изготовитель”, который готовит данные, потребляемые потоком-“потребителем”. Данные сохраняются в блоке памяти, разделяемом между потоками “изготовителем” и “потребителем”. Поток- изготовитель” должен сначала приготовить данные, которые затем поток- потребитель” получит. Такому процессу необходима синхронизация. Если поток- изготовитель” будет поставлять данные гораздо быстрее, чем поток-“потребитель” сможет их потреблять, поток-“изготовитель” несколько раз перезапишет результаты, полученные им ранее, прежде чем поток-“потребитель” успеет их обработать. Но если поток-“потребитель” будет принимать данные гораздо быстрее, чем поток- изготовитель” сможет их поставлять, поток-“потребитель” будет либо снова обраба- тывать уже обработанные им данные, либо попытается принять еще не подготовлен- ные данные. Модель “изготовитель-потребитель” представлена на рис. 4.9. 4.5.5. Модели SPMD и MPMD для потоков ° кажДой из описанных выше моделей потоки вновь и вновь выполняют одну и ту задачу на различных наборах данных или им назначаются различные задачи для -р ОЛНения на различных наборах данных. Эти потоковые модели используют схемы (Single-Program, Multiple-Data — одна программа, несколько потоков данных)
134 Глава 4. Разбиение С++-программ на множество потоков и MPMD (Multiple-Programs, Multiple-Data — множество программ, множество потоков данных). Эти схемы представляют собой модели параллелизма, которые делят про- граммы на потоки инструкций и данных. Их можно использовать для описания типа ра- боты, которую реализуют потоковые модели с использованием параллелизма. В контек- сте нашего изложения материала модель MPMD лучше представить как модель M7MD (Multiples-Threads, Multiple-Data— множество потоков выполнения, множество потоков данных). Эта модель описывает систему с различными потоками выполнения (thread), которые обрабатывают различные наборы данных, или потоки данных (stream). Анало- гично модель SPMD нам лучше рассматривать как модель STMD (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.
4.6. Введение в библиотеку Pthread 135 Рис. 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); // Дополнительная обработка. pthread—join(ThreadA,NULL); // Ожидание завершения pthread—join(ThreadB,NULL); // потоков. return (0); } В листинге 4.1 делается акцент на определении набора инструкций для основно- го потока. Основным в данном случае является управляющий поток, который объ- являет два рабочих потока ThreadA и ThreadB. С помощью функции pthread—create () эти два потока связываются с задачами, которые они должны выполнить (taskl и task2). Здесь (ради простоты примера) эти задачи всего лишь
4.7. Анатомия простой многопоточной программы 137 авляют сообщение в стандартный выходной поток, но понятно, что они могли °ТП^быть запрограммированы на нечто более полезное. При вызове функции ^^read create () потоки немедленно приступают к выполнению назначенных Р задач. Работа функции pthread_join() аналогична работе функции wait () И>я проЧессов' Основн°й поток ожидает до тех пор, пока не завершатся оба рабо- потока. Диаграмма последовательностей, соответствующая листингу 4.1, пока- зана на рис. 4.11. Обратите внимание на то, что происходит с потоками выполне- ния при вызове функций pthread_create () и pthread_j oin (). На рис. 4.11 показано, что вызов функции pthread_create () является причи- ной разветвления, или образования “вилки” в основном потоке выполнения, в ре- зультате чего образуются два дополнительных “ручейка” (по одному для каждой за- дачи), которые выполняются параллельно. Функция pthread_create () заверша- ется сразу же после создания потоков. Эта функция предназначена для создания асинхронных потоков. Это означает, что, как рабочие, так и основной поток, вы- полняют свои инструкции независимо друг от друга. Функция pthread_join () за- ставляет основной поток ожидать до тех пор, пока все рабочие потоки завершатся и “присоединятся” к основному. Рис. 4.11. Диаграмма последовательностей, соответствующая листингу 4.1 ^•7.1. Компиляция и компоновка многопоточных программ u &Се многопоточные программы, использующие библиотеку потоков POSIX, долж- ны Включать заголовок: 'Pthread. h>
138 Глава 4. Разбиение С++-программ на множество потоков Для компиляции многопоточного приложения в средах UNIX или Linux с помо- щью компиляторов командной строки д++ или дсс необходимо скомпоновать его с библиотекой Pthreads. Для задания библиотеки используйте опцию -1. Так, команда -Ipthread обеспечит компоновку вашего приложения с библиотекой, которая согласуется с многопоточным интерфейсом, определенным стандартом POSIX 1003.1с. Библио- теку Pthread, libpthread. so, следует поместить в каталог, в котором хранится сис- темная стандартная библиотека, обычно это /usr/lib. Если она будет находиться не в стандартном каталоге, то для того, чтобы обеспечить поиск компилятора в заданном каталоге до поиска в стандартных, используйте опцию -L. По команде д++ -о blackboard -L /src/local/lib blackboard.cpp -Ipthread компилятор выполнит поиск библиотеки 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*), 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 (&threads, NULL, task2,NULL) ; Это — два вызова функции pthread—create () из листинга 4.1. Оба потока создаются с атрибутами, действующими по умолчанию. В программе 4.1 отображен основной поток, который передает аргумент из ко- мандной строки в функции, выполняемые потоками. / / Программа 4.1 #include <iostream> #include <pthread.h> tfinclude <stdlib.h> int main(int argc, char *argv[]) { pthread—t ThreadA,ThreadB; int N; if(argc 1= 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 v°id *taskl(void *Х) 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 = 1;Count < *Temp;Count++){ cout « "В потоке В: " « Count << " + 2 << Count + 2 « endl; } cout « "Поток В завершен." « endl; В программе 4.2 функции taskl и task2 выполняют цикл, количество итераций ко- торого равно числу, переданному каждой функции в качестве параметра. Одна функция увеличивает переменную цикла на два, вторая — умножает ее на два, а затем каждая из них отправляет результат в стандартный поток вывода данных. По выходу из цикла ка- ждая функция выводит сообщение о завершении выполнения потока. Инструкции по компиляции и выполнению программ 4.1 и 4.2 содержатся в профиле программы 4.1. J Профиль программы 4.1 Имя программы ,program4-12.сс г Описание Принимает целочисленное значение из командной строки и передает функциям; потоков. Каждая функция выполняет цикл, в котором переменная цикла увеличивается (в одной функции на два, а в другой в два раза), а затем результат отсылается в стандартный поток вывода данных. Код основного потока выполнения приведен в программе 4.1, а код функций — в программе 4.2. Требуемая библиотека libpthread Требуемые заголовки <pthread.h> <iostream> <stdlib.h> Инструкции по компиляции и компоновке программ C++ -о program4-12 program4-12.сс -Ipthread Среда для тестирования . SuSE Linux 7.1, gcc 2.95.2, Инструкции по выполнению ./program4~12 34 , Примечания Эта программа требует задания аргумента командной строки.
4.8. Создание потоков 141 мента. Если необходим< структуру (struct) или дайте функции потока ук 3 этом разделе был приведен пример передачи функции потока лишь одного аргу- передать функции потока несколько аргументов, создайте контейнер, содержащий все требуемые аргументы, и пере- азатель на эту структуру. 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 () могут также вызывать равноправные потоки, если потоковый дескриптор является глобальным. Это позволя- любому потоку соединиться с любым другим потоком выполнения в процессе. Если ка Ва1°щий поток аннулируется до завершения заданного (для присоединения) пото- ’ ЭТот 3аДанный поток не станет открепленным (detached) потоком (см. следующий ел'* Если различные равноправные потоки одновременно вызовут функцию аjoin () для одного и того же потока, его дальнейшее поведение не определено.
142 Глава 4. Разбиение С++-программ на множество потоков Синопсис #include <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> int pthread—detach(pthread—t thread thread); При успешном выполнении эта функция возвращает число 0, в противном слу- чае— код ошибки. Функция pthread—detach () не будет успешной, если заданный поток уже откреплен или поток, заданный параметром thread, не был обнаружен. Вот пример открепления уже существующего присоединяемого потока: //. . . pthread—create(&threadA,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 описывает поток как открепленный или присоединяемый. Синопсис #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 атрибут de tachstate принимает по умолчанию. При успешном выполнении функция 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); 1 cout « "Ожидание присоединения потока A.” « endl; 1 pthread—join(ThreadA,NULL); return (0) ; } В листинге 4.2 объявляется атрибутный объект DetachedAttr, для инициализации ко- торого используется функция pthread—attr_init (). После инициализации этого объ- екта вызывается функция pthread—attr—detachstate (), которая изменяет свойство detachstate (“присоединяемость”), установив значение PTHREAD—CREATE—DETACHED (“открепленность”). При создании потока Threads с помощью функции pthread-Create () в качестве ее второго аргумента используется модифицированный объект DetachedAttr. Для потока Threads вызов функции pthread— j oin () не ис- пользуется, поскольку открепленные потоки присоединить невозможно. 4.9. Управление потоками Создавая приложение с несколькими потоками, можно по-разному организовать их выполнение, использование ими ресурсов и состязание за ресурсы. Управление потоками по большей части осуществляется путем установки стратегий планирования и значений приоритета. Эти факторы влияют на эффективность потока. Кроме них, эффективность потока также определяется тем, как потоки состязаются за ресурсы: в рамках одного про- цесса либо в масштабе всей системы. Стратегию планирования, приоритет и область кон- куренции потока можно установить с помощью объекта атрибутов потока. Поскольку по- токи совместно используют ресурсы, доступ к ним необходимо синхронизировать. Эту те- му мы кратко затронем в этой главе и более подробно— в главе5. К вопросам синхронизации также относятся и такие: где и как завершаются и аннулируются потоки. 4.9.1. Завершение потоков Выполнение потока может быть прервано по разным причинам: • в результате выхода из процесса с возвращаемым им статусом завершения (или без него); • в результате собственного завершения и предоставления статуса завершения; • в результате аннулирования другим потоком в том же адресном пространстве. Завершаясь, функция присоединения потока pthread—join() возвращает вызы- вающему потоку статус завершения, передаваемый функции pthread—exit (), кото- рая была вызвана завершающимся потоком. Если завершающийся поток не обращал- я к функции pthread—exit (), то в качестве статуса завершения будет использовано чение, возвращаемое этой функцией, если оно существует; в противном случае завершения равен значению NULL. в °зможна ситуация, когда одному потоку необходимо завершить другой поток eJ°M Же процессе. Например, приложение может иметь поток, который контролиру- работу других потоков. Если окажется, что некоторый поток “плохо себя ведет”
146 Глава 4. Разбиение С++-программ на множество потоков или больше не нужен, то ради экономии системных ресурсов, возможно, его нужцо завершить. Завершающийся поток может окончиться немедленно или отложить за- вершение до тех пор, пока не достигнет в своем выполнении некоторой логической точки. При этом вполне вероятно, что такой поток (прежде чем завершиться) должен выполнить некоторые действия очистительно-восстановительного характера. Поток имеет также возможность отказаться от завершения. Для завершения вызывающего потока используется функция pthread_exit () Значение value_.pt г передается потоку, который вызывает функцию pthread—join() для этого потока. Еще не выполненные процедуры, связанные с “уборкой”, будут выполнены вместе с деструкторами, предусмотренными для потоко- вых данных. Никакие ресурсы, используемые потоками, при этом не освобождаются. Синопсис #include <pthread.h> int pthread—exit(void *value ptr); При завершении последнего потока в процессе завершается сам процесс со статусом завершения, равным 0. Эта функция не может вернуться к вызывающему потоку и не определяет никаких кодов ошибок. Для отмены выполнения некоторого потока по инициативе потока из того же ад- ресного пространства используется функция pthread—cancel (). Отменяемый поток задается параметром thread. Синопсис #include <pthread.h> int pthread—cancel(pthread—t thread thread); Обращение к функции pthread—cancel () представляет собой запрос аннулировать по- ток. Этот запрос может быть удовлетворен немедленно, с отсрочкой или проигнорирован. Когда произойдет аннулирование (и произойдет ли оно вообще), зависит от типа аннули- рования и состояния потока, подлежащего этой кардинальной операции. Для удовлетво- рения запроса на отмену потока предусмотрен процесс аннулирования, который происхо- дит асинхронно (т.е. не совпадает по времени) по отношению к выходу из функции pthread—cancel () и ее возврату в вызывающий поток. Если потоку нужно выполнить “уборочные” задачи, они обязательно выполняются. После выполнения последней та- кой задачи-обработчика вызываются деструкторы потоковых объектов, если таковые предусмотрены, и только после этого поток завершается. В этом и состоит процесс ан- нулирования потока. При успешном выполнении функция pthread—cancel () возвра- щает число 0, в противном случае — код ошибки. Эта функция не выполнится успешно, если параметр thread не соответствует ни одному из существующих потоков. Некоторые потоки могут потребовать принять меры безопасности против преж- девременного их аннулирования. Внесение в потоковую функцию средств безопасно- сти может предотвратить возникновение некоторых нежелательных ситуаций. Пото- ки разделяют общие данные, и (в зависимости от используемой потоковой модели) один поток может обрабатывать данные, которые должны быть переданы другому’ по- току для последующей обработки. Пока поток обрабатывает данные, он является их
4.9. Управление потоками 147 единственным обладателем благодаря блокированию мьютекса, связанного с этими данными. Если поток, имеющий заблокированный мьютекс, аннулируется до его ос- вобождения, возникает взаимоблокировка. Для того чтобы снова использовать дан- ные, их следует привести в определенное состояние. Если поток отменяется до освобо- ждения мьютекса, могут возникнуть нежелательные условия. Другими словами, в зави- симости от типа обработки, которую выполняет поток, его аннулирование должно происходить тогда, когда это безопасно. Об опасных и безопасных периодах “знает” только сам поток, и поэтому только он может предотвратить свое аннулирование в опасные периоды. Следовательно, круг потоков, которые можно аннулировать, должен быть ограничен потоками, которые не относятся к числу “жизненно важных” или ко- торые не имеют блокировок ресурсов. Кроме того, аннулирование может быть отсро- чено до тех пор, пока не будут выполнены “жизненно важные” действия. Состояние готовности к аннулированию (cancelability state) описывает условия, при которых поток может (или не может) быть аннулирован. Тип аннулирования (cancelabilty type) потока определяет способность потока продолжать выполнение после получения запросов на аннулирование. Поток может отреагировать на аннули- рующий запрос немедленно или отложить аннулирование до определенной (более поздней) точки в его выполнении. Состояние готовности к аннулированию и тип ан- нулирования устанавливаются динамически самим потоком. Для определения состояния готовности к аннулированию и типа аннулирова- ния вызывающего потока используются функции pthread—setcancelstate () и pthread_setcanceltype (). Функция pthread—setcancelstate () устанавлива- ет вызывающий поток в состояние, заданное параметром state, и возвращает пре- дыдущее состояние в параметре olds tat е. Синопсис ♦include <pthread.h> int pthread—setcancelstate(int state, int *oldstate); int pthread—setcanceltype(int type, int *oldtype); Параметры state и oldstate могут принимать такие значения: ^THREAD—CANCEL—DI SABLE ^THREAD—CANCEL—ENABLE начение PTHREAD—CANCEL—DISABLE определяет состояние, в котором поток будет игнорировать запрос на аннулирование, а значение PTHREAD—CANCEL—ENABLE — со- стояние, в котором поток “согласится” выполнить соответствующий запрос (это со- стояние по умолчанию устанавливается для каждого нового потока). При успешном циП°ЛНеНИИ Фуикцпя возвращает число 0, в противном случае — код ошибки. Функ- ей* Pthread_ setcancel state () не может выполниться успешно, если переданное значение параметра state окажется недействительным. ед ' нкция pthread—setcanceltype () устанавливает для вызывающего потока тип в п ' ЛИРОВания’ заданный параметром type, и возвращает предыдущее значение типа раметре oldtype. Параметры type и oldtype могут принимать такие значения: ₽ThrfAD~CANCEL-DEFFERED kead_cancel_asynchronous
148 Глава 4. Разбиение С++-программ на множество потоков Значение PTHREAD—CANCEL—DEFFERED определяет тип аннулирования, при котором поток откладывает завершение до тех пор, пока он не достигнет точки, в котором его аннулирование возможно (этот тип по умолчанию устанавливается для каждого ново- го потока). Значение PTHREAD—CANCEL—ASYNCHRONOUS определяет тип аннулирова- ния, при котором поток завершается немедленно. При успешном выполнении функ- ция возвращает число 0, в противном случае— код ошибки. Функция pthread—setcanceltype () не может выполниться успешно, если переданное ей значение параметра type окажется недействительным. Функции pthread— setcancelstate () и pthread—setcanceltype () использу- ются вместе для установки отношения вызывающего потока к потенциальному запро- су на аннулирование. Возможные комбинации значений состояния и типа аннулиро- вания перечислены и описаны в табл. 4.5. Таблица 4.5. Комбинации значений состояния и типа аннулирования Состояние Тип Описание PTHREAD—CANCEL— PTHREAD—CANCEL— Отсроченное аннулирование. Эти состояние ENABLE DEFERRED и тип аннулирования потока устанавлива- ются по умолчанию. Аннулирование пото- ка происходит, когда он достигает соот- ветствующей точки в своем выполнении или когда программист определяет точку аннулирования с помощью функции pthread—testcancel() PTHREAD—CANCEL— PTHREAD—CANCEL- Асинхронное аннулирование. Аннулирование ENABLE ASYNCHRONOUS потока происходит немедленно PTHREAD—CANCEL- Игнорируется Аннулирование запрещено. Оно вообще не вы- DISABLE полняется 4.9.1.1. Точки аннулирования потоков Если удовлетворение запроса на аннулирование потока откладывается, значит, оно произойдет позже, когда это делать “безопасно”, т.е. когда оно не попадает на пе- риод выполнения некоторого критического кода, блокирования мьютекса или пре- бывания данных в некотором “промежуточном” состоянии. Вне этих “опасных” ра3' делов кода потоков вполне можно устанавливать точки аннулирования. Точка анну- лирования — это контрольная точка, в которой поток проверяет факт существования каких-либо ждущих (отложенных) запросов на аннулирование и, если таковые имеют- ся, разрешает завершение. Точки аннулирования можно пометить с помощью функции pthread—test cancel () • Эта функция проверяет наличие необработанных запросов на аннулирование. Если они есть, она активизирует процесс аннулирования в точке своего вызова. В против- ном случае функция продолжает выполнение потока без каких-либо последствий. Вы- зов этой функции можно разместить в любом месте кода потока, которое считается безопасным для его завершения.
4.9. Управление потоками 149 Синопсис ^include <pthread.h> void pthread—testcancel(void); ПрогРамма 4-3 содержит функции, которые вызывают функции pthread—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; 11 Разрешено аннулирование асинхронного типа. 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, &OldState); ptnread—setcanceltype(PTHREAD—CANCEL—DEFERRED, f &OldType); Count = 1;Count < 1000;Count++) cout << "в потоке C: " « 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 () и pthread— setcanceltype () здесь необязательны. Критический код этого потока здесь может спокойно выполняться после установки со- стояния и типа аннулирования, поскольку процедура завершения не стартует до вызова функции pthread—testcancel (). Если не будут обнаружены ждущие запросы, поток продолжит свое выполнение до тех пор, пока не встретит очередные обращения к функции pthread— testcancel () (если таковые предусмотрены). В задаче task3 функция pthread—cancel () вызывается только после того, как переменная Count без остатка разделится на число 100. Код, расположенный между точками аннулиро- вания потока, не должен быть критическим, поскольку он может не выполниться. Программа 4.4 содержит управляющий поток, который делает запрос на аннули- рование каждого рабочего потока. // Программа 4.4 int main(int argc, char *argv[]) { pthread—t Threads[3]; void *Status; pthread—create(&(Threads[0]),NULL,taskl,NULL); pthread—create(&(Threads[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—j oin (). Эта функция завершается ус- пешно даже тогда, когда она пытается присоединить поток, который уже завершен, функция присоединения в этом случае просто считывает статус завершения завер- шенного потока. И такое поведение весьма кстати, поскольку поток, который сделал запрос на аннулирование, и поток, вызвавший функцию pthread—join (), могут ока- заться совсем разными потоками. Мониторинг функционирования всех рабочих по- токов может оказаться единственной задачей того потока, который “по совмести- тельству” и аннулирует потоки. Опрашивать же статус завершения потоков с помо- щью функции pthread—join () может совершенно другой поток. Этот тип информации используется для получения статистической оценки того, какой из по- токов наиболее эффективен. В рассматриваемой нами программе все это делает один управляющий поток: в цикле он и присоединяет рабочие потоки, и проверяет их ста- тус завершения. Поток Thread [0] не аннулирован, поскольку он имеет запрет на ан- нулирование, в то время как два остальных потока были аннулированы. Статус завер- шения аннулируемого потока может иметь, например, значение PTHREAD—CANCELED. Профили программ 4.3 и 4.4 представлены в разделе “Профиль программы 4.2”. ; Профиль программы 4.2 Имя программы j Program4-3 4.сс ; Описание ;Демонстрирует аннулирование потоков. Три потока имеют различные типы ; и состояния аннулирования. Каждый поток выполняет цикл. Состояние и тип аннулирования определяет количество итераций цикла и то, будет ли цикл выполняться вообще. Основной поток определяет статус завершения каждого , рабочего потока. Требуемая библиотека libpthread Требуемые заголовки <Pthread.h> <iostream> Инструкции по компиляции и компоновке программ + -° program4-34 program4-34.сс -lpthread Рада для тестирования ; USE Linux 7.1, gcc 2.95.2. |Инструкции по выполнению ^/^rogram4 - 3 4
152 Глава 4. Разбиение С++-программ на множество потоков В функциях, определенных пользователем, используются точки аннулирования отмеченные обращением к функции pthread—testcancel (). Библиотека Pthread определяет в качестве точек аннулирования выполнение других функций. Эти функ- ции блокируют вызывающий поток, а заблокированному потоку аннулирование не грозит. Вот эти функции библиотеки Pthread: pthread—testcancel() pthread_cond_wait() pthread—timedwait pthread—join() Если поток, пребывающий в состоянии отсроченного аннулирования, имеет ждущий запрос на аннулирование, то при вызове одной из перечисленных выше функций биб- лиотеки Pthread будет инициирована процедура аннулирования. Некоторые из систем- ных функций, претендующих на роль точек аннулирования, перечислены в табл. 4.6. Таблица 4.6. Системные POSIX-функции, претендующие на роль точек аннулирования accept() nanosleep() sem_wait() aio__suspend () open() send() clock—nanosleep() pause() sendmsg() close() pollO sendto() connect() pread() sigpause() creat() pthread—cond_timedwait() sigsuspend() fcntl() pthread—cond_wait() sigtimedwait() fsync() pthread—j oin() sigwait() getmsg() putmsg() sigwaitinfo() lockf() putpmsg() sleep() mq_receive() pwrite() system() mq_send() read() usleep() mq_timedreceive() readv() wait() mq_timedsend () recvfrom() waitpid() msgrcv() recvmsg() write() msgsnd() select() writev() msync() sem timedwait() Несмотря на то что эти функции безопасны для отсроченного аннулирования по- токов, они могут не быть таковыми для асинхронного аннулирования. Асинхронное аннулирование во время вызова библиотечной функции, которая не является асин- хронно-безопасной, может привести к тому, что библиотечные данные останутся не в надлежащем состоянии. Библиотека выделит память от имени потока, и, когда по- ток будет аннулирован, продолжит удерживать “за собой” эту память. Для других биб- лиотечных и системных функций, которые не являются безопасными для аннулир0' вания (асинхронного или отсроченного), возможно, имеет смысл написать код, пре'
4.9. Управление потоками 153 пятствующий завершению потока путем установки категорического запрета на анну- лирование или использования отсроченного аннулирования до тех пор, пока эти функции не будут выполнены. 4.9.1-2- Очистка перед завершением Поток, “позволивший” себя аннулировать, прежде чем завершиться, обычно дол- жен выполнить некоторые заключительные действия. Так, нужно закрыть файлы, привести разделяемые данные в надлежащее состояние, снять блокировки или осво- бодить занимаемые ресурсы. Библиотека Pthread определяет механизм поведения каждого потока “в последние минуты своей жизни”. С каждым потоком связывается стек очистительно-восстановительных операций (cleanup stack), который содержит указатели на процедуры (или функции), предназначенные для выполнения во время аннулирования потока. Для того чтобы поместить в этот стек указатель на процедуру, предусмотрена функция pthread—сleanup_push (). Синопсис #include <pthread.h> void pthread—сleanup_push(void (*routine)(void *), void *arg); void pthread—cl eanup pop (int execute); Параметр routine представляет собой указатель на функцию, помещаемый в стек завершающих процедур. Параметр arg содержит аргумент, передаваемый этой routine-функции, которая вызывается при завершении потока с помощью функции pthread—exit (), когда поток “покоряется” запросу на аннулирование или явным обра- зом вызывает функцию pthread—сleanup_pop () с ненулевым значением параметра execute. Функция, заданная параметром routine, не возвращает никакого значения. Функция pthread—cleanup—pop () удаляет указатель routine-функции из вер- шины стека завершающих процедур вызывающего потока. Параметр execute может принимать значение 1 или 0. Если его значение равно 1, поток выполняет routine- функцию, даже если он при этом и не завершается. Поток продолжает свое выполне- ние с инструкции, расположенной за вызовом функции pthread—cleanup—pop (). Если значение параметра execute равно 0, указатель извлекается из вершины стека потока без выполнения routine-функции. Необходимо позаботиться о том, чтобы для каждой функции занесения в стек tpush) существовала функция извлечения из стека (pop) в пределах одной и той же лексической области видимости. Например, для функции f uncA () обязательно вы- полнение cleanup-обработчика при ее нормальном завершении или аннулировании: (Oid *funcA(void *Х) int *Tid; Tid = new int; // Выполнение некоторых действий. //^pead-cleanuP-Push(cleanup—funcA,Tid); Выполнение некоторых действий. } pthread_c 1 eanup—pop (0) ;
154 Глава 4. Разбиение С++-программ на множество потоков Здесь функция funcAf) помещает указатель на обработчик cleanup—funcА() в стек завершающих процедур путем вызова функции pthread_cleanup—push (). Каждому обращению к этой функции должно соответствовать обращение к функции pthread—cleanup—pop(). Если функции извлечения указателя из стека (pop. функции) передается значение 0, то извлечение из стека состоится, но без выполне- ния обработчика. Обработчик будет выполнен лишь при аннулировании потока, вы- полняющего функцию funcA(). Для функции funcB () также требуется cleanup-обработчик: void *funcB(void *Х) { int *Tid; Tid = new int; // Выполнение некоторых действий. //. . . pthread—cleanup—push(cleanup—funcB,Tid); // Выполнение некоторых действий. //. . . pthread—cleanup_pop(1); } Здесь функция funcB () помещает указатель на обработчик cleanup—funcB () в стек завершающих процедур. Отличие этого примера от предыдущего состоит в том, что функции pthread—cleanup—pop () передается параметр со значением 1, т.е. после из- влечения из стека указателя на обработчик этот обработчик будет туг же выполнен. Необ- ходимо отметить, что выполнение обработчика в данном случае состоится “при любой по- годе”, т.е. и при аннулировании потока, который обеспечивает выполнение функции funcB (), и при обычном его завершении. Обработчики-“уборщики”, cleanup—funcA () и cleanup—funcB (), — это обычные функции, которые можно использовать для закры- тия файлов, освобождения ресурсов, разблокирования мьютексов и пр. 4.9.2. Управление стеком потока Адресное пространство процесса делится на раздел кода, раздел статических дан- ных, свободную память и раздел стеков. Стекам потоков выделяется область из стеко- вого раздела процесса. Стек потока предназначен для хранения стекового фрейма, свя- занного с каждой процедурой (функцией), которая была вызвана, но еще не заверше- на. Стековый фрейм содержит временные переменные, локальные переменные, адреса точек возврата и любую другую дополнительную информацию, которая необ- ходима потоку, чтобы найти “обратную дорогу” к ранее вызванным процедурам. При выходе из процедуры (функции) ее стековый фрейм извлекается из стека. Располо- жение фреймов в стеке схематично показано на рис. 4.12. Предположим, что поток А (см. рис. 4.12) выполняет функцию taskl () , кото- рая создает некоторые локальные переменные, выполняет заданную обработку, а затем вызывает функцию taskX (). При этом для функции taskl () создается сте- ковый фрейм, который помещается в стек потока. Функция taskX () выполняет “свои” действия, создает локальные переменные, а затем вызывает функцию taskC (). Нетрудно догадаться, что стековый фрейм, созданный для функции taskX () , также помещается в стек. Функция taskC () вызывает функцию taskY () и т.д. Каждый стек должен иметь достаточно большой размер, чтобы поместить всю
4.9. Управление потоками 155 информацию, необходимую для выполнения всех функций потока я других подпрограмм, которые будут вызваны „ХО»Ы„„ Jy„KnL’?'pU"”'‘“ „ местопотожением стека поток, управляет о„ерац„„„„ая cnS™. “о L “Р°М ки и считывания этой информации предусмотрим ДЛЯ Устад°в- в объекте атрибутов потока. ды’ которые определены АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА Рис. 4.12. Стековые фреймы, сгенерированные потоками Функция pthread_attr_getstacksize () возвращает минимальный размер стека, устанавливаемый по умолчанию. Параметр attr определяет объект атри утов пото из которого считывается стандартный размер стека. При успешном выполнении срунк ция возвращает значение 0, а стандартный размер стека, выраженный в байтах, сохра няется в параметре stacksize. В случае неудачи функция возвращает код ошибки. Функция pthread_attr_setstacksize() устанавливает минимальный размер стека. Параметр attr определяет объект атрибутов потока, для которого устанав ливается размер стека. Параметр stacksize содержит минимальный размер стека, выраженный в байтах. При успешном выполнении функция возвращает значение , в противном случае — код ошибки. Функция завершается неудачно, если значени параметра stacksize оказывается меньше значения PTHREAD_MIN_STACK или превышает системный минимум. Вероятно, значение PTHREAD_STACK_MIN удет меньше минимального размера стека, возвращаемого функцией Pthread_attr_getstacksize(). Прежде чем увеличивать минимальный размер стека потока, следует поинтересоваться значением, возвращаемым функцией Pthread_attr_getstacksize() размер СТека фиксируется, чтобы его расшире- ние во время выполнения программы ограничивалось рамками фиксированного Пространства стека, установленного во время компиляции.
156 Глава 4. Разбиение С++-программ на множество потоков Синопсис #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—setstackaddr() и 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, void** res trict st ackaddr) ; Атрибуты стека (размер и местоположение) можно установить с помощью одной функции. Функция 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> •Я nthread_attr_setstack(pthread_attr_ t *attr, VO1 void *stackaddr, size_t stacksize) ; void pthread_attr_getstack( const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t stacksize) ; В листинге 4.3 показан пример установки размера стека для потока, создаваемого С помощью атрибутного объекта. // Листинг 4.3. Изменение размера стека для потока / / с использованием смещения //. . • pthread—attr_getstacksize (&SchedAttr, &DefaultSize) ; if (Defaultsize < Min_Stack_Req) { Sizeoffset = Min_Stack—Req - Defaultsize; NewSize = Defaultsize + Sizeoffset; pthread_attr_setstacksize(&Attrl,(size_t)NewSize); } В листинге 4.3 сначала из атрибутного объекта потока считывается размер стека, дей- ствующий по умолчанию. Затем, если окажется, что этот размер меньше желаемого минимального размера стека, вычисляется разность между сравниваемыми размера- ми, после чего значение этой разности (смещение) суммируется с размером стека, ис- пользуемым по умолчанию. Результат суммирования становится новым минимальным размером стека для этого потока. ПРИМЕЧАНИЕ: установка размера и местоположения стека может сделать вашу программу непереносимой. Размер и местоположение стека, устанавливаемые на од- ной платформе, могут оказаться неподходящими для использования в качестве раз- МсРа и местоположения стека для другой платформы. 4-9.3. Установка атрибутов планирования и свойств потоков Tok^0,zio®ho процессам, потоки выполняются независимо один от другого. Каждый по- етсяНаЗНаЧаеТСЯ пР°ЦессоРУ Для выполнения его задачи. Для каждого потока определя- сь - стРатегия планирования и приоритет, которые предписывают, как и когда именно Пы п Назначен процессору. Стратегия планирования и приоритет потока (или груп- Dt-ъ К°В }станавливаются с помощью объекта атрибутов и следующих функций: pthreaH~’attr~Set^n^er^tsc^e<^ ( PthrGZd"attr~Setschedpolicy (} —attr__setschedparam()
158 Глава 4. Разбиение С++-программ на множество потоков Для получения информации о характере выполнения потока используются СЛе дующие функции: pthread_attr_getinheritsched() pthread_attr_getschedpolicy() pthread_attr_getschedparam() Синопсис #include <pthread.h> #include <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 () позво- ляет определить, как будут устанавливаться атрибуты планирования потока: путем на- следования от потока-создателя или в соответствии с содержимым объекта атрибутов. Параметр inheritsched может принимать одно из следующих значений. PTHREAD_INHERIT_SCHED Атрибуты планирования потока должны быть унас- ледованы от потока-создателя, при этом любые ат- рибуты планирования, определяемые параметром attr, будут игнорироваться. PTHREAD_EXPLICIT_SCHED Атрибуты планирования потока должны быть уста- новлены в соответствии с атрибутами планирования, хранимыми в объекте, заданном параметром attr. Если параметр inheritsched получает значение PTHREAD_EXPLICIT_SCHED, то функция pthread_attr_setschedpolicy() используется для установки стратегии планирования, а функция pthread_attr_setschedparam () — установки приоритета. Функция pthread_attr_setschedpolicy () устанавливает член объекта атрибу- тов потока (заданного параметром attr), “отвечающий” за стратегию планирования потока. Параметр policy может принимать одно из следующих значений, опреде- ленных в заголовке <sched. h>. SCHED_FIFO SCHED_RR SCHED_OTHER Стратегия планирования типа FIFO (первым прибыл, первым об- служен), при которой поток выполняется до конца. Стратегия циклического планирования, при которой каждый поток назначается процессору только в течение некоторого кванта времени- Стратегия планирования другого типа (определяемая реализацией)- Для любого нового потока эта стратегия планирования принимается по умолчанию.
4.9. Управление потоками 159 кция pthread_attr_setschedparam() используется для установки членов 1 тНОго объекта (заданного параметром attr), связанных со стратегией плани- аТР ыя Параметр param представляет собой структуру, которая содержит эти чле- СтрУкТУРа sched_param включает по крайней мере такой член данных: struct sched_param { int sched_priority; }; Возможно, эта структура содержит и другие члены данных, а также ряд функций, елназначенных для установки и считывания минимального и максимального зна- чений приоритета, атрибутов планировщика и пр. Но если для задания стратегии планирования используется либо значение SCHED_FIFO, либо значение SCHEDJRR, то в структуре 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. Использование атрибутного объекта потока II для установки стратегии планирования и II приоритета потока define Min_Stack_Req 3000000 Pthread-1 ThreadA; Pthread_attr_t SchedAttr; int^^ Defaultsize, Sizeoffset, NewSize; Scb lnPri°rity, MaxPriority, MidPriority ; e -Param SchedParam; { t rna^n(int argc, char *argv[]) ₽1Нг₽И!аИаЛИЗИР‘>,ем объект атрибутов. a -attr-init(&SchedAttr); // минимальное и максимальное значения Р^тета для стратегии планирования. = sched—get_priority_max(SCHED_RR); rity = sched_get_priority_inin (SCHED_RR) ;
160 Глава 4. Разбиение С++-программ на множество потоков // Вычисляем значение приоритета. MidPriority = (MaxPriority + MinPriority)/2; // Записываем значение приоритета в структуру sched_param. SchedParam.sched_priority = MidPriority; // Устанавливаем объект атрибутов. pthread—attr_setschedparam(&Attrl,&SchedParam); // Обеспечиваем установку атрибутов планирования // с помощью объекта атрибутов. pthread_attr_setinheritsched(SAttrl, 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—set schedparamf) устанавливает как стратегию планирова- так и приоритет потока без использования атрибутного объекта. Параметр НИЯ ad содержит идентификатор потока, параметр policy— новую стратегию пла- L вания и параметр param— значения, связанные с приоритетом. Функция thread-getschedparam () сохраняет значения стратегии планирования и приори- Р а в параметрах policy и param соответственно. При успешном выполнении обе ии возвращают число 0, в противном случае — код ошибки. Условия, при кото- ых эти функции могут завершиться неудачно, перечислены в табл. 4.7. Таблица 4.7. Условия потенциального неудачного завершения функций установки стратегии планирования и приоритета Функции Условия отказа int pthread—getschedparam (pthread-t thread, int *restrict policy, struct sched—param *restrict param) ; Параметр thread не ссылается на сущест- вующий поток int pthread—setschedparam (pthread—t thread, int *policy, const struct sched—param *param); lnt pthread—setschedprio (pthread—t thread, int prio); Некорректен параметр policy или один из членов структуры, на которую указыва- ет параметр param Параметр policy или один из членов структуры, на которую указывает пара- метр param, содержит значение, которое не поддерживается в данной среде Вызывающий поток не имеет соответст- вующего разрешения на установку значе- ний приоритета или стратегии планиро- вания для заданного потока Параметр thread не ссылается на сущест- вующий поток Данная реализация не позволяет прило- жению заменить один из параметров пла- нирования заданным значением Параметр prio не подходит к стратегии планирования заданного потока Параметр prio имеет значение, которое не поддерживается в данной среде Вызывающий поток не имеет соответст- вующего разрешения на установку при- оритета для заданного потока Параметр thread не ссылается на сущест- вующий поток Данная реализация не позволяет прило- жению заменить значение приоритета заданным_____________________________
162 Глава 4. Разбиение С++-программ на множество потоков Функция pthread—setschedprio () используется для установки значения оритета выполняемого потока, идентификатор которого задан параметром threap В результате выполнения этой функции текущее значение приоритета будет заменено значением параметра 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 () возвращает атрибут области конкуренции и3 объекта атрибутов потока, заданного параметром attr. При успешном выполнении значение области конкуренции сохраняется в параметре contentionscope. Обе фунК' ции при успешном выполнении возвращают число 0, в противном случае — код ошибки- 4.9.4. Использование функции sysconf () Знание пределов, устанавливаемых системой на использование ресурсов, позволит на шему приложению эффективно управлять ресурсами. Например, максимальное коли^ест во потоков, приходящихся на один процесс, составляет верхнюю границу числа раб° чих потоков, которое может быть создано процессом. Функция sysconf () используеТ' ся для получения текущего значения конфигурируемых системных пределов или опЦ*114'
4.9. Управление потоками 163 cn»°aCSiC #include #include <unistd.h> <limits.h> ^sysconftint name);_________________________________________________ П аметр 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 сравнивается co значением, возвра- щаемым функцией sysconf (), вызванной с параметром _SC_THREAD_STACK_MIN. Таблица 4.8. Системные переменные и соответствующие им символьные ‘ Г \ константы Переменная Значение Описание -SC_THREADS _POSIX_THREADS Поддерживает потоки _SC_THREAD_ATTR_ STACKADDR _POS IX_THREAD_ATTR_ STACKADDR Поддерживает атрибут адреса стека потока —SC__THREAD_ATTR STACKSIZE _POSIX_THREAD_ATTR_ STACKSIZE Поддерживает атрибут размера стека потока --SC_THREAD_STACK MIN ~ PTHREAD—STACK_MIN Минимальный размер стека потока в байтах -SC_JTHREAD_THREADS MAX ~ -SC-THREAD_KEYS_MAX PTHREAD—THREADS—MAX Максимальное количество по- токов на процесс PTHREAD—KEYS—MAX Максимальное количество twS«THREAD~priO inherit ” -S<THREAD^PRi0 __POSIX_THREAD_PRIO_ INHERIT _POSIX_THREAD—PRIO_ ключей на процесс Поддерживает опцию наследо- вания приоритета Поддерживает опцию приори- тета потока
164 Глава 4. Разбиение С++-программ на множество потоков Окончание табл. 4, $ Переменная Значение - Описание _SC—THREAD-PRIORIТY_ -POSIX—THREAD—PRIORITY— Поддерживает опцию плани- SCHEDULING SCHEDULING рования приоритета потока _SC_THREAD_PROCESS— -POSIX—THREAD—PROCESS. Поддерживает синхронизацию SHARED SHARED на уровне процесса _SC_THREAD—SAFE— -POSIX—THREAD—SAFE— Поддерживает функции безо- functions FUNCTIONS пасности потока _SC_THREAD— -PTHREAD-THREAD- Определяет количество попы- DESTRUCTOR- ITERATIONS DESTRUCTOR-ITERATIONS ток, направленных на разру- шение потоковых данных при завершении потока _SC_CHILD—MAX CHILD_MAX Максимальное количество процессов, разрешенных для UID _SC—PRIORITY— —POSIX—PRIORITY- Поддерживает планирование SCHEDULING SCHEDULING процессов _SC_REALTIME-. _POSIX—REALTIME— Поддерживает сигналы реаль- SIGNALS SIGNALS ного времени —SC—XOPEN—REALTIME- -XOPEN-REALTIME- Поддерживает группу потоко- THREADS THREADS вых средств реального времени X/Open POSIX -SC-STREAM-MAX STREAM-MAX Определяет количество потоков данных, которые один процесс может открыть одновременно -SC.SEMAPHORES —POSIX—SEMAPHORES Поддерживает семафоры _SC—SEM—NSEMS—MAX SEM—NSEMS—MAX Определяет максимальное ко- личество семафоров, которое может иметь процесс _SC_SEM_VALUE—MAX SEM—VALUE—MAX Определяет максимальное зна- чение, которое может иметь се- мафор —SC—SHARED—MEMORY- _POSIX—SHARED—MEMORY- Поддерживает объекты общей OBJECTS OBJECTS памяти 4.9.5. Управление критическими разделами Параллельно выполняемые процессы (или потоки в одном процессе) могут совме- стно использовать структуры данных, переменные или отдельные данные. Разделе- ние глобальной памяти позволяет процессам или потокам взаимодействовать ДрУг с другом и получать доступ к общим данным. При использовании нескольких процес- сов разделяемая глобальная память является внешней по отношению к ним. Внеш* нюю структуру данных можно использовать для передачи данных или команд межДУ процессами. Если же необходимо организовать взаимодействие потоков, то они могу’1'
4.9. Управление потоками 165 ть доступ к структурам данных или переменным, являющимся частью одного получ прОцесса, которому они принадлежат. И Т<Если существуют процессы или потоки, которые получают доступ к разделяемым (Ьицируемым данным, структурам данных или переменным, то все эти данные М ятся в критической области (или разделе) кода процессов или потоков. Крити- Н а тзлел кода — это та его часть, в которой обеспечивается доступ потока или ческии м песса к разделяемому блоку модифицируемой памяти и обработка соответствую- IX данных. Отнесение раздела кода к критическому можно использовать для управ- ления состоянием “гонок”. Например, создаваемые в программе два потока, поток А и поток В, используются для поиска нескольких ключевых слов во всех файлах системы. Поток А просматривает текстовые файлы в каждом каталоге и записывает нужные пу- ти в списочную структуру данных TextFiles, а затем инкрементирует переменную FileCount. ПотокВ выделяет имена файлов из списка TextFiles, декрементирует переменную FileCount, после чего просматривает файл на предмет поиска в нем за- данных ключевых слов. Файл, который их содержит, переписывается в другой файл, и инкрементируется еще одна переменная FoundCount. К переменной FoundCount поток А доступа не имеет. Потоки А и В могут выполняться одновременно на отдель- ных процессорах. Поток А выполняется до тех пор, пока не будут просмотрены все каталоги, в то время как поток В просматривает каждый файл, путь к которому выде- лен из переменной TextFiles. Упомянутый список поддерживается в отсортирован- ном порядке, и в любой момент его содержимое можно отобразить на экране. Здесь возможна масса проблем. Например, поток В может попытаться выделить имя файла из списка TextFiles до того, как потокА его туда поместит. Поток В мо- жет попытаться декрементировать переменную Searchcount до того, как поток А ее инкрементирует, или же оба потока могут попытаться модифицировать эту перемен- ную одновременно. Кроме того, во время сортировки элементов списка TextFiles поток А может попытаться записать в него имя файла, или поток В будет в это время пытаться выделить из него имя файла для выполнения своей задачи. Описанные про- блемы— это примеры условий “гонок”, при которых несколько потоков (или процес- сов) пытаются одновременно модифицировать один и тот же блок общей памяти. Если потоки или процессы одновременно лишь читают один и тот же блок памяти, условия “гонок” не возникают. Они возникают в случае, когда несколько процессов или потоков одновременно получают доступ к одному и тому же блоку памяти, и по крайней мере один из этих процессов или потоков делает попытку модифицировать Данные. Раздел кода становится критическим, когда он делает возможными одновре- менные попытки изменить один и тот же блок памяти. Один из способов защитить критический раздел — разрешить только монопольный доступ к блоку памяти. Моно- польный доступ означает, что к разделяемому блоку памяти будет иметь доступ один НЬ1^ сс Или поток в течение короткого промежутка времени, при этом всем Осталь- ские ПР°Цессам ИЛИ потокам запрещено (путем блокировки) входить в свои критиче- ^разделы, которые обеспечивают доступ к тому же самому блоку памяти. Ровки ^71Равления условиями “гонок” можно использовать такой механизм блоки- “mutual КаК В?аимно исключающий семафор, или мьютекс (mutex— сокращение от критрГ ~ взаимное исключение). Мьютекс используется для блокирования Него — Кого РазДела: он блокируется до входа в критический раздел, а при выходе из Деблокируется:
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(),&StatBuffer); 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 20 int isRegular(string FileName) 21 { 22 struct stat StatBuffer; 23 24 Istat(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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 return (0); void depthFirstTraversal(const char *CurrentDir) { DIR *DirP; string Temp; string FileName; struct dirent *EntryP; chdir(CurrentDir); cout « "Просмотр каталога: " « CurrentDir « endl; DirP = opendir(CurrentDir); if(DirP == NULL){ cout « "He удается открыть файл." « endl; return; } EntryP = readdir(DirP); while(EntryP != NULL) { Temp.erase(); FileName.erase(); Temp = EntryP->d_name; if((Temp != ".") && (Temp != "..")){ FileName.assign(CurrentDir); FileName.append(1,'/'); FileName.append(EntryP->d_name); if(isDirectory(FileName)){ string NewDirectory; NewDirectory = FileName; depthFirstTraversal(NewDirectory.c_str()); } else{ if(isRegular(FileName)){ int Flag; Flag = FileName.find(".cpp"); if(Flag > 0){ pthread_mutex_lock(&CountMutex); Fi1eCount++; pthread_mutex__unlock (&CountMutex) ; pthread_mutex_lock(&QueueMutex) ; TextFiles.push(FileName); pthread_mutex_unlock(&QueueMutex); } } } } Entry? = readdir(DirP); closedir(DirP); v°id *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 Temp, Filename; 4 less<string> Comp; 5 6 while(!Keyfile.eof() && Keyfile.good()) 7 { 8 Keyfile » Temp; 9 if(’Keyfile.eof()){ 10 Keywords.insert(Temp); 11 } 12 } 13 Keyfile.close(); 14 15 while(TextFiles.empty () ) 16 { } 17 18 while (! TextFiles. empty () ) 19 { 20 pthread—mutex_lock(&QueueMutex); 21 Filename = TextFiles.front(); 22 TextFiles.pop(); 23 pthread_mutex—unlock(&QueueMutex); 24 Infile.open(Filename.C—str()); 2 5 Searchwords.erase(Searchwords.begin(), 2 6 Searchwords.end()); 27 while(’Infile.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)){ 35 Outfile « Filename « endl; 3 6 pthread—mutex—lock(fcCountMutex); 37 FileCount—; 38 pthread—mutex—unlock(&CountMutex); 39 FoundCount++; 40 } 41 ) 42 return(NULL); 43 44 }
4.9. Управление потоками 169 Программа потребитель”, 4.7 содержит основной поток для потоков модели “изготовитель- реализованных в программах 4.5 и 4.6. // программа 4.7 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—mu tex_t QueueMutex = PTHREAD—MUTEX—INITIALIZER; 10 pthread—mu tex_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 32 <nt ma^n^nt arUc, char *argv[]) 33 if(argc != 4) { 34 cerr << "Нужна дополнительная информация." « endl; 35 exit (1); 36 } 37 38 Outfile.open(argv[3],ios::app||ios:rate); 40 Keyfile-open(argv[2]); Pthread—create(&Threadl,NULL,task,argv[1]); 42 Pthread—create (&Thread2, NULL, keySearch, argv [ 1 ] ) ; 4з Pthread_join(Threadl,NULL); 44 Pthread-join(Thread2,NULL); 45 pt^read-mutex_destroy (&CountMutex) ; 4g Pthread—mutex—des troy (&Queu eMutex) ; 47 cout « argv[l] << " содержит " << FoundCount 40 <<: " файлов co всеми ключевыми словами." « endl; 49 } return(ОЬ-
170 Глава 4. Разбиение С++-программ на множество потоков С помощью мьютексов доступ к разделяемой памяти для чтения или записи дац. ных разрешается получить только одному потоку. Для гарантии безопасности работы функций, определенных пользователем, можно использовать и другие механизмы и методы, которые реализуют одну из моделей PRAM: EREW (монопольное чтение и монопольная запись) CREW (параллельное чтение и монопольная запись) ERCW (монопольное чтение и параллельная запись) CRCW (параллельное чтение и параллельная запись) Мьютексы используются для реализации EREW-алгоритмов, которые рассматри- ваются в главе 5. 4.10. Безопасность использования потоков и библиотек Климан (Klieman), Шах (Shah) и Смаалдерс (Smaalders) утверждали: “Функция или на- бор функций могут сделать поток безопасным или реентерабельным (повторно входимым), если эти функции могут вызываться не одним, а несколькими потоками без предъявления каких бы то ни было требований к вызывающей части выполнить определенные действия” (1996). При разработ- ке многопоточного приложения программист должен обеспечить безопасность парал- лельно выполняемых функций. Мы уже обсуждали безопасность функций, определенных пользователем, но без учета того, что приложение часто вызывает функции из систем- ных библиотек или библиотек, созданных сторонними производителями. Одни такие функции и/или библиотеки безопасны для потоков, а другие — нет. Если функция не- безопасна, это означает, что в ней используется хотя бы одна статическая переменная, осуществляется доступ к глобальным данным и/или она не является реентерабельной. Известно, что статические переменные поддерживают свои значения между вызо- вами функции. Если некоторая функция содержит статические переменные, то для ее корректного функционирования требуется считывать (и/или изменять) их значения. Если же к такой функции будут обращаться несколько параллельно выполняемых пото- ков, возникнут условия “гонок”. Если функция модифицирует глобальную переменную, то каждый из нескольких потоков, вызывающих функцию, может попытаться модифи- цировать эту глобальную переменную. Возникновения условий “гонок” также не мино- вать, если не синхронизировать множество параллельных доступов к глобальной пере- менной. Например, несколько параллельных потоков могут выполнять функции, кото- рые устанавливают переменную errno. Для некоторых потоков, предположим, эта функция не может выполниться успешно, и переменная errno устанавливается равной сообщению об ошибке, в то время как другие потоки выполняются успешно. Если реа- лизация компилятора не обеспечивает потоковую безопасность поддержки переменной errno, то какое сообщение получит поток при проверке состояния переменной errno? Блок кода считается реентерабельным, если его невозможно изменить при выпол- нении. Реентерабельный код исключает возникновение условий “гонок” благодаря отсутствию ссылок на глобальные переменные и модифицируемые статические дан- ные. Следовательно, такой код могут совместно использовать несколько параллель- ных потоков или процессов без риска создать условия “гонок”. Стандарт POSIX опре' деляет ряд реентерабельных функций. Их легко узнать по наличию “суффикса” —г’ присоединяемого к имени функции. Перечислим некоторые из них:
4.10. Безопасность использования потоков и библиотек 171 getgrgid_r() getgrnanur getpwuid_r< sterror_r<) strt°k-r(\. readdir-r() rand_r0 ^tyTiciiTie—' * Если функция получает доступ к незащищенным глобальным переменным, содер- статические модифицируемые переменные или нереентерабельна, то такая ия считается небезопасной для потока. Системные библиотеки или библиоте- ки созданные сторонними производителями, могут иметь различные версии своих стандартных библиотек. Одна версия предназначена для однопоточных приложений, а другая — для многопоточных. Если предполагается разрабатывать многопоточное приложение, программист должен использовать многопоточные версии нужной ему библиотеки. Некоторые среды требуют не компоновки многопоточных приложений с многопоточной версией библиотеки, а лишь определения макросов, что позволяет объявить реентерабельные версии функций. Такое приложение будет затем компи- лироваться как безопасное для выполнения потоков. Во всех ситуациях использовать многопоточные версии функций попросту невоз- можно. В отдельных случаях многопоточные версии конкретных функций недоступ- ны для данного компилятора или среды. Иногда один интерфейс функции не в со- стоянии сделать ее безопасной. Кроме того, программист может столкнуться с увели- чением числа потоков в среде, которая изначально использовала функции, предназначенные для функционирования в однопоточной среде. В таких условиях обычно используются мьютексы. Например, программа имеет три параллельно вы- полняемых потока. Два из них, threadl и thread2, параллельно выполняют функ- цию f uncA (), которая не является безопасной для одновременной работы потоков. Третий поток, thread3, выполняет функцию funcB (). Для решения проблемы, свя- занной с функцией f uncA (), возможно, достаточно заключить в защитную оболочку мьютекса доступ к ней со стороны потоков threadl и thread2: threadl { lock() funcA() unlock() thread3 { funcB() } thread2 { lock() funcA() unlock() } При реализации таких защитных мер к функции funcA () в любой момент времени обе Г ПОЛ^ЧИТЬ Д°СТУП только один поток. Но проблемы на этом не исчерпываются. Если исции funcA () и funcB () небезопасны для выполнения потоками, они могут обе ифицировать глобальные или статические переменные. И хотя потоки threadl и ead2 используют мьютексы для функции funcA (), поток thread3 может выполнять Не ЧИЮ uncB (одновременно с любым из остальных потоков. В такой ситуации впол- o6><WrrHO возникновение условий “гонок”, поскольку функции funcA () и funcB () мо- модифицировать одну и ту же глобальную или статическую переменную. нИи Р°ИЛЛЮстРиРУем euJc °дин тип условий “гонок”, возникающих при использова- в°дятИбЛИОТеки i°streain- Предположим, у нас есть два потока, А и В, которые вы- °бъе Данные в станДартный выходной поток, cout, который представляет собой типа ostream. При использовании операторов “>>” и “<<” вызываются мето-
172 Глава 4. Разбиение С++-программ на множество потоков ды объекта cout. Вопрос: являются ли эти методы безопасными? Если потока от правляет сообщение “Мы существа разумные” объекту stdout, а поток В отправляет сообщение “Люди алогичные существа”, то не произойдет ли “перемешивание” ВЬ1 ходных данных, в результате которого может получиться сообщение вроде такого- “ Мы Люди существа алогичные разумные существа”? В некоторых случаях безопасные для потоков функции реализуются как атомные. Атомные функции — это функции, ко- торые, если их выполнение началось, не могут быть прерваны. Если операция для объекта cout реализована как атомная, то подобное “перемешивание” не про- изойдет. Если есть несколько обращений к оператору “»”, то они будут выполнены последовательно. Сначала отобразится сообщение потока А, а затем сообщение пото- ка В или наоборот, хотя они вызвали функцию вывода одновременно. Это — пример преобразования параллельных действий в последовательные, которое обеспечит безопасность выполнения потоков. Но это не единственный способ обезопасить функцию. Если функция не оказывает неблагоприятного эффекта, она может смеши- вать свои операции. Например, если метод добавляет или удаляет элементы из струк- туры, которая не отсортирована, и этот метод вызывают два различных потока, то перемешивание их операций не даст неблагоприятного эффекта. Если неизвестно, какие функции из библиотеки являются безопасными, а какие - нет, программист может воспользоваться одним из следующих вариантов действий. • Ограничить использование всех опасных функций одним потоком. • Не использовать безопасные функции вообще. • Собрать все потенциально опасные функции в один набор механизмов синхро- низации. Еще один вариант — создать интерфейсные классы для всех опасных функций, ко- торые должны использоваться в многопоточном приложении, т.е. опасные функции инкапсулируются в одном интерфейсном классе. Такой интерфейсный класс может быть скомбинирован с соответствующими объектами синхронизации с помощью на- следования или композиции и использован специализированным классом. Такой подход устраняет возможность возникновения условий “гонок”. 4.11. Разбиение программы на несколько потоков Выше в этой главе мы рассматривали делегирование работы в соответствии с кон- кретной стратегией или потоковой моделью. Итак, используются следующие распро- страненные модели: • делегирование (“управляющий-рабочий”); • сеть с равноправными узлами; • конвейер; • “изготовитель-потребитель”. Каждая модель характеризуется собственной декомпозъщией работ (Work Breakdown Structure — WBS), которая определяет, кто отвечает за создание потоков и при каких условиях они создаются. В этом разделе мы рассмотрим пример программы для ка* дой модели, использующей функции библиотеки Pthread.
4.11. Разбиение программы на несколько потоков 173 4.11.1. Использование модели делегирования Мы рассмотрели два подхода к реализации модели делегирования при разделении программы на потоки. Вспомним: в модели делегирования один поток (управляющий) соз дает другие потоки (Р^очие) и назначает каждому из них задачу. Управляющий поток делегирует каждому рабочему потоку задачу, которую он должен выполнить путем зала ния некоторой функции. При одном подходе управляющий поток создает рабочие по токи как результат запросов, обращенных к системе. Управляющий поток обрабатывает запрос каждого типа в цикле событий. Как только событие произойдет будет создан па бочий поток и ему будет назначена задача. Функционирование цикла событий в vnnaa ляющем потоке и создание рабочих потоков продемонстрировано в листинге 4.5. // Листинг 4.5. Подход 1: скелет программы реализации // модели управляющего и рабочих потоков // (псевдокод) / / • • • pthread—mutex.t Mutex = PTHREAD_MUTEX_INITIALIZER int AvailableThreads pthread.t Thread [Max_Threads ] void dec remen tThr eadAva i 1 abi 1 i ty (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. incrementThreadAvaliability() return(NULL) } void *taskY(void *Y) { // Обрабатываем запрос типа Y. incrementThreadAvailability() return(NULL) } void *taskZ(void *Z) { // Обрабатываем запрос типа Z. decrementThreadAvailability() return(NULL) } //. . . В листинге 4.5 управляющий поток динамически создает поток для обработки ка- ждого нового запроса, который поступает в систему. Однако существует ограничение на количество потоков (максимальное число потоков), которое можно создать в про- цессе. Для обработки п типов запросов существует п задач. Чтобы гарантировать, что максимальное число потоков на процесс не будет превышено, определяются следую- щие дополнительные функции: threadAvaliability() 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) Ava i1ableThreads — pthread—mutex—unlock(&Mutex) } int threadAvailability(void) {
4.11. Разбиение программы на несколько потоков 175 yiread_mutex_lock (&Mutex) (AvailableThreads > 1) return 1 else return 0 pthread_mutex_unlock(&Mutex) } функция threadAvaliability () возвратит число 1, если максимально допусти- мое количество потоков для процесса еще не достигнуто. Эта функция опрашивает глобальную переменную ThreadAvailability, в которой хранится число потоков, еще доступных для процесса. Управляющий поток вызывает функцию decrementThreadAvailability (), которая декрементирует эту глобальную пере- менную до создания им рабочего потока. Каждый рабочий поток вызывает функцию incrementThreadAvaliability (), которая инкрементирует глобальную перемен- ную ThreadAvaliability до начала его выполнения. Обе функции содержат обра- щение к функции pthread_mutex_lock () до получения доступа к этой глобальной переменной и обращение к функции pthread—mutex_unlock () после него. Если максимально допустимое количество потоков превышено, управляющий поток может отменить создание потока, если это возможно, или породить другой процесс, если это необходимо. Функции taskX (), taskY () и taskZ () выполняют код, предназна- ченный для обработки запроса соответствующего типа. Другой подход к реализации модели делегирования состоит в создании управляю- щим потоком пула потоков, которым (вместо создания под каждый новый запрос ново- го потока) переназначаются новые запросы. Управляющий поток во время инициали- зации создает некоторое количество потоков, а затем каждый созданный поток приос- танавливается до тех пор, пока в очередь не будет добавлен новый запрос. Управляющий поток для выделения запросов из очереди по-прежнему использует цикл событий. Но вместо создания нового потока для обслуживания очередного запроса, управляющий поток уведомляет уже существующий поток о необходимости обработки запроса. Этот подход к реализации модели делегирования представлен в листинге 4.7. // Листинг 4.7. Подход 2: скелет программы реализации модели управляющего и рабочих потоков ' (псевдокод) Pthread-t Thread [N] Управляющий поток. nbkread“Create(&(Thread[1]...taskX...); пьь a<^—creace (& (Thread[2 ] . . .taskY. . . ) ; Pthread-create(&(Thread[3]...taskZ...); Цикл while(Очередь запросов не пуста) Получение запроса Классификация запроса switch(Тип запроса) case X : Ставим запрос в очередь XQueue
176 Глава 4. Разбиение С++-программ на множество потоков Посылаем сигнал потоку Thread[l] case Y : Ставим запрос в очередь YQueue Посылаем сигнал потоку Thread[2] case Z : Ставим запрос в очередь ZQueue Посылаем сигнал потоку Thread[3] //. . . Конец цикла } void *taskX(void *Х) { Цикл Приостанавливаем поток до получения сигнала Цикл 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 4.11 Использование модели сети с равноправными узлами В модели равноправных узлов один поток сначала создает все потоки, необходимые выполнения всех задач. Каждый из равноправных потоков обрабатывает запро- поступающие из собственного входного потока данных. В листинге 4.8 представ- СЫ келет программы, реализующий при разделении программы на потоки метод лен равноправных узлов. / / Листинг 4.8. // Скелет программы реализации модели равноправных потоков //... pthread—t Thread [N] // initial thread pthread—create (& (Thread [ 1 ] . . . taskX ...); pthread—create (& (Thread[2 ]...taskY...); pthread_create(&(Thread[3]...taskZ...); //... void *taskX(void *X) { Цикл while (Доступны запросы типа XRequests) Выделяем запрос Обрабатываем запрос Конец цикла } return (NULL) //... В модели равноправных потоков каждый поток отвечает за собственный входной поток данных. Входные данные могут быть выделены из базы данных, файла и т.п. 4-11.3. Использование модели конвейера этап М°Дели конвейера поток входных данных обрабатывается поэтапно. На каждом ним НеКотоРая порция работы (часть входного потока данных) обрабатывается од- п°рцПиОТОКОМ выполнения’ а затем передается для обработки следующему. Каждая будет ВХодных Данных переходит на очередной этап обработки до тех пор, пока не ВХо 3авеРШена вся обработка. Такой подход позволяет обрабатывать несколько тижени П°ТОКов Данных одновременно. Каждый поток выполнения отвечает за дос- (т,е Промежуточного результата, делая его доступным для следующего этапа РаПп Д^1О1Дего потока конвейера). Скелет программы реализации модели конвейе- Редставлен в листинге 4.9.
178 Глава 4. Разбиение С++-программ на множество потоков // Листинг 4.9. Скелет программы реализации модели / / конвейера //. . . pthread—t Thread[N] Queues[N] // Начальный поток. { Помещаем все входные потоки данных в очередь этапа stagel. pthread—create(&(Thread[1]...stagel...); pthread—create(&(Thread[2]...stage2...); pthread—create(&(Thread[3]...stage3...); //. . . } void *stageX(void *X) { Цикл Приостанавливаем выполнение до появления в очереди порции входных данных. Цикл while (Очередь XQueue не пуста) Извлекаем порцию входных данных. Обрабатываем порцию входных данных. Помещаем результат в очередь следующего этапа. Конец цикла Конец цикла return(NULL) } //. . . В листинге 4.9 объявляется N очередей для N этапов. Начальный поток помещает все порции входных потоков в очередь первого этапа, а затем создает все потоки, необходимые для выполнения всех этапов. Каждый этап содержит свой цикл собы- тий. Поток выполнения находится в состоянии ожидания до тех пор, пока в его очереди не появится порция входных данных. Внутренний цикл продолжается до опустения соответствующей очереди. Порция входных данных извлекается из оче- реди, обрабатывается, а затем помещается в очередь следующего этапа обработки (следующего потока выполнения). 4.11.4. Использование модели “изготовитель- потребитель” В модели “изготовитель-потребитель” поток-“изготовитель” готовит да^нь,е’ “потребляемые” потоком-“потребителем” (причем таких потоков-“потребителеи 1 жет быть несколько). Данные хранятся в блоке памяти, разделяемом всеми потока* как изготовителем, так и потребителями. В листинге 4.10 представлен скелет пр граммы реализации модели “изготовитель-потребитель” (эта модель также исполь валась в программах 4.5, 4.6 и 4.7).
4.11. Разбиение программы на несколько потоков 179 ТИНГ 4.10. Скелет программы реализации модели // "изготовитель-потребитель* и я mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER jjJreadZt Thread[2] Queue // Ha4aJIbHbI^ поток* < thread_create(&(Thread[1]...producer...); pthread—create (& (Thread [2 ] . . .consumer. . .) ; //..• } void *producer(void *X) // Поток-"изготовитель* ( Цикл Выполняем действия pthread—mutex_lock(&Mutex) Помещаем данные в очередь pthread—mutex_unlock(&Mutex) Уведомляем потребителя //. . . Конец цикла void * consumer(void *Х) // Поток-"потребитель* { Цикл Приостанавливаем до получения сигнала Цикл while(Очередь данных не пуста) pthread—mutex—lock(&Mutex) Извлекаем данные из очереди pthread—mutex—unlock(&Mutex) Выполняем действия Конец цикла Конец цикла В листинге 4.9 начальный поток создает оба потока: “изготовителя” и “потребителя”, ^^-“изготовитель” содержит цикл, в котором после выполнения некоторых дейст- Вий блокируется мьютекс для совместно используемой очереди, чтобы поместить в нее подготовленные для потребителя данные. После этого “изготовитель” деблокирует текс и посылает сигнал потоку-“потребителю” о том, что ожидаемые им данные уже Не 6^ЯТСЯ В ОЧеРеди- Поток-“изготовитель” выполняет итерации цикла до тех пор, пока 0 выполнена вся работа. Поток-“потребитель” также выполняет цикл, в котором “ПоПрИостанавливается до тех пор, пока не получит сигнал. Во внутреннем цикле поток- Khdv’ ЬИТСЛЬ Срабатывает все данные до тех пор, пока не опустеет очередь. Он бло- РУет Мьютекс Для разделяемой очереди перед извлечением из нее данных и деблоки- ^ЬЮТеКС после этого- Затем он выполняет обработку извлеченных данных. В про- быть 6 б Поток’“потребитель” помещает свои результаты в файл. Вместо файла может р0ли.11СПользована Другая структура данных. Зачастую потоки-“потребители” играют две так и изготовителя. Сначала возможно “исполнение” роли по- ПотОкТеЛя необработанных данных, подготовленных потоком-“изготовителем”, а затем г°#соИГрает Р°ль иизготовителя”> когда он обрабатывает данные, сохраняемые в дру- естно используемой очереди, “потребляемой” другим потоком.
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_obj ect MObj ; mult1 „И .£а»И1™»1 •> } void *task2(void *) { MObj•c2(); return (NULL) ; } В листинге 4.10 в классе multithread_object объявляются два потока. Они соз- даются и присоединяются к основному потоку в конструкторе этого класса. Поток Threadl выполняет функцию taskl (), а поток Thread2 — функцию task2 (). Функ- ции taskl () и task2 () затем вызывают функции-члены глобального объекта MObj. 4.12. Резюме В последовательной программе всю нагрузку можно разделить между отдельными подпрограммами таким образом, чтобы выполнение очередной подпрограммы было возможно только после завершения предыдущей. Существует и другая организация программ, когда, например, вся работа выполняется в виде мини-программ в рамках основной программы, причем эти мини-программы выполняются параллельно ос- новной. Такие мини-программы могут быть реализованы как процессы или потоки. Если в реализации используются процессы, то каждый процесс должен иметь собст- венное адресное пространство, а если процессы должны взаимодействовать между собой, то такая реализация требует обеспечения механизма межпроцессного взаимо- действия. Для потоков, разделяющих адресное пространство одного процесса, не иркны специальные методы взаимодействия. Но для защиты совместно используемой памяти (чтобы не допустить возникновения условий “гонок”) необходимо использо- такие механизмы синхронизации, как мьютексы. Существует ряд моделей, которые можно использовать для делегирования работы ^0ROKaM И ^пРавления их созданием и аннулированием. В модели делегирования один по- ^рввляющий) создает другие потоки (рабочие) и назначает им задачи. Управляю- Поток ожидает до тех пор, пока каждый рабочий поток не завершит свою задачу, дает Использовании модели равноправных узлов есть один поток, который изначально соз- Pa6QBCe Потоки’ необходимые для выполнения всех задач, причем этот поток считается в «ИМ Пот°ком, поскольку он не осуществляет никакого делегирования. Все потоки Модели имеют одинаковый статус. Применяя модель конвейера, программу можно ць^актеРизовать как сборочную линию, в которой входной поток (поток входных дан- °^Ра^атывается поэтапно. На каждом этапе поток обрабатывает некоторую пор- Ьцц Вх°ДНых элементов. Порция входных элементов перемещается от одного потока °лпения к следующему до тех пор, пока не завершится вся предусмотренная обра-
182 Глава 4. Разбиение С++-программ на множество потоков ботка. На последнем этапе работы конвейера формируются его результаты, т.е. Поел ний поток отвечает за формирование конечных результатов программы. В мои “изготовитель-потребитель” поток-“изготовитель” готовит данные, “потребляемые” И током-“потребителем”. Данные хранятся в блоке памяти, разделяемом всеми Поток ми: как изготовителем, так и потребителями. При использовании объектов функции члены могут создавать потоки для выполнения нескольких задач. Объекты Можц создавать с многопоточной направленностью. В этом случае потоки объявляются в самом объекте. Функция-член может создать поток, который выполняет независи мую функцию, а она (в свою очередь) вызывает одну из функций-членов объекта. Для создания и управления потоками многопоточного приложения можно ис- пользовать библиотеку Pthread. Библиотека Pthread опирается на стандартизирован- ный программный интерфейс, предназначенный для создания и поддержки потоков Этот интерфейс определен комитетом стандартов IEEE в стандарте POSIX 1003.1с Сторонние производители при создании своих продуктов должны придерживаться этого стандарта POSIX.
СИНХРОНИЗАЦИЯ ПАРАЛЛЕЛЬНО ВЫПОЛНЯЕМЫХ ЗАДАЧ В этой главе... 5.1. Координирование порядка выполнения потоков 5.2. Синхронизация доступа к данным 5.3. Что такое семафоры 5.4. Объектно-ориентированный подход к синхронизации 5.5. Резюме
Отношение этих механизмов ко времени требует тщательного изучения. <...> Нас почти не интересовала производительность вычислительной машины для одного входного сигнала. Чтобы адекватно функционировать, она должна показывать удовлетворительную производительность для целого класса входных сигналов, а это будет означать удовлетворительную производительность для класса входных сигналов, получение которого ожидается статистически... — Ноберт Винер (Norbert Wiener), Кибернетика Во всех компьютерных системах ресурсы ограничены. Ведь любой объем памя- ти конечен, как и количество устройств ввода-вывода, портов, аппаратных прерываний и процессоров. Если в среде ограниченных аппаратных ресурсов приложение состоит из нескольких процессов и потоков, то эти составляющие долж- ны конкурировать за память, периферийные устройства и процессорное время. Когда и как долго процесс или поток будет использовать системные ресурсы, определяет операционная система. При использовании приоритетного планирования операнд онная система может прерывать выполняющийся процесс или поток, чтобы удовле творить все остальные процессы и потоки, соревнующиеся за системные ресурсЬ1, Процессам и потокам приходится также соперничать за программные ресурсы и р сурсы данных. Примерами программных ресурсов служат разделяемые библиотеки (которые предоставляют в общее пользование набор процедур или функций для пр цессов и потоков), а также приложения, программы и утилиты. При совместном Пс пользовании программных ресурсов в памяти содержится только одна копия пр граммного кода. Под ресурсами данных подразумеваются объекты, системные даннЫе
5.1. Координация порядка выполнения потоков 185 мер переменные среды), файлы, глобально определенные переменные и струк- 1ДаНныХ- Что касается ресурсов данных, то процессы и потоки могут иметь собст- ТУРЬ1 копии данных. В других случаях желательно и, возможно, даже необходимо, бы данные были разделяемыми. Одни процессы и потоки, работая вместе, исполь- 4 т ограниченные системные ресурсы в определенном порядке, в то время как другие ЗУЙствуют независимо и асинхронно, соревнуясь за использование разделяемых ресур- Дов Для управления процессами и потоками, конкурирующими за использование дан- bix программист может задействовать ряд специальных методов и механизмов. Синхронизация также необходима для координации порядка выполнения парал- лельных задач. Примером может служить модель “изготовитель-потребитель”, кото- ая рассмотрена в главе 4. “Изготовитель” обязательно начинает выполняться до “потребителя”, но не обязательно завершается до него. Подобные задачи нуждаются в синхронизации. Синхронизация данных (синхронизация доступа к данным) и задач (синхронизация последовательностей инструкций) — два типа синхронизации, которые необходимо обеспечить при выполнении нескольких параллельных задач. 5.1. Координация порядка выполнения потоков Предположим, у нас есть три параллельно выполняющихся потока — А, В и С. Все они участвуют в обработке списка. Список необходимо отсортировать, выпол- нить в нем операции поиска и вывода результатов. Каждому потоку назначается от- дельная задача. Так, поток А должен отобразить результаты поиска, В — отсортиро- вать список, а С — провести поиск. Сначала список необходимо отсортировать, за- тем выполнить несколько параллельных операций поиска, а уж потом отобразить результаты. Если задачи, выполняемые потоками, не синхронизировать надлежа- щим образом, то поток А может попытаться отобразить еще не сгенерированные результаты, что нарушит постусловие, или выходное условие (postcondition), про- цесса. Предусловием, или входным условием (precondition), здесь является необхо- димость получения отсортированного списка до выполнения в нем поиска. Поиск в неотсортированном списке может дать неверные результаты. Поэтому для этих трех потоков необходимо обеспечить синхронизацию задач, которая приводит в исполнение постусловия и предусловия логических процессов. UML-диаграмма видов деятельности для этого процесса представлена на рис. 5.1. Сначала поток В должен отсортировать список, затем эстафета управления переда- ется многоканальному” поиску, порождаемому потоком С. И только после завершения Поисковых работ по всем направлениям поток А отображает результаты поиска. 5-1-1. Взаимоотношения между синхронизируемыми задачами Cv ДвугМя1^еСТВ'еТ четыРе основных типа отношений синхронизации между любыми Ло п°токами в одном процессе или между любыми двумя процессами в одном при- (фф) и*11 стаРт’стаРт (СС), финиш-старт (ФС), старт-финиш (СФ) и финиш-финиш Нацию П°МО1ЦЬЮ этих основных типов отношений можно охарактеризовать коорди- Ка>кдо За^ач межДУ потоками и процессами. UML-диаграмма видов деятельности для г° типа отношений синхронизации показана на рис. 5.2.
186 Глава 5. Синхронизация параллельно выполняемых задач Рис. 5.1. Диаграмма видов деятельности для задач сортировки списка, поиска и отображения результатов поиска 5.1.2. Отношения типа старт-старт (СС) В отношениях синхронизации типа старт-старт одна задача не может начаться Д^ тех пор, пока не начнется другая. Одна задача может начаться раньше другой, &° позже. Предположим, у нас есть программа, которая реализует инкарна (воплощение). Инкарнация “материализуется” в виде говорящей головы, создай разумеется, компьютерной программой. Инкарнация обеспечивает своего Р “одушевление” программ. Программа, которая реализует “одушевление”, имеет сколько потоков. Здесь нас в первую очередь интересует поток А, который “отвечает
5.1. Координация порядка выполнения потоков 187 цию результата, и поток В, который управляет звуком, или голосом, говорящей аНИМ Мы хотим создать иллюзию синхронизации звука и движений рта. В идеале они головы- исходить абсолютно одновременно. При наличии нескольких процессоров &оЛ потока могут начинаться одновременно. Эти потоки связаны отношением типа °^а П°гарт. В соответствии с условиями временной синхронизации допускается, чтобы Сп>к А начинался немного раньше потока В (именно немного — иначе будет нарушена П° мя одновременности), но потокВ не может начаться раньше потока А. Голос иллюзия /а г ен ожидать анимацию, а не наоборот. Совершенно нежелательно услышать голос до того как зашевелятся губы (если это не синхронное озвучивание). СТАРТ-СТАРТ ФИНИШ-ФИНИШ ФИНИШ-СТАРТ СТАРТ-ФИНИШ ₽Ис- 5.2. Возможные отношения синхронизации между задачами А и В ^•1 -3. Отношения типа финиш-старт (ФС) до Тех ТН°Шениях синхронизации типа финиш-старт задача А не может завершиться ^новних** Пока не начнется задача В. Этот тип отношений типичен для родительско- т°РЫх о пР°Нсссов. Родительский процесс не может завершить выполнение неко- Не будет РаЦИй до тех пор, пока не будет сгенерирован сыновний процесс или пока Ние. СынП°ЛУ ~еНа °братная связь от сыновнего процесса, который начал выполне- *°ДИМую °ВНи” процесс, “просигналивший” родителю или предоставивший ему необ- может ИНф°РМаВДю* продолжает выполняться, а родительский процесс после это- Заверщ иться.
188 Глава 5. Синхронизация параллельно выполняемых задач 5.1.4. Отношения типа старт-финиш (СФ) Отношения типа старт-финиш можно считать обратным вариантом отношеци^ типа финиш-старт. В отношениях синхронизации типа старт-финиш одна задача це может начаться до тех пор, пока не завершится другая. Задача А не может начать вы полнение до тех пор, пока задача В не финиширует или не завершит выполнение оц ределенной операции. Если процесс А считывает данные из канала, связанного с процессом В, то процесс В должен записать данные в канал, прежде чем процесс Д начнет считывать из него данные. Процесс В должен завершить по крайней мере одну операцию, записав в канал один элемент, прежде чем начнет действовать процесс Д Потоки, действующие по принципу “производитель-потребитель”, — это еще один пример взаимоотношений типа финиш-старт. Потоки, обслуживающие сортировку и поиск элементов в списке (см. рис. 5.1), также демонстрируют этот тип отношений Прежде чем начнут действовать потоки, реализующие поиск, должен завершить свою работу поток сортировки. Во всех этих случаях один поток или процесс должен за- вершить свою операцию, прежде чем другой попытается выполнить свою задачу. Ес- ли работа потоков не будет скоординирована, глобальная цель потока, процесса или приложения достигнута не будет или же будут получены ошибочные результаты. Отношения типа старт-финиш обычно предполагают существование информаци- онной зависимости между задачами. При информационной зависимости для кор- ректной работы потоков или процессов необходимо обеспечить межпоточное или межпроцессное взаимодействие. Например, поток поиска данных в списке сгенери- рует некорректные результаты, если не будет выполнена сортировка списка. И поток- потребитель” не получит файлы для обработки, если поток-“производитель” не под- готовит их для потребителя. 5.1.5. Отношения типа финиш-финиш (ФФ) В отношениях синхронизации типа финиш-финиш одна задача не может завер- шиться до тех пор, пока не завершится другая, т.е. задача А не может финишировать до задачи В. Этот тип отношений можно применить к описанию отношений между родительскими и сыновними процессами, которые рассматривались в главе 3. Роди- тельский процесс должен ожидать до тех пор, пока не завершатся все сыновние про- цессы, и только потом сможет завершиться сам. Если описанная последовательность нарушится, и родительский процесс финиширует раньше своих потомков, то эти за вершенные сыновние процессы перейдут в зомбированное состояние. Родительские процессы не должны завершаться (выходить из системы в данном случае) до тех пор, пока не выполнятся до конца их сыновние процессы. Для родительских процессов это достигается за счет вызова функции wait () для каждого из своих сыновних пр цессов либо ожидания деблокировки (освобождения) мьютекса или условной пеР^ менной, что может быть осуществлено сыновними потоками. Еще одним пример^^ отношений типа финиш-финиш может служить модель “управляющий-рабочии • дача управляющего потока — делегировать работу рабочим потокам. Для управляю го крайне нежелательно завершить работу раньше рабочих. В этом случае не были обработаны новые запросы к системе, не имели работы существующие потоки и создавались новые. Если управляющий поток является основным потоком проН и он завершается, то процесс должен завершиться вместе со всеми рабочими по
5.2. Синхронизация доступа к данным 189 модели равноправных потоков поток А динамически создает объект, пере- мц. поТОКуВ, и потокА затем завершается, то вместе с ним разрушается и соз- даваем мм объект. Если это произойдет до того, как поток В получит возможность данный • этот объект> возникнет ошибка сегментации или нарушится доступ исПО м Чтобы предотвратить возникновение этого типа ошибок, завершение по- КДаН синхронизируется с помощью функции pthread.join(). Обращение к этой токов заставляет вызывающий поток ожидать до тех пор, пока не финиширует за- Ф^ный поток. Таким образом и создается синхронизация типа финиш-финиш. 5.2. Синхронизация доступа к данным Существует разница между данными, разделяемыми между процессами, и данны- ми разделяемыми между потоками. Потоки совместно используют одно и то же ад- ресное пространство, в то время как процессы имеют отдельные адресные простран- ства. Если существуют два процесса А и В, то данные, объявленные в процессе А, не- доступны процессу В, и наоборот. Следовательно, один из методов, используемых процессами для разделения данных, состоит в создании блока памяти, отображаемого затем на адресное пространство процессов, которые должны разделять память. Дру- гой подход предполагает создание блока памяти, существующего вне адресного про- странства обоих процессов. К типам механизмов межпроцессного взаимодействия (МПВ) относятся каналы, файлы и передача сообщений. Именно блок памяти, разделяемый между потоками внутри одного и того же ад- ресного пространства, и блок памяти, разделяемый между процессами вне обоих ад- ресных пространств, требует синхронизации данных. Память, разделяемая между по- токами и процессами, показана на рис. 5.3. Синхронизация данных необходима для управления состоянием “гонок”, а также для того, чтобы позволить параллельным потокам или процессам безопасно получить доступ к блоку памяти. Синхронизация данных позволяет управлять считыванием и модификацией данных в блоке памяти. В многопоточной среде параллельный дос- туп к общей памяти, глобальным переменным и файлам обязательно должен быть синхронизирован. Что касается программного кода задачи, то синхронизация данных необходима в тех его блоках, где делается попытка получить доступ к блоку памяти, глобальным переменным или файлам, разделяемым с другими параллельно выпол- няемыми процессами или потоками. Такие блоки кода называются критическими раз- ыами. В качестве критического раздела может выступать любой блок кода, который ‘ яет позицию файлового указателя, записывает данные в файл, закрывает файл Делени^а^ ИЛИ Устанавливает глобальные переменные либо структуры данных. Вы- Изэтап ТаКИХ задач* которые выполняют чтение или запись данных, является одним управления параллельным доступом к совместно используемой памяти. 5-2-1. Модель PRAM -льньГ (Parallel Random-Access Machine — машина с параллельным произ- в., Р Д°ступом) — это упрощенная модель с А процессорами, обозначаемыми Рр Меццо n п КотоРые разделяют одну глобальную память. Все процессоры одновре- лучают доступ для чтения и записи к совместно используемой глобальной
190 Глава 5. Синхронизация параллельно выполняемых задач памяти. Каждый из этих теоретических процессоров может получить доступ к п ляемой глобальной памяти в течение одного непрерываемого интервала времени дель PRAM включает алгоритмы параллельного, а также исключающего чтения и ° писи. Алгоритмы параллельного чтения позволяют нескольким процессорам одц* временно использовать одну и ту же область памяти без какого бы то ни бы^ искажения данных. Алгоритмы параллельной записи позволяют нескольким проце сорам записывать данные в разделяемую область памяти. Алгоритмы исключающег чтения используются для получения гарантии того, что никакие два процессора нико гда не будут считывать информацию из одной и той же области памяти одновремен но. Алгоритмы исключающей записи гарантируют, что никакие два процессора нико гда не будут записывать данные в одну и ту же область памяти одновременно. Модель PRAM можно использовать для определения характера параллельного доступа к об- щей памяти со стороны нескольких задач. АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А РАЗДЕЛ ДАННЫХ РАЗДЕЛ КОДА РАЗДЕЛ СТЕКОВ СВОБОДНАЯ ПАМЯТЬ Локальные переменные Локальные переменные Глобальные । структуры данных Глобальные I переменные Глобальные I Константы переменные j Статические , переменные МЕХАНИЗМЫ МПВ Сообщения Разделяемая память Файлы | FIFO-очереди/каналы | АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В РАЗДЕЛ СТЕКОВ I Локальные | переменные । СВОБОДНАЯ ПАМЯТЬ Г , । Локальные ! переменные I г » Глобальные I переменные j РАЗДЕЛ ДАННЫХ | Глобальные i структуры данных ; Глобальные i переменные Константы | Статические переменные РАЗДЕЛ КОДА : I Стек потока А Стек потока В Деревья • Графы * Очереди ♦ Стеки Код потока А | ммв»»' Рис. 5.3. Память, разделяемая между потоками и процессами
5.2. Синхронизация доступа к данным 191 и исключающая запись (concurrent read and exclusive и параллельная запись (exclusive read and concurrent и параллельная запись (concurrent read and concurrent 1 1 Параллельный и исключающий доступ к памяти 5*2* ’ параллельного и исключающего чтения и записи можно скомбиниро- АлГОР лучить следующие типы объединенных алгоритмов, которые можно реали- для организации доступа к данным: сключающее чтение и исключающая запись (exclusive read and exclusive write — EREW); • параллельное чтение „rite-CREW); • исключающее чтение write -ERCW); • параллельное чтение write — CRCW). Эти алгоритмы можно рассматривать как стратегии доступа, реализуемые задача- ми, которые совместно используют данные (рис. 5.4). Алгоритм EREW подразумевает последовательный доступ к разделяемой памяти, т.е. к общей памяти в любой момент времени может получить доступ только одна задача. Примером стратегии доступа EREW может служить вариант реализации модели потоков “производитель- потребитель”, рассмотренный в главе 4. Доступ к очереди, содержащей имена фай- лов, может быть ограничен исключающей записью “изготовителя” и исключающим чтением “потребителя”. В любой момент времени доступ к очереди может быть раз- решен только для одной задачи. Стратегия CREW позволяет множественный доступ для чтения общей памяти и исключающий доступ для записи в нее данных. Это озна- чает отсутствие ограничений на количество задач, которые могут одновременно чи- тать разделяемую память, но записывать в нее данные может только одна задача. При этом параллельное чтение может происходить одновременно с записью данных в об- щую память. При использовании этой стратегии доступа все читающие задачи могут прочитать различные значения, поскольку во время чтения значения из общей памя- ти записывающая задача может его модифицировать. Стратегия доступа ERCW — это прямая противоположность стратегии CREW. При использовании стратегии ERCW разрешены параллельные записи в общую память, но лишь одна задача может читать в любой момент времени. Стратегия доступа CRCW позволяет множеству задач вы- °лнять параллельное чтение и запись. ни *ЦЛЯ ЭТИХ четыРех типов алгоритмов требуются различные уровни и типы синхро- ации. Их диапазон довольно широк: от стратегии доступа, реализация которой требу МИНИМальной синхронизации, до стратегии доступа, реализация которой ПоДДе максимальной синхронизации. Наша задача— реализовать эти стратегии, мы. Е^£ууВаЯ Целостность данных и удовлетворительную производительность систе- сутн толыГ СаМаЯ пРостая Для реализации стратегия, поскольку она предполагает, по Показаться ° П°СЛедовательнУю обработку. На первый взгляд самой простой может Кажется CRCW, но она таит в себе массу трудностей. А ведь это только Не Идет о"0 е<-ЛИ К памяти можно получить доступ без ограничений, то в ней и речь Пая Для п КаК°Й то ни было стратегии. Все как раз наоборот: CRCW — самая труд- нзации стратегия, которая требует максимальной синхронизации.
192 Глава 5. Синхронизация параллельно выполняемых задач EREW (исключающее чтение и исключающая запись) ERCW (исключающее чтение и параллельная запись) CREW (параллельное чтение и исключающая запись) CRCW (параллельное чтение и параллельная запись) Рис. 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) xf (Очередь доступа к объекту Mutex не пуста){ } Передача объекта Мьютекс следующей задаче; else{ Mutex++; от конкретной системы. Эти операции неделимы, т.е. их не- Реализация зависит от конкретной системы. Эти операции неделимы, т.е. их не- м°жно прервать. Если операцию Р () попытаются выполнить сразу несколько за- бы Т° ЛИШЬ одна из них получит разрешение продолжить работу. Если объект Mutex 0^ У Же декрементирован, то задача будет заблокирована и займет место в очереди. ерация V () вызывается задачей, которая имеет доступ к объекту Mutex. Если полу- Ия доступа к объекту Мьютекс ожидают другие задачи, он “передается” следующей из очереди. Если очередь задач пуста, объект Mutex инкрементируется. Перации с семафором могут иметь другие имена: Перация Р (): Операция V (): unlock ()
194 Глава 5. Синхронизация параллельно выполняемых задач Значение семафора зависит от его типа. Двоичный семафор будет иметь значение о 1. Вычислительный семафор (определяющий лимиты ресурсов для процессов, получающ^ доступ к ним) может иметь некоторое неотрицательное целочисленное значение Х Стандарт POSIX определяет несколько типов семафоров. Эти семафоры исподь зуются процессами или потоками. Типы семафоров (а также их некоторые основные операции) перечислены в табл. 5.1. Таблица 5.1. Типы семафоров, определенные стандартом POSIX Тип семафора Пользователь Описание Мьютексный семафор Процессы или потоки Механизм, используемый для реализации вза- имного исключения в критическом разделе кода Блокировка для обеспе- чения чтения и записи Процессы или потоки Механизм, используемый для реализации страте- гии доступа для чтения и записи среди потоков Условная переменная Процессы или потоки Механизм, используемый для уведомления по- токов о том, что произошло событие. Событийный мьютекс остается заблокирован- ным потоком до тех пор, пока не будет получен соответствующий сигнал Несколько условных переменных Процессы или потоки Аналогичен событийному мьютексу, но включа- ет несколько событий или условий Операционные системы, которые не противоречат спецификации Single UNIX Specification или стандарту POSIX Standard, поддерживают реализацию семафоров, которые являются частью библиотеки libpthread (соответствующие функции объ- явлены в заголовке pthread. h). 5.3.2. Мьютексные семафоры Стандарт POSIX определяет мьютексный семафор, используемый потоками и про- цессами, как объект типа pthread_mutex_t. Этот мьютекс обеспечивает базовые опе- рации, необходимые для функционирования практического механизма синхронизации. • инициализация; • запрос на монопольное использование; • отказ от монопольного использования; • тестирование монопольного использования; • разрушение. Функции класса pthread_mutex_t, которые используются для выполнения этих базовых операций, перечислены в табл. 5.2. Во время инигшализации выделяется мять, необходимая для функционирования мьютексного семафора, и объекту сеМа фора присваивается некоторое начальное значение. Для двоичного семафора чальным может быть значение 0 или 1. Начальным значением вычислительного мафора может быть неотрицательное число, которое представляет количес
5.3. Что такое семафоры 195 IX ресурсных единиц. Семафорное значение можно использовать для представ- доступ льНОГО количества запросов, которое способна обработать программа в одном денИЯ р отличие от обычных переменных, в инициализации которых сомневаться не сеансе. ^ся факт инициализации мьютекса с помощью вызова соответствующей функции ИР*** иоовать невозможно. Чтобы убедиться в том, что мьютекс проинициализирован, дИмо после вызова операции инициализации принять некоторые меры предос- Не жности (например, проверить значение, возвращаемое соответствующей мьютекс- ^•чьункцией, или значение переменной ептю). Системе не удастся создать мьютекс, если ока^тся, что занята память, предусмотренная ранее для мьютексов, или превышено устимое количество семафоров, или семафор с данным именем уже существует, или же имеет место какая-то другая проблема, связанная с выделением памяти. Таблица 5.2. Функции класса pthread—mutex—t Мъютексные операции Прототипы функций (макросы) ftinclude <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 инициа- ПотокаСТСЯ КаК ДеблокиР°ванный и закрытый. Закрытый мьютекс разделяется между Иесколь” °ДНОГО пР°Цесса. Разделяемый мьютекс совместно используется потоками МьютекКИХ ПРоцессов* При использовании атрибутов, действующих по умолчанию, объект ° М°Жет быть инициализирован статически для статических мьютексных Pth °В С ПОМО1ЦЬЮ следующего макроса: ea<<_mutext Mutex = PTHREAD-MUTEX_INITIALIZER;
196 Глава 5. Синхронизация параллельно выполняемых задач Этот метод менее затратный, но в нем не предусмотрено проверки ошибок. Мьютекс может иметь или не иметь владельца. Операция запроса на монопольное пользование предоставляет право владения мьютексом вызывающему потоку или пв цессу. После того как мьютекс обрел владельца, поток (или процесс) получает моц0 польный доступ к запрашиваемому ресурсу. При попытке завладеть “уже занятым” мьютексом (путем вызова этой операции), совершенной любыми другими потоками или процессами, они будут заблокированы до тех пор, пока мьютекс не станет доступ ным. При освобождении мьютекса следующий (по очереди) поток или процесс (который был заблокирован) деблокируется и получает право собственности на этот мьютекс. И освободить его может только поток, получивший данный мьютекс во вла- дение с помощью функции 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); int pthread—mutexattr_destroy (pthread—mutexattr_t * attr); Инициализирует мьютексный атрибутный объ- ект, заданный параметром attr, значениями, действующими по умолчанию для всех атрибу- тов, определяемых реализацией Разрушает мьютексный атрибутный объект, за- данный параметром attr, в результате чего он становится неинициализированным. Его мож- но инициализировать повторно с помощью функции pthread-mutexattr init ()_______
5.3. Что такое семафоры 197 Продолжение табл. 5.3 — Прототипы функций Описание iJ1hread_mutexattr_setprioceiling Р (pthread_mutexattr_t * attr, int prioceiling); Устанавливает и возвращает атрибут предель- ного приоритета мьютекса, заданного парамет- ром attr. Параметр prioceiling содержит значение предельного приоритета мьютекса. othread_mutexattr_getprioceiling (const pthread_mutexattr_t * restrict attr, Атрибут prioceiling определяет минималь- ный уровень приоритета, при котором еще вы- полняется критический раздел, защищаемый int *restrict prioceiling); мьютексом. Значения, которые попадают в этот диапазон приоритетов, определяются страте- гией планирования SCHED_FIFO int pthread_mutexattr_setprotocol (pthread_mutexattr_t * attr, protocol int protocol); int p thr ead_mutexattr__get protocol (const pthread_mutexattr_t * restrict attr, int *restrict protocol); Устанавливает и возвращает атрибут протокола мьютекса, заданного параметром attr. Параметр protocol может содержать следующие значения: PTHREAD_PRIO_NONE (на приоритет и стратегию планирования пото- ка владение мьютексом не оказывает влияния); PTHREAD_PRIO_INHERIT (при таком протоколе поток, блокирующий другие потоки с более высокими приоритета- ми, благодаря владению таким мьютексом бу- дет выполняться с самым высоким приорите- том из приоритетов потоков, ожидающих ос- вобождения любого из мьютексов, которыми владеет данный поток); PTHREAD_PRIO_PROTECT (при таком протоколе потоки, владеющие та- ким мьютексом, будут выполняться при наи- высших предельных значениях приоритетов всех мьютексов, которыми владеют эти пото- ки, независимо оттого, заблокированы ли дру- int Pthr ead_mu t exa 11 r_s e tpshared <pthread_jnutexattr_t * attr, int pshared); int Pthread-jniitexattr—getpshared °nst pthread_mutexattr_t * . restrict attr, restrict pshared); гие потоки по каким-то из этих мьютексов) Устанавливает и возвращает атрибут process- shared мьютексного атрибутного объекта, за- данного параметром attr. Параметр pshared может содержать следующие значения: PTHREAD_PROCESS_SHARED (разрешает разделять мьютекс с любыми по- токами, которые имеют доступ к выделенной для этого мьютекса памяти, даже если эти по- токи принадлежат различным процессам); PTHREAD_PROCESS_PRIVATE (мьютекс разделяется между потоками одного _ и того же процесса)
198 Глава 5. Синхронизация параллельно выполняемых задач Прототипы функций int р thread—mutеха 11r_s et type (pthread-mutexattr_t * attr, int type); int pthread—mutexattr_gettype (const pthread_mutexattr—t * restrict attr, int *restrict type); __________________________Рычание табл, 5 3 Описание Устанавливает и возвращает атрибут мьютекса^ type мьютексного атрибутного объекта, за- данного параметром attr. Атрибут мьютекса type позволяет определить, будет ли мьютекс распознавать взаимоблокировку, проверять ошибки и т.д. Параметр 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) ; // критический раздел кода. thread-mutex-unlockf&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; рthread_mu tex_t Mutexl; Pthread_mutexattr_t MutexAttr; int main (void) //. . . Pthread_mutexattr_init (&MutexAttr) ; ₽thread__xnutexattr_setpshared (&MutexAttr , PTHREAD_PROCESS_SHARED); if (iead-mutex-init (&Mutexl, &MutexAttr) ; ((Rt = fork()) == 0){ // Сыновний процесс. Pthread_jnutex—lock (&Mutexl) ; // Критический раздел. j pt“hread_mutex_unlock (&Mutexl) ; // Родительский процесс. Pthread_mutex_lock(&Mutexl) ; J. Литический раздел. hread_mutex_unlосk (&Mutexl) ; } //. . . retUrn(0);
200 Глава 5. Синхронизация параллельно выполняемых задач АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА А АДРЕСНОЕ ПРОСТРАНСТВО ПРОЦЕССА В Рис. 5.5. Закрытые и разделяемые мьютексы Важно отметить, что в листинге 5.2 при вызове следующей функции мьютекс инй циализируется как разделяемый: pthread_mutexattr_setpshared(&MutexAttr, PTHREAD_PROCESS_SHARED);
5.3. Что такое семафоры 201 вка этого атрибута равным значению PTHREAD_PROCESS_SHARED позволяет Уста rv 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 () — блокировку записи. Запросив блок Р°ВКУ чтения> поток получит ее в том случае, если нет потоков, удерживающих потокР°ВК> Записи‘ Ес™ же таковые имеются, вызывающий поток блокируется. Если Уде 3аПР°СИТ блокировку записи, он ее получит в том случае, если нет потоков, юЩих блокировку чтения или блокировку записи. Если же таковые имеются, БлоюЩИЙП°ТОКбЛОКИруеТСЯ- PthreadP°BKa чтения-записи реализуется с помощью объектов типа РУет ат t. Этот же тип имеет атрибутный объект, который инкапсули- числены в "ТЫ °^ъекта блокировки. Функции установки и чтения атрибутов пере- Объект * Потоками ™Па 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 Прототипы функций inS,read_rwlockattr_setpshared P!pthread_rwlockattr_t * attr, int pshared); 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; ₽thread_rwlock_t RWLock; void *producerl (void *X) Pthread_rwlock_wrlock(&RWLock) ; ''Критический раздел. r_bread-rwlock_unl°ck(&RWLock); } return(0); (°id ’producer2 (void *X) //h»^ad~rwlock-wrlock(&RWLock) ; РГЬге^ИЧеСКИЙ Ра3«ел‘ } aa—rwlock_unlock(&RWLock);
204 Глава 5. Синхронизация параллельно выполняемых задач void *consumerl(void *Х) { 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,consumed,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 534. Условные переменные Условная переменная представляет собой семафор, используемый для сигнализации тии которое произошло. Сигнала о том, что произошло некоторое событие, ° ожидать один или несколько процессов (или потоков) от других процессов или М ов Следует понимать различие между условными переменными и рассмотрен- П выше мьютексными семафорами. Назначение мьютексного семафора и блоки- HbIMIR чтения-записи — синхронизировать доступ к данным, в то время как условные Р еменные обычно используются для синхронизации последовательности опера- ций По этому поводу в своей книге UNIX Network Programming прекрасно высказался Ричард Стивенс (W. Richard Stevens): “Мьютексы нужно использовать для блокирова- ния, а не для ожидания”. В листинге 4.6 поток-“потребитель” содержал цикл: 15 while(TextFiles.empty() ) 16 {} Поток-“потребитель” выполнял итерации цикла до тех пор, пока в очереди TextFiles были элементы. Этот цикл можно заменить условной переменной. Поток- изготовитель” сигналом уведомляет потребителя о том, что в очередь помещены элементы. Поток-“потребитель” может ожидать до тех пор, пока не получит сигнал, азатем перейдет к обработке очереди. Условная переменная имеет тип pthread—с ond_t. Ниже перечислены типы опе- раций, которые может она выполнять: • инициализация; • разрушение; • ожидание; • ожидание с ограничением по времени; • адресная сигнализация; • всеобщая сигнализация; Операции инициализации и разрушения выполняются условными переменными подобно аналогичным операциям других мьютексов. Функции класса Pthread—с ond_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—сond_t cond = PTHREAD_COND_INITIALIZER;
206 Глава 5. Синхронизация параллельно выполняемых задач Окончание табл. 5 •> Операции Прототипы функций (макросы) Ожидание int pthread—сond_wait( ' ' pthread—сond—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. Если мьютекс не заблокирован, задача будет ожидать сигнала неограни- ченно долго. При ожидании с ограничением по времени задача будет ожидать сигнала в течение заданного интервала времени. Если это время истечет до получения зада- чей сигнала, функция возвратит код ошибки. Затем задача вновь затребует мьютекс. Выполняя адресную сигнализацию, задача уведомляет другой поток или процесс о том, что произошло некоторое событие. Если задача ожидает сигнала для заданной условной переменной, эта задача будет разблокирована и получит мьютекс. Если сраз) несколько задач ожидают сигнала для заданной условной переменной, то разблоки рована будет только одна из них. Остальные задачи будут ожидать в очереди, и их разблокирование будет происходить в соответствии с используемой стратегией ила нирования. При выполнении операции всеобщей сигнализации уведомление получат все задачи, ожидающие сигнала для заданной условной переменной. При разблокир0' вании нескольких задач они будут состязаться за право владения мьютексом в соот ветствии с используемой стратегией планирования. В отличие от ния, задача, выполняющая операцию сигнализации, не предъявляет мьютексом, хотя это и следовало бы сделать. Условная переменная также имеет атрибутный объект, функции которого пер числены в табл. 5.8. операции прав на владений
5.3. Что такое семафоры 207 TfiXnia 5.8. Функции доступа к атрибутному объекту для условной *а ’ переменной типа pthread_cond_t Прототипы функций Описание <pthread.h> •nt pthread—condatt r_in it ( 1 pthread_condattr_t * attr); Инициализирует атрибутный объект условной переменной, заданный параметром attr, зна- чениями, действующими по умолчанию для всех атрибутов, определенных реализацией int pthread—с ondattr.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); int pthread—condattr_getclock ( const pthread—condattr_t * restrict attr, clockid-t * clock—id); restrict Разрушает атрибутный объект условной пере- менной, заданный параметром attr. Этот объ- ект можно инициализировать повторно, вы- звав функцию pthread—condattr_init () Устанавливает или возвращает атрибут process - shared атрибутного объекта условной переменной, заданного параметром attr. Па- раметр pshared может содержать следующие значения: PTHREAD.PROCESS—SHARED (разрешает блокировку чтения-записи, разде- ляемую любыми потоками, которые имеют дос- туп к памяти, выделенной для этой условной пе- ременной, даже если потоки принадлежат раз- личным процессам); PTHREAD—PROCESS—PRIVATE (условная переменная разделяется между пото- ками одного процесса) Устанавливает или возвращает атрибут clock атрибутного объекта условной переменной, за- данного параметром attr. Атрибут clock представляет собой идентификатор часов, ис- пользуемых для измерения лимита времени в функции pthread—cond_timedwait (). По умолчанию для атрибута clock использует- ся идентификатор системных часов ^•3.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 ер реализации ФФ-отношений синхронизации показан в листинге 5.5. R 5. ФФ-отношения синхронизации между // Лис'ГИНх - • дВуМя потоками fthreadUtt>ThreadA, ThreadB; Prhread'mutex_t Mutex, EventMutex; pthreadZcond_t Event; void *workerl (void *X) { fOr(int Count = l;Count < 10;Count++){ pthread—mu tex_lock (&Mu tex) ; Number++; pthread—mu tex_unlock (&Mut ex) ; cout « "worker1: число равно " « Number « endl; pthread—mutex—lock (&EventMutex) ; cout « "Функция worker1 в состоянии ожидания. " « endl; pthread—cond_wait (&Event , &EventMutex) ; pthread-inutex—unlock (&EventMutex) ; return(0); void *worker2 (void *X) { for(int Count = l;Count < 100;Count++){ pthread—mutex—lock (&Mutex) ; Number = Number * 2 ; pthread—mutex-unlock (&Mutex) ; cout « "worker2: число равно " << Number << endl; Pthread_cond_signal (&Event) ; cout « "Функция worker2 послала сигнал " « endl; return(0); int main(int argc, char *argv[]) Pthread__mutex_init (&Mutex,NULL) ; Pthread_mutex_init (&EventMutex, NULL) ; Ptnread_cond_init (&Event z NULL) ; пьъ ad-Create<&ThreadA,NULL,worker1,NULL); Pthread_create (&ThreadB, NULL, worker2. NULL) ; return (0); Шится1ИСТИНГе 5'5 поток ThreadA не может завершиться до тех пор, пока не завер- Пот°ТОК TllreadB‘ Поток ThreadA должен выполнить цикл 10 раз, a ThreadB — °Жидат °К Т^геас^ завершит выполнение своих итераций раньше ThreadB, но будет СС Де° ТеХ П°Р’ пока поток ThreadB не просигналит о своем завершении. Эти м ф’отношения синхронизации невозможно реализовать подобным образом. ДЬ1 используются для синхронизации порядка выполнения процессов.
210 Глава 5. Синхронизация параллельно выполняемых задач 5.4. Объектно-ориентированный подход к синхронизации Одно из преимуществ объектно-ориентированного программирования состои в защите, которую обеспечивает инкапсуляция компонентов данных объекта. Инка^ суляция может обеспечить для пользователя объектов “стратегии доступа к объектам и принципы их применения” [24]. В примерах, представленных в этой главе, за при меняемые стратегии доступа вся ответственность возлагалась на пользователя дан ных. С помощью объектов и инкапсуляции ответственность можно переложить с пользователя данных на сами данные. При таком подходе создаются данные, кото- рые, в отличие от функций, являются безопасными для потоков. Для реализации такого подхода данные многопоточного приложения (по возмож- ности) необходимо инкапсулировать с помощью С++-конструкций class или struct Затем инкапсулируйте такие механизмы синхронизации, как семафоры, блокировки для обеспечения чтения-записи и мьютексы событий. Если данные или механизмы синхронизации представляют собой объекты, создайте для них интерфейсный класс. Наконец, объедините объект данных с объектами синхронизации посредством насле- дования или композиции, чтобы создать объекты данных, которые будут безопасны для потоков. Этот подход подробно рассматривается в главе 11. 5.5. Резюме Для координации порядка выполнения процессов и потоков (синхронизация задач), а также доступа к разделяемым данным (синхронизация данных) можно использовать различные механизмы синхронизации. Существует четыре основных вида отношений синхронизации задач. Отношение вида “старт-старт” (СС) означает, что задача А не может начаться до тех пор, пока не начнется задача В. Отношение вида “финиш- старт” (ФС) означает, что задача А не может завершиться до тех пор, пока не начнет- ся задача В. Отношение вида “старт-финиш” (СФ) означает, что задача А не может на- чаться до тех пор, пока не завершится задача В. Отношение вида “финиш-финиш (ФФ) означает, что задача А не может завершиться до тех пор, пока не завершится за- дача В. Для реализации этих отношений синхронизации задач можно использовать условную переменную THnapthread_cond_t, которая определена стандартом POSIX- Для описания синхронизации данных используются некоторые типы алгоритмов модели PRAM. Стратегию доступа EREW (исключающее чтение и исключающая за пись) можно реализовать с помощью мьютексного семафора. Мьютексный семаф°Р защищает критический раздел, обеспечивая последовательный вход в него. Эта стра тегия разрешает либо доступ для чтения, либо доступ для записи. Стандарт POSIX оП ределяет мьютексный семафор типа pthread_mutex_t, который можно использо вать для реализации стратегии доступа EREW. Чтобы реализовать стратегию Д°сТ^па CREW (параллельное чтение и исключающая запись), можно использовать блокир0^ ки чтения-записи. Стратегия доступа CREW описывает возможность удовлетвори множества запросов на чтение, но при монопольной записи данных. Стандарт РО ' определяет объект блокировки для обеспечения чтения-записи т pthread_rwlock_t, а объектно-ориентированный подход к синхронизации позволяет встроить механизм синхронизации в объект данных.
ОБЪЕДИНЕНИЕ ВОЗМОЖНОСТЕЙ ПАРАЛЛЕЛЬНОГО ПРОГРАММИРОВАНИЯ И С++-СРЕДСТВ НА ОСНОВЕPVM В этой главе... 6.1. Классические модели параллелизма, поддерживаемые системой PVM 6.2. Библиотека PVM для языка C++ 6.3. Базовые механизмы PVM 6.4. Доступ к стандартному входному потоку (stdin) и стандартному выходному потоку (stdout) со стороны PVM-задач 6.5. Резюме
Мы разделили нашу проблему на две части: сгенерированную программу и процесс обучения. Эти две части остаются тесно связанными. Нельзя ожидать, что сгенерированная машина окажется удачной с первой же попытки. Необходимо поэкспериментировать с обучением одной такой машины и посмотреть, как пойдет этот процесс обучения... — Алан Тьюринг (Alan Turing), Может ли машина думать? Система программного обеспечения PVM (Parallel Virtual Machine — параллель- ная виртуальная машина) предоставляет разработчику ПО средства для напи- сания и выполнения программ, использующих параллелизм. Система PVM по- зволяет разработчику представить коллекцию сетевых компьютеров в виде единой логической машины с возможностями параллелизма. Компьютеры этой коллекции могут иметь одинаковые или различные архитектуры. В PVM-систему связываются даже компьютеры, которые попадают в категорию МРР (Massively Parallel Processor - процессор с массовым параллелизмом). Несмотря на то что PVM-программы могут разрабатываться для одного компьютера, реальные преимущества этой системы пр° являются при связывании двух и более компьютеров. Система PVM в качестве средства связи между параллельно выполняющимися за дачами поддерживает модель передачи сообщений. Приложение взаимодействует с PVM посредством библиотеки, которая состоит из API-интерфейсов, предназначе1< ных для управления процессами, отправки и получения сообщений, сигнализаН процессов и т.д. С++-программа взаимодействует с PVM-библиотекой точно так как с любыми другими библиотеками функций. С++-программе для получения досту к функциям PVM-библиотеки не нужно создавать специальную форму или архитектур, ’
даосические модели параллелизма, поддерживаемые системой PVM 213 пемя как программам, написанным на других языках, необходимо вызывать оп- в Т° ленные функции для инициализации среды. Это означает, что С++-програм-мист РеДС сочетать PVM-возможности с другими стилями С++-программирования * имер, объектно-ориентированным, параметризованным, агентно-ориенти- ванным и структурированным программированием). Благодаря использованию та- Р° библиотек, как 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) неприменимы MlSD^eMe PVM* Модель SISD описывает однопроцессорную машину, а для модели вообще трудно найти практическое применение. Две остальные модели, кото- Действ°ЖН° использовать с системой PVM, определяют, как С++-программа взаимо- ный ° КомпьютеРами- Разработчик ПО представляет один логический виртуаль- дач К°МпьютеР как среду для выполнения нескольких различных параллельных за- Bbinojn^^351 КОТОРЫХ п°лучает доступ к собственным данным, либо одной задачи, нымобЯЮЩеЙСЯ В виде на^ора параллельных клонов, получающих доступ к различ- али п ЯМ данных* Таким образом, с PVM-задачами мы будет связывать только мо- F слагающие наличие множества потоков команд и одной программы.
214 Глава 6. Объединение возможностей параллельного программирования Рис. 6.1. Четыре классические модели параллелизма 6.2. Библиотека PVM для языка C++ К функциональным возможностям PVM из С++-программы можно получить доступ с помощью коллекции библиотечных процедур, предоставляемых средой PVM. Эти функции и процедуры PVM обычно делят на семь категорий. • Управление процессами. • Упаковка сообщений и их отправка. • Распаковка сообщений и их прием. • Обмен задач сигналами. • Управление буфером сообщений. • Функции обработки информации и служебные процедуры. • Групповые операции. Эти библиотечные функции легко интегрировать в С++-среду. Префикс pvm_ в и>*е ни каждой функции позволяет не забыть о ее принадлежности соответствующему ПР° странству имен. Для использования PVM-функций необходимо включить в программ? заголовочный файл pvm3 . h и скомпоновать ее с библиотекой libpvm. В программах и 6.2 демонстрируется, как работает простая PVM-программа. Инструкции по компи-71* ции и выполнению программы 6.1 приведены в разделе “Профиль программы 6.1 •
6.2. Библиотека PVM для языка C++ 215 I/ программа 6.1 «include "pvm3.h" «include <iostream> Jinclude <string.h> int main(int argc,char *argv[]) < int RetCode, Messageld; int PTid, Tid; char Message[100] ; float Result[1] ; PTid = pvm__mytid() ; RetCode = pvm_spawn ("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 |ймяпрограммы Fprogram6-1.сс {Описание «Использует функцию pvm_send () для пересылки числа в другую PVM-задачу, кото- рая выполняется параллельно с данной (программа 6.2), и функцию pvm_recv() |ДЛя получения числа от этой задачи. >Требуемая библиотека p-ibpvm3 .* Требуемые заголовки Pvm3.h> <iostream> <string.h> |С^ТРУКЦИИ По компиляции и компоновке программ $PVM ^ogram6-l -I $PVM_ROOT/include -L $PVM_ROOT/lib/ Г . «-ARCH -1 Pvm3 > ДЛЯ ТбСТИПппдиыо .1, gcc 2.95.2.
216 Глава 6. Объединение возможностей параллельного программирования Инструкции по выполнению . /ргодгашб-1 Примечания Необходимо запустить на выполнение программу pvmd. В программе 6.1 использовано восемь самых распространенных PVM-функций- pvm_mytid(), pvm_spawn (), pvm_initsend(), pvm_pkstr(), pvm__send() pvmrecvO, pvm_upkfloat() и 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 порождает PVM-задачу для выполнения программы 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; pvm_recv(Ptid,Messageld); pvm_upkstr(Message); Num = atof(Message); Result = Num / 7.0001; pvm_initsend(PvmDataDefault); pvm_pkfloat(^Result,1,1); Pvm_send(Ptid,Messageld); pvm_exit(); return(0); Профиль программы 6.2 ||мя программы pr6gram6-2. сс Описание ^Эта программа принимает число от родительского процесса и делит его на 7. Затем «она отправляет результат своему родительскому процессу ^Требуемая библиотека iibpvm3 Требуемые заголовки jcpvm3.h> <stdlib.h> Инструкции по компиляции и компоновке программы ?t+<:-O’program6-2 -I $PVM_ROOT/include program6-2. cc -L • .’^PVM_ROOT/lib/PVM_ARCH -lpvm3 :Среда для тестирования ME Unux 7.1 gnu C++ 2.95.2, Solaris 8 Workshop 6, PVM 3.4.3. Инструкции по выполнению ^та программа порождается программой 6.1. Примечания [Й^бходимо запустить на выполнение программу pvmd. 6-2-1. Компиляция и компоновка С++/Р\/М-программ бы скРСИЯ 3 4*Х Р^М’сРеДЬ1 представлена в виде единой библиотеки libpvm3 . а. Что- Файл pvin31JIHPOBaTb Р^М'пРогРаммУ’ необходимо включить в ее код заголовочный $ с++ * k и ск°мпоновать ее вместе с библиотекой libpvm3 . а: TOypvln-pro^rain “I $PVM_ROOT/include °aram.cc -I$PVM_ROOT/lib -lpvm3
218 Глава 6. Объединение возможностей параллельного программирования Переменная среды $PVM_ROOT указывает на каталог, в котором инсталлирована бцб лиотека PVM. При выполнении этой команды создается двоичный фа^ mypvm_program. Для выполнения программ 6.1 и 6.2 сначала необходимо инсталлировать PVM-cne Выполнить 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 & Здесь host file — это файл конфигурации, содержащий специальные параметры для передачи программе 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 ДлЯ Windows. На рис. 6.2 показано диалоговое окно сеанса работы с XPVM-интерфейсО^о Библиотека PVM не требует, чтобы С++-программа придерживалась какой конкретной структуры. Первая PVM-функция, вызываемая программой, “помеШ ее в PVM-среду. Для каждой программы, которая является частью PVM-среды, слеД} всегда вызывать функцию pvm_exit (). Если этого не сделать, система завис
6.2. Библиотека PVM для языка C++ 219 тиКа показывает, что функции pvm_mytid() и pvm_parent () необходимо вы- J вать в начале обработки задачи. Наиболее популярные категории функций PVM ^числены в табл. 6.1. Рис. 6.2. Диалоговое окно графического интерфейса XPVM ^Таблица 6.1. Семь категорий функций библиотеки PVM Категории PVM-функций Управление процессами Упаковка сообщений и их отправка Распаковка сообщений и их прием °6мен задач сигналами Деление буфером, сообщений обработки информа- ^UU^бные процедуры операции Описание Используются для управления PVM-процессами Применяются для упаковки сообщений в пересылочном буфере и отправки их от одного PVM-процесса другому Используются для получения сообщений и распаковки данных из активного буфера Применяются для сигнализации и уведомления PVM- процессов о возникновении события Используются для инициализации, очистки и размещения буферов, предназначенных для приема и отправки сооб- щений, которыми обмениваются PVM-процессы Применяются для получения информации о PVM- процессах и выполнения других важных задач Используются для объединения процессов в группы и выполнения других групповых операций
220 Глава 6. Объединение возможностей параллельного программирования 6.2.3. Требования к PVM-программам Если PVM-среда реализуется в виде сети компьютеров, то, прежде чем ваша C++ программа начнет взаимодействовать с ней, необходимо обработать следующие элементы Параграф 1 Следует установить переменные среды PVM_ROOT и PVM_ARCH. Переменная среды PVM—ROOT должна указывать на каталог, в котором инсталлирована PVM-библиотека Использование оболочки Войте (BASH)_Использование С-оболочки__ $ PVM_ROOT=/usr/lib/pvm3 setenv PVM_ROOT /usr/lib/pvm3 $ export PVM—ROOT Переменная среды PVM_ARCH идентифицирует архитектуру компьютера. Каждый компьютер, включенный в среду PVM, должен быть идентифицирован архитектурой. Например, Ultrasparcs-компьютеры имеют обозначение SUN4SOL2, a Linux- компьютеры — обозначение LINUX. В табл. 6.2 перечислены самые распространен- ные архитектуры для PVM-среды. Эта таблица содержит имя и тип компьютера, соответствующий этому имени. Ус- тановите свою переменную среды PVM_ARCH равной одному из имен, приведенных в табл. 6.2. Например: Использование оболочки Войте (BASH)_Использование С-оболочки___________ $PVM_ARCH=LINUX setenv PVM_ARCH LINUX $export PVM—ARCH Таблица 6.2. Самые распространенные архитектуры для PVM-среды PVM_ARCH Компьютер PVM_ARCH Компьютер AFX8 Alliance LINUX 80386/486 PC (UNIX) ALPHA DEC Alpha MASPAR Maspar BAL Sequent Balance MIPS MIPS 4680 BFLY BBN Butterfly TC2000 NEXT NeXT BSD386 80386/486 PC (UNIX) PGON Intel Paragon СМ2 “Мыслящая машина” СМ2 PMAX DECstation 3100,5100 CM5 “Мыслящая машина” СМ5 RS6K IBM/RS6000 CNVX Convex С-серии RT IBMRT CNVXN Convex С-серии SGI Silicon Graphics IRIS CRAY С-90, YMP,T3D (доступный порт) SGI5 Silicon Graphics IRIS CRAY2 Сгау-2 SGIMP SGI Multiprocessor CRAYSIMP Cray S-MP SUN3 Sun 3 —
6.2. Библиотека PVM для языка C++ 221 Окончание табл. 6.2 Компьютер PVM_ARCH Компьютер "dgav Data General Aviion SUN4 Sun 4, SPARCstation Е88К Encore 88000 SUN2SOL2 Sun 4, SPARCstation нрзоо HP-9000 Model 300 SUNMP SPARC Multiprocessor НРРА HP-9000 PA-RISC SYMM Sequent Symmetry I860 Intel iPSC/860 TITN Stardent Titan IPSC2 Intel iPSC/2 386 Host U370 IBM 370 KSRI Kendall Square KSR-1 UVAX DEC LicroVAX Параграф 2 Выполняемые файлы любых программ, участвующих в среде PVM, должны быть размещены на всех компьютерах, включенных в среду PVM, или доступны всем ком- пьютерам, включенным в среду PVM. При этом каждая программа должна быть ском- пилирована для работы с учетом конкретной архитектуры. Это означает, что, если вереду PVM включены процессоры UltraSparcs, PowerPCs и Intel, то мы должны иметь версию программы, скомпилированную для каждой архитектуры. Эту версию про- граммы следует разместить в известном для PVM месте. Таким местом часто служит каталог $H0ME/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 host file или vnt-hosts необходимо добавить соответствующую запись, например: io=flashgordon Параграф 4 Со ° пьютеп*аИТе На каждом компьютере файл .rhosts, в котором перечислите все ком- Можность ПОДЛежа1цие использованию. Эти компьютеры имеют потенциальную воз- ,xPvm host^n Включения в сРеДУ PVM. В зависимости от содержимого файла Ны в PVM S ИЛИ Файда Pvni-hosts, эти компьютеры автоматически будут добавле- Файлах СРедУ ПРИ запУске программы pvmd. Компьютеры, перечисленные в этих е м°гут динамически включаться в PVM-среду во время работы.
222 Глава 6. Объединение возможностей параллельного программирования Параграф 5 Создайте файл $НОМЕ/.xpvm_hosts и/или файл $HOME/pvm__hosts, в кото перечислите все подлежащие использованию компьютеры с приставкой Нали^ приставки означает неавтоматическое включение компьютера. Без этой Приста С ки компьютер будет включен в PVM-среду автоматически. Файл pvm_hosts создае В пользователем и может иметь произвольное имя. Но в среде XPVM необходимо ис пользовать только имя .xpvm_hosts. Пример такого файла показан на рис. 6.3. ДНа логичный формат следует использовать для pvm_hosts- или . xpvm_ho st 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. Примерpvrnjiosts-файла 6.2.4. Объединение динамической С++-библиотеки с библиотекой PVM Поскольку доступ к PVM-средствам обеспечивается через коллекцию библи°те^ ных функций, С++-программа использует PVM как любую другую библиотеку. СлеД)
6.2. Библиотека PVM для языка C++ 223 в виду» что каждая PVM-программа представляет собой автономную С++- ИмеТЬ mv с собственной функцией main(). Это означает, что все PVM-программы ПрогР сОбственное адресное пространство. При порождении каждой PVM-задачи ется ее собственный процесс с новым адресным пространством и, соответст- С° о идентификационный номер процесса. PVM-процессы видимы для утилиты ps. и отря на то что несколько PVM-задач могут выполняться вместе для решения не- орой проблемы, они будут иметь собственные копии динамической С++- библиотеки. Каждая программа имеет собственный поток iostr earn, библиотеку шаблонов, алгоритмы и пр. В область видимости глобальных С++-переменных адрес- ное пространство не попадает. Это означает, что глобальные переменные одной pVM-задачи невидимы для других PVM-задач. Для взаимодействия отдельных задач используется механизм передачи сообщений. Этим они отличаются от многопоточ- ных программ, в которых потоки разделяют одно адресное пространство и могут взаимодействовать посредством глобальных переменных и передачи параметров. Ес- ли PVM-программы выполняются на одном компьютере с несколькими процессорами, то как дополнительные средства коммуникации программы могут совместно исполь- зовать файловую систему, каналы, FIFO-очереди и общую память. Несмотря на то что передача сообщений — основной метод взаимодействия между PVM-задачами, ничто не мешает им в качестве дополнительных средств использовать файловую систему, буфер обмена или даже аргументы командной строки. PVM-библиотека не ограничи- вает, а расширяет возможности динамической С++-библиотеки. 6.2.5. Методы использования PVM-задач Работу, которую выполняет С++-программа, можно распределить между функциями, объектами или их сочетаниями. Действия, выполняемые программой, обычно делятся на такие логические категории: операции ввода-вывода, интерфейс пользователя, обра- ботка базы данных, обработка сигналов и ошибок, числовые вычисления и т.д. Отделяя код интерфейса пользователя от кода обработки файлов, а также код процедур печати от кода числовых вычислений, мы не только распределяем работу программы между’ функциями или объектами, но и стараемся выделять категории действий в соответствии с их характером. Логические группы организуются в библиотеки, модули, объектные шаблоны, компоненты и оболочки. Такой тип организации мы поддерживаем и при внесении PVM-задач в С++-программу. Мы можем подойти к декомпозиции работ wor breakdown structure), используя метод либо восходящего, либо нисходящего боту КТИРования* любом случае параллелизм должен естественно вписываться в ра- pj которая намечена для выполнения функцией, модулем или объектом. ИскуСсСаМаЯ Удачная идея — попытаться директивно навязать параллелизм программе, кой ССТВенно насаждаемый параллелизм является причиной формирования громозд- C4oXXHTeK WbI’ котоРая’ как правило, трудна для понимания и поддержки и создает Подьзуе^р ПРИ ОПРеделении корректности программы. Поэтому, если программа ис- граммы К ^-задачи, они должны быть результатом естественного разбиения про- НацпИМегч кдую PVM-задачу следует отнести к одной из функциональных категорий. На естеств ССЛИ МЫ РазРа^атываем приложение, которое содержит обработку данных пР<>Изве еННОМ ЯЗЬ1ке (Natural Language Processing — NLP), механизм речевого вос- ^вателд X** Текста (text-to-speech engine — TTS-engine) как часть интерфейса поль- Ф°рмирование логических выводов как часть выборки данных, то парал-
224 Глава 6. Объединение возможностей параллельного программирования лелизм (естественный для NLP-компонента) должен быть представлен в виде внутри NLP-модуля или объекта, который отвечает за NLP-обработку. Аналогично^^ раллелизм внутри компонента формирования логических выводов следует пре вить в виде задач, составляющих модуль (объект или оболочку) выборки данных вечающий за выборку данных. Другими словами, мы идентифицируем PVM-зал °Т там, где они логически вписываются в работу, выполняемую программой, а не просу11 разбиваем работу программы на набор некоторых общих PVM-задач. 0 Соблюдение первичности логики и вторичности параллелизма имеет нескольк последствий для С++-программ. Это означает, что мы могли бы порождать 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 TaskId[10]; int Taskld2[5); // 1-е порождение: pvm_spawn("set_combination",NULL,0,"",10,Taskld); // 2-е порождение: pvm_spawn("set_combination",argv,0,"",5,Taskld2); //. . . } В листинге 6.1 при первом порождении создается 10 задач. Каждая задача ^УдеТ^е поднять один и тот же набор инструкций, содержащихся в прогр^-^^ set_combination. При успешном выполнении функции pvm_spawn () массив Tas будет содержать идентификаторы PVM-задач. Если программа в листинге 6.1 идентификатор Tasklds, то она может использовать функции pvm_send() для оТ^а. ки данных, подготовленных для обработки каждой программой. Это возможно бляг ря тому, что функция pvm_send () содержит идентификатор задачи-получателя-
6.2. Библиотека PVM для языка C++ 225 Р*10* 6.4. Некоторые возможные конфигурации для порождения PVM-задач
226 Глава 6. Объединение возможностей параллельного программирования При втором порождении (см. листинг 6.1) создается пять задач, но в этом сл каждой задаче с помощью параметра argv передается необходимая информа Это __ дополнительный способ передачи информации задачам при их запуске. Тем ** мым сыновние задачи получают еще одну возможность уникальным образом Идец^ фицировать себя с помощью значений, получаемых в параметре argv. В листинге 6 2 чтобы создать 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,1, "hostl",1,&Taskl); pvm_spawn("set_combination",argv,1,"host2",1,&Task2); pvm_spawn("set_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-задачах, поэтому7 им и не нужно иметь соответствующий PVM-код. Вариант 3 (см. рис. 6.4) представляет сценарий, в котором функции main () и другим функциям в программе присущ естест- венный параллелизм. В этом случае роль “других” функций играет функция funcA () • PVM-задачи, порождаемые функциями main () и f uncA (), выполняют различный код- Несмотря на то что задачи, порожденные функцией main (), выполняют идентичный код, и задачи, порожденные функцией funcA (), выполняют идентичный код, эти Два набора задач совершенно различны. Этот вариант иллюстрирует возможность С+ программы использовать коллекции задач для одновременного решения различны* проблем. Ведь не существует причины, по которой на программу бы налагалось ОГР^4) чение решать в любой момент времени только одну проблему. Вариант 4 (см. рис- представляет случай, когда параллелизм заключен внутри объекта, поэтому порождс PVM-задач реализует один из методов этого объекта. Этот вариант показывает, что г необходимости параллелизм может исходить из класса, а не из “свободной” функции- Как и в других вариантах, все PVM-задачи, порожденные в варианте 4, выпол одинаковый набор инструкций, но с различными данными. Этот SPMD-метод (S10»
6.2. Библиотека PVM для языка C++ 227 Multiple-Data — одна программа, множество потоков данных) часто исполь- progra111»^ реаЛИЗации параллельного решения проблем некоторого типа. И то, что 3УсТС* ^ЯобЛадает поддержкой объектов и средств обобщенного программирования язык шаблонов, делает его основным инструментом при решении подобных за- НаО<Объекты и шаблоны позволяют С++-программисту представлять обобщенные даЧ бкие решения для различных проблем с помощью одной-единственной про- И ГИ ой единицы. Наличие единой программной единицы прекрасно вписывается ель параллелизма SPMD. Понятие класса расширяет модель SPMD, позволяя ре- в м целЫй класс проблем. Шаблоны дают возможность решать определенный класс ШаТблем для практически любого типа данных. Поэтому, хотя все задачи в модели SPMD выполняют один и тот же код (программную единицу), он может быть предназначен для любого объекта или любого из его потомков и рассчитан на различные типы данных (азначит, и на различные объекты!). Например, в листинге 6.3 используется четыре PVM-задачи для генерирования четырех множеств, в каждом из которых имеется С(п,г) элементов: С(24,9), С(24,12), С(7,4) и С(7,3). В частности, в листинге 6.3 перечисляются возможные сочетания из 24 цветов, взятые по 9 и по 12. Здесь также перечисляются возможные сочетания из 7 чисел с плавающей точкой, взятые по 4 и по 3. Пояснения по обозначению С(п,г) приведены в разделе $ 6.1 (“Обозначение сочетаний”). / / Листинг 6.3. Создание сочетаний из заданных множеств int main(int argc,char *argv[]) { int RetCode,Taskld[4]; RetCode = pvm_spawn ("pvm_generic_combination", NULL, 0, 4,Taskld); if(RetCode == 4) { colorCombinations (TaskId[0] ,9); colorCombinations (Taskld [ 1 ] ,12) ; numericCombinations (Taskld [2 ] , 4) ; numericCombinations(Taskld[3],3); saveResult(Taskld[0]); saveResult(Taskld[1]); saveResult(Taskld[2]); saveResult(Taskld[3]); j pvxn._exit else{ cerr « "Ошибка при порождении сыновнего процесса." endl ; Pvm_exit(); } return(о); Истинге 6.3 обратите внимание на порождение четырех PVM-задач: .spawn (" глт v Pv^generic-confoination" ,NULL, О, и" ,4,Taskld) ; Каждая п pvm gener. Рожденная задача должна выполнять программу с именем Чает, что С~coinbination. Аргумент NULL в вызове функции pvm_spawn() озна- Функциц ЧеРез ПаРаметр argv [ ] не передаются никакие опции. Значение 0 в вызове Pvin^spawn () свидетельствует, что нас не беспокоит, на каком компьютере
228 Глава 6. Объединение возможностей параллельного программирования будет выполняться наша задача. Аргумент Taskld представляет массив, предцаз ченный для хранения четырех целочисленных значений, который при условии усп^ ного выполнения функции pvm_spawn () будет содержать идентификаторы каждой по рожденной PVM-задачи. В листинге 6.3 обратите также внимание на вызов функци- colorCombinations () и numericCombinations (). Они “дают работу” 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); pvm_pkbyte(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 О) генерирует сочетания цветов, используя в качестве входных данных набор строк. Но когда она орудует “руками” функции numericCombinations (), показанной в листин ге 6.5, в качестве входных данных используется набор чисел с плавающей точкой. КоД листинга 6.3 также содержит два вызова функции numericCombinations () . Первый генерирует С(7,4) сочетаний, а второй — С(7,3). // Листинг 6.5. Использование PVM-задач для генерирования // сочетаний чисел void numericCombinations(int Taskld,int Choices) { int Messageld = 2; int re- double ImportantNumbers[7] = {3.00e+8,6.67e-ll,1.99e+30,
6.2. Библиотека PVM для языка C++ 229 1.67е-27,6.023е+23,6.63e-34, 3.14159265359}; nvm initsend(PvmDataDefault); N = nkint(^Choices,1,1); ₽'*HLnd(TaskId,Messageld) ; ₽ ."pvm initsend(PvmDataDefault); N ™ okdouble(ImportantNumbers,5,1); pvmZsend(TaskId, Messageld) ; } В функции numericCombinations () из листинга 6.4 PVM-задача использует мас- чисел с плавающей точкой, а не массив байтов, представляющих строки. Поэтому А-нкция colorCombinations() отправляет свои данные PVM-задачам с помощью вызовов таких функций: pvm_send(Task!d,Messageld) ; А функция numericCombination() отправляет свои данные PVM-задачам таким образом: pvm_pkdouble (ImportantNumbers ,5,1); pvm_send(Taskld,Messageld) ; Функция colorCombinations () в листинге 6.4 создает строку названий цветов, азатем копирует ее в массив Buffer типа char. Этот массив затем упаковывается и от- правляется PVM-задаче с помощью функций pvm__pkby te () и pvm_send (). Функция numericCombinations () в листинге 6.5 создает массив типа double и отсылает его PVM-задаче с помощью функций pvm__pkdouble () и pvm_send(). Одна функ- ция отправляет символьный массив, а другая — массив типа double. В обоих случаях PVM-задачи выполняют одну и туже программу pvm_generic_combination. Именно здесь нас выручает преимущество использования С++-шаблонов. Одинаковые задачи бла- годаря этому могут работать не только с различными данными, но и с различными типами данных без изменения самого кода. Использование шаблонов в C++ позволяет сделать мо- -Дель SPMD более гибкой и эффективной. Программе pvm_geneгic_combination практически безразлично, с какими типами данных ей придется работать. Использо- вание контейнерных С++-классов позволяет генерировать любые комбинации векто- ров (vector<T>) объектов. Программа pvm_geneгic_combination “не знает”, что °на будет работать с двумя типами данных. В листинге 6.6 представлен раздел кода из программы pvm_generic_combination. II Листинг 6.6. Использование тега Messageld для распознания типов данных if &NumBytes, &MessageId, &Ptid) ; 'messageld == i){ BuftOr<String> S°urce; pvm char[NumBytes]; st^strobYte(Buf-NumBYtes,1) ; ream Buffer; S-SV" Buf « ^ds; { (Buffer.good()) Buffer » color;
230 Глава 6. Объединение возможностей параллельного программирования if(!Buffer.eof()){ Source.push_back(Color); } } generateCombinations<string>(Source,Ptid,Value); delete Buf; } if(Messageld == 2){ vector<double> Source; double *ImportantNumber; NumBytes = NumBytes I 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_geneгic_combination можно легко обобщить. В листин- ге 6.6 обратите внимание на то, что каждая инструкция if () вызывает функцию generateCombinations (), которая является шаблонной. Эта шаблонная архитекту- ра позволяет достичь такой степени универсальности, которая распространяет сце- нарии SPMD и MPMD на наши PVM-программы. Мы вернемся к обсуждению нашей программы pvm_geneгic_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 для реализации у/ дистии MPMD-модели вычисления Task2[50] int mainfint argc, char *argv[]) ( int Taskl[20] ; •„t Task2[50]; int Task3[30] ; ' awn("pvm_generic_combination", NULL,1, P™- p “hostl", 20,Taskl); nvm spawn("generate_plans",argv,050,Task2); pvm spawn(“agent_filters”,argv++,l,"host 3",30,&Task3); } При выполнении кода, представленного в листинге 6.7, создается 100 задач. Пер- вые 20 задач генерируют сочетания. Следующие 50 по мере создания сочетаний гене- рируют планы на их основе. Последние 30 задач отфильтровывают самые удачные планы из набора планов, сгенерированного предыдущими 50 задачами. Уже только это краткое описание позволяет ощутить отличие модели MPMD от модели SPMD, в которой все программы, порожденные функцией pvm_spawn (), были одинаковы. Здесь же за работу, назначаемую PVM-задачам, “отвечают” программы pvm_generic—combination, generate_plans и agent—fiIters. Все эти задачи выполняются параллельно и работают с собственными наборами данных, несмотря на то что одни наборы являются результатом преобразования других. Программа pvm_generic_combination преобразует свой входной набор данных в набор, кото- рый затем может использовать программа generate—plans. Программа generate—plans, в свою очередь, преобразует входной набор данных в набор, кото- рый может затем использовать программа agent_f ilters. Очевидно, что эти задачи должны обмениваться сообщениями. Эти сообщения представляют собой входную и управляющую информацию, которая передается между процессами. Необходимо также отметить, что в листинге 6.7 функция pvm_spawn () используется для размеще- ния 20 задач pvm—generic—combination на компьютере с именем hostl. Задача generate-plans была размещена на 50 безымянных процессорах, но каждая из этих задач получила при этом один и тот же аргумент командной строки с помощью па- раметра argv. Задачи agent_f ilters также были направлены на конкретный ком- HoftTeP ИМенем h°st 3), и каждая задача получила один и тот же аргумент команд- ние СТР°КИ ПосРедств°м параметра argv. Этот пример — лишь еще одно подтвержде- МРМп^КОСТИ И мощи библиотеки PVM. Некоторые варианты реализации модели ц с использованием среды PVM показаны на рис. 6.5. конк И Желании МЬ1 можем воспользоваться преимуществами конкретных ресурсов них безНЫХ КомпьютеРов или же “положиться на судьбу” в виде “заказа” произволь- Разли лянных компьютеров. Мы можем также назначить различные виды работ Пьюте задачам одновременно. На рис. 6.5 компьютер А представляет собой ком- РЫм ко Массовым параллелизмом (МП-компьютер), а компьютер В оснащен некото- Те, что Р\тлСТВОМ специализиРованных математических процессоров. Также отметь- Sparcs Сга\ С^еДа в данном случае состоит из таких компьютеров, как PowerPCs, стях компь S И Т одних случаях можно не беспокоиться о конкретных возможно- теров в PVM-среде, а в других требуется иной подход. Использование
232 Глава 6. Объединение возможностей параллельного программирования функции pvm_spawn () позволяет С++-программисту не указывать конкретный пьютер для решения задачи, когда это не важно. Но если вам известно, что компь^ тер оснащен специализированными средствами, то их можно эффективно исполь вать, определив соответствующий параметр при вызове функции pvm_spawn (). 3° Программа 1 Задача С ОДНА ЛОГИЧЕСКАЯ ПАРАЛЛЕЛЬНАЯ ВИРТУАЛЬНАЯ МАШИНА (КЛАСТЕР)! Программа 2 Задача В Программа 3 Задача А Задача D Рис. 6.5. Некоторые варианты модели MPMD доступны для реализации благодаря использованию среды PVM § 6.1. Обозначение сочетаний Предположим, мы хотели бы набрать команду программистов (в количестве восьми человек) из 24 кандидатов. Сколько различных команд из восьми программистов можно было бы составить из этого числа кандидатов? Один из результатов, коТ°т рый следует из основного закона комбинаторики, говорит о том, что существ) 735 471 различных команд, состоящих из восьми программистов, которые мо быть выбраны из 24 кандидатов. Обозначение С(п,г) читается как сочетание элементов по г (и означает количество комбинаций из п элементов по г). Сочет С(п,г) вычисляется по формуле:
6.3. Базовые механизмы PVM 233 п\ г\(п - г)! нас есть множество, которое представляет сочетания, например {а,Ь,с}, то счи- ЕслИ' что оно совпадает с множеством {Ь,а,с} или {с,Ь,а}. Другими словами, нас ин- тастся. ПОрЯДОК членов в этом множестве, а сами члены. Многие параллельные теРе ' ммЫ а именно программы, использующие алгоритмы поиска, эвристические ПР°о ы и средства искусственного интеллекта, обрабатывают огромные множества сочетаний и их близких родственников перестановок. 6.3. Базовые механизмы PVM Среда PVM состоит из двух компонентов: PVM-демона (pvmd) и библиотеки pvmd. Один PVM-демон pvmd выполняется на каждом компьютере в виртуальной машине. Этот демон служит в качестве маршрутизатора сообщений и контроллера. Каждый демон pvmd управляет списком PVM-задач на своем компьютере. Демон управляет процессами, выполняет минимальную аутентификацию и отвечает за отказоустойчи- вость. Обычно первый демон запускается вручную. Затем он запускает другие демоны. Только исходный демон может запускать дополнительные демоны. И только исход- ный демон может безусловно остановить другой демон. Библиотека pvmd состоит из функций, которые позволяют одной PVM-задаче взаимодействовать с другими. Эта библиотека также включает функции, которые по- зволяют PVM-задаче связываться со своим демоном pvmd. Базовая архитектура PVM- среды показана на рис. 6.6. PVM-среда состоит из нескольких PVM-задач. Каждая задача должна содержать один или несколько буферов отправки сообщений, но в каждый момент времени активным может быть только один буфер (он называется активным буфером отправки сообщений). Каждая задача имеет активный буфер приема сообщений. Обратите внимание (см. рис. 6.6) на то, что взаимодействие между PVM-задачами реально выполняется с ис- пользованием TCP-сокетов. Функции pvm_send () делают доступ к сокетам прозрачным, рограммист не получает доступа к функциям TCP-сокетов напрямую. На рис. 6.6 также показано взаимодействие PVM-задач со своими демонами pvmd с помощью ТСР- сокетов и взаимодействие между самими демонами с помощью UDP-сокетов. И снова- ло И °бРаи1ения к сокетам выполняются посредством PVM-функций. Программист не топ еН заниматься программированием сокетов на низком уровне. PVM-функции, ко- используются в этой книге, делятся на четыре следующие категории: Управление процессами; упаковка сообщений и их отправка; Распаковка сообщений и их получение; Управление буфером сообщений. °нные и0^ На сУ1цествование Других категорий PVM-функций (например, информаци- Внимание на 1СНЫе ФУ11КЦИИ 147114 функции групповой обработки), рекомендуем обратить Же Функции б^ЪКции обработки сообщений и функции управления процессами. Другие ' ДУТ рассмотрены в контексте программ, в которых они используются.
234 Глава 6. Объединение возможностей параллельного программирования Упрощенная архитектура PVM-программы PVM-ЗАДАЧА PVM-ЗАДАЧА Среда UNIX/Linux Рис. 6.6. Базовая архитектура PVM-среды 6.3.1. Функции управления процессами Библиотека PVM содержит шесть часто используемых функций. Функция pvm_spawn () используется для создания новых PVM-задач. При вызове этой функции можно указать количество создаваемых задач, место их создания и ар гументы, передаваемые каждой задаче, например: pvni—Spawn ("agent—filters", ar gv+f-, 1, "host 3 ", 30, &Task3) ;
6.3. Базовые механизмы PVM 235 СИИОПСИС 7 И" „include "pvm3.h spawn(char *task, char **argv, int flag, int P - char *location,int ntask,int *taskids); pvm kill<int taskid); pvm_exit(void); ovm addhosts(char **hosts,int nhosts,int *status); pvm^delhosts(char **hosts,int nhosts,int *status); pvmZhalt(void) ; int int int int int Параметр task содержит имя программы, которую должна выполнить функция spawn(). Поскольку программа, которая запускается посредством функции pvrrTspawn (), является автономной, ей могут потребоваться аргументы командной строки. Поэтому для их передачи используется параметр 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 содержит количество Ное ТеРов’ подлежащих добавлению в среду7 PVM, а параметр status — значение, рав- Ес1иЗНаЧеПИЮ паРаметРа nhosts при успешном выполнении функции pvm_addhosts (). функПРИ~ее ВЬ13ове не удалось добавить ни одного компьютера, значение, возвращаемое УсПещ11еИ будет меньше числа 1. Если выполнение этой функции было лишь частично ЛенньГМ’ ЗНачение’ возвращаемое функцией, будет равно количеству реально добав- среды рЛ°МпьютеРов- Функция pvm_delhosts () позволяет динамически извлечь из спцсок один или несколько заданных компьютеров. Параметр hosts содержит их ’а ПаРаметр nhosts — количество выводимых компьютеров, например: -delhosts ("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 и т.д. Список pvm—pk-функций представлен в табл. 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) ; 1 XDR (eXtemal Data Representation) - стандарт для аппаратно-независимых структур данных, Р работанный фирмой Sun Microsystems.
6.3. Базовые механизмы PVM 237 Окончание табл. 6.3 Значения типа float. ‘nt pvm_Pkfl°at(float *fp, int count, int std); Значения типа int. int pvm_pkint(int *np, int Значения типа long. int pvm_pklong(long *np, int count, int std); Значения типа short: count, int std) ; Строки: int pvm pkstr(char *cp) ; Все функции упаковки, перечисленные в табл. 6.3, используются для сохранения массива данных в буфере отправки. Обратите внимание на то, что каждая PVM-задача (см. рис. 6.6) должна иметь по крайней мере один буфер отправки и один буфер приема. Каждая функция упаковки принимает указатель на массив соответствующего типа данных. Все функции упаковки, за исключением функции pvm_pkstr (), прини- мают общее количество элементов, подлежащих сохранению в массиве (а не количе- ство байтов!). Для функции pvm_pkstr() предполагается, что символьный массив, с которым она работает, завершается значением NULL. Каждая функция упаковки, за исключением функции pvm_pkstr (), в качестве последнего параметра принимает значение, которое представляет способ обхода элементов исходного массива при их упаковке в буфер отправки. Этот параметр часто называют шагом по индексу (stride). Например, если этот шаг равен четырем, то в буфер упаковки будет помещен каждый четвертый элемент исходного массива. Важно отметить, что до отправки каждого со- общения необходимо использовать функцию pvm_initsend (), которая очищает буфер и готовит его к пересылке следующего сообщения. Функция pvm_initsend () готовит буфер к пересылке сообщения в одном из трех форматов: XDR, Raw или In Place. Формат XDR (External Data .Representation) — это стандарт, используемый для опи- сания и шифрования данных. Следует иметь в виду, что компьютеры, включенные среду PVM, могут быть совершенно разными, т.е. среда PVM, например, может со- стоять из Sun-, Macintosh-, Crays- и AMD-компьютеров. Эти компьютеры могут отли- В ся размерами машинных слов и по-разному сохранять различные типы данных. ДаНе1уп°РЬ1Х слУчаях компьютеры могут различаться и битовой организацией. Стан- а £ К позволяет компьютерам обмениваться данными вне зависимости от типа их комп еКТУРы< Формат Raw используется для отправки данных в собственном формате стся фоеРа’отпРавителя- При этом никакое специальное кодирование не применя- правки °РМаТ 1п Р1асе в действительности не требует упаковки данных в буфере от- случае * И ^Р^и^У отправляются лишь указатели на данные и размер данных. В этом кодип 3аДача’ПолУчатель напрямую копирует данные. В библиотеке PVM эти три типа ия данных представляются соответствующими тремя константами: XDR ₽W)ataRaw к Ьез специального кодирования асе В буфер отправки копируются лишь указатели и размер данных
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 psendCint 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_buf inf о (N, &NumBytes, &MessageId, &Ptid) ; //. . . switch(Messageld) { case 1 : // Некоторые действия, break; case 2 : // Другие действия, break //. . . } В данном случае функция pvm_buf inf о () используется для получения информа ции о последнем сообщении, принятом в буфер приема N. Мы можем получить коли чество байтов, идентификатор сообщения (messageid) и узнать, кто его отправил Зная значение messageid, мы можем выполнить соответствующие логические дейст- вия. Функция pvm_send () посылает заданной задаче команду псевдоблокирования, после приема которой задача блокируется до тех пор, пока отправитель не убеди в том, что сообщение было послано правильно. Задача-отправитель не ожидает р ального получения сообщения. Функция pvm_psend () отправляет сообше
6.3. Базовые механизмы PVM 239 лственно указанной задаче. Обратите внимание на то, что функция непоср > имеет параметр buffer, используемый в качестве буфера для хранения ₽Л^ылаемого сообщения. Функция pvm_mcast () используется для отправки сообще- П°С нескольким задачам одновременно. Аргументы, передаваемые функции нИЯ mcaSt (), включают массив идентификаторов задач-получателей сообщения pVTn"^i J), количество задач — участников “широковещания” (ntask) и идентифика- сообщения (messageid) для идентификации отправляемого сообщения. На Т°Р показано, что у каждой PVM-задачи есть собственный буфер отправки, кото- рый существует в течение промежутка времени, длительности которого было бы дос- таточно, чтобы сообщение гарантированно дошло до адресата. За исключением управляющих сообщений, значение сообщений, которыми обмени- ваются любые две PVM-задачи, заранее определено логикой конкретного приложения, т е назначение каждого сообщения должно быть заранее известно для задачи- отправителя и задачи-получателя. Эти сообщения передаются асинхронно, могут иметь любой тип данных и произвольную длину. Тем самым для приложения обеспечивается максимальная гибкость. Аналогами отправляемых PVM-сообщений являются прини- маемые PVM-сообщения. Так, за прием сообщений “отвечают” пять основных функций. Синопсис # inc lude ” pvm3 . h" int pvm_recv(int taskid, int messageid) ; int pvm_nrecv(int taskid, int messageid) ; int pvm_precv(int taskid, int messageid, char *buffer, int size, int type, int sender, int messagetag, int messagelength); int pvm_trecv(int taskid,int messageid, struct timeval *timeout); int pvm probe (int taskid , int messageid); Функция pvm_recv () используется одними PVM-задачами для получения сообще- нии от других. Эта функция создает новый активный буфер, предназначенный для хранения полученного сообщения. Параметр taskid определяет идентификатор за- дачи-отправителя. Параметр messageid идентифицирует сообщение, которое по- слано отправителем. Следует иметь в виду, что задача может отправить несколько сообщений, имеющих различные или одинаковые идентификаторы (messageid). и taskid = -1, то функция pvm_recv () примет сообщение от любой задачи. Ес- HM:eSSS9eid = Т° ФУНКЦИЯ пРимет любое сообщение. При успешном выполне- нии функция pvm_recv() возвращает идентификатор нового активного буфера, Противном случае — отрицательное значение. После вызова функции pvm_recv () лух^н 'Дет сблокирована и станет ожидать до тех пор, пока сообщение не будет по- одн^0 П°СЛе нолучения сообщение считывается из активного буфера с помощью °и из функций распаковки, например: Value [1°1 ; ^uSr°002'2); cout vaiat(4°0002' Value'1);
240 Глава 6. Объединение возможностей параллельного программирования Здесь функция pvm_recv() обеспечивает ожидание сообщения от задачи, идентигЬ катор которой равен 400002. Идентификатор сообщения (messageid), получецц И от задачи с номером 400002, должен быть равен значению 2. Затем использу °Г° функция распаковки для считывания массива чисел с плавающей точкой типа f 1О Тогда как функция 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_SHORT PVM_FLOAT PVM_LONG PVM-CPLX PVMJJINT Функция pvm_trecv() позволяет программисту организовать процедуру по лучения сообщений с ограничением по времени. Эта функция заставляет вызы вающую задачу перейти в заблокированное состояние и ожидать прихода соо щения, но лишь в течение промежутка времени, заданного параметром timeout- Этот параметр представляет собой структуру типа timeval, определенную в за головке time.h, например: #include "pvm3.h" PVM_BYTE PVM_INT PVM_DOUBLE PVM-USHORT PVM_DCPLX PVM-ULONG struct timeval TimeOut; TimeOut.tv_sec = 1000; int Taskid; int Messageld;
6.3. Базовые механизмы PVM 241 , тя = pvm_parent(); Task eld = 2; ^esS^eCv(Taskld, Messageld, &TimeOut) ; pVflL-tr //••* сь переменная TimeOut содержит член tv_sec, установленный равным 1000 с. timeval можно использовать для установки временных значений в секун- микросекундах. Структура timeval имеет следующий вид: struct timeval{ long tv_sec; // секунды long tv_usec; // микросекунды }; Этот пример означает, что функция pvm_trecv () заблокирует вызывающую задачу максимум на 1000 с. Если сообщение будет получено до истечения заданных 1000 с, функция сразу завершится. Функцию pvm_trecv () можно использовать для предот- вращения бесконечных задержек и взаимоблокировок. При успешном выполнении функция pvm_trecv() возвращает номер нового активного буфера, в противном слу- чае (при возникновении ошибки) — отрицательное значение. Если taskid = -1, функция примет сообщение от любого отправителя. Если messageid = -1, функция примет любое сообщение. Функция pvm_probe () определяет, поступило ли сообщение, заданное парамет- ром messageid, от отправителя, заданного параметром taskid. Если функция pvm_probe () “видит” указанное сообщение, она возвращает номер нового активного буфера. Если заданное сообщение не прибыло, функция возвращает число 0. При возникновении сбоя функция возвращает отрицательное значение. Синопсис # inc lude " pvm3 . h" int pvm_getsbuf (void) ; int pvm_getrbuf (void) ; int pvm__setsbuf (int bufferid); int pvm_setrbuf (int bufferid); int pvm_mkbuf (int Code) ; Jgt pvm_freebuf (int buf f erid) ; В библиотеке PVM предусмотрено шесть полезных функций управления буфе- рами, которые можно использовать для установки, идентификации и динамическо- ДляСОЗДаНИЯ буферОВ отпРавки и приема. Функция pvm_getsbuf () используется н Получения номера активного буфера отправки. Если текущего буфера отправки существует, функция возвращает число 0. Функция pvm_getrbuf () использует- иМеть П°Л^чения идентификационного номера активного буфера приема. Следует фер ВиДУ» что при каждом получении сообщения создается новый активный бу- Функ Тек^’1Ци^ буфер очищается. Если текущего буфера приема не существует, . Возвращает число 0. Функция pvm_setsbuf () устанавливает параметр -лькеоГ1а Равным номеру активного буфера отправки. Обычно PVM-задача имеет КИх ^°ДИН буФеР отправки. Но иногда возникает необходимость в нескольких та- <Рерах. Хотя в любой момент времени активным может быть только один
242 Глава 6. Объединение возможностей параллельного программирования буфер отправки, PVM-задача может создавать дополнительные буфера отпп с помощью функции pvm_mkbuf (). Функцию pvm_setsbuf () можно использо И для установки в качестве активного буфера одного из буферов отправки, кото^^ были созданы во время работы приложения. Эта функция возвращает идентифик С тор предыдущего активного буфера отправки. Функция pvm_setrbuf () устанавл^ вает активный буфер приема равным значению bufferid. Помните, что PVM функции распаковки работают с активным буфером приема. Если существует Не сколько буферов, функция pvm_setrbuf () позволит применить текущий буфер Для использования функциями распаковки. При успешном выполнении функци pvm_setrbuf () возвращает идентификатор предыдущего активного буфера. Если идентификатора буфера, переданного функции pmv_setrbuf (), не существует или он ока