Обложка
Титульный лист
Выходные данные
Оглавление
Предисловие
Глава 1. Для нетерпеливого читателя
1.2. Ускорение работы за счет параллелизма
1.3. Параллельная программа, использующая процессы
1.4. Параллельная программа, использующая задачи
1.5. Параллельная программа, использующая MPI
Глава 2. Пути повышения производительности процессоров
2.2. Основные черты RISC-архитектуры
2.3. Конвейеризация
2.4. Кэш-память
2.5. Многопроцессорные архитектуры
2.5.2. Комбинированные архитектуры
2.5.3. Обанкротившиеся архитектуры
2.6. Поддержка многозадачности и многопроцессорности
2.7. Использование параллелизма процессора для повышения эффективности программ
Глава 3. Пути повышения производительности оперативной памяти
Глава 4. Организация данных во внешней памяти
Глава 5. Основные положения
5.2. Виды ресурсов
5.3. Типы взаимодействия процессов
5.4. Состояния процесса
Глава 6. Стандарты на операционные системы UNIX
6.2. Стандарт UNIX System V Release 4
6.3. Стандарт POSIX 1003
6.4. Стандарт UNIX X/Open
Глава 7. Управление процессами
7.2. Функции execl, execv
7.3. Функция waitpid
7.4. Функция kill
7.5. Функция signal
Глава 8. Синхронизация и взаимодействие процессов
8.1.2. Функция shmat
8.1.3. Функция shmctl
8.2. Семафоры
8.2.2. Функция semop
8.2.3. Функция semctl
8.2.4. Пример использования семафоров и разделяемой памяти
8.3. События
8.4.1. Функция msgget
8.4.2. Функция msgsnd
8.4.3. Функция msgrcv
8.4.4. Функция msgctl
8.4.5. Пример использования очередей
8.4.6. Функция pipe
8.5. Пример многопроцессной программы, вычисляющей произведение матрицы на вектор
9.1. Функция pthread_create
9.2. Функция pthread_join
9.3. Функция sched_yield
Глава 10. Синхронизация и взаимодействие задач
10.1.2. Функция pthread_mutex_lock
10.1.3. Функция pthread_ mutex _ try lock
10.1.4. Функция pthread_mutex_unlock
10.1.5. Функция pthread_mutex_destroy
10.1.6. Пример использования mutex
10.2. Пример multithread-программы, вычисляющей определенный интеграл
10.3. Объекты синхронизации типа condvar
10.3.2. Функция pthread_cond_signal
10.3.3. Функция pthread_cond_broadcast
10.3.4. Функция pthread_cond_wait
10.3.5. Функция pthread_cond_destroy
10.3.6. Пример использования condvar
10.4. Пример multithread-программы, вычисляющей произведение матрицы на вектор
10.5. Влияние дисциплины доступа к памяти на эффективность параллельной программы
10.6. Пример multithread-программы, решающей задачу Дирихле для уравнения Пуассона
11.1. Общее устройство MPI-программы
11.2. Сообщения
11.3. Коммуникаторы
11.4. Попарный обмен сообщениями
11.5. Операции ввода-вывода в MPI-программах
11.6. Пример простейшей MPI-программы
11.7. Дополнительные функции для попарного обмена сообщениями
11.8. Коллективный обмен сообщениями
11.9. Пример MPI-программы, вычисляющей определенный интеграл
11.10. Работа с временем
11.11. Пример MPI-программы, вычисляющей произведение матрицы на вектор
11.12. Дополнительные функции коллективного обмена сообщениями для работы с массивами
11.13. Пересылка структур данных
11.13.2. Пересылка распределенных однородных данных
11.14. Ограничение коллективного обмена на подмножество процессов
11.15. Пример MPI-программы, решающей задачу Дирихле для уравнения Пуассона
Источники дополнительной информации
Программа курса
Список задач
Указатель русских терминов
Указатель английских терминов
Список иллюстраций
Список таблиц
Список примеров
Текст
                    К. Ю. Богачев
ОСНОВЫ
параллельного
программирования
3-е издание (электронное)
Москва
БИНОМ. Лаборатория знаний
2015


УДК 004.65 ББК 32.073 Б73 Богачев К. Ю. Б73 Основы параллельного программирования [Электронный pecypc]: учебное пособие / K. Ю. Богачев. 3-е изд. (эл.). Электрон. текстовые дан. (1 файл рсп': 345 с.). М.: БИНОМ. Лаборатория знаний, 2015. (Математика). Систем. требо- вания: Adobe Reader XI; экран 10". ISBN 978-5-9963-2995-3 Данная книга представляет собой введение в методы программи- рования для параллельных ЭВМ. Основной ее целью является научить читателя самостоятельно разрабатывать максимально эффективные программы для таких компьютеров. Вопросы распараллеливания конкретных алгоритмов рассмотре- ны на многочисленных примерах программ на языке С. В основу книги положен курс лекций для студентов механико-математическо- го факультета МГУ им. М. В. Ломоносова. Для студентов, аспирантов, научных работников, программистов и всех, кто хочет научиться разрабатывать программы для парал- лельных ЭВМ. УДК 004.65 ББК 32.073 Деривативное электронное издание на основе печатного аналога: Основы параллельного программирования: учебное посо- бие,1 К. Ю. Богачев. 2-е изд. М.: БИНОМ. Лаборатория знаний, 2013. 342 с.: ил. (Математика). ISBN 978-5-9963-1616-8. В соответствии со ст. 1299 и 1301 ГК РФ при устранении ограничений, установленных техническими средствами защиты авторских прав, правообладатель вправе требовать от нарушителя возмещения убытков или выплаты компенсации ISBN 978-5-9963-2995-3 © БИНОМ. Лаборатория знаний, 2003 
Оглавление Предисловие 7 Порядок чтения 9 Глава 1. Для нетерпеливого читателя . 10 1.1. Последовательная программа 10 1.2. Ускорение работы за счет параллелизма ............. 12 1.3. Параллельная программа, использующая процессы . 13 1.4. Параллельная программа, использующая задачи .... 18 1.5. Параллельная программа, использующая MPI ...... 21 Глава 2. Пути повышения производительности процессоров ... 24 2.1. CISC- u RISC-процессоры . 24 2.2. Основные черты RISC-архитектуры ................. 25 2.3. Конвейеризация 26 2.4. Кэш-память .......................................... 34 2.5. Многопроцессорные архитектуры 39 2.5.1. Основные архитектуры 39 2.5.2. Комбинированные архитектуры ............. 40 2.5.3. Обанкротившиеся архитектуры .............. 43 2.6. Поддержка многозадачности и многопроцессорности 44 2.7. Использование параллелизма процессора для повыше- ния эффективности программ 45 Глава 3. Пути повышения производительности оперативной па- мяти 61 Глава 4. Организация данных во внешней памяти ............. 64 
4 Оглавление Глава 5. Основные положения 66 5.1. Основные определения 66 5.2. Виды ресурсов ...................................... 72 5.3. Типы взаимодействия процессов 73 5.4. Состояния процесса 77 Глава 6. Стандарты на операционные системы UNIX .......... 79 6.1. Стандарт BSD 4.3 79 6.2. Стандарт UNIX System V Release 4 .................. 79 6.3. Стандарт POSIX 1003 80 6.4. Стандарт UNIX Х/Open 80 Глава 7. Управление процессами 81 7.1. Функция fork . 81 7.2. Функции ехес1, execv 84 7.3. Функция waitpid 84 7.4. Функция kill 87 7.5. Функция signal 88 Глава 8. Синхронизация и взаимодействие процессов .......... 96 8.1. Разделяемая память 97 8.1.1. Функция shmget 98 8.1.2. Функция shmat 99 8.1.3. Функция shmctl 99 8.2. Семафоры 100 8.2.1. Функция semget ............................. 103 8.2.2. Функция semop 103 8.2.3. Функция semctl 104 8.2.4. Пример использования семафоров и разделяе- мой памяти 104 8.3. События 120 8.4. Очереди сообщений (почтовые ящики) .............. 122 8.4.1. Функция msgget ............................. 124 8.4.2. Функция msgsnd 124 8.4.3. Функция msgrcv 125 8.4.4. Функция msgctl 126 8.4.5. Пример использования очередей ............. 126 8.4.6. Функция pipe 133 8.5. Пример многопроцессной программы, вычисляющей произведение матрицы на вектор 135 
Оглавление 5 Глава 9. Управление задачами (threads) 156 9.1. Функция pthread create 156 9.2. Функция pthread join 157 9.3. Функция sched yield 157 Глава 10. Синхронизация и взаимодействие задач ............. 158 10.1. Объекты синхронизации типа mutex ................. 158 10.1.1. Функция pthread mutex init ................ 161 10.1.2. Функция pthread mutex lock ............... 162 10.1.3. Функция pthread mutex trylock ............ 162 10.1.4. Функция pthread mutex unlock ............. 162 10.1.5. Функция pthread mutex destroy ............ 163 10.1.6. Пример использования mutex ................ 163 10.2. Пример multithread-программы, вычисляющей опре- деленный интеграл . 168 10.3. Объекты синхронизации типа condvar ............... 168 10.3.1. Функция pthread cond init ................. 170 10.3.2. Функция pthread cond signal ............... 171 10.3.3. Функция pthread cond broadcast ........... 171 10.3.4. Функция pthread cond wait ................. 172 10.3.5. Функция pthread cond destroy .............. 172 10.3.6. Пример использования condvar .............. 172 10.4. Пример multithread-программы, вычисляющей произ- ведение матрицы на вектор 178 10.5. Влияние дисциплины доступа к памяти на эффектив- ность параллельной программы . 192 10.6. Пример multithread-программы, решающей задачу Дирихле для уравнения Пуассона ................... 202 Глава 11. Интерфейс MPI (Message Passing Interface) ......... 232 11.1. Общее устройство MPI-программы .................. 232 11.2. Сообщения 234 11.3. Коммуникаторы 237 11.4. Попарный обмен сообщениями 238 11.5. Операции ввода вывода в MPI-программах ......... 240 11.6. Пример простейшей MPI-программы ................ 242 11.7. Дополнительные функции для попарного обмена сооб- щениями 243 11.8. Коллективный обмен сообщениями .................. 250 
б Оглавление 11.9. Пример MPI-программы, вычисляющей определенный интеграл 256 11.10. Работа с временем 259 11.11. Пример MPI-программы, вычисляющей произведение матрицы на вектор . 261 11.12. Дополнительные функции коллективного обмена со- общениями для работы с массивами ................. 273 11.13. Пересылка структур данных 277 11.13.1. Пересылка локализованных разнородных дан- ных 277 11.13.2. Пересылка распределенных однородных дан- ных 288 11.14. Ограничение коллективного обмена на подмножество процессов 290 11.15. Пример MPI-программы, решающей задачу Дирихле для уравнения Пуассона 292 Источники дополнительной информации 323 Программа курса . 325 Список задач 329 Указатель русских терминов 334 Указатель английских терминов 336 Список иллюстраций 339 Список таблиц 340 Список примеров 341 
Предисловие Параллельные ЭВМ, возникшие преимущественно для высоко- производительных научных вычислений, получают все более широкое распространение: их можно встретить в небольших научных подразделениях и офисах. Этому способствуют как непрерывное падение цен на них, так и постоянное усложнени» решаемых задач. Можно без преувеличения сгазать что парал- лельные компьютеры сейчас используются или планируются к использованию всеми, кто работает на передовых рубежах на- уки и техники: ° научными работниками, применяющими ЭВМ для решения реальных задач физики, химии, биологии, медицины и дру- гих наук, поскольку упрощенные модельш ~е задачи уже рас- считаны на «обычных» ЭВМ. а переход к реальным задачам сопряжен с качественным ростом объема вычислений; ° программистами, разрабатьп~ающими системы управления базами данных (СУБД), разнообразные 1~й61.I16t-серверы за- просов (AVWW, FTP, DNS и др.) и совместного использова- ния данных (NFS, ЯМВ и др.), автоматизированные систе- мы управления производством (АСУП) и технологическими процессами (АСУТП), поскольку требуется обеспечить об- служивание максимального количества запросов в единицу времени, а сложность самого запроса постоянно растет. В настоящий момент практически все крупные разрабатыва- емые программные проекты как научной, так и коммерческой 
Предисловие направленности либо уже содержат поддержку параллельной работы на компьютерах разных типов, либо эта поддержка за- планирована на, ближайшее время (причем промедление здесь часто вызывает пора>ке ие прое т в конкурент ой борьб Настоящая книга представляет собой введение в методы про- граммирования параллельных ЭВМ. Основной ее целью являет- ся научить читателя самостоятельно разрабатывать максималь- но эффективные программы для таких комш,ютеров. Вопросы распараллеливания конкретных алгоритмов рассматриваются на многочисленных примерах. В качестве языка программиро- вания использован язык С, как наиболее распространенный (и, заметим, единственный (не считая своего расширения С++), на котором можно реализовать все приведенные примеры). Про- граммы, посвященные использованию параллелизма процесса и МР1, могут быть легко переписаны на языке FORTRAN-77. Для иллюстрации подпрограмма умножения двух матриц, дающая почти 14-кратное ускорение на одном процессоре, приведена на двух языках: С и FORTRAN-77. Изложение начинается с изучения параллелизма в работе процессора, оперативной памяти и методов ero использования. Затем приводится описание архитектур параллельных ЭВМ и базовых понятий межпроцессного взаимодействия. Для систем с общей памятью подробно рассматриваются два метода програм- мирования: с использованием процессов и использованием задач (threads). Для систем с распределенной памятью рассматрива— ется ставший фактическим стандартом интерфейс МР1. Для указанных систем приведены описания основных функций и примеры их применения. В описаниях намеренно выброшены редко используемые детали, чтобы не пугать читателя большим обьемом информации (чем страдают большинство руководств пользователя) . Книга используется в качестве учебного пособия в основном курсе «Практикум на ЭВМ» на механико-математическом фа- культете МГУ им. М. В.Ломоносова по инициативе и при под- держке академика РАН Н. С. Бахвалова. 
1 9 Порядок чтения Порядок чтения Книгу можно разделить на 5 достаточно независимых частей: 1. В главах 2, 3, 4 описан параллелизм в работе процессора и оперативной памяти, а также разнообразные приемы, ис- пользуемые для повышения эффективности их работы. Эту информацию можно использовать для достижения значи- тельного ускорения работы программы даже на однопро- цессорном компьютере. 2. В главах 5 и 6 изложены основные понятия, используемые при рассмотрении параллельных программ, а также стан- дарты на операционные системы UNIX, установленные на подавляющем большинстве параллельных ЭВМ. 3. В главах 7 и 8 описаны основные функции для управле- ния процессами и осуществления межпроцессного взаимо- действия. Эти функции можно использовать для запуска многих совместно работающих процессов в системах с общей памятью, а также для разработки параллельного прило> ния для систем, не поддерживающих задачи (threads). 4. В главах 9 и 10 описаны основные функции для управления задачами (threads) и осуществления межзадачного взаимо- действия. Эти функции можно использовать для разработки параллельного приложения в системах с общей памятью. 5. В главе 11 описаны основные функции Message Passing Interface (МР1). Эти функции можно использовать для раз- работки параллельного приложения в системах как с общей, так и с распределенной памятью. Части расположены в рекомендуемом порядке чтения. По- следние три независимы друг от друга и могут изучаться в про- извольной последовательности. Главу 1, адресованную нетерпе- ливому читателю, при систематическом изучении рекоменду- ется разбирать IIQ мере ознакомления с материалом основных частей книги. Книгой можно воспользоваться и в качестве учебника. Для этого в конце приведены программа курса и список типовых экзаменационных задач. Эти материалы будут полезны и для самостоятельной подготовки. 
Для нетерпеливого читателя Для нетерпеливого читателя, желающего как можно быстрее научиться писать параллельные приложения, сразу же приве- дем пример превращения последовательной программы в парал- лельную. Для простоты рассмотрим задачу вычисления опреде- ленного интеграла от заданной функции и будем считать, что все входные параметры (концы отрезка интегрирования и ко- личество точек, в которых вычисляется функция) заданы кон- стантами. Все использованные функции будут описаны в после- д~' ющих Главах. 1.1. Последовательная программа Для вычисления приближения к определенному интегралу от функции f по отрезку [а Ь] используем составную формулу тра- пеций: Ь и — 1 f(z) dz = li(f(a)/2+ ) /(а -«- jh) «- f(b)/2), j=-1 где /), = (t) — а)//в, а параметр и задает точность вЪ|числений. Вначале — файл integral с с текстом последовательной Ф' программы, вычисляющей определенный интеграл этим спосо- бом: ¹include "integral.h" 
1.1. Последовательная программа 11 /+ Интегрируемая функция ~/ ( double f (double х) return х; & /+ Вычислить интеграл по отрезку [a, b] с числом точек разбиения и метадон трапеций. +/ double integrate (double à, double Ь, int n) с double res; /+ результат +/ double h; /+ шаг интегрирования +/ int h= (Ь-а) /n; res = 0.5 + (f (а) + f (b)) * h; for (i = 1; i & t; n; i res += f (а + i + h) + h; return res; Соответствующий заголоио шый файл integral. h: double integrate (double à, double Ь, int n); Файл sequential. с с текстом последовательной ирограммы: ¹include <stdio ¹include "integral.h" /+ Все параметры для простоты задаются константами +/ static double а = О.; /+ левый конец интервала */ static double Ь = ; /+ правый конец интервала +/ static int n = 100000000; /+ число точек разбиения +/ int main () с 
1 Глава 1. Для нетерпеливого читателя double total = О.; /+ результат: интеграл +/ /+ Вычислить интеграл +/ total = integrate (а, Ь, n); printf (" Integral from /lf to /lf = /.181f(n", а, Ь, total); return О; Компиляция этих файлов: сс sequential.c integral.ñ -o sequential и запуск ./sequential 1.2. Ускорение работы за счет параллелизма Для ускорения работы программы на вычислительной установ- ке с р процессорами мы воспользуемся аддитивностью интегра- ла: Ь Ь, f(z) dz = g f(z) dz, а z=O где а; = а + i *1, 6; = а; + 1, 1 = (6 — а)/р. Использовав для Ь, прибли~кенного определения каждого из слагаемых ( * f(z)dx ©г этой суммы составную формулу трапеций, взяв n/р в качестве п, и поручив эти вычисления своему процессору, мы получим р-кратное ускорение работы программы. Ниже мы рассмотрим три способа запуска р заданий для исполнения на отдельных процессорах: 1. создание новых процессов (раздел 1.3, с. 13), 2. создание новых задач (threads) (раздел 1.4, с. 18), 3. hlessage Passing Interface (МР1) (раздел 1.5, с. 21). 
1.3. Параллельная программа, использующая процессы 13 Первые два подхода удобно использовать в системах с общей памятью (см. раздел 2.5.1, с. 39). Последний подход применим и в системах с распределенной памятью. 1.3. Параллельная программа, использующая процессы Рассмотрим пример параллельной программы вычисления опре- деленного интеграла, использующей процессы. Описание ее ра- боты приведено в разделе 8.4.6, с. 133, где рассматривается функция pipe; о функции fork см. раздел 7.1, с. 81. Файл process с с текстом программы: ¹include <stdio ¹include <unistd ¹include <sys/types ¹include <stdlib ¹include "integral.h" /+ Все параметры для простоты задаются константами +/ static double а = О.; /+ левый конец интервала +/ static double Ь = 1.; /+ правый конец интервала +/ static int n = 100000000; /+ число точек разбиения +/ /+ Канал вывода из главного процесса в порожденные. from root[0] — для чтения (в порожденных процессах), from root[1] — для записи (в главном процессе). +/ static int from root [2]; /+ Канал вывода из порожденных процессов в главный. to root[0] — для чтения (в порожденных процессах), Со гсов [1] — для записи (в главном процессе) . */ static int to root[2]; /+ Функция, работающая в процессе с номером myrank, при общем числе процессов р. +/ void process function (int my rank, int р) 
1 Глава 1. Для нетерпеливого читателя char byte; /+ длина отрезка интегрирования для текущего процесса*/ double len = (b — а) / р; /+ число точек разбиения для текущего процесса +/ int local n = n / р; /+ левый конец интервала для текущего процесса */ double local а = а + my rank + len; /+ правый конец интервала для текущего процесса +/ double 1оса1 Ь = local а + len; /+ значение интеграла в текущем процессе +/ double integral; /+ Вычислить интеграл в каждан из процессов +/ integral = integrate (local à, local Ь, local п); /+ Ждать сообщения от главного процесса +/ if (read (from root[0], &b te 1 ! /+ Ошибка чтения +/ fprintf (stderr, "Error reading in process /d, pid = /d(n", myrank, getpid ()); return; /+ Передать результат главноиу процессу */ if (write (to root[1], &integ al, si eof (doub ( != sizeof (double)) /+ Ошибка записи +/ fprintf (stderr, "Error writing in process '/d, pid = /фп", myrank, getpid ()); return; 
1.3. Параллельная программа, использующая процессы } 15 int main (int argc, char * argv [] ) /+ Идентификатор запускаемого процесса +/ pid t pid; /+ Общее количество процессов +/ int р; int char byte; double integral = О.; double total = О.; /+ результат: интеграл +/ if (argc != 2) printf (" Usage: /s <instances&g ;(n" ar v return i; /+ Получаем количество процессов +/ р = (int) strtol (argv[1], О, 10); /~ Создаеи каналы +/ if (pipe (from root) == -1 ~~ pipe (to root) == -1) с fprintf (stderr, "Cannot р1ре!~п"); return 2; /+ Запускаем процессы +/ for (i = О; i & t ; i с /+ Клонировать себя +/ pid = fork (); 
1 Глава 1. Для нетерпеливого читателя if (pid == -1) & fprintf (stderr, "Cannot fork!(n"); return 3 + i; } else if (pid == О) с /* Процесс — потомок */ /+ Закрываем ненужные направления обмена +/ close (from root[1]); close (to root[0]); /+ Проводим вычисления +/ process function (i, р); /+ Закрываем каналы */ close (Хrom root[0]); close (to root[1]); /+ Завершаем потомка +/ return О; /+ Цикл продолжает процесс — родитель +/ /+ Закрываем ненужные направления обмена +/ close (from root[0]); close (to root[1] ); /+ Получаем результаты */ for (i = 0; i & t ; i & /+ Сигнализируем процессу */ byte = (char) if (write (антош гоой[1], &b te 1 ! 
1.3. Параллельная программа, использующая процессы 17 /+ Ошибка записи +/ fprintf (stderr, "Error writing in root procession"); & return 100; /+ Считываеи результат +/ if (read (to root[0], &inte ral si eof (doub != sizeof (double)) & /+ Ошибка чтения +/ fprintf (stderr, "Error reading in root ргосевв~п"); return iOi; & total += integral; & /+ Закрываеи каналы +/ close (fromroot[11); close (toroot[0]); printf (" Integral from /lf to /lf = '/.181f(n", а, Ь, total); return 0; } Компиляция этих файлов: сс process.c integral.ñ -î process и запуск ./process 2 где аргумент программы (в данном случае 2) — количество па- раллельно работающих процессов (обычно равен количеству имеющихся процессоров). Ъ 
1 Глава 1. Для нетерпеливого читателя 1А. Параллельная программа, использующая задачи Рассмотрим пример параллельной программы вычисления опре- деленного интеграла, использующей задачи (threads). Описание ее работы см. в разделе 10.2, с. 168; описание использованных функций: ° pthread create — см. раздел 9.1, с. 156; ° pthread join — см. раздел 9.2, с. 157; ° pthread mutex lock — см. раздел 10.1.2, с. 162: ° pthread mutex unlock — - см. раздел 10.1.4, с. 162. Файл thread. c с текстом программы: ¹include <stdio ¹include <stdlib ¹include <pthread ¹include "integral.h" /+ Все параметры для простоты задаются константами +/ static double а = О.; /+ левый конец интервала +/ static double Ь = 1.; /+ правый конец интервала +/ static int и = 100000000; /+ число точек разбиения +/ /+ Результат: интеграл +/ static double total = О.; /+ Объект типа mutex для синхронизации доступа к total +/ statiс pthread mutех t total mutex = РТНВЕАО МОТЕХ ПЮТТАЫЕЕН.; /+ Общее количество процессов +/ static int p; /+ Функция, работающая в задаче с номером myrank +/ void + process function (void ~ра) 
1.4. Параллельная программа, использующая задачи 19 /+ номер текущей задачи +/ int myrank = (int) ра; /+ длина отрезка интегрирования для текущего процесса~/ double len = (Ь — а) / р; /+ число точек разбиения для текущего процесса +/ int localn = и / р; /* левый конец интервала для текущего процесса */ double 1оса1 а = а + myrank + len; /+ правый конец интервала для текущего процесса +/ double local b = local а + len; /* значение интеграла в текущем процессе +/ double integral; /+ Вычислить интеграл в каждой из задач +/ integral = integrate (local à, local Ь, local n); /+ "захватить" mutex для работы с total +/ pthread mutex lock (&t tal mut /* сложить все ответы +/ total += integral; /+ "освободить" mutex +/ pthread mutex unlock (&t tal mu return О; int main (int argc, char * argv[] ) /+ массив идентификаторов созданных задач +/ pthread t + threads; int & if (argc != 2) printf (" Usage: /s <instances&g ;(n", arg return i; 
Глава 1. Для нетерпеливого читателя /+ Получаем количество процессов +/ p = (int) strtol (argv[1], О, 10); if (! (threads = (pthreadt+) ( malloc (р + sizeof (pthread t)))) fprintf (stderr, "Not enough memory!Nn"); return i; } /+ Запускаем задачи +/ for (i = О; i & t; р; i ( if (pthread create (threads + i, О, process function, (void+)i)) ( fprintf (stderr, "cannot create thread №/d!(n", i); return 2; & & /+ Ожидаем окончания задач &l for (i = О; i & t; р; i ( if (pthread join (threads[i], 0)) fprintf (stderr, "cannot wait thread №/d!(n", i); /+ Освобождаем память ~/ free (threads); printf (" Integral from '/lf to '/lf = '/.181f(n", 
1.5. Параллельная программа, использующая МР! 21 а, Ь, total); & return О; Компиляция этих файлов: cc thread. c integral. с -î thread и запуск ./thread 2 где аргумент программы (в данном случае 2) — количество па- раллельно работающих задач (обычно равен количеству имею- щихся процессоров). 1.5. Параллельная программа, использующая MPI Рассмотрим пример параллельной программы вычисления опре- деленного интеграла, использующей MPI. Описание ее работы см. в разделе 11.9, с. 256; описание использованных функций: ° MPI Init u MPI Finalize — см. раздел 11.1, с. 232; ° MPI Comm rank u MPI Comm size — см. раздел 11.3, с. 237; ° MPI Reduce — см. раздел 11.8 с. 250. Файл mpi. c c текстом программы: ¹include <stdio ¹include "mpi.h" ¹include "integral.h" /+ Все параметры для простоты задаются константами +/ static double а = О.; /+ левый конец интервала +/ static double Ь = ; /+ правый конец интервала +/ static int и = 100000000; /* число точек разбиения +/ /+ Результат: интеграл +/ static double total = О.; 
Глава 1. Для нетерпеливого читателя /+ Функция, работающая в процессе с номером myrank, при общем числе процессов р. */ с void process function (int myrank, int р) /+ длина отрезка интегрирования для текущего процесса~/ double len = (Ь — а) / р; /+ число точек разбиения для текущего процесса +/ int local n = n / р; /* левый конец интервала для текущего процесса +/ double local à = а + myrank * len; /+ правый конец интервала для текущего процесса */ double 1оса1Ь = local à + len; /+ значение интеграла в текущем процессе */ double integral; /~ Вычислить интеграл в каждом из процессов +/ integral = integrate (local а, local Ь, local n); /+ Сложить все ответы и передать процессу О +/ MPI Reduce (&integ al, &am ;t tal 1, MPI DOU LE, О, MPI COMM WORLD); & int main (int argc, char ++argv) int myrank; /+ ранг текущего процесса +/ int р; /+ общее число процессов +/ /+ Начать работу с MPI +/ MPI Init (&a gc, &am /+ Получить номер текущего процесса в группе всех процессов +/ 
2 1.5. Гараллельная программа, использующая МР! MPI Comm rank (MPI COMM WORLD, &myra /+ Получить общее количество запущенных процессов +/ MPI Comm size (MPI COMM WORLD, &amp /+ Вычислить интеграл в каждом из процессов */ process function (myrank, р); /+ Напечатать ответ в процессе О +/ if (myrank == 0) printf (" Integral from /lf to /lf = /.181fgn", а, Ь, total); /+ Заканчиваем работу с MPI +/ MPI Finalize (); return О; & Компиляция этих файлов: mpicc mpi. с inte0;ral с — î mpi и запуск mpirun -np 2 mpi где опция -np задает количество параллельно работающих про- цессов (в данном случае 2). 
Пути повышения производительности процессоров Рассмотрим основные пути, которыми разработчики процессо- ров пытаются повысить их производительность. Нетерпеливый читатель может сразу заглянуть в раздел 2.7, где приведен при- мер программы умножения двух матриц, написанный с исполь- зованием знаний о внутреннем параллелизме в работе процессо- ра, и дающий на обычном однопроцессорном компьютере почти 14-кратное ускорение работы по сравнению со стандартной про- граммой. 2.1. С1$С- и RISC-процессоры Основной временной характеристикой для процессора является время цикла, равное 1/F, где F — тактовая частота процессора. Время, затрачиваемое процессором на задачу, может быть вы- числено по формуле С * Т * I, где С — число циклов на одну инструкцию, Т — время на один цикл, I — число инструкций на задачу. Разработчики «классических» систем (которые теперь на- зывают CISC (Complete Instructiou Set Computer)) стремились уменьшить фактор I. В процессорах реализовывались все бо- лее сложные инструкции, для выполнения которых внутри негО самого запускались специальные процедуры (так называемый микрокод). загружаемые из ПЗУ внутри процессора. Этому пу- 
2.2. Основные черты RISC-архитектуры ти способствовало то, что улучшения в технике производства по- лупроводников делали возможным реализацию все более слож- ных интегрированных цепей. Однако на этом пути очень труд- но уменьшить два других фактора: С — поскольку инструкции сложные и требуют программного декодирования, и Т — в силу аппаратной сложности. Концепция RISC (Reduced Instruction Set Computer) возник- ла из статистического анализа того, как программное обеспе- чение использует ресурсы процессора. Исследования систем- ных ядер и объектных модулей, порожденных оптимизирую- щими компиляторами, показали подавляющее доминирование простейших инструкций даже в коде для CISC-машин. Сложные инструкции используются редко, поскольку микрокод обычно не содержит в точности те процедуры, которые нужны для под- держки различных языков высокого уровня и сред исполнения программ. Поэтому разработчики RISC-процессоров убрали ре- ализованные в микрокоде процедуры и передали программному обеспечению низкоуровневое управление машиной. Это позволи- ло заменить процессорный микрокод в ПЗУ на подпрограмму в более быстрой ОЗУ. Разработчики RISC-процессоров улу.чшили производитель- ность за счет уменьшения двух факторов: С (за счет использо- вания только простых инструкций) и Т (за счет упрощения про- цессора). Однако изменения, внесенные для уменьшения числа циклов на инструкцию и времени на цикл, имеют тенденцию к увеличению числа инструкций на задачу. Этот момент был в центре внимания критиков RISC-архитектуры. Однако исполь- зование оптимизирующих KoMIIlfJIHTopoB и других технических приемов, практически ликвидирует эту проблему. 2.2. Основные черты RISC-архитектуры У RISC-процессора все инструкции имеют одинаковый формат и состоят из битовых полей, определяющих код инструкции и идентифицирующих ее операнды. В силу этого декодирование 
26 Глава 2. Пути повышения производительности процессоров инструкций производится аггггаратгго, т. е. микрокод не требу- ется. При этом в силу одинакового строения всех инструкций процессор может декодировать несколько полей одновременно для ускорения этого процесса. Инструкции, производящие операции в памяти, обычно либо увеличивают время цикла, либо число циклов на инструкцию. Такие инструкции требуют дополнительного времени для свое- го исполнения, так как требуется вычислить адреса операндов, считать их из памяти, вы шслить результат операции и запи- сать его обратно в память. Для уменьшения негативного влия- ния таких инструкций разработчики RISC-процессоров выбрали архггтектуру чтениеlзапггсг в которой все операции выполняют- ся над операндм~и в регистрах процессора, а основная память доступна, только посредством инструкций чтения/записи. Для эффективности этого подхода RISC-процессоры имеют больгггое ко.ггичгство регистров. Архитектура чтение/запись также поз- воляет умеш шить количество режимов адресации памяти что позволяет упростить декодирование инструкций. Для CISC-архитектур время исполнения инструкцгги обычно ггзмеряется в числе циклов на инструкцшо. Разработчики RISC- архитектур, однако, стремились получить скорость выполнения инструкции, равну1о одпой инструкции 3В цикл. Для RISC-процессора во ицогггх случаях только наличие оп- тиютзирующего «имит~ятора позволяет реализовать все его воз- можности. Отметим, что компилятор может наилучшим обра- зом оптимизировать код именно для RISC-архитектур (в силу их простоты). !1рог раимирование на языке ассемблера исчезает для RISC-прггложенигг, так как компиляторы языков высокого уровня могут производить очень сильную оптимизацию. 2.3. Конвейеризация Конвейеризация является одним из основных способов повы-. пгения производительности процессора. Коггвейерный процес- сор приш~мает новую инструкцию каждый цикл, даже если 
2.3. Конвейеризация предыдущие инструкции не завершены. В результате выполне- ние нескольких инструкций перекрывается и в процессоре нахо- дятся сразу несколько инструкций в разной степени готовности. Исполнение инструкций может быть разделено на несколько стадий: выборка, декодирование, исполнение, запись ре- зуххьтатов. Конвейер инструкций может уменьшить число циклов на инструкцию посредством одновременного исполне- ния нескольких инструкций, находящихся на разных стадиях (рис. 2.1). При правильной аппаратной реализации конвейер имеющий и стади~~, может одновременно исполнять и последо- ватеххьххьхх инструкций. Новая ххххструкцххя может ххриних~хаться к исполнению на. каждом цикле и эффективная скорость испол- нения, таким образом, есть один цикл на инструкцию. Однако ПОТОК инструкций В Д И 3 инструкция 1 В Д И 3 инструкция 3 В Д И 3 инстру.кция 2 В Д И 3 инструкция 1 t процессор без конвейера ПОТОК инструкций В Д И 3 инструкция 4 В Д И 3 инструкция 3 В Д И 3 инструкция 2 В Д И 3 инструкция 1 t процессор с конвейером В — выборка, Д — - декодирование. И — исполнение, 3 — запись Рис. 2.1. Ускорение работы процессора за счет конвейера 
28 Глава 2. Пути повышения производительности процессоров это предполагает, что конвейер всегда заполнен полезными ин- струкциями и нет задержек в прохождении инструкций через конвейер. Управление конвейером инструкций требует надлежащего эффективного управления такими событиями, как переходы, исключения или прерывания, которые могут полностью на- рушить поток инструкций. Например, результат условного пе- рехода известен, только когда эта инструкция будет исполнена. Если конвейер был заполнен инструкциями, следующими за ин- струкцией условного перехода и переход состоялся, то все эти инструкции должны быть выброшены из конвейера. Более того, внутри конвейера могут оказаться взаимоза- висимые инструкции. Например, если инструкция в стадии декодирования должна читать из ячейки памяти, значение ко- торой является результатом работы инструкции, находящейся в стадии исполнения, то конвейер будет остановлен на один цикл, поскольку этот результат будет доступен только после стадии записи результатов. Поэтому компилятору необходимо лереулорядочить инструкции в программе так, чтобы по воз- можности изоежать зависимостей между инструкциями внутри конвейера. Для уменьшения времени простоя конвейера применяют ряд мер. ° Таблица регистров (register scoreboarding) позволяет проследить за использованием регистров. Она имеет бит для каждого регистра процессора. Если этот бит установлен, то регистр находится в состоянии ожидания записи результата. После записи результата этот бит сбрасывается, разрешая использование данного регистра. Если этот бит сброшен для всех регистров, значения которых используются в текущей инструкции то ее можно выполнять, не дожидаясь заверше- ния исполнения предыдущих инструкций. ° Переименование регистров (register renaming) являет- ся аппаратной техникой уменьшения конфликтов из-за реги- 
2.3. Конвейеризация стровых ресурсов. Компиляторы преобразуют языки высо- кого уровня в ассемблерный код, назначая регистрам те или иные значения. В конвейерном процессоре операция может потребовать регистр до того, как предыдущая инструкция закончила использование этого регистра. Такое состояние не является конфликтом данных, поскольку этой операции не I требуется значение регистра, а только сам регистр. Одна- ко эта ситуация приводит к остановке конвейера до освобо- ждения регистра. Идея разрешения этой проблемы состоит в следующем: берем свободный регистр, переименовываем его для соответствия параметрам инструкции, и даем инструк- ции его использовать в качестве требуемого ей регистра. Задержки внутри конвейера могут быть также вызваны вре- менем доступа к оперативной памяти DRAM, которое намного превышает время цикла. Эта проблема в значительной степени снимается при использовании кэш-памяти и буфера предвыбор- ки инструкций (очереди инструкций). Так как поток инструкций в CISC-процессоре нерегуляр- ный и время исполнения одной инструкции (С * Т) не посто- янно, то конвейеризация в этом случае имеет серьезный недо- статок, делающий ее малопригодной к использованию в CISC- процессорах: именно, она приводит к очень сильному усложне- нию процессора. RISC-процессоры используют один и тот же формат всех ин- струкций для того, чтобы ускорить декодирование и упростить управление конвейером, поэтому все инструкции исполняются за один цикл. Одним из способов дальнейшего повышения быстродействия является конвейеризация стадий конвейера. Такие процессоры называются суперконвейерными. При таком подходе каждая стадия конвейера, такая как кэш (см. ниже) или АЛУ (ариф- метическое и логическое устройство), MoiKeT принимать новую инструкцию каждый цикл, даже если эта стадия не заверши- ла исполнение текущей инструкции. Отметим, что добавление 
30 Глава 2. Пути повышения производительности процессоров новых уровней конвейеризации имеет смысл только в случае, если разработчик может значительно увеличить частоту процессор ~. Однако увеличение производительности за счет уве- личения внутренней частоты процессора имеет ряд недостатков. Во-первых, это увеличивает потребление энергии процессором, что делает суперконвейерные процессоры малопригодными для встраиваемых (бортовых) систем а также увеличивает габари- ты вычислительной установки за счет внушительной системы oxJI3iKlcIIICЯ. Bo-BIo~~~Ix, 3To BBogIIT II0BblP. тРУДности B сопРЯже- нпн процессора с памятью нижнего уровня такой как DRAKI. Быстродействие этой памяти растет не так быстро, как скорость процессоров, поэтому чем быстрее процессор, reit больше раз- рыв в производительности между ним и основной памятью. Другим способом увеличения производительности процессо- ров является выполнени» более хем одной операции одновре- менно. Такие процессоры называются суперскалярными. Они имеют два или более конвейеров инструкций, работаюших параллельно что значительно увеличиваст скорость обработки потока инструкций. Одним из достоинств суперскалярной ар- хитектуры является во~мо.кность увеличения производитель- ности без необходимости увеличения частот~~ процессора. Су- перскалярному процессору требуется более широкий доступ к памяти, так, чтобы он мог брать сразу ц>у пу нз неско ких инструкций чля исполнения. Диспетчер ан шизирует эти группы и заполняет каждый из конвейеров так, чтобы снизить взаимозависимость данных и конфликты регистров. Выполне- ние инструкций может быть не по порядку поступления, так, чтобы команды перехода были проанализированы раныпе, уби- рая задержки в случае осуществления перехода. Компилятор должен оптимизировать код для обеспечения заполнения всех конвейеров. Требование одного и того же ресурса несколькими инструк- циями блокирует их продвижение по конвейеру и приводит к вставке циглов ожи ~ ~ния требуемого ресурса. Суперскалярная архитектура с тремя исполняющими устройствами будет полно- Ъ 
2.3. Конвейеризация 1 стью эффективной, только если поток инструкций обеспечивает одновременное использование этих трех устройств. Для обеспе- чения этого условия с минимальными расходами в процессоре выделяют исполняющие устройства, работающие независимо: ° Целочисленное устройство (IU, Integer Unit) — выполня- ет целочисленные операции (арифметические, логические и операции сравнения) в своем АЛУ. ° устройство для работы с плавающей точкой (FPU. Floating Point Unit) — обычно отделено от целочисленного устройства, которое работает только с целыми числами и числами с фиксированной точкой. Большинство FPU совме- стимо со стандартом AibSI/IEEE для двоичной арифметики с плавающей точкой. ° устройство управления памятью (IVIMU, Ъ|еп1огу Alanagement Unit) — вычисляет реальный физический ад- рес по виртуальному адресу. ° устройство предсказания переходов (BU, Branch Unit) — занимается предсказанием условных переходов для того, чтобы избежать простоя конвейера в ожидании результата вычисления условия перехода. Поскольку условные переходы могут свести на нет все пре- имущества конвейерной организации процессора, останои~мс» более подробно на приемах, используемых BU для уменьихе!ия их негативного влияния. ° «Отложенные слоты» (delay slots). Инструкцию, переда- ющун~ управление от одной части программы друго~~, труд- но исполнить за один цикл. Обычно загрузка процессорно- го указателя на следующую инструкцию требует один цикл, ' предвыборка новой инструкции требует еще один. Для из- бежания простоя некоторые RISC-процессоры (например, SPARC) позволяют вставить дополнительную инструкцшо в так называемый «отложенный слот». Эта инструкция рас- положена непосредственно после команды перехода, но она будет выполнена до того, как будет совершен переход. 
32 Глава 2. Пути повышения производительности процессоров Однако, для суперскалярных RISC-процессоров отложен- ные слоты работают не очень хорошо. Задержка при перехо- де может быть равна двум циклам, а суперскалярный про- цессор, который выполняет за цикл и инструкций, должен найти и инструкций для помещения в конвейеры. ° «Спекулятивное» исполнение инструкций (speculative execution). Некоторые RISC-процессоры (например, старшие модели семейств PowerPC и SPARC) используют так назы- ваемое «спекулятивное» исполнение инструкций: процессор загружает в конвейер и начинает исполнять инструкции, на- ходящиеся за точкой ветвления, еще не зная, произойдет пе- реход или нет. При этом часто выбирается наиболее вероят- ная ветвь программы (на основе того или иного подхода, см. ниже). Если после исполнения команды перехода оказалось, что процессор начал исполнять не ту ветвь, то все загружен- ные в конвейер инструкции из этой ветви и результаты их обработки сбрасываются, и загружается правильная ветвь. ° Биты предсказания перехода в инструкции. Неко- торые RISC-процессоры (например, PowerPC) используют биты предсказания перехода, устанавливаечяС компилято- ром в инструкции перехода, которые предсказывают, будет или нет совершен переход. ° Эвристическое предсказание переходов. Некоторые процессоры уменьшают задержки, вносимые переходами, за счет использования встроенного предсказателя пере- ходов. Он предсказывает, что переходы вперед (проверки) произведены не будут, а, переходы назад (циклы) — будут. Для эффективной работы устройства предсказания перехода важно, чтобы код условия для условного перехода был вычислен как мох&lt но ран ше за нескол ко инструк ий до са ой кома перехода). Этого добиваются несколькими способами. ° Независимость арифметических операций и када условия. В CISC-архитектурах все арифметические опе- рации выставляют код условия по своему результату. Это 
2.3. Конвейеризация сделано для уменьшения фактора I — числа инструкций на задачу, поскольку есть вероятность того, что следующая инструкция будет вычислять код условия по результату предыдущей инструкции и, следовательно, может быть уда- лена. Однако это приводит к тому, что между командой вь~числения кода условия и командой перехода очень труд- но вставить полезные инструкции, так как они изменят код условия. В RISC-архитектурах арифметические опе- рации не изменяют код условия (если противное явно не указано в инструкции, см. ниже). Поэтому возможно между инструкцие~~, вычисляющей код условия, и командой пе- рехода вставить другие инструкции (переупорядочив их). Это позволит заранее узнать, произойдет или нет переход, и загрузить конвейер инструкциями. Поскольку возможна ситуация, когда следующая ин- струкция будет вычислять код условия по результату преды- дущей инструкции, то в RISC-архитектурах часть (SPARC) или все (PowerPC) арифметические операции также имеют вторую форму, в которой будет выставляться код условия по их результату. Таким образом, часть или все арифме- тические операции присутствуют в двух вариантах: один не изменяет код условия (подавляющее большинство слу- чаев использования), а другой вычисляет код условия по результату операции. ° Использование нескольких равноправных регистров с кодом условия. Некоторые RISC-процессоры (например, PowerPC) используют несколько равноправных регистров, в которых образуется результат вычисления условия. Над эти- ми регистрами определены логические операции, что иногда позволяет оптимизирующему компилятору заменить коман- ды перехода при вычислении сложных логических выраже- ний на команды логических операций с этими регистрами. ° Использование кода условия в каждой инструкции. Некоторые RISC-процессоры (например, АЕМ) использу- ют код условия в каждой инструкции. В формате каждой 2 4017 
34 Глава 2. Пути повышения производительности процессоров инструкции предусмотрено поле, где компилятором записы- вается код условия, при котором она будет выполнена. Если в момент исполнения инструкции код условия не тако~~, как в инструкции, то она игнорируется. Это позволяет вообще обойтись без команд перехода при вычислении результатов условных операций. 2.4. Кэш-память Время, необходимое для выборки инструкций, в основном зави- сит от подсистемы памяти и часто является ограничивающим фактором для RISC-процессоров в силу высокой скорости ис- полнения инструкций. Например, если процессор может брать инструкции только из DRAM с временем доступа 60 нс, то ско- рость их обработки (при расчете одна инструкция за цикл) бу- дет соответствовать тактовой частоте 16.7 МГц. Эта проблема в значительной степени снимается за счет использования кэш- памяти. Кэш-память (cache) — это быстрое статическое ОЗУ (БРАМ), вставленное между исполнительными устройствами и системным ОЗУ (ВАМ). Она сохраняет последние исполь- зованные инструкции и данные, так, что циклы и операции с массивамп будут выполняться быстрее. Когда исполняющему устройству нужны данные и они не находятся в кэш-памяти, то это кэш-промах: процессор должен обратиться к внешней памяти для выборки данных. Если требуемые данные находят- ся в кэше, то это кэш-попадание: доступ к внешней памяти не требуется. Таким образом, кэши разгружают внешние шины, уменьшая потребность в них для процессора. Это позволяет нескольким процессорам разделять внешние шины без уменьшения произ- водительности каждого из них. Кэш содержит строки из нескольких последовательных бай- тов (обычно 32 байта), которые загружаются процессором, ис- пользуя так называемый импульсный (или блочный) доступ 
2.4. Кэш-память (burst access). Даже если CPU нужен один байт, все равно будет загружена целая строка, так как вероятно, что тем самым будут :сгружены следующие выполняемые инструкции или используе- мые данные. Блочные передачи обеспечивают высокие скорости передачи для инструкций или данных в последовательных адре- сах памяти. При таких передачах только адрес первой инструк- ции или данного будет послан в подсистему внешней памяти. Все последующие запросы инструкций или данных в последова- тельных адресах памяти не требуют дополнительной передачи адреса. Например, загрузка 16 байтов требует 5 циклов, если IC68040 делает блочную передачу для загрузки строки кэша, и 8 циклов. если память не поддерживает блочный режим пе- редачи. Кэш, в котором вместе хранятся данные и инструкции. назы- вается единым кэшем. Одним из способов повышения произ- водительности является введение в процессоре трех шин: адре- са., инструкций и данных. В Гарвардской архитектуре кэша разделяют кэши для инструкций и данных для удвоения эф- фективности кэш-памяти. В типичной Гарвардской архитектуре присутствуют три вида кэш-памяти: специальные кэши (напри- мер, ТЬВ), внутренние кэши инструкций и данных (первого уровня или Ll-кэш) и внешний единый кэш (второго уровня или Ь2-кэш). В процессорах, имеющих интегрированные кэши первого и второго уровней (т. е. внутри корпуса процессора), ча- сто дополнительно устанавливают единый кэш третьего уров- ня (ЬЗ) вне процессора. Обычно Ll-кэш работает на частоте процессора и имеет размер 8... 32K6, L2-кэш работает на часто- те процессора или ее половине и имеет размер 128K6...4М6 L3-кэш работает на частоте внешней шины и имеет размер 512К6... 8Мб. Кэши данных в зависимости от их поведения при записи дан- ных в кэш разделяют на два вида. 1. Кэш с прямой записью (write-through cache). Этот вид кэш- памяти при записи в нее сразу инициирует цикл записи во 
36 Глава 2. Пути повышения производительности процессоров внешнюю память. Основным достоинством такого кэша яв- ляется простота и то, что данные в кэше и в памяти все- гда идентичны, что упрощает построение многопроцессор- ных систем. 2. Кэш с обратной записью (write-back cache). Этот вид кэш- памяти при записи в нее не записывает данные сразу во внешнюю память. Запись в память осуществляется при вы- ходе строки из кэша или по запросу системы синхрониза- ции в многопроцессорных системах. Такая организация кэш- памяти может значительно ускорить выполнение циклов, в которых обновляется одна и та же ячейка памяти (будет за- писано только последнее, а не все промежуточные значения, как в кэше с прямой записью). Другим достоинством явля- ется уменьшение потребности процессора во внешней шине, что позволяет разделять ее нескольким процессорам. Недо- статком такой организации является усложнение схемы син- хронизации кэшей в многопроцессорных системах. В силу его значительно большей эффективности, большинство современных процессоров используют кэш с обратной записью. Организация кэш-памяти. Кэш основан на сравнении адреса. Для каждой строки кэша хранится адрес ее первого элемента, называемый адресом строки. Для уменьшения объема дополнительно хранимой информации (адресов строк) и ускоре- ния поиска адреса используют несколько технических приемов. ° Пусть длина строки есть 2 байт. Адреса строк выровнены на границу своего размера т. е. последние О бит адреса— нулевые и потому не хранятся (т. е. размер адреса уменьшен до 32 — b бит). ° Фиксируется некоторое i. Строки хранятся как один или несколько (N) массивов, отсортированными по порядку i младших битов адреса (т. е. младших среди оставшихся 32— Ь). Таким образом, k-й элемент массива имеет адрес, биты которого в позициях от 32 — i — b+ 1 до 32 — b образуют чис- 
2.4. Кэш-память ло, равное k. Это позволяет не хранить эти биты (т. е. размер адреса уменьшен до 32 — О — i бит). ° Комбинация чисел б и iподбирается так, ,чтобы 6+i младших битов логического адреса совпадали бы с соответствующими битами физического адреса при страничном преобразовании (т. е. были бы смещением в странице). Это позволяет парал- лельно производить трансляцию адреса и поиск в кэше (т. е. параллельно работать MMU и кэшу). При типичном размере страницы 4Кб это означает b+ i = 12. ° Определение того, содержится ли данный адрес в кэше, про- изводится следующим образом. Берутся биты в позициях от 32 — i — 0+1 до 32 — б, образующие число k. Затем берутся эле- менты с номером k в каждом из N массивов и у полученных N строк сравниваются адреса с 32 — i — О битами адреса (ко- торые уже транслированы MMU в физический адрес). Если обнаружено совпадение (т. е. имеет место кэш-попадание), то берется байт с номером О в строке. Если рассматривается внешний кэш, то согласовывать его рабо- ту с ММБ не требуется, поскольку внешний кэш работает уже с физическим адресом. Пример: для PowerPC 603 выбрано б = 5 (т. е. длина строки 32 байта), i = 7 (т. е. длина массива 127), N = 2 (т. е. исполь- зуются два массива). Для каждой строки кэша с обратной записью помимо адреса хранится также признак того, что эта строка содержит коррект- ные данные, т. е. данные в кэш-памяти и в основной памяти сов- падают. Этот признак используется для записи строки в память при ее выходе из кэш-памяти, а также в многопроцессорных системах. Алгоритмы замены данных в кэш-памяти. Если все строки кэш-памяти содержат корректные данные, то для обеспе- чения кэширования новых областей памяти необходимо выбрать строку, которая будет перезаписана. Эта строка выходит из кэ- 
38 Глава 2. Пути повышения производительности процессоров ша и, если требуется, ее содержимое будет записано обратно в память. Существуют три алгоритма замены данных в кэше: ° вероятностный алгоритм: в качестве номера перезаписы- ваемой строки используется случайное число; ° FIFO-алгоритм: первая записанная строка будет первой пе- резапи сан а; ° LRU-алгоритм (Last Recently Used): наименее используемая строка будет заменена новой. Для повышения производительности процессора вводится ряд специальных кэшей. ° TLB (Translation Look-aside Buffers) — это кэш-память, ис- пользуемая liVIU для хранения результатов последних трансляций логического адреса в физический. Содер> ит ры: логический адрес и соответствующий физический адрес. ° ВТС (Branch Target Cache) — это кэш-память, используемая BU для хранения адреса предыдущего перехода и первой ин- струкции, выполненной после перехода. Имеет целью без за- держки заполнить конвейер инструкцие~т, если переход уже ранее состоялся. ВТС может значительно повысить произво- дительность процессора, учЇтывая время, которое он бы про- стаивал в ожидании заполнения конвейера после перехода. Согласование кэшей в мультипроцессорных систе- мах. Если несколько процессоров подсоединены к одной и той же шине адреса и данных и разделяют одну и ту же внешнюю память, то должен быть реализован определенный следящий механизм (snooping) для того, чтобы все внутрипроцессорные кэши всегда содержали одни и те же данные. Рассмотрим, например, систему, содержащую два процессо- ра, каждый из которых может брать управление общей шиной. Если процессор 1, управляющий в данный момент шиной, запи- сывает в ячейку памяти, которая кэширована процессором 2, то данные в кэше последнего становятся устаревшими. Следящий механизм позволяет второму процессору отслеживать состоя- 
2.5. Многопроцессорные архитектуры иие шины адреса, даже если он не является в данный момент I лавным (т. е. управляющим внешней шиной). Если на шине по- «вился адрес кэшированных данных, то эти данные помечаются и кэше как некорректные. Когда второй процессор станет глав- llbIM, он должен будет выбрать в случае необходимости обнов- ленные данные из разделяемой памяти. Если процессор 1, управляющий в данный момент шиной, читает из ячейки памяти, которая кэширована процессором 2, то возможно, что реальные данные находятся в кэше процессо- ра 2 (еще не записаны в память, т. е. реализован кэш с обрат- ной записью). Если это так, то следящий механизм инициирует цикл записи строки кэша процессора 2, содержащей затребован- ные процессором 1 данные, в разделяемую память. После этого эти данные становятся доступными процессору 1 и цикл чтения процессора 1 продолжается. 2.5. Многопроцессорные архитектуры Одним из самых радикальных способов повышения произ- водительности вычислительной системы является установка нескольких процессоров. 2.5.1. Основные архитектуры Выделяют несколько типов построения многопроцессорных си- стем в зависимости от степени связи между отдельными про- цессорами в системе. 1. Сильно связанные процессоры (или симметричные мультипроцессорные системы, symmetrical multiprocessor system, ЯМР). Все процессоры разделяют общую шину и об- щую память, могут выполнять одну и ту же задачу, причем задача может переходить от одного процессора к другому. Если один процессор отказывает, он может быть заменен другим. SMP подразумевает наличие аппаратного протокола 
40 Глава 2. Пути повышения производительности процессоров синхронизации кэшей всех процессоров (см. выше). Типич- ный пример: плата с двумя процессорами Pentium. 2. Слабо связанные процессоры. Часть системной памя- ти может быть разделяема, но переход задачи от одного процессора к другому невозможен. Механизмы синхрони- зации специфичны для каждой системы (почтовые ящики, 0Рй.АХ!, прерывания). Типичный пример: стойка промыш- ленного стандарта VOGIE c несколькими процессорными пла- тами и разделяемой памятью на одной из плат. 3. Распределенные процессоры. Несколько процессоров не разделяют ни одного общего ресурса, за исключением линии связи. Типи шый пример: соединенные посредством Ethernet рабочие станции. Очень часто многопроцессорные системы деляг на два клас- са в зависимости от способа доступа процессов к оперативной памяти (см. раздел 5.1 о разделяемой памяти): 1. Системы с общей памятью — процессы могут разделять блок оперативной памяти. 2. Системы с распределенной памятью — у каждого из процессов своя оперативная память, которая не может быть сделана разделяемой между ними. Система 111. сильно связанных процессорах, конечно, является системой с общей памятью. Отметим что система на распреде- ленных процессорах с помощью программного обеспечения тоже может быть превращена в систему с общей памятью. 2.5.2. Комбинированные архитектуры Архитектура ЯЫР является самой дорогой с точки зрения аппа- ратной реализации и самой дешевой с точки:зрения разработки программного обеспечения. И наоборот распределенные процес- соры почти не требуют аппаратных затрат, но являются самым дорогим решением с точки зрения разработки ПО. Для достиже- 
2.5. Многопроцессорные архитектуры 1 ния оптимального компромисса для круга решаемых задач ис- пользуют различные комбинации описанных выше технологий. ° Гибридные схемы SMP используются для ускорения ра- боты программ, требующих интенсивной работы с оператив- ной памятью. Описанная выше схема S1VIP ускоряет выпол- нение задач, которым не нужен постоянный обмен с ВАМ, поскольку все процессоры разделяют одну шину и в каж- дый момент времени только один процессор может работать с памятью. Поэтому иногда (это очень дорогое решение) по- ступают следующим образом: система строится на базе моду- лей, каждый из которых является самостоятельной процес- сорной платой с двумя (или четырьмя) процессорами, вклю- ченными по схеме ЯМР. В каждом модуле расположена своя оперативная память, к которой через общую шину имеют доступ процессоры этого модуля. Логически память каждо- го модуля включена в общую память системы, т. е. память всей системы образована как обьединение памяти каждого из модулей. Физически доступ к памяти других модулей осу- ществляется через быстродействующую коммуникационную шину, которая может быть как общей для всех модулей, так и быть соединением типа «точка-точка» по принципу «все со всеми» (обычно используется комбинация этих способов организации шины). Если программа может быть организо- вана так, что в основном группе процессоров нужна не вся память, а только ее часть (размерои с память модуля), и обмены между этими частями идут не часто (это весьма ре- алистичные предположения, особенно, если память каждого из модулей достаточно велика), то такая программа может эффективно работать на описанной архитектуре. ° Гибридные схемы слабо связанных (распределен- ных) процессоров используются для повышения надежно- сти работы систем на основе архитектуры со слабо связан- ными (распределенными) процессорами. Используются для ускорения работы систем, в которых требуется обрабатывать 
42 Глава 2. Пути повышения производительности процессоров одновременно много процессов и обеспечить отказоустойчи- вость. Вместо слабо связанных (распределенных) процессо- ров используют слабо связанные (распределенные) процес- сорные модули, каждый из которых является самостоятель- ной процессорной платой с двумя (или четырьмя) процессо- рами., включенными по схеме ЯМР. В случае отказа одного из процессоров в модуле, имеется потенциальная возможность передать все процессы оставшимся процессорам. ° Кластерная организация процессоров является част- ным случаем описанных выше архитектур и используется для ускорения работы систем, в которых требуется обраба- тывать одновременно много процессов. Физически кластер строится либо на базе распределенных процессоров, либо на базе слабо связанных процессоров (без разделяемой па- мяти, фактически несколько независимых процессорных систем, объединенных общим корпусом, источниками пита- ния и системами ввода-вывода). Первый способ был более распространен в момент появления понятия «кластер», вто- рой становится популярным в настоящее время в связи с миниатюризацией процессорных систем. Логически кластер представляет собой систему, в которой каждый из процес- соров независим и может независимо от других принимать к исполнению задания. Операционная система направляет вновь пришедший процесс на исполнение тому процессору, который в данный момент менее загружен (т. е. для пользо- вателя вся система выглядит как единая многопроцессорная установка). Еслн коммуникационный канал, связывающий отдель- ные процессорные модули, имеет высокую пропускную спо- собность, то операционная система может эмулировать на такой системе поведение описанной выше системы гибрид- ной ЯМР, а, именно, она может позволить исполнять одно и то же приложение на. нескольких процессорах. Это отно- сится не только к кластерам, но и ко всем архитектурам с распределенными процессорами. 
2.5. Многопроцессорные архитектуры 4 2.5.3. Обанкротившиеся архитектуры В этом разделе мы рассмотрим многопроцессорные архитекту- ры, которые в чистом виде уже не применяются в новых разра- ботках. Однако они оказали значительное влияние на развитие вычислительной техники, и некоторые их черты, уже в новом технологическом воплощении, присутствуют и в современных архитектурах. Основными причинами, приведшими к выходу из употреб- ления этих архитектур (и разорению фирм, выпускавших на их основе ЭВМ), являются: ° необходимость разрабатывать программное обеспечение спе- циально под конкретную RpxHTGKTypy; ° конкуренция с параллельными архитектурами; ° снижение расходов на оборонные разработки (большинство проектов по высокопроизводительным вычислениям финан- cHpoBBJIocb из Военных источников). Основные архитектуры: ° Векторные процессоры — это даже не многопроцессорные архитектуры, а одиночные процессоры, способные выпол- нять операции с векторами как примитивные инструкции. Это может значительно ускорить выполнение программ вы- числительной математики. поскольку в таких задачах основ- ное время работы приходится на обработку векторов и мат- риц. ° Матричные процессоры — это процессоры, способные выполнять операции над матрицами как примитивные ин- струкции. Обычно представляют собой группу процессоров, разделяющих общую память и выполняющих один ноток ин- струкц&gt ° Транспьютеры — это многопроцессорные архитектуры, со- стоящие из независимых процессоров (обычно кратных по количеству 4) каждый из которых имеет свою подсистему памяти. Процессоры связаны между собой высокоскорост- 
44 Глава 2. Пути повышения производительности процессоров ными соединениями, организованными по принципу «точка- точка». Каждый процессор связан с четырьмя другими. 2.6. Поддержка многозадачности и многопроцессорности В современных процессорах поддержка многозадачности и мно- гопроцессорности не ограничивается аппаратной частью (вроде рассмотренного выше механизма синхронизации кэшей). Для организации доступа к критическим разделяемым ресурсам необходимо в наборе инструкций процессора предусмотреть спе- циальные инструкции, обеспечивающие доступ к объектам син- хронизации. Действительно, сами обьекты синхронизации яв- ляются разделяел~ыии, а для обеспечения правильного доступа к разделяемым объектам необходимо вводить объекты синхро- низации, которые в свою очередь тоже являются разделяемы- ми и т. д. Для выхода из этого замкнутого круга определяют некоторые «примитивные» об ьекты синхронизации, к которым возможен одновременный доступ нескольких задач или процес- соров за счет использования специальны инструкций доступа. Рассмотрим более подробно случай булевского семафора. За- дача или процессор, собирающийся взять управление разделя- емым ресурсом, начинают с чтения значения семафора. Если он обнулен, то задача или процессор должны ждать, пока ре- сурс станет доступным. Если семафор установлен в 1, то за- дача или процессор немедленно его обнуляют, чтобы показать, что они контролируют ресурс. В процессе изменения семафо- ра можно выделить три фазы: чтение, изменение, запись. Если на стадии чтения возникнет переключение задач или дру- гой процессор станет главным на шине, то может возникнуть ошибка, так как две задачи или два процессора контролируют один и тот же ресурс. Аналогично, если переключение контек- с га произойдет между циклом чтения и записи, то два процесса могут взять семафор, что тоже приведет к системной ошибке. )IJI&g ;I реше ия э ой пробл мы большинс во процессо ов им I 
2.7. Использование параллелизма процессора инструкцию, выполняющую неделимый цикл чтение — измене- ние- — запись. Поскольку это одна инструкция, то переключение задач во время операции с семафором невозможно. Так как она производит неделимый цикл, то процессор, ее выполняющий, остается владельцем шины до окончания операции с семафором. 2.7. Использование параллелизма процессора для повышения эффективности программ Рассмотрим задачу умножения двух п х п квадратных матриц: С = АВ. Элементы матриц в языке С хранятся в оперативной памяти по строкам: вначале элементы первой строки, затем второй и т. д. Например, последовательность элементов мат- рицы А: < ,1 & t 1 2 ° - &lt l,и l ; , &lt 2,2 ° ° - lt;2 п 6 3 1 ° . Таким образом, последовательные элементы строки матрицы лежат друг за другом и доступ к ним будет максимально быст- рым, так как стратегия предвыборки элементов в кэш (т. е. чтение каждый раз целого блока оперативной памяти, равного по размеру строке кэша, см. раздел 2.4, с. 34) себя полностью оправдывает. С другой стороны, последовательные элементы столбца матрицы лежат на расстоянии и + sizeof (double) байт друг от друга и доступ к ним будет максимально мед- ленным (поскольку строка кэш-памяти существенно меньше расстояния между элементами). Вычислим произведение матриц несколькими способами: 1. Цикл проведем по строкам матрицы С (стандартный спо- соб): для всех т = 1,2,...,п для всех i = 1,2,...,и П ВЫЧИСЛЯТЬ &lt mi j=1 В цикле по g (вычисление суммы) элементы одной строки матрицы А (к которым доступ и так быстрый) будут исполь- 
46 Глава 2. Пути повышения производительности процессоров зованы многократно (в цикле по i), элементы me столбцов матрицы В (к которым доступ относительно медленный) IIQ- вторно (в цикле по i) не используются. Тем самым при досту- пе к элементам В кэш-память практически ничего не дает. 2. Цикл проведем по столбцам матрицы C. для всех m = 1,2,...,и для всех i = 1,2,...,и П ВЫЧИСЛЯТЬ С~~ = < j=1 В цикле по х' (вы хххслеххххе суммы) элементы матрицы А повторно (в цикле по х) не используются. Элементы же столбцов матрицы В используются многократно (в цххкле по i). 1(эхи-ххамять ускоряет доступ к элементам А за счет предвыборкп подряд идущих элементов, и ускоряет доступ к элементам В, если столбец В целиком поместился в кэш-память (тогда в цпкле по iобращепие к :элементам В в оперативной памяти будет только на первом шаге). Отме- тим, что если размер кэш-памяти недостаточен (напрпмер, и велико), то этот способ может оказаться хуже прсдьхдущего, так как мы уху;1хшххли способ доступа к А и не получили выигрыша при доступе к В. 3. Цикл проведем по N x N, N = 10, блокам матрицы С, внутри блока идем по столбцам: для всех 6„, = 1, 1+ Л, 1+ 2N..., 6 ( и для всех 6; = 1,1+ N,1+ 2N..., 6, ( хх для «зсех m=Ь„„Ь +1,...,6,„,+N — 1, т(и для всех i =b,,b;+1,...,6,+N — 1, i (и И Bhl IHCJI 9Th C~~~: Gz~~ j=1 Теперь мы максимально локализуем количество вовлечен- ных в самые внутренние циклы элементов, увелю~чивая тем самым эффективность работы кэш-памяти. Особенно боль- хпой выигрыих в скорости работы мы получим, если исполь- зуемьхе в циклах но т..i, х N строк матрицы А и N столбцов 
2.7. Использование параллелизма процессора матрицы В поместились в кэш-память. Если me и так вели- ко, что даже один столбец В не поместился целиком в кэш, то этот способ может оказаться хуже самого первого. 4. Зададимся параметром N (для простоты ниже предполагает- ся, что и делится на N нацело, хотя программа разработана для произвольного N). Всякая матрица Ы может быть пред- ставлена как составленная из блоков: Ml l Ml 2 М1/с М21 M22 .. 1lI2k М= ° ° ° ° ° ° ° ° ° М1,1 М~2 ... Мц„ где М, — N x N матрица, Й = п1И. Тогда каждый блок С;„, произведения С = АВ матриц А и В маркет быть вычислен через блоки матриц А и В: A С,=~А;В j=1 В вычислении произведения N x Nматриц А, ,и В, участ- вует только подмножество из N~ элемеггов матриц А и В. При небольшом N (в нашем тесте N = 40) это подмножество полностью поместится в кэш-памяти и каждое слагаемое по- следней суммы будет вычислено максимально быстро. 5. В предыдущем варианте мы уже практически исчерпали все преимущества, которые дает нам кэш-память. Для получе- ния дальнейшего прироста производительности вспомним, что все современные процессоры имеют конвейер инструк- ций (см. раздел 2.3, с. 26), причем весьма глубокий (может доходить до полутора десятков стадий). Для его заполнения длина линейного участка программы (т. е. не содержащего команд перехода) должна быть как минимум больше глу- бины конвейера (желательно — в несколько раз больше). Во всех же предыдущих вариантах в самом внутреннем цикле (по g) находится 1 оператор языка С, который, в зависимо- сти от целевого процессора, транслируется компилятором в 
48 Глава 2. Пути повышения производительности процессоров 7... 12 инструкций (включая операторы обслуживания цик- ла). Для увеличения длины линейного участка программы «развернем» цикл в предыдущем варианте: за один виток цикла по 7 будем вычислять сразу 4 элемента матрицы C: cj m cj щ»], cj+] щ. cj+] щ»]. Поскольку при их вычислении используются повторяющиеся элементы матриц А и В, то также появляются дополнительные резервы для ускорения работы за счет оптимизации компилятора и кэш-памяти. Исходный текст этих пяти вариантов приведен ниже. ¹include "mmult h" /+ Умножить матрицу а на матрицу Ь, с = ab, цикл по строкам с +/ void matrix mult matrixi (double *a, double ~Ь, double ~с, int n) int i, j, m; double ~ра, ~рЬ, ~рс, s; & for (m = О, pc = c; m & t; и; m for (i = О, pb = b; i & t; и; i +, pb for (s = 0., j = О, pa = a + m ~ n; j & t; n; j s += ~(ра++) + pb[j + n]; ~(рс++) = s; /+ Умножить матрицу а на матрицу Ь, с = аЬ, цикл по столбцам с */ void matrix mult matrix 2 (double ~а, double ~Ь, double ~с, int n) 
2.7. Использование параллелизма процессора 49 с int i, j, m; double *ра, *рЬ, *рс, s; for (m = О, pc = c; m & t; и; m +, pc for (i = О, ра = а, рЬ = Ь + m; i & t; n; i с for (s = 0., j = О; j & t; п; j s += ~(ра++) + pb[j + n]; pc[i ~п] =s; /+ Размер блока +/ ¹define N 10 /+ Умножить матрицу а на матрицу Ь, с = аЬ, цикл по блокам с +/ void matrix mult matrix 3 (double ~а, double ~Ь, double ~с, int n) с int bm, bi, nbm, nbi; int i, j, m; double *ра, *рЬ, *рс, s; for (bm = О; bm & t; и; bm += с nbm = (bm + N &l ; ? b : for (bi = О; bi & t; п; bi += с nbi = (Ьх + И &l ; ? Ь : 
50 Глава 2. Пути повышения производительности процессоров /+ Вычислить результирующий блок матрицы с с верхним левым углом (bi, bm) и правым нижним (nbi-1, nbm-1) +/ & for (m = bm, рс = с + bm; m & t; n m; m +, рс for (i = bi, ра = а + bi + и, рЬ = Ь + m; i & t; n i; i с for (s = О., j = 0; j & t; и; j s += ~(ра++) + pb[j + n]; pc[i *n] = s; & & ¹undef N /+ Размер блока +/ ¹define N 40 /+ Умножить матрицу а на матрицу b, с = аЬ, блочное умножение матриц +/ void matrixmult matrix4 (double ~а, double ~Ь, double ~с, int n) с int bm, bi, nbm, nbi; int 1, nl; int i, j, m; double *ра, *рЬ, *рс, s; for (bm = 0; bm & t; и; bm += с nbm = (bm + N &l ; ? b : 
1 2.7. Использование параллелизма процессора for (bi = О; bi & t; и; bi += & nbi = (bi + N &l ; ? b : /+ Вычислить результирующий блок матрицы с с верхним левым углом (bi, bm) и правым нижним (nbi-1, nbm-1) +/ /+ Вычисляем как произведения блоков матриц а и Ь */ for (m = bm, рс = с + bm; m & t; n m; m +, рс for (i = bi; i & t; n i; i pc[i + n] = О.; for (1 = О; 1 & t; и 1 += & nl = (l + N &l ; : /+ Вычисляем произведение блока матрицы а [(о1,1) х (nbi-1,nl-1)] на блок матрицы Ь [(1,Ьш)х(п1-1,nbm-1)] и прибавляем к блоку матрицы с [(bi,bm) х (nbi-1,nbm-1)] +/ for (т = Ьт, рс = c+bm; m & t; n m; m +, рс for (i = bi, pb = b + m; i & t; n i; i & ра = а + 1 + i + n; for (s = О., j = 1; j & t; l; j s += *(pa++) * pb[j + n]; pc[i + и] += s; ¹undef N 
52 Глава 2. Пути повышения производительности процессоров /+ Размер блока, должен делиться на 2 +/ ¹define И 40 /+ Умножить матрицу а на матрицу Ь, с = аЬ, блочное умножение матриц, внутренний цикл развернут +/ /+ Работает только для четных и (для всех и придется усложнять разворачивание цикла) */ void matrix mult matrix 5 (double ~а, double ~Ь, & double ~с, int n) int bm, bi, nbm, nbi; int 1, nl; int i, j, m; double *ра, *рЬ, *рс; double s00, sOi, siO, sii; for (bm = 0; bm & t; и; bm += & nbm = (bm + N &l ; ? b : for (bi = 0; bi & t; n; bi += & nbi = (bi + N &l ; ? b : /* Вычислить результирующий блок матрицы с с верхним левым углом (bi, bm) и правым нижним (nbi-1, nbm-1) +/ /+ Вычисляем как произведения блоков матриц а и Ь */ for (m = bm, рс = с + bm; m & t; n m; m +, рс for (i = bi; i & t; n i; i pc[i ~ и] = О.; for (1 = 0; 1 & t; и 1 += 
2.7. Использование параллелизма процессора nl = (1 + N &l ; + N: /+ Вычисляем произведение блока матрицы а [(bi,1) х (nbi-1,nl-1)] на блок матрицы Ь [(1,Ьш)х(п1-1,nbm-1)] и прибавляем к блоку матрицы с [(bi, bm) х (nbi-1, nbm-1) ] +/ for (m = bm, рс = с + bm; m & t; n m += 2, рс += 2) for (i = bi, pb = b + m; i & t; n i i += ра = а + 1 + i + n; s00 = sOi = siO = sii = О.; for (j = 1; j & t; l; j +, ра & /+ элемент (i, m) +/ s00 += ра[0] * pb[j + n]; /+ элемент (i, m + 1) +/ s01 += ра[0] + pb[j + и + 1]; /e элемент (i. + 1, m) e/ s10 += pa[n] + pb[j + n]; /+ элемент (i + 1, m + 1) е/ sll += ра[п] + рЬ[j * и + 1]; pc[i + n] += s00; pc[i + и+ 1] += sOi; pc [(i + 1) * и] += s10; pc[(i + 1) * и + 1] += в11; Отношение времени работы первого варианта (стандартно- го) к времени работы каждого из 5 вариантов для матриц 
54 Глава 2. Пути повышения производительности процессоров Таблица 2.1. Соотношение скорости работы различныгс алгоритмов умножения матриц Процессор Pentiuni Ш Pentium Ш Pa~ver PC 603е Размерность 1000 2000 1000 Алгоритм 1 1.0 1.0 1.0 Алгоритм 2 1.3 1.0 1.0 Алгоритм 3 1.9 1.0 1.0 Алгоритм 4 2.3 7.б 5.6 Алгоритм 5 4.3 13.8 11.0 1000 х 1000 и 2000 х 2000, вычисленное на процессорах Intel Pentium III и !ВХ!/ 'Iotorola PoxverPC 603e, приведено в таб- лице 2.1. Отметим, что просто переписав программу с учетом наличия кэш-памяти и конвейера инструкций мы получили более чем 10-кратное ускорение на одном процессоре, без использования параллельной вычислительной установки! По просьбам штателей ниже приво~ится текст описанш rx выше вариантов умно~кения магриц на языке FORTRAN-77. С Умножить матрицу а на матрицу b, с = аЬ, С цикл по строкам с subroutine matrix mult matrix 1 (а, Ь, с, n) геа1~8 а (и, и), Ь (и, и), с (n, n) integer n integer i, j, m геа1~8 s do m = 1, и do i = 1, и s = О.dO бо ] = 1, Il s = s + à (m, j) + Ь (j, i) 
2.7. Использование параллелизма процессора end do с (m, i) = s end do end do end subroutine С Умножить матрицу а на матрицу b, с = аЬ, С цикл по столбцам с subroutine matrix mult matrix 2 (а, b, с, n) геа1~8 а (и, и), Ь (и, n), с (n, n) integer n integer i, j, m геа1~8 s do m = 1, и do i = 1, и s = О.dO do j = 1, и s = s + a (i, ]) ~ Ь (), m) end do c(i,m)=s end do end do end subroutine С Умножить матрицу а на матрицу b, с = аЬ, С цикл по блокам с subroutine matrixmult matrix 3 (а, Ь, с, n) геа1~8 а (и, и), b (n, n), с (n, n) integer n integer bm, bi, nbm, nbi integer i, j, m геа1~8 s 
56 Глава 2. Пути повышения производительности процессоров t parameter (nn = 10) do bm = i, n, nn if (bm + nn .le. n + 1) then nbm = bm + nn else nbm = n + 1 endif do bi = i, n, nn if (bi + nn .le. n + 1) then nbi = bi + nn else nbi = n + i endif С Вычислить результирующий блок матрицы с С с верхним левым углом (bi, bm) С и правым нижним (nbi-1, nbm-1) do m = bm, nbm — 1 do i = bi, nbi — 1 s = О.dO do j = 1, и s=s+a(i,j)+b(j,m) end do с (i, m) = s end do end do end do end do end subroutine С Умножить матрицу а на матрицу b, с = аЬ, С блочное умножение матриц subroutine matrix mult matrix 4 (а, Ь, с, n) геа1~8 а (и, и), Ь (n, n), с (n, n) integer n 
2.7. Использование параллелизма процессора integer bm, bi, nbm, nbi integer 1, nl integer i, j, m геа1~8 s parameter (nn = 40) do bm = i, n, nn if (bm + nn .le. n + 1) then nbm = bm + nn else nbm = n + 1 endif do bi = i, n, nn if (bi + nn .le. n + 1) then nbi = bi + пп else nbi = n + 1 endif С Вычислить результирующий блок матрицы с С с верхним левым углом (bi bm) С и правым нижним (nbi-1, nbm-1) С Вычисляем как произведения блоков матриц С аиЬ do m = bm, пЬа — 1 do i bi, nbi - 1 с (i, m) = О.dO end do end do do 1 = i, n, пп if (1 + nn .le. и + 1) then nl = 1 + nn else nl =n+1 
58 Глава 2. Пути повышения производительности процессоров endif С Вычисляем произведение С блока матрицы а [(Ы,1) х (nbi-1,nl-1)] С на блок матрицы Ь [(1, bm) x (nl-1, nbm-1) ] С и прибавляем к блоку С матрицы с [(Ы, bm) х (nbi-1, nbm-1) ] do m = bm, nbm — 1 do i = bi, nbi — 1 s = О.dO do j = 1, nl — 1 s=s+a(i, j) *Ь (), m) end do с (i, m) = с (i, m) + s end do end do end do end do end do end subroutine С Умножить матрицу а на матрицу b, с = аЬ, С блочное умножение матриц, внутренний цикл развернут С Работает только для четных и (для всех и придется С усложнять разворачивание цикла) subroutine matrix mult matrix 5 (а, Ь, с, n) геа1~8 à (n, и), Ь (n, n), с (n, n) integer n integer bm, bi, nbm, nbi integer 1, nl integer i, j, m геа1~8 s00, sOi, siO, sii parameter (nn = 40) do bm = i, n, nn 
2.7. Использование параллелизма процессора if (bm + nn .le. и + 1) then nbm = bm + nn else пЬа = и + 1 endif do bi = 1, n, nn if (bi + nn .le и + 1) then пЫ =bi+nn else nbi = n + i endif С Вычислить результирующий блок матрицы с С с верхним левым углом (bi, bm) С и правым нижним (nbi-1, nbm-1) С Вычисляем как произведения блоков матриц С аиЬ do m = bm, nbm — 1 do i bi, nbi - 1 с (i, m) = О.dO end do end do do 1 = i, if (1 + nn .le. и + 1) then nl = 1 + nn else -п1 = и + 1 endif С Вычисляем произведение С блока матрицы а [(Ы,1) х (nbi-1,n1-1)] С на блок матрицы Ь [(1,bm)x(nl-1,nbm-1)] С и прибавляем к блоку С матрицы с [(Ы,bm) х (nbi-1,nbm-1)] do m = bm, nbm — i, 2 do i = bi, nbi — 1, 2 
60 Глава 2. Пути повышения производительности процессоров s00 = О.dO sOi = О.dO siO = О.dO sii = О.dO do j = 1, nl — 1 С элемент (i, m) s00 = s00 + à (i, j) + Ь (j, m) С элемент (i, m + 1) s01 = s01 + à (i, j) + Ь (j, m + 1) С элемент (i + 1, m) s10 = s10 + à (i + 1, j) + b (j, m) С элемент (i + 1, m + 1) sii = sii + à (i + 1, j) ~ Ь (j, m+1) end do с (i, m) = с (i, т) + s00 с (i, m + 1) = с (i, m + 1) + s01 с (i + 1, m) = с (i + 1, m) + s10 с (i + 1, m + 1) = с (i + 1, m+1) + s11 end do end do end do end do end do end subroutine Отметим, что пз-за хранения матриц по столбцам при п = 1000 на Intel Pentium III скорости работы первого и второго ал- горитмов меняются местами; при и = 2000 Hà Illtel РеШшш III алгоритм 5 в 16 раз превосходит по скорости алгоритм 1. 
Пути повышения производительности оперативной памяти К сожалению, разрыв в производительности процессора и опера- тивной памяти увеличивается с каждым годом. Дело в том, что в процессор не только внедряют все более изощренные механиз- мы ускорения его работы, но и поднимают его тактовую часто- ту. Последнее не годится для оперативной памяти (по крайней иере, для ЭВМ общего назначения), поскольку это приводит к значительному повышению выделения тепла, которое не уда- ется отводить обычными способами. (Заметим, что существу- ют ЭВМ, где использована значительная частота оперативной памяти, расплатой за что является необходимость охлаждения модулей памяти водяным либо ультразвуковым воздушным по- током.) Поэтому в настоящий момент частота процессора может более, чем в 10 раз превосходить частоту оперативной памяти. С учетом параллелизма в работе процессора это означает, что за время одного обращения к памяти он может выполнить более 20 инструкций (!), таких как перемножение пары вещественных чисел. Для повышения производительности оперативной памяти применяют несколько приемов, идеологически схожих с мето- дами ускорения работы процессора. Именно, оперативная па- мять должна параллельно обрабатывать (т. е. принимать (за- 
62 Глава 3. Повышение производительности оперативной памяти пись) или выдавать (чтение)) каг можно большее ко.лпчество данных: ° Увеличение ширины шины данных: переход от Я1ММ (32-битная шина) к модулям Р1ММ, DDR (64-, 128- или 256- битная шина) при построении подсистемы памяти. Это дает прирост в 2, 4 или 8 раз при доступе к последовательным данным (т. е. когда стратегия предвыборки строки кэша себя оправдывает). ° Введение небольшой статической памяти ЯВАМ для буферизации DRAM-модулей: буферизованные Р1ММ- модули. Этот способ ап шогичсн введению кэша в процессор. ° Введение конвейера в модули DRAM. Используется та же идея, что и п1&g ;и введе ии конвей р в процессо ы. Вн ри модуля памяти находится несколько обрабатываемых об- ращений к памяти в разной степени готовности (формиро- вание адреса, выбор банка, выборка да~ных, запись их на внешнюю шину и т. д.). Это позволяет модулю памяти при- нимать/выдавать данные каждый цикл шины (при усло- вии оптимального функционирования конвейера). В силу этого такая память получила название вупсЬгопопв РВАМ (ЯРВАМ). Для такой памяти в качестве времени досту- па производители в рекламных целях пишут ъ~инимальный цикл шины, что дает фантастические времена доступа ме- нее 10нс (т. е. частота шины более 100МГц). На самом же деле в модулях ЯРВАМ использованы обычные микросхемы памяти, в1> мя дост п к кото ым Ђ” ок ло 60 ° «Расслоение» оперативной памяти. Поскольку процес- сор обменивается с памятью только блоками размером со строку кэша, то можно разделить этот блок Hà N частей (N обычно '2, 4, 8) и передать каждую из частей своей подсисте- ме памяти. В результате получаются N подсистем памяти, работакпцих параллельно. Если программа требует последо- вательные адреса памяти (т. е. стратегия предвыборки стро- 
Глава 3. Повышение производительности оперативной памяти 63 ки кэша себя оправдывает), то этот подход может в N раз увеличить производительность подсистемы памяти. Подчеркнем еще раз, что все описанные приемы (как и в си- туации с процессором) дают выигрыш только для «правильно» устроенной программы, т. е. той, которая в основном обраща- ется к последовательно расположенным данным и за каждое обращение читает/записывает блок памяти, не меньший шири- ны шины. 
Организация данных BO ВНЕШНЕИ ПаМЯтИ Многопроцессорные системы иногда строятся на процессорах разных производителей. Этп процессоры решают общую зада- чу и обмениваются между собой данными через разделяемую память или коммуникационный канал. В этой ситуации необхо- димо согласование представления данных в памяти каждым из участвующих в обмене процессоров. При доступе к данным (целочисленным или с плавающей точкой) основным вопросом является способ нумерации байтов в слове. Если бы в 32-битной архитектуре минимальным адресуемым элементом оперативной памяти являлось 32-битное слово, то во- прос о нумерации байтов в слове не вставал бы. В реальной ситу- ации, когда минимальным адресуемым элементом оперативной памяти является байт (8-битное слово), данные большего раз- мера образуются как объединение подряд идущих байт. Выбор нумерации байт в 32-битном (4 байта) слове может быть произ- вольн~~м что дает 24 = 4! способа. На практике используются только два: ABCD (называемый big-endian) и РСВА (называе- мый little-endian). В модели big-endian байты в слове нумеруются от наиболее значимого к наименее значимому. В модели little-endian байты в слове нумеруются от наименее значимого к наиболее значимо- му. Примеры big-endian-процессоров: ihlotorola 68ххх, PowerPC 
6 Глава 4. Организация данных во внешней памяти (по умолчанию), SPARC; пример little-endian-процессора: Intel 80х86. Процессоры PowerPC, Intel 80960x, ARM, SPARC (64- битные модели) могут работать как в big-endian-режиме, так и в little-endian. Если процессоры, осуществляющие обмен данными, исполь- :зуют разный режим нумерации байтов, то потребуется преобра- :ювывать все полученные или переданные данные. Практически »се современные процессоры имеют для этой цели специальную инструкцию. Все современные процессоры поддерживают в организации данных с плавающей точкой стандарт ANSI/IEEE 754-1985. По- этому дополнительных проблем в этом случае не возникает. Необходимо только учитывать, что данные с плавающей точ- кой содержат несколько байт, поэтому на них также оказывает влияние способ нумерации байтов в слове. 3 - 4017 
Основные положения В этой главе мы введем основные понятия, используемые при рассмотрении любой процэаммной системы. 5.1. Основные определения Определение. Программа — это описание на некотором фор- мализованноъх языке алгоритма, решающего поставленную за- дачу. Программа является статической единицей, т. е. неизменя- емой с точки зрен11я операционной системы, ее выполняющей. Определение. Процесс — это динамическая сущность про- граммы, ее код в процессе своего выполнения. Имеет ° собственные области памяти под код и данные, ° собственный стек, ° (в системах с виртуальной памятью) собственное отображе- ние виртуальной памяти на физическую, ° собственное состояние. Процесс может находиться в одном из следующих типичных со- стояний (точное количество и свойства того или иного состоя- ния зависят от операционной системы): 1. «остановлен» — процесс остановлен и не использует процес- сор: например, в таком состоянии процесс находится сразу после создания:, 
5.1. Основные определения 67 Адрес ~( ~( P вр «р врв в:р» :™. В~щвввр мв $, у~ P в»вв»рв«'»' c4" :, "<'~ : 'ф.. sp -р ввв'ввврвввр'.ввр%4%вв:;вв в«в..":."'..:».' -'Ж4@.ж.- О а) б) в) г) Рис. 5.1. Виды стеков 2. «терминирован» — — процесс терминирован и не использует процессор; например, процесс закончился, но еще не удален операционной системой; 3. «ждет» — процесс ждет некоторого события (которым может быть аппаратное или программное прерывание, сигнал или другая форма межпроцессного взаимодействия); 4. «готов» — процесс не остановлен, не терминирован, не ожи- дает, не удален, Но и не работает; например, процесс может не получать доступа к процессору, если в данный момент выполняется другой, более приоритетный процесс; 5. «выполняется» -- процесс выполняется и использует процес- сор. Определение. Стек (stack) — это область памяти, в ко- торой размещаются локальные переменные, аргументы и воз- вращаемые значения функций. Вместе с областью статических данных полностью задает текущее состояние процесса. По ло- гическому устройству стеки бывают четырех видов (рис. 5.1): а) full descending — растет вниз (к младшим адресам памяти), указатель стека (SP, Stack Pointer) указывает на последшою занятую ячейку стека (называемую вершиной стека); б) empty descending — растет вниз, SP указывает на первую свободную ячейку стека за ero вершиной; 
Глава 5. Основные положения в) full ascending — - растет вверх, SP указывает на первую за- нятую ячейку стека; г) empty ascending — растет вверх, SP указывает на первую свободную ячейку стека за его вершиной. В ряде процессоров тип стека закреплен аппаратно (напри- мер, в Мо1ого1а 68ххх и Intel 80х86 используется стек 5.1а, в Intel 809бО — 5.1в), для других процессоров тип стека может выбираться разработчиками операционных систем произволь- но (практически всегда выбирается стек 5.1а). Определение. Виртуальная память — это «память», в адресном пространстве которой работает процесс. Виртуальная память: 1. позволяет увеличить объем памяти, доступной процессам за счет дисковой памяти; 2. обеспечивает выделение каждому из процессов виртуально непрерывного блока памяти, начинающегося (виртуально) с одного и того же адреса; 3. обеспечивает изоляцию одного процесса от другого. Трансляцией виртуального адреса в физический занимает- ся операционная система. Для ускорения этого процесса многие компьютерные системы имеют поддержку со стороны аппара- туры, которая может быть либо прямо в процессоре, либо в специальном устройстве управления памятью. Среди механиз- мов трансляции виртуального адреса преобладает страничный, при котором виртуальная и физическая память разбиваются на куски равного размера, называемые страницами (типичный раз- мер — 4Кб), между страницами виртуальной и физической па- мяти устанавливается взаимно-однозначное (для каждого про- цесса) отображение. Определение. Межпроцессное взаимодействие — это тот или иной способ передачи информации из одного процесса в другой. Наиболее распространенными формами взаимодействия 
5.1. Основные определения 69 являются (не все системы поддерживают перечисленные ниже возможности 1. Разделяемая память — два (или более) процесса имеют до- ступ к одному и тому же блоку памяти. В системах с вир- туальной памятью организация такого вида взаимодействия требует поддержки со стороны операционной системы, по- скольку необходимо отобразить соответствующие блоки вир- туальной памяти процессов на один и тот же блок физиче- ской памяти. 2. Селсафоры — два (или более) процесса имеют доступ к одной переменной, принимающей значение О или 1. Сама перемен- ная часто находится в области данных операционной систе- мы и доступ к ней организуется посредством специальных функций. 3. Сигналы — 3TQ сообщения, доставляемые процессу посред- ством операционной системы. Процесс должен зарегистри- ровать обработчик этого сообщения у операционной систе- мы, чтобы получить возможность реагировать на него. Ча- сто операционная система извещает процесс сигналом о на- ступлении какого-либо сбоя, например, делении на О, или о каком-либо аппаратном прерывании, например, прерывании таймера. 4. Почтовые ящики — это очередь сообщений (обычно — тех или иных структур данных), которые помещаются в по- чтовый ящик процессами и/или операционной системой. Несколько процессов могут ждать поступления сообщения в почтовый ящик и активизироваться по ero поступлении. Требует поддержки со стороны операционной системы. Определение. Событие — это оповещение процесса со сто- роны операционной системы о той или иной форме межпроцесс- ного взаимодействия, например, о принятии семафором нуж- ного значения, о наличии сигнала, о поступлении сообщения в ПОЧТОВЫЙ ЯЩИК. 
Глава 5. Основные положения Создание, обеспечение взаимодействия. разделение процес- сорного времени требует от операционной системы значитель- ных вычислительных затрат, особенно в системах с виртуальной памятью. Это связано прежде всего с тем, что каждый процесс имеет свое отображение виртуальной памяти на физическую, которое надо менять прн переключении процессов и при обес- печении их доступа к объектам взаимодействия (общей памяти, семафорам, почтовым ящикам). Очень часто бывает так, что требуется запустить несколько копий одной и той &gt ке прогр мы, например, для использования всех процессоров системы. В этом случае мы несем двойные накладные расходы: держим в оперативной памяти несколько копий кода одной программы и еще тратим дополнительное время нъ обеспечение их взаимо- действи». ~'~~учшает ситуацию введение задач. Определение. Задача (или поток, или нить, thread)— это как бы одна из ветвей исполнения процесса: ° разделяет с процессом область памяти под код и данные, ° имеет собственный стек, ° (в системах с виртуальной памятью) разделяет с процессом отображение виртуальной памяти на физическую, ° имеет соГ~ственное состояние. Таким образом, у двух задач в одном процессе вся память явля- ется разделяемо~~. и дополнительные расходы, связанные с раз- ным отображением виртуальной памяти на физическую, сведе- ны к нулю. Для задач так > е, ак ля процесс в, определяю понятия состояния задачи и иежзадачного взаимодействия. От- метим, что для двух процессов обычно требуется организовать что-то общее (память, канал и т. д.) для их взаимодействия, в то время как для двух потоков часто требуется организовать что-то (например, область памяти), имеющее свое значение в каждом из них. Определение. Ресурс — это объект, необходимый для ра; боты процессу или задаче. 
5.1. Основные определения 1 Определение. Приоритет — это число, приписанное опе- рационной системой каждому процессу и задаче. Чем больше это число, тем важнее этот процесс или задача и тем больше процессорного времени он или она получит. Если в операционной системе могут одновременно существо- вать несколько процессов или/и задач, находящихся в состоя- нии «выполняется», то говорят, что это многозадачная система, а эти. процессы называют параллельными. Отметим, что ес- ли процессор один, то в каждый момент времени на самом деле реально выполняется только один процесс или задача. Система разделяет время между такими «выполняющимися» процесса- ми/задачами, давая каждому из них квант времени, пропорци- ональный его приоритету. Определение. Связывание (линковка, linkage) — это про- цесс превращения скомпилированного кода (объектных моду- лей) в загрузочный модуль (т. е. то, что может исполняться процессором при поддержке операционной системы). Различа- ют: ° статическое связывание, когда код необходимых для ра- боты программы библиотечных функций физически добав- ляется к коду объектных модулей для получения загрузоч- ного модуля; ° динамическое связывание, когда в результирующем за- грузочном модуле проставляются лишь ссылки на код необ- ходимых библиотечных функций; сам код будет реально до- бавлен к загрузочному модулю только при его исполнении. При статическом связывании загрузочные модули получают- ся очень большого размера. Поэтому подавляющее большин- ство современных операционных систем использует динамиче- ское связывание, несмотря на то, что при этом начальная за; грузка процесса на исполнение медленнее, чем при статическом связывании из-за необходимости поиска н загрузки кода нуж- ных библиотечных функций (часто только тех из них, которые не были загружены для других процессов). 
Глава 5. Основные положения 5.2. Виды ресурсов По своей природе ресурсы можно разделить на ° аппаратные: — процессор, — область памяти, — периферийные устройства, — прерывания, ° программные: — программа, — данные, — файлы, — сообщения. По своим характеристикам ресурсы разделяют на: ° активные: — способны изменять информацию (процессор), ° пассивные: — способны хранить информацию, ° локальные: — принадлежат одному процессу; время жизни совпадает с временем жизни процесса, ° разделяемые: — могут быть использованы несколькими процессами; суще- ствуют, пока есть хоть один процесс, который их исполь- зует, ° постоянные: — используются посредством операций «захватить» и «осво- бодить», ° временные: — используются посредством операций «создать» и «уда- лить» ° Разделяемые ресурсы бывают (рис. 5.2): ° некритичными: 
5.3. Типы взаимодействия процессов Процесс 1 Разделяемый Процесс 2 некритичный Процесс 3 ресурс Разделяемый Процесс 1 Процесс 2 Процесс 3 критичный ресурс Рис. 5.2. Виды разделяемых ресурсов — могут быть использованы одновременно несколькими про- цессами (например, жесткий диск или канал Ethernet); ° критичными: — могут быть использованы только однил1 процессом, и пока этот процесс не завершит работу с ресурсом, последний не доступен другим процессам (например, разделяемая память, доступная на запись). 5.3. Типы взаимодействия процессов По типу взаимодействия различают ° независимые процессы (рис. 5.3); Процесс 1 Процесс 2 Ресурс 1 Ресурс 2 Рис. 5.3. Типы взаимодействия процессов: независимые процессы 
Глава 5. Основные положения ° сотрудничающие процессы (рис. 5.4): — процессы, разделяю~цие только коымуникационный ка- на.л, по которому один передает данные, а другой их по- лучает; — процессы, осуществляющие взаимную синхронизацию: ко- гда работает один, другой ждет окончания его работы; ° конкурирующие процессы (рис. 5.5): — процессы, использующие совместно разделяемый ресурс; — процессы, использующие критические секции; — процессы, использующие взаимные исключения. Процесс 1 Процесс 2 Разделяемый ресурс Рис. 5.4. Типы взаимодействия процессов: сипхрош1зирующиеся процессы Про] ~«сс Процесс 2 Разделяемый ресурс Рис. 5.5. Типы вша.имодействия процессов: конкурирующие процессы 
5.3. Типы взаимодействия процессов Определение. Критическая секция -- это участок про- граммы, на котором запрещается переключение задач для обеспечения исключительного использования ресурсов текущим процессом (задачей). Большинство операционных систем обще- го назначения не предоставляют такой возможности пользова- тельским программам. Определение. Взаимное исключение (mutual exclusion, mutex) — 3TO способ синхронизации параллельно работаю- щих процессов (задач), использующих разделяемый постоян- ный критичный ресурс. Если ресурс занят, то системный вызов «захватить ресурс» переводит процесс (задачу) из состояния вы- полнения в состояние ожидания. Когда ресурс будет освобожден посредством системного вызова «освободить ресурс», то этот процесс (задача) вернется в состояние выполнения и продолжит свою работу. Ресурс при этом перейдет в состояние «занят». Если процессы независимы (не имеют совместно используе- мых ресурсов), то синхронизация их работы не требуется. Если же процессы используют ралделясмый ресурс, то их деятель- ность необходимо синхронизировать. Например при использо- вании общего блока памяти проблемы могут возникнуть даже если один процесс (задача) только штает данные а другой- толи ко пишет. На, рпс. 5.6показан пример, в котором ,два про- цесса P и Я увеличивыот на 1 значение разделяемой ячейки памяти VAL. Результат работы будет различным в случае пла- нирования задач 5.6 а и 5.6 б. При синхронизации задач необходимо борогься с тремя про- блема.ми: 1. «блокировка» («lockout»): ° процесс (задача) ожидает ресурс, который никогда не освободи гся, 2. «тупик» («cleadlock»): ° два процесса (задачи) владеют каждый по ресурсу и ожи- дают освобождения ресурса, которым владеет другой про- цесс (задача), 
Глава 5. Основные положения Задача P Задача. Q х=УА? y=VAL х=х+1 у=у+1 VAL=x VAL=y VAL Разделяемый ресурс A Рг Рз Р1 P Рз ql Q2 Q3 Q Q аз 1 1 1 1 1 1 1 1 1 1 1 t) ~г ty t2 а) 6) Примеры планирования задач Рис. 5.6. Работа с разделяемым ресурсом 3. «застой» («starvation»): ° процесс (задача) монополизировал процессор. Для минимизации этих проблем используются следующие идеи. ° Количество ресурсов ограничено, поэтому нельзя допускать создания задач, для которых недостаточно ресурсов для вы- полнения. ° Задачи делятся на группы: — неактивные задачи, которым не хватило даже пассив- ных ресурсов, и ожидающие событий задачи; таким за- дачаи активный ресурс (процессор) не дается вообще; — готовые задачи, у которых есть все необходимые пассив- ные ресурсы, но нет процессора; являются кандидатами на получение процессора в случае его освобождения; 
5.4. Состояния процесса — выполняющиеся задачи, у которых есть все необходи- мые пассивные ресурсы и процессор. 5.4. Состояния процесса Рассмотрим более детально состояния процесса и переходы из одного состояния в другое (рис. 5.7). Состояния: 1. не существует, 2. не обслуживается, 3. готов, 4. выполняется, 5. ожидает ресурс, 6. ожидает назначенное время, 7. ожидает события. Переходы из состояния в состояние: 1. переход 1 — 2: создание процесса 2. переход 2 — 1: уничтожение процесса 3. переход 2 — 3: активизация процесса диспетчером 1. Не суще- 1 з 2. е обсу» ае с 3. оо стВует Ъ ,/ 6 5 1~', / I 13 / 4. выполняется ° Ф / 14. ° Ф / I ° Ф / I / 10 I r 8 9 12 У Ю I / ° Ф / б. ожидает 5. ожидает 7. ожидает назначенное ресурс события время Рис. 5.7. Состояния процесса 
Глава 5. Основные положения 4. переход 3 — 2: дезактивизация процесса 5. переход 3 — 4: загрузка на выполнение процесса диспетчером 6. переход 4 — 3: требование обслуживания от процессора дру- гим процессом 7. пере~од 4 — 2: завершение процесса 8. переход 4-5: блокировка процесса до освобождения требуе- мого ресурса 9. переход 4-6: блокировка процесса до истечения задашюго времсни 10. переход 4 — 7: блокировка процесса до прихо.~а события 11. переход 2 — 6: актшизацня процесса приводит к ожнданик& временной задержки 12. перекод 2--7: «ктивизация процесса приводит к ожидани1о со- бытия 13. переход 2-5: агтшзизация процесса приводит к ожиданию освобождения ресурса 14. перехо~~ 5 — 3: активизация процесса из-за освобождения ожн- )$3BIIIf.'ГОСЯ р 'CVp( R 15. переход 6 — 3: активизация процесса по истечении заданного «ремнями 16. переход 7--3: активи ~ация процесса из-за прихода ожидавше- Гося соОь1тня 
Стандарты на операционные системы UNIX На подавляющем большинстве параллельных вычислительных установок используется тот или иной вариант операционной си- стемы UNIX. B этом разделе мы рассмотрим несколько суще- ствующих стандартов на UNIX-системы. Как и во многих дру- гих областях, стандарты стали появляться лишь после того, как yme был создан ряд диалектов UNIX. Основной целью введения стандартов является облегчение переноса программного обеспе- чения из одной системы в другую. Мы кратко рассмотрим несколько стандартов. Подробно бу- дет рассмотрен только наиболее развитый из них — - РОЯХ. 6.1. Стандарт BSD 4.3 BSD 4.3 (Berkley Software Distribution версии 4.3) — это да&gt кс стандарт на UNIX-системы, а эталонная реализация UiVIX, вы- пс>лнен а в Калифорнийс ом университ те г. Бер л в в 1980-е годы. Она оказала очень болыпое влияние на дальней- шее развитие UNIX. 6.2. Стандарт UNIX System V Release 4 UNIX System V Release 4 — это тоже не стандарт на UNIX- системы, а эталонная реализация LNIX, выполненная фирмой 
80 Глава б. Стандарты на операционные системы UNIX АТЙТ и включающая в себя особенности большинства суще- ствовавших UNIX-систем. Она оказала очень большое влияние на дальнейшее развитие UNIX. 6.3. Стандарт POSIX 1003 Стандарт POSIX (Portable Operating System Interface), разрабо- танный IEEE (Institute of Electrical and Electronical Engineers), состоит из следующих частей. 1. POSIX 1003.1 — определяет стандарт на основные компо- ненты операционной системы, API (application interface) для процессов, файловой системы, устройств и т. д. 2. POSIX 1003.2 — определяет стандарт на основные утилиты. 3. POSIX 1003.1b — определяет стандарт на основные расши- рения «реального времени». 4. POSIX 1003.1с — - определяет стандарт на задачи (threads). 5. POSIX 1003.1d — определяет стандарт на дополнительные расширения «реального времени» (такие, как, например, поддержка обработчиков прерываний). Стандарт POSIX 1003 является наиболее всеобъемлющим из всех рассмотренных ранее. Большинство существующих UNIX-систем были адаптированы для удовлетворения этому стандарту. 6.4. Стандарт UNIX Х/Open Стандарт UNIX X/Ореп Portability Guide 3 — это стандарт на UNIX-системы, разработанный группой фирм-разработчиков программного обеспечения. Этот стандарт сокращенно называ- ют XPG3. В 2000г. ywe вышел XPG6. 
Управление процессами В этой главе мы рассмотрим основные функции, позволяющие запускать новые процессы и управлять ими. 7.1. Функция fork Функция fork, описанная в заголовочном файле <unis d. (тип возвращаемого значения описан в файле <sys/ty es h позволяет «клонировать» текущий процесс. Прототип: pid t fork (void) Эта функция создает процесс-потомок, полностью идентичный текущему процессу (отличие только в том, что у них разные идентификаторы и потомок не наследует файловые блокировки и очередь сигналов). Фактически создается копия виртуальной памяти процесса-родителя. Во многих системах физическая па- мять реально не копируется, а просто создается новое отображе- ние виртуальной памяти на ту же самую физическую. И только при записи в память создается копия изменяемой страницы для потомка. Функция возвращает идентификатор потомка в роди- тельском процессе и О в порожденном процессе. Рассмотрим задачу создания «демона» — процесса, работа- ющего в фоновом режиме даже после выхода запустившего его пользователя из системы. Прежде всего необходимо запу- 
82 Глава 7. Управление процессами стить фоновый процесс. Для этого используем функцию fork для создания процесса,— потомка и завершим родительский про- цесс. Процесс — потомок продолжает работать в фоновом режиме, Но он унаследовал от родителя связь с терминалом, с которого он был запущен, и будет терминирован операционной системой при окон сании работы пользователя на этом терминале. Для создания не 'зависимого от терминала «демона» мы фактиче- ски повторим описаштые вьште действия еще раз: используем функцшо ХогЫ для создания процесса — потомка и завершим ро- днтел~ скпй процесс. Процесс — потомок продол>к ет работ т фоновом режиме и вызывает функцию daemon process, являю- щуюся телом «демон»&g ; В на ем прим ре та функ ия печат 10 раз с интервалом 2 секунды приветствие и завершает свою работу. Текст программы: ¹include <stdio ¹include <unistd ¹include <sys/types & void daemon process () int i fprintf (stderr, "Daemon started! (n"); /» Используеи для тестирования конечный цикл »/ for (i = 0; i & t; 0; i & fprintf (stderr, "Hello '/d!gn", i); sleep (2); fprintf (stderr, "Daemon finished!Kn"); } int main () 
7.1. функция fork pid t pid; /+ Клонировать себя +/ pid = fork (); & if (pid == -1) fprintf (stderr, "Cannot fork!gn"); return i; else if (pid != 0) & /+ Завершить родительский процесс +/ return 0; } /+ Процесс-потомок продолжает работу +/ /+ Клонировать себя +/ pid = fork (); if (pid == -1) & fprintf (stderr, "Cannot fork!Nn"); return i; } else if (pid != 0) & /+ Завершить родительский процесс +/ return 0; /+ Процесс-потомок продолжает работу +/ daemonprocess (); return 0; 
Глава 7. Управление процессами 7.2. Функции execl, ехесч Функции execl, execv, описанные в файле <unistd. >, ляют заменить текущий процесс новым. Прототипы: int execl (const char +path, const char +arg, ...); int execv (const char +path, char +const argv[] ); где входные параметры: ° path — имя исполнимого файла процесса, ° arg, ... — список аргументов (завершаемый нулевым указа- телем, ° argv — указатель на список аргументов (завершаемый нуле- вым указателеи). В случае успеха эта функция не возвращает управления (по- скольку текущий процесс больше не существует), в случае ошиб- ки она возвращает — 1. Пример использования для запуска программы см. в разде- ле 7.3. 7.3. Функция waitpid Функция waitpid, описанная в файле <sys/w it h&gt (тип в щаемого значения описан в файле <sys/types.h gt;), поз ждать окончания порожденного процесса. Прототип: pid t waitpid (pid t pid, int +status, int options); Эта функция останавливает выполнение текущего процесса до завершепия процесса-потомка, указанного аргументом рЫ: ° если pid & t О (наибо ее используе ая форм ), то ожид окончания потомка с идентификатором рЫ; ° если pid = О, то ожидать окончания любого потомка, при- надлежащего к той же группе, что и текущий процесс; ° если pid ( — 1, то ожидать окончания любого потомка, при- надлежащего к группе ~рЫ~; ° если pid = — 1, то ожидать окончания любого потомка. 
7.3. Функция waitpid Причину завершения процесса можно узнать через перемен- ную status с помощью специальных макросов (см. примеры ниже). Аргумент options задает поведение функции в случае, если процесс — потомок уже завершился или остановлен. В слу- чае успеха функция возвращает идентификатор закончившегося процесса, в случае ошибки — — 1. Рассмотрим в качестве примера следующую задачу: требу- ется запустить заданную программу (мы используем /bin/ls) с заданным параметром (-1) в виде отдельного процесса и до- ждаться его окончания. Для этого мы с помощью функции fork создадим процесс — потомок, в котором сразу выполним ехес1 для его замены на указанный процесс. Напомним, что пер- вым аргументом любой программы (в нашем случае /bin/ls) является ее собственное путевое имя, поэтому список аргумен- тов execl выглядит следующим образом: 1. "/bin/ls" — имя исполнимого файла процесса, 2. "/bin/ls" — первый аргумент, 3. "-1" — второй аргумент, 4. О — нулевой указатель, обозначающий конец списка аргумен- тов ехес1. В родительском процессе мы будем ожидать окончания процесса — потомка с помощью waitpid. После этого можно про- верить причину окончания процесса, используя содержимое rre- ременной status и специальные макросы: ° WIFEXITED (status) — возвращает истинное значение, если программа завершилас~ нормально (например, с помощью оператора return в функции main или посредством вызо- ва функции exit). О второй возможной причине окончания процесса см. раздел 7.5, с. 88. ° WEXITSTATUS (status) — дает значение, которое вернула. программа операционной системе, т. е. аргумент оператора return в функции main или аргумент функции exit. Этот макрос можно использовать только в случае, если значение WIFEXITED (status) истинно. В UNIX-системах признаком 
Глава 7. Управление процессами нормального завершения программы является нулевое зна- чение WEXITSTATUS (status) . Текст прогрэ|мы: ¹include <stdio ¹include <unistd ¹include <sys/types ¹include <sys/wait int main () pid t pid; int status; /+ Клонировать себя +/ pid = fork (); & if (pid == -1) fprintf (stderr, "Cannot fork!gn"); return 1; if (pid == 0) & /+ Запускаем программу в порожденном процессе +/ execl ("/bin/ls", "/bin/ls", "-1", 0); /+ Если этот код выполняется, значит ошибка при запуске +/ fprintf (stderr, "Cannot ехес!~п"); return 2; } /+ В родительском процессе: ожидаем завершения потомка +/ if (waitpid (pid, &sta us 0 & 
7.4. функция kill 7 fprintf (stderr, "Cannot waitpid child, terminating...~п"); & return 3; /+ Проверяем успешность завершения потомка +/ if (!WIFEXITED (status)) fprintf (stderr, "Child finished abnormally, terminating...~n"); return 4; /+ Проверяем код завершения потомка +/ if (WEXITSTATUS (status)) & fprintf (stderr, "Child finished with non-гего ~ return code, terminating...~п"); return 5; } /+ завершаем работу +/ return О; & 7.4. Функция kill Функция kill, описанная в заголовочном файле <signal (тип первого аргумента описан в файле <sys/types.h ), п воляет послать сигнал процессу. Прототип: int kill (pid t pid int sig) Поведение функции различно в зависимости от рЫ: ° если pid ) О (наиболее используем'~я форма), то сигнал с номером sig будет послан процессу с идентификатором pid; 
Глава 7. Управление процессами ° если pid = О, то сигнал с номером sig будет послан всем процессам, принадлежащим к той же группе, что и текущий процесс; ° если pid ( — 1, то сигнал с номером sig будет послан всем процессам, принадлежащим к группе /риф ° если pid = — 1, то сигнал с номером sig будет послан всем процессам (за исключением процесса с номером 1, которому часто вообще нельзя послать сигнал). В случае успеха функция возвращает О, в случае ошибки — — 1. Процесс-получатель сигнала должен установить обработчик для сигнала (иапример, с помощью функции signal, см. раздел 7.5). В противном случае будет вызван стандартный обработчик, ко- торый во многих операционшлх системах терминирует процесс. 7.5. Функция signal Функция signal, описанная в заголовочном файле <signal. позволяет указать функцию, которук& t; след ет вызыв ть ри ступлении сигнала с заданным номером. Прототип: sighandler t signal (int signum, sighandler t handler); где тип обработчика сигнала есть typedef void (+sighandler t) (int); Функция возвращает старый обработчик сигнала. Рассмотрим задачу создания «отказоустойчивого» фоново- го процесса («демона»). Требуется разработать программу, псу- ществляющу1о запуск и мониторинг состояния заданного про- цесса (в нашем примере — "./daemon"). В случае (нормального или аварийного) завершения последнего программа должна осу- ществлять ero перезапуск. Приведем вначале тексты программ, а затеи дадим необходимые пояснения. Программа, осуществ- л~пощая мониторинг состояния демона: ¹include <stdio 
7.5. функция signal ¹include <stdlib ¹include <unistd ¹include <sys/types ¹include <sys/wait ¹include <signal ¹include <erma /+ Номер запущенного процесса +/ static pid t pid; int daemon process () static const char + program = "./daemon"; static char + const program args[] = & t; "./daemo ", pid t res; int status; int fprintf (stderr, "Daemon started!gn"); /+ Используем для тестирования конечный цикл +/ & for (i = 0; i & t; 0; i /+ Клонировать себя +/ pid = fork (); if (pid == -1) & fprintf (stderr, "Cannot fork!gn"); return 1; if (pid == О) & /+ Запускаем программу в порожденном процессе~ execv (program, programargs); /+ Если этот код выполняется, значит ошибка при запуске +/ 
Глава 7. Управление процессами fprintf (stderr, "Cannot ехес!~п"); return 2; /+ В родительском процессе: ожидаем завершения потомка; при этом игнорируем все прерывания от сигналов +/ while (((res = waitpid (pid, &sta us ) & ЙЙ (errno == EINTR)); if (res != pid) fprintf (stderr, "Cannot waitpid child, terminating...~n"); return 3; /+ Проверяем причину завершения потомка +/ & if (WIFEXITED (status)) fprintf (stderr, "Child process '/d exited with code /dNn", pid, WEXITSTATUS (status)); else if (WIFSIGNALED (status)) & fprintf (stderr, "Child process /d killed by signal '/d$n", pid, WTERMSIG (status)); else & /+ никогда! +/ fprintf (stderr, "Child process '/d terminantedg by unknown геавоп~п", 
7.5. Функция signal pid); fprintf (stderr, "Daemon finished!gn"); & return О; /~ Обработчик сигналов +/ void signal handler (int signum) с switch (signum) с саяе SIGTERM: саяе SIGKILL: /+ Ликвидируем потомка +/ fprintf (stderr, "Killing child process '/dgn", pid); if (kill (pid, 9)) fprintf (stderr, "Error while killing child process /dgn", pid); fprintf (stderr, "Daemon finished!gn"); exit(0); break default break; & int main () pld t /~ игнорируем сигнал SIGTTOU — остановка фоновог~ 
Глава 7. Управление процессами процесса при попытке вывода на свой управляющий терминал */ signal (SIGTTOU, SIG IGN); /+ Игнорируем сигнал SIGTTIN — остановка фонового процесса при попытке ввода со своего управляющего терминала */ signal (SIGTTIN, SIG IGN); /+ Игнорируем сигнал SIGTSTP — разрыв связи с управляющим терминалом +/ signal (SIGHUP, SIG IGN); /+ Игнорируем сигнал SIGTSTP — остановка процесса нажатием на клавиатуре Ctrl+Z +/ signal (SIGTSTP, SIG IGN); /~ Устанавливаем свой обработчик сигнала SIG TERM завершение процесса ~/ signal (SIGTERM, signal handler); /+ Клонировать себя +/ pid = fork (); & if (pid == -1) fprintf (stderr, "Cannot fork!gn"); return i; else if (pid != 0) & /* Завершить родительский процесс ~/ return О; & /+ Процесс-потомок продолжает работу +/ /+ Клонировать себя +/ pid = fork (); if (pid == -1) 
7.5. Функция signal с fprintf (stderr, "Cannot fork!gn"); return 1; } else if (pid != 0) & /+ Завершить родительский процесс +/ return 0; & /+ Процесс-потомок продолжает работу +/ return daemon process (); Программа — демон: ¹include <stdio ¹include <unistd ¹include <sys/types int main () с pid t pid; int + p = 0; pid = getpid (); fprintf (stderr, "Program started, pid = /dgn" pid) /+ подождать указанное время +/ sleep (10); fprintf (stderr, "Program died, pid = /dNn" pid) /+ Сделать segmentation fault +/ *p=0; return 0; 
Глава 7. Управление процессами Рассмотрим вна ~але подробнее программу — демон. Она печа- тает свой идентификатор, ждет 10 секунд, печатает сообщение о своем окончании и производит запись по нулевому адресу. Это приводит к посылке ей операционной системой сигнала с номе- ром 11 (Я10БЕСУ — segmentation fault), стандартный обработ- IHK которого терминирует програьв|у. Рассмотрим подробнее програмчу, осуществляющую мони- торинг состояния демона. Функция main представляет собой улучшенный вариант описанной в разделе 7.1, с. 81 програм- мы, запускающей фоновый процесс. Отличие состоит в том, что иы с помощью функции signal с предопределенным значением Я?С ?СЮ зака~ываем игнорирова~~ие ряда сигпалов, свяЗываю- щих фоновьш процесс с термпналолr, с которого его запустили. Также посредством signal мы регистрируем новый обработчик signal handler для сигнала 15 (ЯСТЕВМ). Это делается для того, чтобы при получении этого сигнала программа, осуществ- ляющая мониторинг состояния демона, завершала его работу. Функция daemon process, работающая в фоновом процессе, в цикле (конечном в нашем примере и обычно бесконечном в ре- альном применении) осуществляет следующие действия. В на- чале цикла с помощью функций fork u execv осуществляет- ся запуск нового процесса . /daemon Затем с помощью функции waitpid программа переходит в режим ожидания завершения этого процесса. Процесс о>кича ия мо& t;ке быть п ерван пивпшм сигналом. Эта ситуация проверяется по состоянию пе- ременной errno, содер>ка ей од ошиб и, возник ей ри боте последней вызванной библиотечной функции. Если значе- ние errno равно EINTR, то ожидание было прервано сигналом, и м1&g ;! го возо0повляе !. Призна ом успил!н го оконча ия ци ожидания является возврат функцией waitpid идентификатора ожидавшегося процесса. После этого daemon process печатает информацию о причине окончания процесса, используя содер- жимое переменной status и специальные макросы: 
7.5. Функция signal 95 ° WIFEXITED (status) и WEXITSTATUS (status) — см. раз- дел 7.3, с. 84. ° WIFSIGNALED (status) — возвращает истинное значение, ес- ли программа завершилась вследствие получения сигнала. ° WTERMSIG (status) — дает номер сигнала, полученного про- цессом. Этот макрос можно использовать только в случае, если значение WIFSIGNALED (status) истинно. Функция signal handler, регистрируемая в функции main как обработчик сигнала SIGTER51, посылает с помощью функции kill сигнал 9 (SIGKILL) запущенному процессу. Это позволя- ет терминировать процесс — демон, послав сигнал ЯСТЕВМ (на- пример, с помощью программы kill) процессу, осуществляю- щел~у ero мониторинг. 
Синхронизация и взаимодеиствие процессов Доступ процессов (задач) к различным ресурсам (особенно раз- деляемым) в многозадачных системах требует синхронизации действий этих процессов (задач). Способы осуществления взаи- модействия подразделяют на: ° безопасное взаимодействие, когда обмен данными осу- ществляется посредством «объектов» взаимодействия, пре- доставляемых системой; при этом целостность информации и неделимость операций с нею (т. е. отсутствие нежелатель- ного переключения задач) неявно обеспечиваются системой; примерами таких «обьектов» взаимодействия являются се- мафоры, сигналы и почтовые ящики; ° небезопасное взаимодействие, когда обмен данными осу- ществляется посредством разделяемых ресурсов (например, общих переменных), не зависимых от системных объек- тов взаимодействия; при этом целостность информации и неделимость явно обеспе п~ваются самим приложением (в подавляющем болыпинстве случаев — посредством того или иного системного объекта синхронизации и взаимодей- ствия . Поскольку для любого типа взаимодействия требуются систем- ные объекты синхронизации, то все имеющиеся операционные системы предоставляют приложениям некоторый набор таких 
8.1. Разделяемая память объектов. Ниже мы рассмотрим самые распространенные из I I HX. 8.1. Разделяемая память Определение. Разделяемая память — это область памяти, к ко- торой имеют доступ несколько процессов. Взаимодействие через разделяемую память является базовым механизмом взаимодей- ствия процессов, к которому сводятся все остальные. Оно, с од- ной стороны, является самым быстрым видом взаимодействия, поскольку процессы напрямую (т. е. без участия операционной системы) передают данные друг другу. С другой стороны, оно является небезопасным, и для обеспечения правильности пере- дачи информации используются те или иные объекты синхро- низации. В системах с виртуальной памятью существуют два подхода к логической организации разделяемой памяти: 1. Разделяемая память находится в адресном пространстве опе- рационной системы, а виртуальные адресные пространства процессов отображаются на нее. В зависимости от реализа- ции операционной системы это может приводить к переклю- чению задач при работе с разделяемой памятью (поскольку она принадлежит ядру, а не процессу) и фиксации разделя- емой памяти в физической памяти (поскольку само ядро не участвует в страничном обмене). 2. Разделяемая память логически представляется как файл, отображенный на память (т. е. файл, рассматриваемый как массив байтов в памяти). При этом разделяемая память пол- ностью находится в пользовательском адресном простран- стве и отсутствуют дополнительные зацержки при доступе к ней. В силу большей эффективности рекомендуется использовать второй способ работы с разделяемой памятью. 4- -4017 
98 Глава 8. Синхронизация и взаимодействие процессов В системах с виртуальной памятью над разделяемой памя- тью определены следующие элементарные операции. ° слетать (или открыть) разделяемую память, при этом разде- ляемая память появляется в процессе как объект, но доступ к ее содержимол~у еще невозможен; ° по сое ишnb разделяемую пал~ять к адресному простран- ству процесса, при этом происходит отображение разделяе- мой пал~яти на виртуальное адресное пространство процесса; после этой операции разделяемая память доступна для ис- пользования; ° отсое унять разделяемую память от адресного пространства процесса, после этой операции доступ к содержилюлry разде- ляемой памяти невозможен; ° удалить [или закрыть,) разделяемую память, реально разде- ляемая память будет удалена, когда с ней закончит работать последний из процессов. В системах без виртуальной памяти эти операции тривиальны. Отметим, что в разных процессах разделяемая память мо- жет быть отображена в разные адреса виртуальной памяти. Сле- довательно, в разделяемой памяти нельзя хранить указатели на другие элементы разделяемой памяти (например, классическую реализацию однонаправленного списка нельзя без изменений хранить в разделяемой памяти). 8.1.1. Функция shmget Функция shmget, описанная в файле <sys/shm h> (испо мые тины описаны в заголовочном файле <sys/ipc.? ), п воляет создать (или открыть существующую) область разде- ляемой пал~яти указанного размера и с указанныл~и правами доступа. Прототип: int shmget (key t key, size t size, int shmflg); где входные параметры: 
8.1. Разделяемая память 99 ° key — числовой идентификатор области разделяемой памя- ТИ; ° s ize — размер области разделяемой памяти (округляется вверх до величины, кратной размеру страницы виртуаль- ной памяти); ° shmf lg — атрибуты, используемые при создании области раз- деляемой памяти (флаги и права доступа). Функция возвращает идентификатор области разделяемой па- мяти (не путать с key) или — 1 в случае ошибки. 8.1.2. Функция shmat Функция shmat, описанная в файле <sys/shm h> (исполь типы описаны в заголовочном файле <sys/ipc.h gt;), поз подсоединить указанную область разделяемой памяти к адрес- ному пространству процесса. Прототип: void +shmat (int shmid, const void +shmaddr, int shmflg) где входные параметры: ° shmid -- идентификатор области разделяемой памяти; ° shmaddr-- пожелания процесса об адресе, начиная с которо- го следует подсоединить ооласть раздедяелюй памяти, или О, если адрес безразличен (операционная система MoiKQT иг- норировать этот аргумент); ° shmflg — права доступа к области разделяемой памяти. Функция возвращает адрес, начиная с которого подсоедипена область разделяемой памяти, или — 1 в случае ошибки. 8.1.3. Функция shmctl Функция shmctl, описанная в файле <sys/shm h> (испо мые типы описаны в заголовочном файле <sys/ipc.h gt;) воляет управлять областью разделяемой памяти (в частности, удалять ее). Прототип: 
100 Глава 8. Синхронизация и взаимодействие процессов int shmctl (int shmid, int cmd, struct shmid ds +buf); где входные параметры: ° shmid — идентификатор области разделяемой памяти; ° cmd — задает операцию над областью разделяемой памяти; ° buf — аргументы операции (также служит для возврата ин- формации о результате операции). Функция возвращает 0 в случае успеха или — 1 в случае ошибки. 8.2. Семафоры Определение. Семафор — это объект синхронизации, задаю- щий количество пользователей (задач, процессов), имиощих од- новременный доступ к некоторому ресурсу. С каждым семафо- ром связаны счетчик (значение семафора) и очередь ожидания (процессов, задач, о.кидающих принятие счетчиком определен- ного значения). Различают: ° двоичные (булевские) семафоры — это механизм взаимного исключения для защиты критичного разделяемого ресурса; начальное значение счетчика такого семафора равно 1; ° счетные семафоры--это механизм взаимного исключения для защиты ресурса, который может быть одновременно ис- пользован не более, чем ограниченным фиксированным чис- лом задач и; начальное значение счетчика такого семафора ря,вно и. Над семафорами определены следующие элементарные опера- ции (ниже lс = 1 для булевских семафоров): ° взять /с единиц из семафора, т. е. уменьшить счетчик на lс (если в счетчике нет tc единиц, то эта операция переводит задачу в состояние ожидания наличия как минимум Й еди- ниц в семафоре, и добавляет ее в конец очереди ожидания этого семафора); ° вернуть Й единиц в семафор, т. е. увеличить счетчик на Й (если семафор ожидается другой задачей и ей требуется не 
8.2. Семафоры 1 1 более, чем новое текущее значение счетчика единиц,то она может быть активизирована, удалена из очереди ожидания и может вытеснить текущую задачу, например, если ее при- оритет выше); ° поп обовать взять Й единиц из семафора (если в счетчике & t Й един ц, то вз т k еди иц из не о, ин че верн ть п знак занятости семафора без перевода задачи в состояние ожидания); ° проверить семафор, т. е. получить значение счетчика; ° блоки рвать семафор, т. е. взять из него столько единиц, сколько в нем есть (при этом иногда бывают две разновидно- сти этой операции: взять столько, сколько есть в данный мо- мент, или взять столько, сколько есть в начальный момент, т. е. максимально возможное количество, именно последнее обычно называют блокировкой, поскольку такая задача бу- дет монопольно владеть ресурсом); ° >азбл к» &gt рвать се аф р, т. е. в рнуть ст ль>& сколько всего было взято данной задачей по команде блоки рвать. Логическая структура двоичных семафоров особенно про- ста. Счетчик семафора s инициализируется 1 при создании. Для доступа к нему определены две примитивные операции: ° Get(8) — взять (или закрыть) семафор в, т. е. запросить ре- сурс; эта операция вычитает из счетчика 1; ° P»t(8) — вернуть (или открыть) семафор, т. p.. освободить ре- сурс; эта операция прибавляет к счетчику 1. Эти операции неделимы, т. е. переключение задач Во время их исполнения запрещено. В процессе работы состояние счетчика может быть: ° 1 — ресурс свободен, ° 0 — ресурс занят, очередь ожидания пуста, ° т ( 0 — ресурс занят, в очереди ожидания находятся Jm[ задач. 
102 Глава 8. Синхронизация и взаимодействие процессов Рассмотрим пример (рис. 8.1). 1. А.Get — задача А завладела ресурсом, 2. С.Get — задача С запросила ресурс, который занят, следо- вательно, задача С заблокирована и помещена в очередь ожидания, 3. B.Get — задача В запросила ресурс, который занят следо- вательно, задача В заблокирована и помещена в очередь ожидания, 4. А.Put — задача А освободила ресурс который был передан первой задаче и очереди ожидания, т. е. С (которая в свою очередь активизирована), 5. С.Put — задача С освободила ресурс, который был передан первой задаче в очереди ожидания, т. е. В (которая в свою очередь активизирована), состояние ресурса занят 1 1 1 1 1 1 1 1 1 1 1 1 свободен 4 'Ь '~з '4 '4 '4 t. состояние 1 1 1 1 1 1 1 1 1 1 1 1 зада|и А 1 1 1 1 1 1 1 1 1 1 1 1 активна 1 1 1 1 1 1 I 1 1 1 1 1 1 1 1 1 1 1 блокирована 1~1 ! 2 1~3 1ь1 1~5 1~6 состояние задачи В 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 активна 1 1 1 1 1 1 1 1 1 1 1 блокирована ~1 ~2 ~3 ~4 t состояние 1 1 1 1 1 ' 1 1 1 I 1 1 1 зада ш С 1 1 1 1 1 1 1 I 1 1 1 1 активна 1 1 1 1 1 1 1 1 1 1 1 1 1 блокирована 4 Ь ~з ~4 4 ~б Рис. 8.1. Пример раооты с семафором 
8.2. Семафоры 1 6. В.Put — — задача В освободила ресурс, который будет передан первой задаче, которая вызовет Get. 8.2.1. Функция semget Функция semget, описанная в файле <sys/sem Ь> (испо мые типы описаны в заголовочном файле <sys/ipc.h gt;), ляет создать (или открыть существующий) семафор (или груп- пу семафоров) с указанными правами доступа. Прототип: int semget (key t key, size t semnum, int semflg); где входные параметры: ° key — числовой идентификатор набора семафоров; ° в1ке — количество семафоров B наборе; ° semflg — атрибуты, используемые при создании набора се- мафоров (флаги и права доступа). Функция возвращает идентификатор набора семафоров (не пу- тать с key) или — 1 в случае ошибки. 8.2.2. Функция semop Функция semop, описанная в файле <sys/s m. h> (исполь типы описаны в заголовочном файле <sys/ipc.h gt;), поз изменять значения семафора. Прототип: int semop (int semid, struct sembuf ~варя, size t nsops); где входные параметры: ° semid — идентификатор набора .семафоров; ° sops — указатель на массив структур типа sembuf, задающих операцию над семафорами из набора; ° nsops — количество элементов в массиве, на который указы- вает sops. Функция возвращает О в случае успеха или — 1 в случае ошибки. 
104 Глава 8. Синхронизация и взаимодействие процессов 8.2.3. Функция semctl Функция semctl, описанная в файле <вув/sem Ь> (испо мые типы описаны в заголовочном файле <sys/ipc.h gt;), ляет управлять семафором (в частности, инициализировать и удалять его). Прототип: int semctl (int semid, int semnum, int cmd, ...); где входные параметры: ° semid — идентификатор набора семафоров; ° semnum — номер семафора из набора, над которым надо про- извести операцию; ° cmd — задает операцшо над семафором semnum. Функция возвращает неотрицательное число, зависящее от опе- рации cmd, или — 1 в случае ошибки. 8.2.4. Пример использования семафоров и разделяемой памяти Рассмотрим пример типичного клиент-серверного приложения. Процесс-клиент (текст которого находится в файле reader.с) считывает строку со стандартного ввода и передает ее через раз- деляемую память процессу-серверу (текст которого находится в файле writer.c) выводящему ее на стандартный вывод. Для организации взаимного исключения при доступе к критическим разделяемым ресурсам (переменным done, msglen, msgbuf, нахо- дящимся в разделяемой памяти), используется семафор, иден- тификатор которого тоже содержится в разделяемой памяти. Как клиент, так и сервер используют одни и те же функции по работе с разделяемой памятью и семафором, находящиеся в файлах shdata.h u shdata.c. Общая часть — заголовочный файл shdata. h, в котором опи- саны структура разделяемой памяти (SHDATA) и функции рабо- ты с ней: ¹include <st io 
8.2. Семафоры 1 ¹include <stdlib ¹include <unistd Oinclude <string Oinclude <sys/types Oinclude <sys/ipc Oinclude <sys/shm ¹include <sys/sem ¹define BUF LEN 256 & typedef struct shdata int semid; /+ идентификатор семафора +/ int done; /+ признак окончания работы +/ int ms0;len; /+ длина сообщения +/ char msgbuf[BUFLEN]; /+ буфер для хранения сообщения~/ ) SHDATA; /+ Начать работу с разделяемой памятью +/ SHDATA + shdata open (void); /+ Закончить работу с разделяемой памятью +/ int shdataclose (SHDATA + data) /+ Обеспечить исключительную работу с разделяемой памятью текущему процессу. Возвращает не 0 в случае ошибки. +/ int shdata lock (SHDATA + data); /+ Разрешить другим процессам работать с разделяемой памятью. Возвращает не 0 в случае ошибки. +/ int shdataunlock (SHDATA + data); Общая часть (для клиента и сервера) находится в файле shdata. с (см. пояснения ниже): ¹include "shdata.h" ¹include <errno 
106 Глава 8. Синхронизация и взаимодействие процессов /+ Идентификатор разделяемой памяти +/ ¹define SHMKEY 1221 /+ Идентификатор семафора +/ ¹def inc SEM KEY (1221+1) /+ Идентификатор разделяемой памяти */ static int shmid; /+ Идентификатор семафора +/ static int sem id; /+ "Открыть" разделяемую память с ключом key и размером size Возвращает указатель на память, 0 в случае ошибки. Значение +pcreated будет истинным, если текущий процесс создал разделяемую память. */ static void * shm open (int Кеу, int size, int +pcreated) void + res = 0;/+ результат +/ +pcreated = О; /+ память не создали +/ /+ Попытаться открыть разделяемую память +/ if ((shm id = shmget (key, size, О)) == -1) /+ Не удалось, рассмотрим причину +/ с if (errno == ENOENT) /+ Запрошенная разделяемая память не существует. Создаем ее. +/ if ((shm id = shmget (key, size, IPCCREAT I 0664)) == -1) & fprintf (stderr, "Cannot create shared memory(n"); return 0; 
8.2. Семафоры 1 +pcreated = 1; /+ память была создана +/ & else fprintf (stderr, "Cannot get shared memory(n") return 0; & /+ Подсоединяем разделяемую память к адресному пространству процесса +/ if ((res = (void +)shmat (shm id, О, О)) == (void +)-1 & fprintf (stderr, "Cannot attach shared memory(n"); return 0; } return res; /+ "Закрыть" разделяемую память. Возвращает не О в случае ошибки. +/ static int shm close (void + mern) /+ Отсоединяем разделяемую память от адресного пространства процесса +/ if (shmdt (mern) == -1) fprintf (stderr, "Cannot detach shared шешогу~п") return i; } /+ Удаляем разделяемую память. Реально удаление произойдет, когда закончится последний процесс, 
108 Глава 8. Синхронизация и взаимодействие процессов использующий разделяемую память. Если несколько процессов вызывают удаление, то успешным будет только первый вызов, поэтому возвращаемое значение функции не проверяется. +/ shmctl (shm id, IPC RMID, О); return О; /+ "Открыть" семафор с ключом key Возвращает идентификатор семафора, -1 в случае ошибки. Значение +pcreated будет истинным, если текущий процесс создал семафор. +/ static int sem open (int key, int +pcreated) int id; /+ результат */ +pcreated = О; /+ семафор не создан +/ /+ Попытаться получить семафор +/ if ((id = semget (key, 1, О)) == -1) /+ Не удалось, рассмотрим причину +/ if (errno == ENOENT) /+ Запрошенный семафор не существует. Создаем его. +/ if ((id = semget (key, 1, IPC CREAT ~ 0664)) — -1) fprintf (stderr, "Cannot create semaphoreNn"); } return id; +pcreated = 1; /+ семафор был создан +/ 
8.2. Семафоры 1 & else fprintf (stderr, "Cannot get semaphore(n"); return id; ) /+ Инициализировать семафор, если текущий процесс его создал +/ if (+pcreated) & /+ Структура данных для управления семафором +/ ¹ifdef SEM SEMUN UNDEFINED union semun с /+ Значение для команды SETVAL +/ int val; /+ Буфер для команд ?РС ЯТАТ и ?РС ЯЕТ +/ struct semid ds +buf /+ Массив для команд GETALL u SETALL +/ unsigned short int ~аггау; /+ Буфер для команды IPCINFO +/ struct seminfo +buf; ¹endif union semun s un; /* Начальное значение семафора +/ s un.val = 1; /+ Установить начальное значение семафора +/ if (semctl (id, О, SETVAL, s un) == -1) fprintf (stderr, "Cannot init semaphoreNn"); /+ С семафором работать нельзя, заканчиваем +/ return -|; 
»о Глава 8. Синхронизация и взаимодействие процессов return id; } /+ "Закрыть" семафор с идентификатором id. Возвращает не О в случае ошибки. +/ statiñ int sem close (int id) /+ Удаляем семафор. Удаление происходит сразу. Все ожидающие семафор процессы активизируются, ожидание завершается с ошибкой EIDRM— Error IDentifier ReMoved. У них вся последующая работа с этим семафором будет давать ошибку, поэтому возвращаемое значение функции не проверяется. */ semctl (id, О, IPC RMID, О); return О; /+ Заблокировать семафор с идентификатором id Возвращает не О в случае ошибки. +/ static int sem lock (int id) /+ Структура данных для операций с семафором +/ struct sembuf s buf = & t; О, 1, if (semop (id, &a p;s uf 1 == & /+ Проверяем причину +/ if (errno == EIDRM) 
8.2. Семафоры 111 /+ Семафор был удален другим процессом. +/ } return -1; /+ Еще может быть EINTR — ожидание прервано сигналом */ fprintf (stderr "Cannot lock semaphoregn"); return 1; /+ ошибка */ ) return 0; } /+ Разблокировать семафор с идентификатором id.. Возвращает не О в случае ошибки. +/ static int sem unlock (int id) /+ Структура данных для операций с семафором +/ struct sembuf s buf = & t; О, 1, if (semop (id, 8свЬиХ, 1) == -1) & fprintf (stderr, "Cannot unlock semaphoregn"); return 1; /+ ошибка +/ ) return 0; } /+ Начать работу с разделяемой памятью +/ SHDATA + shdata open (void) /+ Результат +/ SHDATA + res = 0; /+ Истинно, если текущий процесс создал разделяемую 
112 Глава 8. Синхронизация и взаимодействие процессов память +/ int shm created; /+ Истинно, если текущий процесс создал семафор +/ int sem created; /+ "Открыть" разделяемую память +/ res = (ЯНОАТА*) shm open (SHM KEY, sizeof (SHDATA), kshmcreated); if (!res) return 0; /+ ошибка +/ /+ "Открыть" семафор +/ sem id = sem open (SEM KEY, ksemcreated); { if (sem id == -1) /+ Ошибка, удаляем разделяемую память и заканчиваем +/ shm close (res); return 0; /+ ошибка +/ ) /+ Заблокировать семафор +/ if (sem lock (sem id)) & /+ Ошибка, удаляем все и заканчиваем +/ shdata close (res); return 0; /+ ошибка +/ ) /+ Теперь только один процесс работает с памятью +/ /+ Пусть инициализацией структур данных занимается тот процесс, который создал разделяемую память +/ if (shm created) { /+ обнулить область разделяемой памяти +/ 
8.2. Семафоры 11 memset ((char+)res, О, sizeof (SHDATA)); /~ занести идентификатор семафора в разделяемую память */ } res->se i = em /+ Разблокировать семафор +/ if (sem unlock (sem id)) { /+ Ошибка, удаляем все и заканчиваем +/ shdata close (res); return О; /+ ошибка e/ ) return res; /+ Закончить работу с разделяемой памятью Возвращает не О в случае ошибки; +/ int shdata close (SHDATA + data) /+ Удаляем семафор +/ if (sem close (sem id)) return 1; /+ ошибка +/ /+ Удаляем разделяемую память +/ if (shm close (data)) return 2; /+ ошибка +/ return О; } /+ Обеспечить исключительную работу с разделяемой памятью текущему процессу. Возвращает не О в случае ошибки. +/ int shdata lock (SHDATA + data) 
114 Глава 8. Синхронизация и взаимодействие процессов /+ Берем идентификатор семафора из разделяемой памяти»/ int id = data-> em ) return sem lock (id) /+ Разрешить другим процессам работать с разделяемой памятью. Возвращает не О в случае ошибки. +/ int shdata unlock (SHDATA + data) { /+ Берем идентификатор семафора из разделяемой памяти»/ int id = data-> em return sem unlock (id) } Для облегчения понимания код, работающий с блоком разделя- емой памяти разбит в файле shdata.с на ряд подпрограмм: ° shm open — пытается открыть существующую разделяемую память, а в случае неуда..ш создает новую; информация о том, что был создан новый блок разделяемой памяти, воз- вращается в вызвавшую процедуру для проведения ceo ини- циал изации; ° shm close — закрывает разделяемую память и посылает за- прос на pQ удаление (который будет выполнен, когда все про- цессы вызовут эту функцию); после завершения shm close любое обращение к разделяемой памяти вызовет ошибку; ° sem open — пытается открыть существующий семафор, а в случае неудачи создает новый; информация о том, что был создан новый семафор. используется для проведения его инициализацшт и возвращается в вызваилую процедуру; ° sem close — удаляет семафор; после этого активизируются все ожидавшие его процессы и любое иоследукнцее обраще- ние к нему вызовет ошибку; ° sem lock — «закрыть» семафор; после этого текущий про- цесс становится владельцем семафора; 
8.2. Семафоры 11 ° sem unlock — «открыть» семафор; после этого другие про- цессы могут «закрывать» семафор; ° shdata open — -открывает разделяемую память и семафор, затем инициализирует разделяемую память (если она была создана, исключительный доступ к памяти текущему про- цессу обеспечивается с помощью семафора); ° shdata close- — удаляет разделяемую память и семафор: ° shdata lock — обеспечить исключительный доступ к разде- ляемой памяти текущему процессу; ° shdata unlock - — разрешить другим процессам работать с разделяемой памятью. Программа-клиент находится в файле reader.с (см. пояс- нения ниже): Oinclude "shdata.h" /+ Считать сообщение с терминала и записать в разделяемую память +/ void reader (SHDATA ~ data) int ret; for (;;) /+ Обеспечить исключительную работу с разделяемой памятью текущему процессу */ ret = shdata lock (data); if (!ret) & if (!data->msgl { /+ Выход по Ctrl+D +/ if (!fgets (data->msgb f, BUFL N, stdr break; data->msg e = str en (data->m g uf 
116 Глава 8. Синхронизация и взаимодействие процессов /+ Разрешить другим процессам работать с разделяемой памятью. +/ shdata unlock (data); /+ Дать поработать другим +/ sleep (1); data->d n = fprintf (stderr "reader process '/d exitsgn" getpid()); /+ Разрешить другим процессам работать с разделяемой памятью. +/ shdata unlock (data); int main () SHDATA + data = shdata open (); if (data) { reader (data); shdata close (data); } return О; } Процедура reader в бесконечном цикле, прерываемом по ошиб- ке ввода: 1. обеспечивает себе исключительный доступ к разделяемой памяти с помощью shdatalock; 2. если предыдущее сообщение обработано сервером (т. е. длина сообщения data->msg en ра на ), то считыв ет стр ку стандартного ввода с помощью f gets; 
8.2. Семафоры 11 :). в случае успешного чтения устанавливает длину сообщения data->msgl n, ина е: прерыв ет ци л, устанавлив ет пе менную data->d n в 1, разреш ет дру им процес ам ра тать с разделяемой памятью с помощью shdata unlock u завершает работу; 4. разрешает другим процессам работать с разделяемой памя- тью с помощью shdata unlock; 5. для обеспечения переключения задач к процессу — серверу останавливает на 1 секунду выполнение текущего процесса с помощью sleep (иначе бы этот цикл работал вхолостую до окончания кванта времени, выделенного текущему про- цессу); это крайне плохое (исключительно модельное) реше- ние, но у нас еще нет механизма, позволяющего ожидать, пока сервер не обработал запрос; см. «правильное» решение в разделе 8.4.5. Программа — клиент находится в файле writer.с (см. пояс- нения ниже: ¹include "shdata.h" /+ Взять сообщение из разделяемой памяти и вывести на экран +/ void writer (SHDATA + data) int ret; for (;;) /+ Обеспечить исключительную работу с разделяемой памятью текущему процессу */ ret = shdata lock (data); /+ Проверяем признак конца работы в любом случае +/ & if (data->do /+ Конец работы +/ fprintf (stderr "writer process /d exitsgn" 
118 Глава 8. Синхронизация и взаимодействие процессов getpid ()); /+ Разрешить другим процессам работать с разделяемой памятью. +/ if (!ret) shdata unlock (data); return; if (!ret) & if (data->msgl { /+ есть сообщение для вывода +/ printf ("-& t; /в~ ", data->m data->msg e = & /+ Разрешить другим процессам работать с разделяемой памятью. +/ shdata unlock (data); & /+ Дать поработать другим +/ sleep (1); & int main () SHDATA + data = shdataopen (); Н (data) { writer (dat a); shdata close (data); return 0; } 
8.2. Семафоры 11 1!роцедура writer в бесконечном цикле, прерываемом в случае иенулевого значения data->do 1. обеспечивает себе исключительный доступ к разделяемой памяти с помощью shdata lock; 2. если data->d ne не ра но О, то разреш ет дру им процес работать с разделяемой памятью с помощью shdata unlock и завершает работу; 3. если есть сообщение для обработки сервером (т. е. длина со- общения data->msg en не ра на ), то выво ит стр к с мощью printf и устанавливает data->msg e в 4. разрешает другим процессам работать с разделяемой памя- тью с помощью shdataunlock; 5. для обеспечения переключения задач к процессам-клиентам останавливает на 1 секунду выполнение текущего процесса с помощью sleep (иначе бы этот цикл работал вхолостую до окончания кванта времени, выделенного текущему про- цессу). Сделаем полезное замечание об отладке программ, IIC- пользующих описанные в разделах 8.1 8.2 и 8.4 функции. В случае аварийного завершения программы созданные ею обь- екты межпроцессного в:заимодействия (блоки разделяемой па- мяти, семафоры и очереди сообщений) не удаляются. Следова- тельно, при повторном старте программы она получит обьекты, оставшиеся от предыдущего запуска. При этом, например, сема- фор может оказаться занятым уже не существующим процессом и потому никогда не освободится. С большинством UNIX-систем поставляются утилиты, позволяющие посмотреть состояние та- ких обьектов и удалить их: ° ipcs (!11йегРгосевв Communication Status) — выводит список блоков разделяемой памяти, семафоров и очередей сообще- ний с указанием их идентификатора, владельца, прав досту- па ит.д. 
120 Глава 8. Синхронизация и взаимодействие процессов ° ipcrm (InterProcess Communication RelVIove) — позволяет уда- лить: — ipcrm shm id — блок разделяемой памяти, — ipcrm sem id — семафор, — ipcrm msg id — очередь сообщений с идентификатором id, который можно получить с помощью команды ipcs. 8.3. События Определение. Событие — это логический сигнал (оповеще- ние), приходящий асинхронно по отношению к течению про- цесса. С каждым событием связаны булевская переменная Е, принимающая два значения (О — собьгтие не пришло, и 1 — со- бытие пришло), и очередь ожидания (процессов, задач, ожидаю- щих прихода события). Над событиями определены следующие элементарные операции: ° $гдпиЦЕ) — - послать событие, т. е. установить переменную Е в 1, при этом первая в очереди ожидающая задача активизи- руется (в некоторых системах активизируется одна из задач, т. е. очередность не гарантируется); ° Broadcast(E) — послать событие т. е. установить перемен- ную Е в 1, при этом все задачи из очереди ожидания ак- ТИВИЗЩХ~ ЮТСЯ; ° Vsxt(E) — ожидать события (если событн» нет, т. е. пореыен- ная Е равна О. то эта операция переводит задачу в состояние ожидания прихода события и добавляет ее в конец очереди ожидания этого события; как только событие придет, задача буудет активизирована); ° Яеset(E) — очистить (удалить поступившее событие), т. е. установить переменную Е в 0; ° Test)E) -- проаеуить (поступление) -- получить значение пе- ременной E. Рассмотрим пример (рис. 8.2). 
H.Ç. События 1 1 состояние ресурса пришло 1 1 не пришло ! ~1 1~2 1~3 1~4 1~5 1~6 t состояние задачи А 1 1 1. 1 1 1 1 1 1 1 1 активна 1 1 1 1 1 1 1 1 1 1 1 1 1 блокирована 1 ' ~2 ' ~3 ' ~4 ' ~5 ' ~6 состояние 1 1 1 1 1 задачи В 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 активна 1 1 1 1 1 1 1 1 1 блокирована 1 1 1 1 состояние задачи С 1 1 1 1 1 1 1 1 1 1 1 активна 1 1 1 1 1 1 1 1 1 1 1 1 блокирована ~1 ~2 ~3 ~4 ~5 ~6 Рис. 8.2. Пример работы с событием 1. В.Wait — задача В вызвала Wait; поскольку Е = 1, то это не имеет никакого эффекта, В продолжает исполнение; 2. Reset — была вызвана функция установки Е = О (одной из задач А, В или С, или другой задачей); 3. B.Wait — поскольку Е = О, то задача В заблокирована и помещена в очередь ожидания; 4. С.И~ait — поскольку Е = О, то задача С заблокирована и помещена в очередь ожидания; 5. А.Broaclcast — установить Е = 1, активизировать все задачи из очереди ожидания, т. е. активизировать В и С. С помощью событий легко организовать обмен между. двумя задачами по типу клиент-сервер. Пусть есть два события Е1 и Е2. Задача Т1 (сервер) сразу после старта вызывает функцию Е1.Wait(). Тем самым она блокируется до получения события Е& t; Зад ча Т2 (клие т) ср зу по ле ста та подготавлив ет д 
122 Глава 8. Синхронизация и взаимодействие процессов хпхе в разделяемой с задачей Тх памяти и вызывает функцию Ех.Яеиd(). Это приводит к активизации задачи Тх. Задача Т2 продолжает работу и вызывает функцххкх Е2.И'аИ(), блокиру- ясь до получения события Е2. Задача Тх (сервер) по окончании обработки данных вызывает функции Е2.Send(), Ех.И'ажх,(), ак- тивизнруя задачу Т2 (клиеита) и блокируя себя. Ниже (см. раздел 10.3) мы рассмотрим реализацию событий для задач. Для процессов общепринятая реализация событий в UNIX-системах отсу гствует. 8.4. Очереди сообщений (почтовые ящики) Определение. Очереди сообщений (почтовые ящики) — это объект обмена даххххьхлххх между задачами, устроенный в виде очереди FIFO (рис. 8.3). С каждым почтовым ящиком связа- НЫ: 1. очередь сообщений (образующая содержимое почтового ящика) ,. 2. очередь задач, ожидающих сообщений в почтовом ящике, 3. очередь задач, ожидающих освобождения места в почтовом ящике, 4. мехаххххзлх взаимного исключения, обеспечивающххй правиль- ный доступ нескольких задач к одним и тем me сообщениям в почтовол~ ящике. Количество задач, ожххдахощххх сообщения в почтовом ящике, обычно не ограничено. Количество же сообщений в ящике обыч- но ограничено паралхетролх, указаххххьхлх ири создании ящика. Это — Сообщение N . Сообщение 1 Рис. 8.3. Почтовый ящик 
8.4. Очереди сообщений (почтовые ящики) «<&g ; юно те , что азмер к ждого соо щения указы ает ««дании ящика и может быть значительным. Если ящик полон II:<;ù ÷à пытае ся помест т в н го но ое сообщен е, то на б к«руется до тех пор, пока в ящике не появится свободное место. 11;щ почтовыми ящиками определены следующие элементарные ~ н I('.ðàöèè: ° положить сообщение в почтовый ящик, при этом задача, вы- звавшая эту операцию, может быть блокирована и помещена в очередь задач, ожидающих освобождения места в ящике, если в ящике нет свободного места (активизация наступит сразу после взятия первого же сообщения из очереди сооб- щений); если очередь ожидающих сообщения зада I непуста, то активизируются все задачи из этой очереди; ° поп обовать положить сообщение и почтовый ящик — если в ящике есть свободное место, то эта операция эквивалентна положить, иначе вернуть признак отсутствия места; ° положить в начало — то же, что положил, только сообще- ние помещается в голову очереди; ° поп обовать положить в начало — то > е, то оп обов положить, только сообщение помещается в голову очереди; ° взять сообщение из почтового ящика, при этом задача, вы- звавшая эту операцию, может быть блокирована и помещена в очередь задач, ожидающих сообщения, если в ящике нет со- общений; при поступлении сообщения она будет активизиро- вана; при этом, если в очереди ожидающих сообщения задач находится более одной задачи, то при выполнении операции взять обеспечивается механизм взаимного исключения задач (т. е. пока выполняется эта операция одной задачей, все дру- гие задачи, запросившие ту же операцию, заблокированы); ° поп >обов ть вз ть сообще ие из почтов го ящ ка Ђ” е л ящике есть сооощенпя, то эта операция эквивалентна взять, иначе вернуть признак отсутствия сообщений; ° очистить ящик — удалить все сообщения из очереди. 
124 Глава 8. Синхронизация и взаимодействие процессов Фактически описанный механизм представляет собой взаимо- действие клиент — сервер, когда задачи — клиенты помещают за- просы в почтовый ящик, а задачи-серверы их оттуда забирают. 8.4.1. Функция msgget Функция msgget, описанная в файле <sys/msg h> (испо мые типы описаны в заголовочном файле <sys/ pc h gt;), ляет создать (или открыть существующую) очередь сообщений с указанными правами доступа. Прототип: int msgget (key t key, int msgflg); где входные параметрь1: ° key -- числовой идентификатор очереди сообщений; ° msgflg — атрибуты, используемые при создании очереди со- общений (флаги и права доступа). Функция возвращает идентификатор очереди сообщений (не пу- тать с key) или — 1 в случае ошибки. 8.4.2. Функция msgsnd Функция msgsnd, описанная в файле <sys/msg h> (испо мые типы описаны в заголовочном файле <sys/ipc.h gt;), ляет положить сообщение в очередь. Прототип: int msgsnd (int msqid, const void +msgp, size t msgsz, int msgflg); где входные параметры: ° msqid — -идентификатор очереди сообщений; ° msgp — указатель на сообщение, которое должно иметь сле- дующий тип (формат): struct ms0;buf /+ Тип (тег) сообщения +/ long int mtype; 
8.4. Очереди сообщений {почтовые ящики) /+ Данные сообщения +/ char mtext [11; ° msgsz — размер сообщения; ° msgf lg — дополнительные флаги, определяющие поведение процесса в случае, если в очереди сообщений нет свободно- го места (можно указать: ожидать появления достаточного места или сигнализировать об ошибке). Функция возвращает О в случае успеха или — 1 в случае ошибки. 8.4.3. Функция msgrcv Функция msgrcv, описанная в файле <sys/msg Ь> (испо мые типы описаны в заголовочном файле <sys/ pc h gt;), ляет взять сообщение с указанным типом (тегом) из очереди. Прототип: int msgrcv (int msqid, void *msgp, size t msgsz, long int msgtyp, int msgflg); где взводные параметры: ° msqid — идентификатор очереди сообщений; ° msgp — указатель на буфер для сообщения, которое должно иметь тип (формат) struct msgbuf; ° msgsz — размер буфера для сообщения; ° msgtyp — тип (тег) сообщения: — если msgtyp равно О, то взять первое сообщение из оче- реди, — если msgtyp ( О, то взять первое сообщение из очереди с типом ( [msgtyp[, — если msgtyp ) О, то взять первое сообщение из очереди с типом у~ Jmsgtyp[; ° msgflg — дополнительные флаги, определяющие поведение процесса в случае, если очередь сообщений пуста (можно указать: ожидать появления сообщений или сигнализиро- вать об ошибке) или выбранное сообщение имеет длину боль- 
126 Глава 8. Синхронизация и взаимодействие процессов ше msgsz (можно указать: обрезать сообщение или сигнали- зировать об ошибке). Функция возвращает длину полученного сообщения в случае успеха или — 1 в случае ошибки. 8.4.4. Функция msgctl Функция msgctl, описашиая в файле <sys/msg h> (испо мые типы описаны в заголовочном файле <sys/ipc.h gt;), ляет управлять очередью сообщений (в частности, удалять ее). Прототип: int msgctl (int msgid, int cmd, struct msqidds +buf); где взводные параметры: ° msqid — идентификатор очереди сообщений; ° cmd — задает операцию над очередью сообщений; ° buf — аргументы операции (также служит для возврата ин- формации о результате операции). Функция возвращает О в случае успеха или — 1 в случае ошибки. 8.4.5. Пример использования очередей Рассмотрим ту же задачу, что и в разделе 8.2.4. Приведенная в разделе 8.2.4 реализация нехороша тем что для избежания бесполезного цикла опроса переменных используется функция sleep. С помощью очередей сообщений мы можем избежать этой проблемы и при этом сократить сами программы. Общая часть — заголовочный файл msgdata. h, в котором описаны структура сообщения (MSGDATA) и функции работы с шв~: ¹include <stdio ¹include <stdlib ¹include <unistd ¹include <string 
8.4. Очереди сообщений {почтовые ящики) 1 ¹include <errno ¹include <sys/types ¹include <sys/ipc ¹include <sys/msg ¹define BUF LEN 256 /+ Первые два поля должны быть HMeHHQ такиии и в ТаКОМ порядке для образования msgbuf +/ & typedef struct msgdata long int mtype; /+ тип сообщения +/ char msgbuf[BUF LEN]; /+ буфер для хранения сообщения~/ int done; /+ признак окончания работы +/ int ms0;len; /+ длина сообщения +/ ) MSGDATA; /+ Длина данных сообщения (без первого поля) +/ ¹define MSGDATA LEN (BUF LEN + 2 + sizeof (int)) /+ Идентификатор очереди сообщений +/ ¹define MSG KEY 1223 int msg open (int key); int msg close (int id); Общая часть (для клиента и сервера) находится в файле msgdata. c (см. пояснения ниже): ¹include "msgdata.h" /+ Идентификатор разделяемой памяти +/ ¹define SHM KEY 1221 /+ Идентификатор разделяемой памяти +/ ¹define SEM KEY (1221+1) /* Начать работу с очередью сообщений с ключои key. 
128 Глава 8. Синхронизация и взаимодействие процессов Возвращает идентификатор очереди, -1 в случае ошибки.+/ int msgopen (int key) int msg id; /+ результат */ /+ Попытаться открыть очередь сообщений */ if ((msg id = msgget (key, 0)) == -1) /* Не удалось, рассиотрии причину +/ if (errno == ENOENT) /+ Запрошенная очередь сообщений не существует. Создаеи ее. +/ if ((msg id = msgget (key, IPC CREAT ~ 0664)) -1) fprintf (stderr, "Cannot create mesage queueNn"); l return 0; l else & fprintf (stderr, "Cannot get mesage queueNn"); return 0; & & return ms0;id; l /+ Закончить работу с очередью сообщений с идентификатором id. r 
8.4. Очереди сообщений {почтовые ящики) Возвращает не О в случае ошибки. +/ 1ПС msgclose (int id) & /+ Удаляем очередь сообщений. Удаление происходит сразу. Все ожидающие сообщений процессы активизируются, ожидание завершается с ошибкой EIDRM — Error IDentifier ReMoved. У них вся последующая работа с этой очередью будет давать ошибку, поэтому возвращаемое значение функции не проверяется. */ msgctl (id, IPC RMID, 0); return О; Функции, работающие с сообщением: ° msgopen — пытается открыть существующую очередь сооб- щений, а в случае неудачи создает новую; ° msgclose — удаляет очередь сообщений; после этого акти- визируются все процессы, ожидавшие сообщений в очереди, и любое последующее обращение к ней вызовет ошибку. Программа — клиент находится в файле reader с (см. пояс- нения ниже): ¹include "ms0;data. h" /+ Считать сообщение с терминала и записать в очередь */ void reader (int msg id) с MSGDATA data; for (;;) & /+ Выход по Ctrl+D +/ if (!Хgets (data.msgbuf, BUFLEN, stdin)) break; data.msglen = strlen (data.msgbuf) + 1; 5 --4017 
130 Глава 8. Синхронизация и взаимодействие процессов data. done = 0; data.mtype = 1; /+ Посылаем сообщение в очередь +/ if (msgsnd (msg id, &d ta, MSG ATA EN О == perror ("send"); fprintf (stderr, "Cannot send messageNn"); & return; & data.mtype = 1; data. done = 1; data.òsglån = О; /+ Посылаем сообщение в очередь +/ if (msgsnd (msg id, &d ta, MSG ATA EN О == fprintf (stderr, "Cannot send messageNn"); return; & fprintf (stderr, "reader process '/d exits(n", getpid()) & int main () /+ Идентификатор очереди сообщений +/ int msgid; if ((msg id = msg open (MSG KEY)) == -1) return i; reader (msg id) msg close (msg id) return 0; & 
8.4. Очереди сообщений (почтовые ящики) 1 1 !процедура reader в бесконечном цикле, прерываемом по ошиб- е(' ВВодя,: 1. считывает строку со стандартного ввода с помощью f gets; 2. в случае успешного чтения устанавливает реальную длину сообщения data.msglen и посылает его, иначе: прерывает цикл, устанавливает переменную data. done в 1, а реальную длину сообщения data.msglen — -в О посылает это сообще- ние и завершает работу. Программа — клиент находится в файле writer.ñ (см. пояс- нения ниже: Oinclude "msgdata.h" /+ Взять сообщение из очереди и вывести на экран +/ { void writer (int msg id) NSGDATA data; for (;;) & /+ Получаем сообщение из очереди +/ if (msgrcv (msg id, &d ta, MSGDATA EN О О = { /+ Проверяем причину +/ if (errno == EIDRM) с /+ Очередь была удалена другим процессом.+/ break; /+ Заканчиваем работу +/ & fprintf (stderr, "Cannot recieve message(n"); return; & /+ Проверяем признак конца работы +/ 
132 Глава 8. Синхронизация и взаимодействие процессов if (data. done) { /+ Конец работы +/ break; & if (data.msglen) { /+ есть сообщение для вывода +/ printf ("-) '/s(n", data.msgbuf); data.msglen = О; & & fprintf (stderr, "writer process /d exits(n", getpid ()); & int main () /+ Идентификатор очереди сообщений +/ int msg id; if ((msg id = msg open (MSGKEY)) == -1) return i; writer (msg id) msg close (msg id) return О; & Процедура writer в бесконечном цикле, прерываемом в слу- чае ненулевого значения поля done в полученном сообщении: 1. получает сообщение; 2. если поле done в сообщении равно О, то завершает работу; 3. если поле msglen в сообщении не равно О (т. е. есть строка для вывода), выводит строку из сообщения. 
8.4. Очереди сообщений {почтовые ящики} По поводу отладки программ, использующих очереди сооб- щений, см. замечание в разделе 8.2.4. 8.4.6. Функция pipe Функция pipe, описанная в заголовочном файле <unistd. предоставляет альтернативный по отношению к описанным вы- ше функциям msgget, msgsnd, msgrcv, msgctl механизм созда- ния очереди сообщений. Прототип: int pipe (int filedesc[2]); где f iledesc — результат работы функции, представляющий со- бой массив из двух дескрипторов файлов. В случае успешного выполнения эта функция создает очередь сообщений, из ко- торой можно читать данные обычными функциями файлово- го ввода (например, read), используя дескриптор f iledesc[0], и в которую можно записывать данные обычными функция- ми файлового вывода (например, write), используя дескриптор f iledesc [1], функция возвращает — 1 в случае ошибки. Основные отличия функции pipe от интерфейса, предостав- ляемого функциями msgget, msgsnd, msgrcv, msgctl: ° Созданная pipe очередь не имеет идентификатора, позволя- ющего выделить конкретную очередь (ср. с msgget, где есть параметр Кеу). Поэтому типичным использованием pipe яв- ляется организация очереди между процессами — потомками одного родителя: pipe вызывается до функции fork и пото- му дескрипторы f iledesc доступны потомку и родителю. ° Использование обычных функций файлового ввода-вывода (вроде read и write) вместо специализированных (msgsnd и msgrcv) приводит к тому, что отсутствует гарантия целостно- сти получаемых и передаваемых сообщений (т. е. возможно одновременное чтение или запись от нескольких процессов с перемешиванием их данных). Поэтому необходимо приме- нять тот или иной механизм взаимного исключения процес- сов. Поскольку по определению чтение из пустой очереди 
134 Глава 8. Синхронизация и взаимодействие процессов и запись в заполненную очередь блокируют процесс, то это можно использовать в качестве возможного механизма син- хронизации. ° Функция удаления созданной pipe очереди отсутствует (ср. с msgctl). Очередь удаляется как только будут закрыты (с помощью обы шой файловой функции close) все дескрип- то> ,> айлов fi e esc в аж ом из проц ссо . При ес.ли:закрыты все дескрипторы f iledesc [1] (для записи), то по окончивши данных в очереди все операции чтения бу- дут давать ошибку «конец файла». Если же закрыты все дескрппторь& t f iled sc 0] ( ля чтени ), то лю ая опера записи в очередь Г)удет генерировать сигнал SIGPIPE, стан- дартный обработчик которого терминирует процесс. Пример использования руре см. в разделе 1.3. В этой про- грамме очередь f romroot служит для взаимного исключения процессов (передаваемые данные не используются) очередь to root служит для передачи результата от потомка к роди- телю. Очереди создаются и основном (т. е. изначально запущен- ном пользователем) процессе, и только затем р раз вызывается функция fork, создавая р процессов-потомков. Следователь- но, все процессы имеют одни и те же значения в from root и to root. Процессы-потомки сразу после старта закрыва- ют свои копии дескрипторов для записи в очередь from root (f rom root [1] ) и для чтения из очереди to root (to root [0] ). Основной процесс после запуска потомков закрывает свои ко- пии дескрипторов для записи в очередь to root (to root [1]) и для чтения из очереди агап>г оС (агап&g ;гооС [О] . Тем формируются две очереди сообщений: ° Хromroot, в которую основной процесс люжет только запи- сывать данные, а процессы — потомки — только их считывать, ° to root, из которой основной процесс может только считы- вать данные, а процессы — потомки — только их записывать. 
8.5. Пример вычисления произведения матрицы на вектор 135 Сразу после запуска процесс — потомок вычисляет свою часть интеграла с помощью функции integrate и переходит в ре- > им ожида ия появле ия сообще ия из одн го бай а в о реди from root. Один байт является минимальной величиной, неделимой между процессами, поэтому при отправке основным процессом такого сообщения будет активизирован ровно один из его потомков. Дождавшись сообщения, процесс — потомок за- писывает результат в очередь to root и заканчивает свою рабо- ту. Основной процесс после запуска потомков р раз последова- тельно записывает один байт в очередь from root (тем самым активизируя один из процессов — потомков) H переходит в режим ожидания появления сообщения в очереди to root. Получив со- общение — — число типа double, основной процесс прибавляет его к результату total, формируя окончательный ответ. 8.5. Пример многопроцессной программы, вычисляющей произведение матрицы на вектор Рассмотрим задачу вычисления произведения матрицы на век- тор. Для повышения скорости работы на многопроцессорной вычислительной установке с общей памятью создадим несколь- ко процессов (по числу процессоров) и разделим работу меж- ду ними, разместив все данные в разделяемой памяти. Основ- ным элементом синхронизации задач будет являться функция synchronize, которая дает возможность процессу ожидать, пока эту же функцию вызовут все другие процессы. Файлы проекта: ° synchronize ñ, synchronize h — исходный текст и соответ- ствующий заголовочный файл для функций синхронизации и взаимного исключения процессов; ° get time.с, get time.h — исходный текст и соответствую- щий заголовочный файл для функций работы со временем; ° main. с — запуск процессов и вывод результатов; ° matrices.с, matrices.h — исходный текст и соответствую- щий заголовочный файл для функций, работающих с мат- рицами; 
1 ~() Глава 8. Синхронизация и взаимодействие процессов ° Makef ile — для сборки проекта. Заголовочный файл synchronize h: iieet init synchronize lock (int total processes); int destroy synchronize lock (void) int lock data (void); int unlock data (void); void synchronize (int process num, int total processes); В файле synchronize.c находятся функции: ° semopen — создает новый набор из указанного количества семафоров и устанавливает каждый из них в указанное на- чальное значение; для создания уникального (т. е. отлично- го от уже имеющихся в каком-либо процессе) набора в ка- честве ключа для функции вешЕес использовано значение IPC PRIVATE, для инициализации всех созданных семафоров в функции semctl использовано значение SETALL; ° sem close — удаляет указанный набор семафоров; ° init synchronize lock -- создает все используемые объек- ты синхронизации и взаимного исключения процессов: — 1 семафор с начальным значением 1 для взаимного ис- ключения процессов при доступе к разделяемой памяти; — 2 набора по total processes семафоров с начальным зна- чением О для синхронизации процессов; — массив 1осКа11, используемый для операций с послед- ними двумя наборами; ° destroy synchronize lock — удаляет все созданные в предылущей функции объекты; ° lock data — обеспечить исключительную работу с разделя- емой памятью текущему процессу; ° unlock data — разрешить другим процессам работать с раз- деляемой памятью; ° synchronize internal — основная подпрограмма функции синхронизации; блокирует вызвавший ее процесс до тех пор, пока ее не вызовут все total processes процессов; схема работы: 
8.5. Пример вычисления произведения матрицы на вектор 137 1. добавить total processes к значению семафора с номе- ром process пиш из указанного набора, состоящего из total processes семафоров; эта операция не приводит к блокировке процесса; 2. вычесть 1 из значения каждого из total processes се- мафоров в указанном наборе; эта операция приводит к блокировке процесса до тех пор, пока значения всех се- мафоров не станут положительными; другими словами, каждый из total processes процессов прибавляет total processes к одному семафору, и вычитает 1 из всех, поэтому, например, перед приходом в эту функцию последнего процесса, имеющего номер k, k-й семафор в набо- ре имеет значение 1-total processes (и потому блокирует все остальные total processes-1 процессов), а остальные total processes-1 семафоров имеют значение 1; последний пришедший процесс описанной выше операцией 1 разрешает работать всем процессам, а затем операцией 2 приводит все семафоры в начальное значение О; ° synchronize — основная функция синхронизации; блокиру- ет вызвавший ее процесс до тех пор, пока ее не вызовут все totalprocesses процессов; дважды вызывает предыдущую функцию для двух разных наборов по total processes се- мафоров для того, чтобы дождаться, пока в эту функцию все процессы войдут и выйдут, не допуская ситуации, ко- гда один из процессов, выйдя из synchronize, войдет в нее раныпе, чем остальные вышли при предыдущей синхрониза- ции, и нарушит тем самым ее работу. Файл synchronize.c: ¹include <stdio ¹include <stdlib ¹include <unistd ¹include <errno Oinclude <sys/types Oinclude <sys/ipc 
138 Глава 8. Синхронизация и взаимодействие процессов Oinclude <sys/sem ¹include "synchronize.h" /+ Идентификатор семафора взаимного исключения +/ static int lock data sem id; /+ Идентификаторы наборов семафоров, используемых для синхронизации. +/ static int processes in sem id; static int processes out sem id; /+ Структура данных для операций с набором семафоров +/ struct sembuf + lock а11; /+ "Открыть" новый набор семафоров длины и и установить начальное значение каждого семафора val. Возвращает идентификатор набора, -1 в случае ошибки. +/ static int & sem open (int и, int val) int id; /+ результат +/ /+ Структура данных для управления семафором +/ ¹ifdef SEM SEMUN UNDEFINED union semun { /+ Значение для команды SETVAL +/ int val; /+ Буфер для команд 1РС ЯТАТ и IPC SET +/ struct semid ds +buf; /+ Массив для команд GETALL u SETALL +/ unsigned short int *аггау; /* Буфер для команды IPCINFO +/ struct seminfo +buf; & ¹endif union semun s un; 
8.5. Пример вычисления произведения матрицы на вектор 139 int /+ Создаем новый набор семафоров +/ if ((id = semget (IPC PRIVATE, n, IPC CREAT I 0664)) == -1) { fprintf (stderr, "Cannot create semaphore(n"); return -1; & /+ Выделяем память под массив начальных значений +/ if (!(s un.array = (unsigned short int+) malloc (n + sizeof (unsigned short int)))) & /+ Мало памяти +/ fprintf (stderr, "Not enough memory(n"); return -1; /+ Начальное значение набора семафоров +/ for (i = 0; i ( и; i++) яип.array[i] = val; /* Установить начальное значение каждого семафора +/ if (semctl (id, О, SETALL, s un) == -1) & fprintf (stderr, "Cannot init semaphoreNn"); /+ С семафором работать нельзя, заканчиваем +/ free (s un.array); return -1; free (s un.array); return id; 
140 Глава 8. Синхронизация и взаимодействие процессов /+ "Закрыть" семафор с идентификатором id. Возвращает не О в случае ошибки. +/ static int sem close (int id) /+ Удаляем семафор. +/ { if (semctl (id, О, IPCRMID, О) == -1) fprintf (stderr, "Cannot delete semaphore(n"); return -| & return О; & /+ Инициализировать все объекты синхронизации и взаимного исключения. Возвращает -1 в случае ошибки.+/ int init synchronize lock (int total processes) int /+ Создаем 1 семафор с начальным значением 1 +/ lock data sem id = sem open (1, 1); /* Создаем total processes семафоров с начальным значением О +/ processes in sem id = sem open (total processes, О); /* Создаем СоСа1 ргосеввев семафоров с начальным значением О +/ processes out sem id = sem open (total processes, О); /* Проверяем успешность создания +/ if (lock data sem id == -1 ~~ processes in sem id == -1 II processes out sem id == -1) return -1; /+ семафоры не созданы +/ /+ Выделяем память под массив +/ 
8.5. Пример вычисления произведения матрицы на вектор 141 if (!(lock all = (struct sembuf +) malloc (total processes + sizeof (struct sembuf)))) return -2; /+ нет памяти +/ /+ Инициализируем массив +/ с for (i = 0; i & t; to al process s; i lock all[i] .semnum = i; lock all[i] .sem op = -1; lock all[i] .semflg = 0; & return 0; & /+ Закончить работу со всеми объектами синхронизации и взаимного исключения. Возвращает -1 в случае ошибки.+/ int destroysynchronize lock () & if (sem close (lock data sem id) == -1 sem close (processes in sem id) == -1 sem close (processes out sem id) == -1) return -1; /+ ошибка +/ free (lock all); return 0; & /+ Обеспечить исключительную работу с разделяемой памятью текущему процессу. Возвращает не 0 в случае ошибки. &l int lock data () /+ Структура данных для операций с семафором +/ struct sembuf s buf = ( О, -1, 0&g 
142 Глава 8. Синхронизация и взаимодействие процессов if (semop (lock data sem id, &s uf 1 == & fprintf (stderr, "Cannot lock semaphoregn"); return -1; /+ ошибка +/ & return 0; & /+ Разрешить другим процессам работать с разделяемой памятью. Возвращает не 0 в случае ошибки. +/ int unlock data () & /+ Структура данных для операций с семафором +/ struct sembuf s buf = & t; О, 1, if (semop (lock data sem id, &a p;s uf 1 == & fprintf (stderr, "Cannot ип1осК semaphoregn"); return -1; /+ ошибка +/ & return 0; & /+ Подпрограмма для synchronize +/ static int synchronize internal (int id, int process пит, int total processes) & /* Добавить total processes единиц к семафору с номером process num из набора с идентификатором id. .+/ /® Структура данных для операций с семафором ®/ struct sembuf s buf = <proc ss n m, to al process s, if (semop (id, &a p;s uf 1 == 
8.5. Пример вычисления произведения матрицы на вектор 143 return -1; /+ ошибка +/ /+ Вычесть 1 из каждого из total processes семафоров из набора с идентификатором id. */ if (semop (id, 1осКа11, total processes) == -1) return -2; /+ ошибка +/ & return О; /* Дождаться в текущем процессе с номером process пит остальных процессов (из общего числа totalprocesses)+/ void synchronize (int process пит, int total processes) & /+ Дождаться, пока все процессы войдут в эту функцию +/ synchronize internal (processes in sem id, processnum, total processes); /+ Дождаться, пока все процессы выйдут из этой функции~/ synchronize internal (processes out sem id, process пшп, total processes); & Заголовочный файл get time h: long int get time (); long int get full time (); Файл get time.c: ¹include <sys/time ¹include <sys/resource ¹include "get time.h" /+ Вернуть процессорное время, затраченное на текущий процесс, в сотых долях секунды. Берется время только самого процесса, время работы системных вызовов не прибавляется. +/ 
144 Глава 8. Синхронизация и взаимодействие процессов с long int get time () struct rusage buf; getrusage (RUSAGE SELF, &b /+ преобразуем время в секундах в сотые доли секунды */ return buf.ru utime.tv sec + 100 /+ преобразуем время в микросекундах в сотые доли секунды */ + buf.ãè utime.tv usec / 10000; & /+ Возвращает астрономическое (!) время в сотых долях секунды +/ long int get full time () struct timeval buf; gettimeofday (& uf, /+ преобразуем время в секундах в сотые доли секунды */ return buf.tv sec + 100 /+ преобразуем время в микросекундах в сотые доли секунды +/ + buf.tv usec / 10000; & Результат, выдаваемый get f ull time, имеет смысл лишь в том случае, если в системе нет других выполняющихся процессов, кроме запущенной этой программой группы. В файле main. с находится главная функция main, которая: ° создает блок разделяемой памяти, достаточный для разме- щения: 1. 1-й матрицы и х n., 
8.5. Пример вычисления произведения матрицы на вектор 145 2. 2-х векторов длины и, 3. 1-го целого числа, в котором будет суммироваться время работы всех процессов; для создания уникального (т. е. отличного от уже имеющих- ся в каком-либо процессе) блока разделяемой памяти в ка- честве ключа для функции shmget использовано значение IPCPRIVATE; ° подсоединяет созданный блок разделяемой памяти к адрес- ному пространству процесса; ° с помощью init synchronize lock создает все используе- мые объекты синхронизации и взаимного исключения про- цессов; ° размещает матрицу, 2 вектора, счетчик времени в разделяе- мой памяти и инициализирует их; ° запускает total processes-1 копий текущего процесса с по- мощью fork, при этом созданный блок разделяемой памяти и все семафоры наследуются порожденными процессами; ° каждый из total processes процессов вызывает функцию matrixmult vector process, которая: — вычисляет компоненты ответа с индексами в диапазоне n + process пшп / total processes, n + (process num + i) / total processes — i с помощью функции matrix mult vector; — вычисляет суммарное процессорное время, затраченное на все запущенные процессы; для взаимного исключения при доступе процессов к одной и той же ячейке разделяемой памяти с адресом p total time используются функции lock data, unlock data (см. выше); — для целей отладки и более точного замера времени рабо- ты функция вычисления произведения матрицы на вектор вызывается в цикле Ю TESTS раз. ° каждый из total processes процессов вызывает функцию synchronize для ожидания завершения всех процессов; 
146 Глава 8. Синхронизация и взаимодействие процессов ° total processes-1 порожденных процессов завершаются; главный процесс (с номером 0) выводит результаты работы и удаляет все объекты синхронизации и разделяемую память. Файл main. c: ¹include <stdio ¹include <stdlib ¹include <unistd ¹include <errno ¹include <sys/types ¹include <sys/ipc ¹include <sys/shm ¹include "matrices.h" ¹include "get time.h" ¹include "synchronize.h" /+ Количество тестов (для отладки) +/ ¹define N TESTS 10 /+ Умножение матрицы на вектор для одного процесса +/ void matrix mult vector process ( double * matrix, /* матрица */ double * vector, /* вектор */ double + result, /* результат */ int n, /+ размерность +/ long + ptotaltime, /* суммарное время */ int process num, /+ номер процесса +/ & int total processes) /* всего процессов */ long int t; int printf (" Process /d startedgn", process num); 
8.5. Пример вычисления произведения матрицы на вектор 147 t = get time (); /+ время начала работы +/ for (i = О; i & t N TES S; i & matrix mult vector (matrix, vector, result, п, process пиш, total processes); printf (" Process '/d mult '/d timespan", process num, i); & t = get time () — t; /* время конца работы */ /+ Суммируем времена работы +/ /+ Обеспечить исключительный доступ к памяти +/ lock data (); +ptotaltime += t /+ Разрешить другим работать с разделяемой памятью +/ unlock data (); printf (" Process '/d finished, time = /1фп", processnum, t); int main () int total processes; /+ число создаваемых процессов int process num; /+ номер процесса */ /+ астрономическое время работы всего процесса +/ long int t full; void +shmem; /+ указатель на разделяемую память +/ int shm id; /+ идентификатор разделяемой памяти +/ int и; /* размер матрицы и векторов +/ double +matrix; /* матрица */ double +vector; /+ вектор */ double +result; /+ результирующий вектор +/ long ~p total time; /* общее время всех процессов~/ 
148 Глава 8. Синхронизация и взаимодействие процессов int 1; pid t pid; printf (" Input processes number: "); scanf ("'/d", &t tal process printf ("matrix size = "); scanf ("/d", &amp /+ Общий размер требуемой памяти: под массивы: 1 матрица и х и типа double 2 вектора длины и типа double для счетчика времени: 1 число типа long int +/ 1 = (п + n + 2 + п) + sizeof (double) + sizeof (long) /+ Создаем новый блок разделяемой памяти +/ if ((shmid = shmget (IPCPRIVATE, 1, ТРС СВЕАТ I 0664)) == -1) & fprintf (stderr, "Cannot create shared шешогу~п"); return 1; /* завершаем работу */ & /+ Подсоединяем разделяемую память к адресному пространству процесса +/ if ((shmem = (void +)shmat (shm id, О, О)) == (void+) 1) fprintf (stderr, "Cannot attach shared memory(n"); гесигп 3; /+ завершаем работу +/ & /+ Устанавливаем указатели на данные в разделяемой памяти +/ 
8.5. Пример вычисления произведения матрицы на вектор 149 matrix = (double+) shmem; vector = matrix + n * n; result = vector + n; р total time = (long int+) (result + n); +ptotal time = 0; /+ начальное значение +/ /+ Инициализируем все объекты взаимодействия +/ if (init synchronize lock (total processes)) return 4; /+ завершаем работу в случае ошибки +/ /+ Инициализация массивов +/ init matrix (matrix, n); init vector (vector, n); printf ("Matrix:~п"); print matrix (matrix, n); printf ("Vector:~п"); print vector (vector, n); printf (" Allocated /d bytes ('/dKb or '/dMb) of шешогу~п" 1, 1 » 10, 1 » 20); /+ Засекаем астрономическое время начала работы +/ t full = get full time (); /+ Запускаем total processes — 1 процессов, процесс с номером 0 — текущий +/ for (process num = total processes — 1; с processnum & t; 0; proc ss num /* Клонировать себя +/ pid = fork (); if (pid == -1) & fprintf (stderr, "Cannot fork!gn"); return 5; /+ завершаем работу +/ 
150 Глава 8. Синхронизация и взаимодействие процессов & if (pid == О) /+ Выполняем работу в порожденном процессе +/ matrix mult vector process (matrix, vector, result, n, ptotal time, process num, total processes); /* Дождаться остальных процессов */ synchronize (process num, total processes); return О; /+ завершить процесс +/ & /+ Здесь работает главный процесс с номером О. +/ /+ Выполняем работу в процессе process num = О +/ matrix mult vector process (matrix, vector, result, n, p total time, process num, total processes); /* Дождаться остальных процессов */ synchronize (process num, total processes); t full = get full time () — t full. if (t full == О) t full = 1; /+ очень быстрый компьютер... +/ /+ Здесь можно работать с результатом +/ print vector (result, n); printf (" Total full time = '/ld, total processes time = '/ld (/ld//), per process = /ld(n", t full, +ptotaltime (+ptotaltime + 100) / t full, *р total time / total processes); /® Ликвидируем созданные объекты синхронизации +/ destroy synchronize lock (); 
.5. Пример вычисления произведения матрицы на вектор 151 /+ Отсоединяем разделяем~ею память от адресного пространства процесса +/ & if (shmdt (shmem) == -1) fprintf (stderr, "Cannot detach shared шешогу~п"); return 10; /+ Удаляем разделяемую память. +/ if (shmctl (shm id, IPC RMID, 0) == -1) & fprintf (stderr, "Cannot delete shared шешогу~п"); return ii; } return 0; Заголовочный файл matrices h: void init matrix (double + matrix, int n); void init vector (double * vector, int n); void print matrix (double * matrix, int n); void print vector (double * vector, int n); void matrix mult vector (double ~а, double ~Ь, double ~c int n, int process num, int total processes); Файл matrices. с: ¹include <stdio ¹include "matrices h" ¹include "synchronize.h" /+ Инициализация матрицы +/ void init matrix (double + matrix, int n) 
152 Глава 8. Синхронизация и взаимодействие процес int i, double *а = matrix; for (i = 0; i & t; n; i for (j = 0; j & t; n; j *(а++) = (i & t; j /+ Инициализация вектора +/ & void init vector (double + vector, int n) int double ~Ь = vector; for (i = 0; i & t; n; i ~(Ь++) = 1.; ) redefine NMAX 5 /+ Вывод матрицы +/ void print matrix (double + matrix, int n) & int int m = (n & t N A N M X: for (i = 0; i & t; m; i & for (j = 0; j & t; m; j printf (" /12.61f matrix [i + n + j) ); printf ("(n"); ) /~ Вывод вектора +/ 
8.5. Пример вычисления произведения матрицы на вектор 153 & void print vector (double * vector, int n) 1ПС 1; int m = (n & t Ы А Ы А : for (i = 0; i & t; m; i printf (" /12.61f", vector[i)); printf ("(n"); /+ Умножить матрицу а на вектор Ь, с = аЬ для процесса с номером process num из общего количества total processes. +/ void matrixmult vector (double ~а, double *Ь, double ~с, int n, int process num, int total processes) & int i, double ~р, s; int first row, last row; /+ Первая участвующая строка матрицы +/ first row = n * process num; first row /= total processes; /+ Последняя участвующая строка матрицы +/ last row = n + (processnum + 1); last row = last row / total processes — 1; for (i = first row, р = а + i + n; i &l ;= l st r w; i & for (s = О., j = 0; j ( и; j++) я += *(p++) * ЬЦ]; c[i] = s; иynchronize (process num, totalprocesses); 
154 Глава 8. Синхронизация и взаимодействие процессов Файл Makef ile: NAME = process mult DEBUG СС = gcc -с LD = gcc CFLAGS = $(DEBUG) -W -Wall LIBS = -lm LDFLAGS = $(DEBUG) OBJS = main.o matrices.o synchronize.o get time.o all : $(NAME) $(NAME) : $(OBJS) $(LD) $(LDFLAGS) $ $(LIBS) -o $6 .с.о: $(СС) $(CFLAGS) $& t; -о clean: rm -f $(OBJS) $(NAME) main.o main.c matrices h get time h synchronize h matrices.î : matrices.c matrices.h synchronize.h synchronize.î: synchronize.c synchronize.h get time.o : get time.c get time.h Сделаем замечание об отладке приведенной выше про- граммы. В случае ее аварийного завершения созданные объ- екты межпроцессного взаимодействия (блок разделяемой памя- ти и наборы семафоров) не удаляютсч. При повторном запуске программа будет требовать создания новых объектов, что мо- жет оказаться невозмо'к\ым из-за исчерпания объема доступ- 
8.5. Пример вычисления произведения матрицы на вектор 155 ~п&g ;й разделяе ой пам ти ли количес ва семафор в. Поэт <и:тавши ся по ле оши к в програ ме обье ты PC необхо мо удалить внешней процедуро~i, см. описание утилит ipcs u ipcrm в разделе 8.2.4 (с. 119). Заметим также, что максималы~ый объем разделяемой па- мяти, выделяемой процессу, является важным параметром лю- бой операционной системы, и обычно существенно меньше объ- < ма доступ ой проце су виртуаль ой памя и. Ча то э от па метр нельзя изменигь динамически (во время работы системы) а только путем перекомпиляции ядра. Поэтому для описанной выше задачи, требующей при больших и значительных объе- мов памяти, предпочтительнее использовать механизм задач (threads), где объемы разделяемой и виртуальной памяти сов- надают. I 
Управление задачами (threads) В этой главе мы рассмотрим основные функции, позволяющие запускать новые задачи и управлять ими. 9.1. Функция pthread create Функция pthread create, описанная в заголовочном файле <pthread. >, поз оляет с здать новую а ачу и ач ть полнение с указанной функции. Прототип: int pthread create (pthread t +thread, const pthread attr t +attr, void +(+start routine) (void +), void +arg); где ° thread — возвращаемое значение — идентификатор создан- ной за.дачи; ° attr — указатель на структуру с атрибутами создаваемой за- дачи (может быть 0 для использования значений по умолча- нию; ° start routine — указатель на функцию, с которой надо на- чать выполнение задачи; ° arg — аргумент этой функции. Функция возвращает 0 в случае успеха. 
9.3. Функция sched yield 1 9.2. Функция pthread Join <Dytmu sr pthr ad jo n, описан а в фа ле <pthre d.h& воляет текущей задаче ожидать окончания указанной задачи (этого же процесса). Прототип: int pthread join (pthread t thread, void +*threadreturn); где ° thread — идентификатор задачи, завершения которой надо ожидать) ° thread return — указатель на переменную, где будет сохра- нено возвращаемое значение задачи (может быть 0, если это не требуется). Функция возвращает 0 в случае успеха. Любую задачу установкой специального атрибута при со- здании или с помощью специальной функции (pthreaddetach) можно перевести в такое состояние, в котором ее нельзя будет ожидать посредством pthread» j oin. 9.3. Функция sched yield Функция sched yield, описанная в файле (sched.h&g ;, позвол поставить текущую задачу (процесс) в конец очереди готовых задач (процессов) с тем же приоритетом. Прототип: int sched yield (void) Функция возвращает 0 в случае успеха. 
Синхронизация и взаимодеиствие задач Вся память у работающих задач является разделяемой между ними, поэтому для совместного использования любой глобаль- ной переменной необходимо использовать тот или иной объект синхронизации. Для повышения эффективности работы про- грамм и удобства программирования для задач введены специ- альные средства синхронизации. 10.1. Объекты синхронизации тила mutex Обьекты синхронизации типа 1п111ех фактически представляют собой некоторое развитие булевских семафоров в плане повы- шения безопасности и эффективности работы программы. Типичный цикл работы с разделяемым ресурсом следую- щей: взять семафор, работать с реоурсом, вернуть семафор. Однако, если в результате ошибки в программе она вначале вы- зовет функпщо ееЗзнуть семафор (не взяв его!) а затем выпол- нит приведенный выше цикл работы с разделяемым ресурсом, то функция взять семафор пе блокирует задачу, если ресурс занят. Другой проблемой при работе с семафорами является необ- ходимость переключения задач при каждом вызове функций, работающих с семафором. Дело в том, что сам счетчик семафо- ра в (фактически являющийся разделяемой между процессами 
10.1. Объекты синхронизации типа mutex 1 памятью) находится в области данных операционной системы, и любая функция, работающая с семафором, является систем- ным вызовом. Это особенно плохо при синхронизации между задачами, поскольку в этом случае сам обмен данными не тре- бует переключения задач (так как вся память у задач общая). Для борьбы с этими явлениями вводится объект тпи~ех, кото- рый фактически состоит из пары: булевского семафора и иден- тификатора задачи — текущего владельца семафора (т. е. той задачи, которая успешно вызвала функцию взять и стала вла- дельцем разделяемого ресурса). При этом сама эта пара хранит- ся в разделяемой между задачами памяти (в случае threads — в любом месте их общей памяти). Для доступа к объекту шп~ех m определены три примитивные операции: ° Lock(m) — блокировать mutex т, если т уже заблокирован другой задаче~~, "o эта операция переводит задачу в состоя- ние ожидания разблокирования т,; ° Unlock(m) — разблокировать mutex т, (если m ожидается другой задачей, то она может быть активизирована, удале- на из очереди ожидания и может вытеснить текущую зада- чу, например, если ее приоритет выше); если вызвавшая эту операцию зад'ла не является владельцем m, то операция не имеет никакого эффекта; ° TryLock(m) — попробовать блокировать mutex m, если т не блокирован, то эта операция эквивалентна Lock(m), иначе возвращается признак неудачи. Эти операции неделимы, т. е. переключение задач во время их исполнения запрещено. Объекты тпи~ех бывают: ° локальными — доступны для синхронизации между зада- чаии (threads) одного процесса; размещаются в любом месте их общей памяти; ° глобальными — доступны для синхронизации между зада- чами (threads) разных процессов; размещаются в разделяе- мой между процессами памяти. 
16О Глава 10. Синхронизация и взаимодействие задач ()тметим, что глобальные mutex появились значительно поз- ~ке локальных и предоставляются не всеми операционными си- &l ;: тема и. Причи ой их появле ия явил сь удобс во локаль ппйех для синхронизации задач, и глобальные mlltBx являются обобщением последних на процессы. Некоторые операционные системы предоставляют объекты r»utex со специальными свойствами, полезными в ряде случаев. Рассмотрим следующую ситуацию. Функции f1 и f2 работают с разделяемыми ресурсами и используют mutex m для синхро- низации между задача.ми: & f1() Lock(m); ° ° ° Unlo ck (m); } Х2() Lock(m); ° ° ° Unlock(m); & В результате развития программы нам потребовалось вызвать функцию f1 из К2: Х2() & Lock (ш); <операт ры Х1(); <операт ры Unlock(m); 
10.1. Объекты синхронизации типа mutex 1 1 Однако это приводит к ситуации с1еасПос1с задача переходит в вечный цикл ожидания освобождения mutex m в функции f1, поскольку она уже является владельцем этого тиФех. Поэто- му некоторые операционные системы предоставляют объекты mutex дополнительных типов: ° error check — вызов Lock владельцем mutex, а также Unlock не владельцем не производят никакого действия; отметим, что mutex этого типа решит проблему, описанную выше, но появляется новая: <операт ры 2> может быть, рабо с разделяемым ресурсом) выполняются, когда задача уже не является владельцем mUtBx; ° recurcive — вызов Lock владел~ цем mutex у.величивает счет- чик таких вызовов, вызов Unlock владельцем — уменьшает счетчик; реально разблокирование пшСех происходит при значении счетчика, равном О. 10.1.1. Функция pthread mutex init Функция pthread mutex init, описанная в заголовочном файле <pthre d. >, поз оляет инициализи овать utex. Про int pthread mutex init (pthread mutex t +mutex, pthread mutexattr t *а11г); где ° mutex — указатель íà muted, ° attr — указатель на структуру с атрибутами mutex (может быть 0 для использования значений по умолчанию). Функция возвращает 0 в случае успеха. Для mutex (объекта типа pthread mutex t) вместо функции pthread mutex init можно использовать статическую ишщиа- лизацию при объявлении: pthread mutex t mutex = PTHREAD MUTEX INITIALIZER. (1- 4017 
162 Глава 10. Синхронизация и взаимодействие задач где макрос PTHREAD MUTEX INITIALIZER определен в заголовоч- ном файле <pthread. >. Пос едняя апись эквива ентна довательности: pthread mutex t mutex; pthread mutex init (&mu ex, но, в отличие от нее, может быть использована д.пя инициали- зации глобальных объектов. 10.1.2. Функция pthread mutex lock Функция pthread mutex lock, описанная в заголовочном файле <pthre d. >, поз оляет блоки овать utex е ли н уже кирован другой задачей, то эта функция переводит задачу в состояние ожидания разблокирования пшйех. Прототип: int pthread mutex lock (pthread mutex t +mutex); где mutex — указатель на пш(ех. Функция возвращает 0 в случае успеха. 10.1.3. Функция pthread mutex trylock Функция pthread mutex trylock, описанная в заголовочном файле <pthre d. >, поз оляет блоки овать utex е ли заблокирован другой задачей, то эта функция возвращает ошиб- ку, не меняя состояния nautex. Прототип: int pthread mutex trylock (pthread mutex t +mutex); где mutex — указатель Hà mutex. Функция возвращает 0 в случае успеха. 10.1.4. Функция pthread mutex unlock Функция pthread mutex unlock, описанная в заголовочном файле <pthread. >, поз оляет разблоки овать utex. тип: int pthread mutex unlock (pthread mutex t +mutex); 
1 10.1. Объекты синхронизации типа mutex iде mutex -- указатель Hà mutex. Функция возвращает 0 в случае успеха. 10.1.5. Функция pthread mutex destroy Функция pthread mutex destroy, описанная в заголовочном файле <pthread. ), позвол ет удал ть mut x, кото ый дол быть в этот момент разблокированным. Прототип: int pthread mutex destroy (pthread mutex t ~пш~ех); где mutex — указатель íà mutex. Функция возвращает 0 в случае успеха. 10.1.6. Пример использования mutex Рассмотрим ту же задачу, что и в разделе 8.2.4, только вместо процессов будем использовать задачи. Приводимая ниже про- грамма считывает строку со стандартного ввода и выводит се на стандартный вывод. Для чтения строки и для ее вывода созда- ются две задачи (threacl), кодом для которых являются функции геайег и чг~тег соответственно. Поскольку задачи работают в том же адресном пространстве, что и создавший их процесс, то они автоматически разделяют все переменные, включая бу- фер сообщений msgbuf Для организации взаимного исключения при доступе к критическим разделяемым ресурсам (перемен- ныи done, msglen, msgbuf) используется объект синхронизации mutex mutex. ¹include <stdio ¹include <strin0; ¹include <pthread ¹define BUF LEN 256 /+ Объект синхронизации типа mutex +/ pthreadmutex t mutex; 
164 Глава 10. Синхронизация и взаимодействие задач /+ Признак окончания работы +/ int done; /+ Длина сообщения +/ int ms0;len; /* Буфер для сообщения +/ char msgbuf[BUF LENj; /+ Считать сообщение и поместить его в буфер. Функция работает как независимая задача, вызываемая операционной системой, поэтому прототип фиксирован. Аргумент argp не используется. +/ void + reader (void +argp) { for (;;) & /* "захватить" mutex */ pthread mutex lock (&mut if (!msglen) с /+ Считать сообщение. Выход по Ctrl+D +/ if (!fgets (msgbuf, BUF LEN, stdin)) break; msglen = strlen (msgbuf) + 1; /+ "освободить" mutex +/ pthread mutex unlock (&mut /+ Поместить задачу в конец очереди готовых задач с тем же приоритетом +/ sched yield (); & /* Напечатать идентификатор текущей задачи +/ printf (" Thread /х exitsgn", (int)pthread self ()); done = i; /+ "освободить" mutex +/ pthread mutex unlock (&mut 
10.1. Объекты синхронизации типа mutex 1 /* завершить задачу */ } return 0; /+ Ждать сообщения, по его поступлении вывести его на экран. Функция работает как независимая задача, вызываемая операционной системой, поэтому прототип фиксирован. Аргумент argp не используется. +/ void * writer (void * argp) & for (;;) & /* "захватить" mutex */ pthread mutex lock (&mut if (done) { /+ Напечатать идентификатор текущей задачи */ printf (" Thread '/х exitsgn", (int)pthread self ()); /+ "освободить" mutex +/ pthread mutex unlock (&mut /* завершить задачу */ return 0; & if (msglen) & /* вывести на экран +/ printf ("-& t; '/в~ ", msgbu msglen = 0; & /+ "освободить" mutex +/ pthread mutex unlock (&mut /~ Поместить задачу в конец очереди готовых задач с тем же приоритетои */ sched yield (); 
166 & Глава 10. Синхронизация и взаимодействие задач int main () /* дескрипторы задач */ pthreadt writer thread, readerthread; /+ создать mutex +/ if (pthread mutex init (&mu ex, & fprintf (stderr, "Cannot init mutexgn"); return 1; & /+ создать задачу reader +/ if (pthread create (&re der thr ad О, rea er, & fprintf (stderr, "Cannot create reader threadgn"); return 2; /* создать задачу writer +/ if (pthread create (&wr ter thr ad О, wri er & fprintf (stderr, "Cannot create writer threadgn"); return 3; & /* ждать окончания задачи reader thread +/ pthread join (reader thread, О); /* ждать окончания задачи writer thread +/ pthread join (writer thread, О); /* удалить mutex */ 
10.1. Объекты синхронизации типа mutex 1 if (pthread mutex destroy (&mu e & fprintf (stderr, "Cannot destroy пш~ех~п"); return 4; & return О; & Процедура reader в бесконечном цикле, прерываемом по ошиб- ке ввода: 1. обеспечивает себе исключительный доступ к общим перемен- ным с помощью блокировки mutex; 2. если предыдущее сообщение обработано сервером (т. е. дли- на сообщения msglen равна О), то считывает строку со стан- дартного ввода с помощью fgets; 3. в случае успешного чтения устанавливает длину сообщения msglen, иначе: прерывает цикл, устанавливает переменную done в 1, разрешает другим задачам работать с общими пе- ременными с помощью разблокировки mutex и завершает работу; 4. разрешает другим задачам работать с общими переменными с помощью разблокировки видех; 5. для обеспечения переключения задач к задаче — серверу ис- пользует функцию sched yield (иначе бы этот цикл рабо- тал вхолостую до окончания кванта времени, выделенного текущей задаче); более надежное и универсальное решение см. в разделе 10.3.6. Процедура writer в бесконечном цикле, прерываемом в случае ненулевого значения done: 1. обеспечивает себе исключительный доступ к общим перемен- ным с помощью блокировки mutex: 
168 Глава 10. Синхронизация и взаимодействие задач 2. если done не равно 0, то разрешает другим задачам работать с общими переменными с помощью разблокировки mutex u завершает работу; 3. если есть сообщение для обработки сервером (т. е. длина со- общения msglen не равна 0), то выводит строку с помощью printf и устанавливает msglen в О; 4. разрешает другим задачам работать с общими переменными с помощью разблокировки mutex; 5. для обеспе юния переключения задач к задачам-клиентам использует функцию sched yield. 10.2. Пример multithread-программы, вычисляющей определенный интеграл Рассмотрим в качестве примера программу, вычисляющую определенный интеграл. II)~åÿ ускорения работы описана в раз- деле 1.2, с. 12. Текст программы см. в разделе 1.4, с. 18. Задачи (threads) создаются в основном (т. е. изначально запушенном пользователем) процессе. Идентификаторы задач, полученные от pthread create, запомииаются в массиве threads и исполь- зуются основным процессом,цля ожидания завершения запу- щенных задач с помощью функции pthread join. В каждой из запущенных задач работает функция process function с аргу- ментом — номером этой зада ш. Эта функция вычисляет свою часть интеграла с помощью функции integrate и прибавля- ет его к ответу total, обеспечивая взаимное исключение при доступе к переменной total с помощью mutex total mutex. 10.3. Обьекты синхронизации типа condvar Объект синхронизации типа condvar дает возможность задаче ожидать выполнения некоторых условий. Фактически являет- ся событием Е (см. раздел 8.3) с дополнительными функциями по работе с указанным объектом mutex. Как и mutex, объект condvar явно размещается в разделяемой между задачами (про- 
10.3. Объекты синхронизации типа сопбчаг 1 цессами) памяти, что позволяет обойтись без системных вызо- вов при синхронизации между задачами (threads). Для доступа к объекту condvar Е определены три примитивные операции: ° Wait(E,т) (где т типа mutex) — производит следующие действия (первые два из них неделимы, т. е. переключе- ние задач во время их исполнения запрещено): — вызвать Unlock(m) для текущей задачи (т. е. вызвавшей эту операцию), — вызвать И/аЮ(Е), — вызвать Lock(m) (произойдет, когда текущая зада ~а «до- ждется» события Е), mutex т перед вызовом этой функции должен быть забло- кированным; ° $ьдпа1(Е) — вызвать Si;gnal(E), если нет ожидающих задач, то ничсго не происходит; ° Broaclcast(E) — вызвать Втоайса81(Е), если нет ожидающих задач, то ничего не происходит. Рассмотрим пример использования объектов condvar. Пусть есть задачи Т~,Т~,...,Z'„, разделяющие общую область памя- ти Х. Для синхронизации доступа к Х используется п1Шех т. Пусть задача То активизируется только если вьшолнсно неко- торое условие P(X). Для обеспечения этого взаимодействия ис- пользуем объект coridvar Е. Тогда алгоритм работы Тр может быть схематически записан так: ТО () & for (;;) & Lock (m); Wait (Е, m); /+ Можеи работать с раэделяеиой паиятью Х, поскольку являеися владельцеи mutex m +/ if (P(X)) 
170 Глава 10. Синхронизация и взаимодействие задач /+ Условие выполнено: исполняеи необходииые действия +/ & ° ° ° Unlock (m) l & Тогда алгоритм работы Т, (i = 1,... и может быть схематиче- ски записан так: Ti () & for (;;) & Lock (m); /+ Иожеи работать с разделяеиой паиятью Х, поскольку являеися владельцем mutex m +/ /+ Записываеи данные в Х +/ ° ° ° Signal (Е); Unlock (т); & 10.3.1. Функция pthread cond init Функция pt;hread cond init, описанная в заголовочном файле <pthre d. >, поз оляет инициализи овать co dvar. Про int pthread cond init (pthread cond t +cond, pthread condattr t +attr); где ° cond — — указатель íà condvar, ° attr óêàçàòåëü на структуру с атрибутами condvar (может быть 0 для использования значений по умолчанию). 
10.3. Объекты синхронизации типа condvar 1 1 Функция возвращает 0 в случае успеха. Для condvar (объекта типа pthread cond t) вместо функ- ции pthreadcondinit можно использовать статическую ини- циализацию при объявлении: pthread cond t cond = PTHREAD COND INITIALIZER. где макрос РТНВЕА0 СОИ0 ХИХТХА?.Х2ЕВ определен в <pthread. >. Пос едняя апись эквива ентна последов ности: pthread cond t cond; pthread cond init (&c nd, Ho) в отличие QT нее, может быть использована для инициали- зации глобальных объектов. 10.3.2. Функция pthread cond signal Функция pthread cond signal, описанная в заголово шои фай- ле <pthread h> поз оляет возоб овить аботу од ой из ожидающей condvar (какой именно, не определено), если таких задач нет то ничего не происхо THT. Прототип: int pthread cond signal (pthread cond t +cond); где cond — указатель па condvar. Функция возвращает 0 в случае успеха. 10.3.3. Функция pthread cond broadcast Функция pthread cond broadcast, оггисанная в заголовочном файле <pthread. >, поз оляет возоб овить абот всех ожидающих сопсЬаг, если таких задач нет, то ничего не проис- ходит. Прототип: int pthread cond broadcast (pthread cond t +cond); где cond — указатель на condvar. Функция возвращает 0 в случае успеха. 
172 Глава 10. Синхронизация и взаимодействие задач 10.3.4. Функция pthread cond wait Функция pthread cond wait, о||||са||||ая в заголово'|||ои фай- л» <pthread. >, разбло ирует п ||Се (к к при ис||опьзо pthread unlock mutex) и оста||авливает работу теку|цей за- .'!.;! |||, пока какая-нибудь другая задача не вызовет функцию pthread cond signal или фу||кц|||о pthread cond broadcast, причем эти операции неделимы (не допускается перекл|очсние :зад|1 ! м!'.жд j ||и|1||!). IIocIN'. Возвр|1|це||||я из состоя|||||! Ожида||||1! и|! фу||кци|! блокирует |п||Сех (как прп pthread lock mutex). Прототи! |: int pthread cond wait (pthread cond t +cond, pthread mutex t +mutex) ! г|е cond -ука;||!.|е||ь па со||с1ча|, mutex -- указатель Hà. mutex. ФУTI K JIH$I BO3BpRIII, О В с.Л'у' ЧЗ.й ус И(.'~З, 10.3.5. Функция pthread cond destroy Функция pthread cond destroy, о||исаппая и заголовочном файле <pthre d. >, ||оз| оляет уд| л||ть сои 1ъаг, к то ый н ж»н в этот момент ожидаться (с помощью pthread cond wait) нп одной задачей. Прототип: int pthread cond destroy (pthread cond t +cond); гд» cond — — указатель па сопс1чаг. Функция во |вра|цает 0 в случае успеха. 10.3.6. Пример использования condvar Рассмотрим ту же задачу что и в разделах 8.2А и 10.1.6. Приве|!»||на! в разделе 10.1.G программа является не со- всем корректной в следующем смысле. Каждая из функций reader/writer делает предположение о тсв|, что вторая функ- ция writer/reader в момент вызова функции sched yield ждет только освобожде||ия mutex mutex. Для данной программы это предположение верно, но в общем случае требуется обеспечить, 
10.3. Объекты синхронизации типа condvar 1 ~тобы после выполнения функции reader/writer следующей выполнялась writer, reader, если же после.~няя занята (не об- работала предыдущее сообщение), то текущая задача должна перейти в состояние ожидания, а, не потреблять процессорные ресурсы в бесполезном пикле опроса. Ниже приводится модифи- цированный вариант этой программы. в котором помимо об ьек- та пи1Сех для в.заимцого исклн)чеция при доступе к разделяемым данныи используется обьект coudvar для синхронизации самих .зада.ч. ¹include <stdio ¹include <string ¹include <pthread ¹define BUF LEN 256 /+ Объект синхронизации типа mutex +/ pthread mutex t mutex; /+ Объект синхронизации типа condvar +/ pthread cond t condvar; /+ Признак окончания работы +/ int done; /+ Длина сообщения +/ int msglen; /+ Буфер для сообщения +/ char msgbuf[BUFLEN]; /+ Считать сообщение и поместить его в буфер. Функция работает как независимая задача, вызываемая операционной системой, поэтому прототип фиксирован. Аргумент argp не используется. +/ void + reader (void +argp) for (;;) 
174 Глава 10. Синхронизация и взаимодействие задач /+ "захватить" mutex +/ pthread mutex lock (&mut if (!msglen) & /+ Считать сообщение. Выход по Ctrl+D +/ if (!fgets (msgbuf, BUF LEN, stdin)) break; msglen = strlen (msgbuf) + 1; /+ Послать сигнал condvar +/ pthread cond signal (&condv while (msglen) & /+ Освободить тиСех и ждать сигнала от condvar, затем "захватить" mutex опять +/ pthread cond wait (&cond ar, &amp /+ "освободить" mutex +/ pthread mutex ип1осК (&mut /+ Напечатать идентификатор текущей задачи +/ printf (" Thread '/х exits(n", (int)pthread self ()); done = 1; pthread cond signal (&condv /* "освободить" mutex +/ pthread mutex unlock (&mut /* завершить задачу */ return О; /+ Ждать сообщения, по его поступлении вывести его на экран. Функция работает как независимая задача, вызываемая операционной системой, поэтому прототип фиксирован. Аргумент argp не используется. +/ void * writer (void + argp) 
10.3. Объекты синхронизации типа condvar 1 for (;;) /+ "захватить" mutex +/ pthread mutex lock (&mut & while (!msglen) if (!done) & /+ Если еще есть задачи-клиенты, то освободить mutex z ждать сигнала от condvar, затем "захватить" mutex опять~ pthread cond wait (&cond ar, &amp & if (done) с /~Напечатать идентификатор текущей задачи~ printf (" Thread '/х exits(n", (int)pthreadself ()); /+ "освободить" mutex +/ pthread mutex unlock (&mut /+ завершить задачу +/ return 0; & & /+ вывести на экран +/ printf ("-& t; /я~ ", msgbu ms0;len = 0; /+ Послать сигнал condvar +/ pthread cond signal (&condv /+ "освободить" mutex +/ pthread mutexunlock (&mut 
176 Глава 10. Синхронизация и взаимодействие задач & int main () /* дескрипторы задач +/ pthreadt writer thread, reader thread; /+ создать mutex */ if (pthread mutexinit (&mu ex, & fprintf (stderr, "Cannot init mutex(n"); return i; & /* создать condvar +/ if (pthread cond init (&cond ar, & fprintf (stderr, "Cannot init condvar(n"); return 2; & /+ создать задачу reader +/ if (pthread create (&re der thr ad О, rea er, & fprintf (stderr, "Cannot create reader thread(n"); return 3; & /* создать задачу writer +/ if (pthread create (&wr ter thr ad О, wri er, & fprintf (stderr, "Cannot create writer thread(n"); return 4; & /+ ждать окончания задачи readerthread +/ pthread join (reader thread, 0); /+ ждать окончания задачи writer thread +/ 
10.3. Объекты синхронизации типа condvar 177 pthread join (writer thread, 0); /* удалить mutex +/ if (pthread mutex destroy (&mut fprintf (stderr, "Cannot destroy mutex(n"); & return 5; /~ удалить condvar +/ if (pthread cond destroy (&condv & fprintf (stderr, "Cannot destroy condvar(n"); return б; & return О; Процедура reader в бесконечном цикле, прерываемом по ошиб- ке ввода: 1. обеспечивает себе исключительный доступ к общим перемен- ным с помощью блокировки видех; 2. если предыдущее сообщение обработано сервером (т. е. дли- на сообщения msglen равна 0), то считывает строку со стан- дартного ввода с помощью Хgets; 3. в случае успешного чтения устанавливает длину сообщения msglen, иначе: прерывает цикл, устанавливает переменную done в 1, посылает сигнал в condvar (означающий наличие очередного сообщения для обработки сервером), разрешает другим задачам работать с общими переменными с помощью разблокировки mutex и завершает работу; 1. посылает сигнал в coI16v2Lr означак~щий наличие очередного сообщения для обработки сервером; 
178 Глава 10. Синхронизация и взаимодействие задач 5. о>кид ет обрабо ки сообще ия сервер м, ля че о, п msglen не станет равной 0, ожидает прихода сигнала в condvar; б. разрешает другим задачам работать с общими переменными с помощью разблокировки mutex. Процедура writer в бесконечном цикле, прерываемом в случае ненулевого значения done: 1. обеспечивает себе исключительный доступ к общим перемен- ныи с помощью блокировки mutex; 2. ожидает прихода сообщения от клиента, для чего, пока msglen не станет равной 0, ожидает прихода сигнала в condvar; 3. если done не равно О, то разрешает другим задачам работать с общими переменными с помощью разблокировки mutex H завершает работу; 4. дождавшись сообщения для обработки сервером (т. е. длина сообщения msglen не равна 0), выводит строку с помощью printf и устанавливает msglen в О; 5. посылает сигнал в condvar означающий окончание обработ- ки сервером очередного сообщения; 6. разрешает другим задачам работать с общими переменными с помощью разблокировки mutex. 10.4. Пример multithread-программы, вычисляющей произведение матрицы на вектор Рассмотрим задачу вычисления произведения матрицы на век- тор (см. так&gt ке раз ел 8. ). ля повыше ия скоро ти раб на многопроцессорной вычислигельной установке с общей па- мятью создадим несколько задач (по числу процессоров) и раз- делим работу между ними. Основным элементом синхрониза- ции задач будет являться функция synchronize, которая дает возмо>кно ть зад че ожида ь, п ка ту gt;ке ф нкцию в 
10.4. Пример вычисления произведения матрицы на вектор 179 все другие задачи (ср. с очноименной функцией в разделе 8.5). Файлы проекта: ° synchronize.c, synchronize h — исходный текст и соответ- ствующпй заголовочный файл для функции синхронизации; ° get time.с, get time.h — исходный текст и соответствую- щий заголово шый файл для функций работы со временем, которые совпадают с одноименными файлами, приведенны- ми в разделе 8.5); функция get time выдает процессорное время, затраченное на задачу (thread), в которой ее вызвали; ° main. с — запуск задач и вывод результатов; ° matrices с, matrices h — исходный текст и соответствую- щий заголовочный файл для функций, работающих с мат- рицами; ° Makefile — для сборки проекта. Заголовочный файл synchronize.h: void synchronize (int total threads); Файл synchronize с: ¹include (pthread.h& ¹include "synchronize.h" /*,Цождаться в текущем потоке остальных потоков (из общего числа total threads). +/ void synchronize (int total threads) & /+ Объект синхронизации типа mutex +/ static pthread mutex t mutex = PTHREAD MUTEXINITIALIZER; /+ Объект синхронизации типа condvar +/ static pthread cond t condvar in = PTHREAD COND INITIALIZER; /+ Объект синхронизации типа condvar +/ static pthread cond t condvar out = PTHREAD COND INITIALIZER; 
180 Глава 10. Синхронизация и взаимодействие задач /+ Число пришедших в функцию задач +/ static int threads in = 0; /+ Число ожидающих выхода из функции задач +/ static int threads out = 0; /+ "захватить" mutex для работы с переменными threads in u threads out +/ pthread mutex lock (&mut /+ увеличить на 1 количество прибывших в эту функцию задач +/ threads in++; /+ проверяем количество прибывших задач +/ & if (threads in )= total threads) /* текущий поток пришел последним е/ /+ устанавливаем начальное значение для threads out +/ threads out = 0; /+ разрешаем остальным продолжать работу +/ pthread cond broadcast (&con var & else & /+ есть еще не пришедшие потоки +/ /+ ожидаем, пока в эту функцию не придут все потоки +/ while (threads in & t; to al threa & /+ ожидаем разрешения продолжить работу: освободить mutex и ждать сигнала от condvar, затем "захватить" mutex опять +/ 
10.4. Пример вычисления произведения матрицы на вектор 181 pthread cond wait (&соп чаг in, &amp /+ увеличить на 1 количество ожидающих выхода задач +/ threads out++; /+ проверяем количество прибывших задач +/ if (threads out &g ;= to al threa & /* текущий поток пришел в очередь последним */ /+ устанавливаем начальное значение для threads in +/ threadsin = О; /+ разрешаем остальным продолжать работу +/ pthread cond broadcast (&condvaro else & /* в очереди ожидания еще есть потоки +/ /+ ожидаем, пока в очередь ожидания не придет последний поток */ while (threads out & t; to al threa /+ ожидаем разрешения продолжить работу: освободить mutex и ждать сигнала от condvar, затем "захватить" mutex опять +/ pthread cond wait (&con var ut, &amp /+ "освободить" mutex +/ pthread mutex ип1осК (&mut 
182 Глава 10. Синхронизация и взаимодействие задач Функция synchronize получилась достаточно сложной, по- скольку она допускз,ет использование внутри циклов: & for (...) ° ° ° synchronize (total threads); ° ° ° & В этой с итуа1 ~ни недостато шо подсчитывать, сколько за- дач вошло в эту функцию, поскольку возможно, что, выйдя из нее, одна из зада I опять войдет в функцию раиыие, чем остальные успеют из нее выйти. Поэтому необходимо подсчи- тывать еще сколько задач вышло из функции. В функ- ции synchronize объект mutex обеспечивает взаимное исклю- чение задач при доступе к общим для всех задач перемен- ш.тм: threads in — счетчику числа вошедших в функцию задач, threads out -- с и'тчику числа ожидающих выхода из функции задач. Обьект condvar in используется д.~я ожидания, пока счетчик threads in не примет значение total threads (т. е. в фуllKIIHK) ие вой 1ут все задачи). Сигнал о наступлении это- го гобьггия посыласт последняя пришедшая задача, она же устанавливает в начальное зиачешiе О счет шк threads out. Объект condvar out используется для ожидания, пока счетчик threads out не примет значение total threads (т. е. в очередь иа выход ие встанут все зада ш). Сигнал о наступлении этого со- бытия посылает последняя вставшая в очередь задача, она же устанавливает в начальное значение О счетчик threads in. Файл main. c: ¹include <studio ¹include <stdlib ¹include <pthread ¹include "matrices.h" 
10.4. Пример вычисления произведения матрицы на вектор ¹include "get time.h" /+ Аргументы для потока +/ & typedef struct ARGS double +matrix; /+ матрица */ double +vector; /+ вектор */ double +result; /* результирующий вектор */ int n; /+ размер матрицы и векторов int thread num; /+ номер задачи +/ int total threads; /* всего задач */ ] ARGS; /+ Суммарное время работы всех задач +/ static long int threads total time = 0; /+ Объект типа mutex для синхронизации доступа к threads total time +/ static pthread mutex t threads total time mutex = PTHREADMUTEXINITIALIZER; /+ Количество тестов (для отладки) +/ ¹define N TESTS 10 /+ Умножение матрицы на вектор для одной задачи +/ void + matrix mult vectorthreaded (void ~ра) & ARGS ~рагуне = (ARGS+)ра; long int t; int printf (" Thread '/d started(n", pargs->thr ad nu t = get time (); /+ время начала работы +/ for (i = 0; i & t N TES S; i & matrix mult vector (pargs->matr x, pargs-> 
184 Глава 10. Синхронизация и взаимодействие задач pargs->resu t, pargs- pargs->thr ad n parяв->to al thread printf (" Thread /d mult /d times(n", pargs->thr ad n m, t = get time () — t; /+ время конца работы +/ /+ Суммируем времена работы +/ /+ "захватить" mutex для работы с threads total time +/ pthread mutex lock (kthreadstotaltime mutex); threads total time += t; /+ "освободить" mutex +/ pthreadmutexunlock (kthreadstotal timemutex); printf (" Thread /d finished, time = /ld(n", pargs->thr ad n m, return О; int main () /* массив идентификаторов созданных задач */ pthread t * threads; /* массив аргументов для созданных задач */ ARGS + ar0;s; /* число создаваемых задач */ int nthreads; /+ астрономическое время работы всего процесса +/ long int t full; int n; /+ размер матрицы и векторов */ double +matrix; /* матрица */ double +vector; /+ вектор */ 
10.4. Пример вычисления произведения матрицы на вектор 185 double +result /* результирующий вектор +/ int i, 1; printf (" Input threads number: "); scanf ("/d", knthreads); if (!(threads = (pthread t+) & malloc (nthreads + sizeof (pthread t)))) fprintf (stderr, "Not enough memory!Nn"); return 1; & if (!(args = (ARGS+) malloc (nthreads * sizeof (ARGS)))) & fprintf (stderr, "Not enough тетогу!~п"); return 2; & printf ("matrix size = "); scanf ("/d", Йп); /+ Выделение памяти под массивы +/ if (!(matrix = (double+) malloc (n + и + sizeof (double)))) & fprintf (stderr, "Not enough memory!(n"); return 3; & if (!(vector = (double+) malloc (n + sizeof (double)))) & fprintf (stderr, "Not enough memory!(n"); return 4; & if (!(result = (double+) malloc (n + sizeof (double)))) 
186 Глава 10. Синхронизация и взаимодействие задач fprintf (stderr, "Not enough memory!Nn"); } return 5; /+ Инициализация массивов +/ init matrix (matrix, n); init vector (vector, n); printf ("Matrix:Nn"); print matrix (matrix, n); printf (" Vector:(n"); print vector (vector, n); 1 = (и ~ п + 2 + n) + sizeof (double); printf (" Allocated /d bytes (/dKb or /dMb) of memoryXn", 1, 1 » 10, 1 » 20); /+ Инициализация аргументов задач +/ for (i = 0; i & t; nthread .; i & args[i] .matrix = matrix; args[i] .vector = vector; args[i] .result = result; args[i] .n = n; args[i] .thread num = i; args[i] .total threads = nthreads; & /+ Засекаем астрономическое время начала работы задач~/ t full = get full time (); /+ Запускаем задачи +/ for (i = 0; i & t; nthrea s; i & if (pthread create (threads + i, О, matrix mult vector threaded, args + i) ) 
10.4. Пример вычисления произведения матрицы на вектор 187 & fprintf (stderr, "cannot create thread №/d!(n", i); return iO; } } /+ Ожидаем окончания задач +/ for (i = 0; i & t; nthrea s; i & if (pthread join (threads[i], О)) fprintf (stderr, "cannot wait thread №/d!(n", i); t full = get full time () — t full; if (t full == О) tfull = 1; /+ очень быстрый компьютер... +/ /+ Здесь можно работать с результатом +/ print vector (result, n); /+ Освобождаем память +/ Йree (threads); free (args); Хree (matrix); free (vector); free (result); printf (" Total full time = /ld, total threads time = /ld (/ld//), per thread = /ld(n", t Хи11, threads totaltime, (threads total time + 100) / tfull, threads total time / nthreads); return О; 
188 Глава 10. Синхронизация и взаимодействие задач Функция matrix mult vector СЬгеайей запускается в функ- ции main в виде задач. Еаждой из задач необходимо пе- редать свой набор аргументов. Но функция запускаемая как задача, может иметь только один аргумент типа void*. Для решения этой проблеьп т опреде|ляется структура данных ARGS содержащая необходимые аргументы. Указатель на эту структуру передается функции matrix mult vector threaded как void+ и преобразуется к типу ARGS+. Затем вызывается matrix mult vector, вычислян)щая компоненты ответа с ин- дексами в диапазоне n + thread num / total threads, n + (thread num + 1) / total threads — 1 Для ожидания завершения всех задач используется функция synchronize. Для вычис.пения суммарного процессорного времени затра- ченного па процесс, в функции matrix mult vector threaded время работы каждой задачи прибавляется к значению разде- ляемой переменной threadstotal time. Для взаимного исклю- чения задач при доступе к этой переложенной используется muted threads total time mutex. Для целей отладки и более точного замера времени работы функция вычисления произведения матрицы на вектор вызыва- ется в цикле N TESTS раз. Заголовочный файл matrices h: void init matrix (double + matrix, int n); void init vector (double + vector, int n); void print matrix (double + matrix, int n); void print vector (double + vector, int n); void matrix mult vector (double ~а, double ~Ь, double *с, int n, int thread num, int total threads); Файл matrices. c: Oinclude <stdio 
10.4. Пример вычисления произведения матрицы на вектор 189 Oinclude "matrices.h" Oinclude "synchronize.h" /+ Инициализация матрицы +/ & void init matrix (double + matrix int n) int double ~а = matrix; for (i = 0; i & t; и; i for (j = 0; ] & t; n; j *(а++) = (1 & t; j /+ Инициализация вектора +/ void init vector (double + vector, int n) & int double *Ь = vector; for (i = 0; i & t; и; i ~(Ь++) = 1.; & redefine NMAX 5 /+ Вывод матрицы +/ void print matrix (double + matrix, int n) & ю int m = (n & t N A N AX for (i = 0; i & t; m; i & for (j = 0; j & t; m; j 
190 Глава 10. Синхронизация и взаимодействие задач printf (" '/12.61f matrix [i + и + j] ); printf ("(n"); /+ Вывод вектора +/ & void printvector (double + vector, int n) int int m = (n & t N A N A : for (i = О; i & t; m; i printf (" /12.61f", vector[i]); printf ("(n"); & /+ Умножить матрицу а на вектор Ь, с = аЬ для задачи с номером thread num из общего количества total threads. +/ void matrix mult vector (double ~а, double ~Ь, double ~с, int n, int thread num, int total threads) & int double ~р, s; int first row, last row; /+ Первая участвующая строка матрицы +/ first row = n * thread num; first row /= total threads; /+ Последняя участвующая строка матрицы +/ last row = и + (thread num + 1); last row = last row / total threads — 1; for (i = first row, р = а + i * и; i &l ;= l st r w; i 
10.4. Пример вычисления произведения матрицы на вектор 191 & for (s=0., j =О; j &lt n; j я += 9(р++) 9 Ь[j]; c[i] = s; & synchronize (total threads); & Файл Makef ile: NAME = threadmult DEBUG СС = gcc -c LD = gcc CFLAGS = $(DEBUG) -W -Wall LIBS = -lpthread -lm LDFLAGS = $(DEBUG) OBIS = main.o matrices.o synchronize.o gettime.о all : $(NAME) $(NAME) : $(ОВОРЯ) $(LD) $(LDFLAGS) $" $(LIBS) -о $6 .С.О: $(CC) $(CFLAGS) $& t; -о clean: rm -f $(ОВОРЯ) $(NAME) main.o main.c matrices.h get time.h matrices.o matrices ñ matrices.h synchronize h synchronize.î : synchronize.c synchronize.h 0;et time.o get time.c get time.h 
192 Глава 10. Синхронизация и взаимодействие задач 10.5. Влияние дисциплины доступа к памяти на эффективность параллельной программы Попробуем обобщить программу предыдущего раздела, получив из нее multithreacl-программу умножения двух матриц С = АВ. Если обозначить через х, а-й столбец матрицы Х, а через Х = [х1,....х„] — матрицу, составленную из столбцов х;,то по опре- делению умножения матриц С = [с~,...,c„] = АВ = [Аб~,...,АЬ„], что позволяет легко получить из программы умножения матри- цы на вектор программу умножения матрицы на матрицу. (3a; метим, что этот подход полностью игнорирует все, что сказано в разделе 2.7 об эффективном х множснии матриц.) Мы приведем только текст функции matrix mult matrix, обобщающей опи- санную вьппе matrix mult vector и работаюсцую совершенно аналоги ч нО: ¹include <stdio ¹include "matrices.h" ¹include "synchronize.h" /+ Умножить матрицу а на матрицу Ь, с = аЬ для задачи с номером threadnum из общего количества total threads +/ void matrix mult matrix (double ~а, double ~Ь, double ~с, int n, int thread num, & int total threads) int i, j, m; double ~ра, ~рЬ, ~рс, s; int first row, last row; /® Первая участвующая строка матрицы +/ first row = и * thread num; first row /= total threads 
10.5. Дисциплина доступа к памяти и эффективность 193 /« Последняя участвующая строка матрицы +/ last row = n * (thread num + 1); last row = last row / total threads — 1; & for (m = О, рс = c; m & t; n; m +, рс for (i = firstrow, ра = а + i « n, рЬ = Ь + m; &l ;= l st r w; i for (s = 0., ) = О; j & t; n; j я += ~(ра++) 4 pQ [j 4 хх]; pc[i «n] = s; synchronize (totalthreads); I Отношение в1эемеххи работы «обьх хххой» программы умножения матриц (пример 1 раздела 2.7) к времени работы этой ххрограм- мы (для процесса в целом и для одной задачи) для двух ЯМР- сххстем при и = 2000 приведено в табл. 10.1. Эта таблица показывает: ° практически 100% эффективность распараллелива- ния (астрономическое время работы программы совпадает с временем работы одной задачи); Таблица 10.1. Относительное время работы параллельной программы уьшоженим матриц 2 x Pentium Ш 4 х Pentium Pro Число ~дд®~ процесс 1 thread процесс 1 thread 1 1.00 1.00 1.00 1.00 2 1.05 1.05 0.73 0.73 4 — — 1.70 1.70 7 4017 
194 Глава 10. Синхронизация и взаимодействие задач ° практически полное отсутствие ускорения в работе (во второй системе астрономическое время на двух процес- сорах даже увеличилось в 1.37 раз по сравнению с одним процессором); ° практически одинаковое время работы «обычной» про- граммы на двух рассматриваемых системах (хотя частоты процессоров отличаются почти в пять раз). Следовательно, предложенная программа совершенно непри- годна для параллельных вычислительных установок. Основной причиной этого является неудачная дисциплина доступа к об- щей памяти, поскольку кыкдая из задач одновременно обра; щается к одним и тем же элементам матрицы Ь. Несмотря на то, что эти элементы используются только для чтения, это приводит к конфликтам по данным в подсистемах оперативной памяти и кэш-памяти. Заметим, что подобный эффект можно отнести к недоработкам при создании управ.пяющей аппарат- ной логики рассыатриваемых систем. Попробуем исправить эту ситуацию, организовав работу про- граммы таким образом, чтобы исключить одновременный до- ступ нескольких задач к одним и тем же элементам общей па- мяти. Улучшенный вариант умножения матрицы на матрицу и объяснение его работы приведены ниже: ¹include <st io ¹include "matrices.h" ¹include "synchronize h" /+ Умножить матрицу а на матрицу Ь, с = аЬ для задачи с номером threadnum из общего количества total threads +/ void matrix mult matrix (double ~а, double ~Ь, double ~c int n, int thread num, & int total threads) int i, j, k, 1, m; 
10.5. Дисциплина доступа к памяти и эффективность 195 int first row, last row, len; int first row k, last row k; double *ра, *рЬ, *рс, s; /+ Первая участвующая строка матрицы +/ first row = n * threadnum; first row /= total threads; /+ Последняя участвующая строка матрицы +/ last row = n + (thread num + 1); last row = last row / total threads — 1; /+ Обнуляем результат +/ len = (last row — first row + 1) + n; /* длина с +/ for (i = О, рс = с + firstrow + n; i & t; l n; i ~(рс++) = О.; /+ Цикл по блокам +/ & (1 = О; 1 & t; to al threa s; 1 /+ Текущий номер блока для полосы Ь +/ k = (thread num + 1) / total threads; /+ Первая строка полосы матрицы Ь +/ first row k = n * k; first row k /= total threads; /+ Последняя строка полосы матрицы Ь +/ last row k = n + (k + 1); last row k = last row k / total threads — 1. /+ Умножаем прямоугольный блок матрицы а в строках first row ... last row и столбцах first row k ... last row k на столбцы полосы b, стоящие в строках first row k ... first row k +/ for (m = О, рЬ = Ь, рс = c; m & t; n; m +, рЬ +, рс 
196 Глава 30. Синхронизация и взаимодействие задач & for (i = first row; i &l ;= l st r w; i for (s = О., ) = first row k, pa=a+i*n+ j; &l ;= l st ow k; j в += ~(ра++) * рЬ[) 4 и]; pc[i «n] += s; synchronize (total ~Ьгеа~1в); Функция matrix multmatrix в цикле по ! = 0,1,...,р — 1, p = totalthreads, вычисляет матрицу, являющуюся произве- дением блока матрицы А, стоящего в строках пп~/р,..., пфв+ 1)/р — 1, т = thread num (10.1) и столбцах (тп/р+ !) (naod р)...., (~i(m. + 1)/р — 1+ К) (mod р), (10.2) и блока матрицы В, стоящего в строках с померамп (10.2) и столбцах 1>. ., ' ~. (10 Эта матрица прибавляется к блоку матрицы С, стоящему в стро- ках с номерами (10.1) и столбцах с номерами (10.3). В этом цикле по t мы использови~и synchronize (см. раздел 10.4) для того чтобы гарантированно исключить одновременный доступ нескольких задач к одним и тем же элементам 6. Иллюстрация этого процесса. при total threads = 3 при- ведена на рпс. 10.1: в каж 1ой из задач 1, 2 3 (здесь мы для )добства нумеруем их, на ~иная с 1) вычисляются произведения блоков матриц А и В1 отмеченных на рисунке соответствующей цифрой, и прибавляются к результату в матрице С. Отноиюние времени работы «обычной» программы умноже- ния матриц (пример 1 раздела 2.7) к времени работы этой про- граммы (для процесса в целом и для одной зада ~и) для rex же 
10.5. Дисциплина доступа к памяти и эффективность 197 С а Ь 1 1 1 2 += 2 X 2 3 3 3 а) l=0 С а Ь 1 1 3 2 += 2 х 1 3 3 2 б) I=1 С а Ь 1 1 2 2 += 2 х 3 3 3 1 в) l=2 Рис. 10.1. Организация вычислений при умножении матрицы на матрицу ЯМР-систем и того же и приведено в табл. 1().2, где в скобках приведено также отношение времени работы этой программы на 1 процессоре к времени ее работы (для процесса в целом и для одной зада ~и) на многих процессорах. Таблица 10.2. Относительное время работы улучшенной параллельной программы умножения матриц 
198 Глава 10. Синхронизация и взаимодействие задач Эта таблица показывает: ° практически 100% эффективность распараллелива- ния (астрономическое время работы программы совпадает с временем работы одной задачи); ° хорошее ускорение в работе при увеличении числа ис- пользуемых процессоров (см. коэффициенты в скобках); ° замедление работы «улучшенной» программы на од- ном процессоре (это распространенное для параллельных программ явление в данной ситуации объясняется усложне- нием индексных выражений при обращении к массивам, что особенно чувствительно для второй системы с более низкой частотой процессоров); ° ускорение работы на величину, превышающую количество использованных процессоров (для первой системы — в 3.73 раз на 2-х процессорах), которое некоторые авторы назы- вают суперускорением (это достаточно частое для па- раллельных программ явление здесь объясняется влиянием кэш-памяти, поскольку работа с подматрицами уменьшает количество вовлеченных элементов и увеличивает эффек- тивность кэширования, см. подробное обсуждение этого в разделе 2.7). Подчеркнем, что полученное ускорение работы в основном связано с изменением дисциплины обращения к общей памяти. Для дальнейшего повышения эффективности программы мож- но обратиться к таблице 2.1 и выбрать в последней версии функ- ции ва~гхх ши1йвайгхх для перемножения блоков матриц а и Ь алгоритм 5 вместо использованного алгоритма 1. В результате получаем следующую (уже «хорошую») подпрограмму: ¹include <stdio ¹include "matrices.h" ¹include "synchronize.h" /+ Размер блока, должен делиться на 2 +/ ¹define N 40 
10.5. Дисциплина доступа к памяти и эффективность 199 /+ Умножить матрицу а на матрицу Ь, с = аЬ для задачи с номером thread num из общего количества total threads +/ /+ Работает только для n/total threads — четных (для всех и придется усложнять разворачивание цикла) +/ void matrix mult matrix (double ~а, double ~Ь, double ~с, int n, int threadnum, & int total threads) int i, j, k, 1, m; int first row, last row, len; int first row k, last row k; double ~ра, ~рЬ, ~рс; double s00, sOi, siO, sii; int с соl, c ncol, crow, с nrow, а соl, a ncol; /+ Первая участвующая строка матрицы +/ first row = n + thread num; first row /= total threads; /+ Последняя участвующая строка матрицы +/ last row = n + (thread num + 1); last row = last row / total threads — 1; /+ Обнуляем результат +/ len = (last row — first row + 1) + n; /+ длина с &l for (i = О, рс = с + first row + n; i & t; l n; i ~(рс++) = О.; /+ Цикл по блокам */ for (1 = О; 1 & t; to al threa s; 1 & /+ Текущий номер блока для полосы Ь +/ k = (thread num + 1) / total threads; /+ Первая строка полосы матрицы Ь ~/ first row k = n * k; first row k /= total threads; 
200 Глава 10. Синхронизация и взаимодействие задач /+ Последняя строка полосы матрицы Ь +/ last row k = n + (k + 1); last row k = last row k / total threads — 1. /~ Умножаем прямоугольный блок матрицы а в строках first row ... last row и столбцах first row k ... lastrowk на столбцы полосы b, стоящие в строках first row k ... last row k Получаем блок матрицы с, находящийся в строках first row ... last row +/ for (ecol = 0; ecol & t; n; e ol += c ncol = (ecol + N &l ; с o : n) Ђ” for (с row = first row; с row &l ;= l st r & crow += N) с nrow = (c row + N — 1 &l ;= last ? с row + N — 1 : last row); /+ Вычислить результирующий блок матрицы с с верхним левым углом (с row, ecol) и правым нижним (c nrow, с ncol) &l /+ Вычисляем как произведения блоков матриц аиЪ~/ for (acol = first row k; а col &l ;= l st ow k à ol += & а ncol = (а col + N — 1 &l ;= l st o ? acol + N — 1 : last row k); /+ Вычисляем произведение блока матрицы а [(c row,а col) х (c nrow,а ncol)] на блок матрицы Ь [(a col,ecol) х (а ncol,c ncol)] и прибавляем к блоку матрицы 
10.5. Дисциплина доступа к памяти и эффективность 201 с [(с row,с col) х (c nrow,с ncol)] е/ for (m = ecol, pb = b + m, рс = с + m; m &l ;= cnc l m += 2, pb += рс += 2) for (i = crow; i &l ; c nr w i += s00 = sOi = siO = sii = О.; for (j = acol, ра = а + i + n + j; & j &l ;= anc l; j +, pa /* элемент (1, m) */ s00 += ра[0] + pb[j * и]; /+ элемент (1, m + 1) +/ s01 += ра[0] + pb[j + и + 1]; /+ элемент (i + 1, m) +/ s10 += ра[и] + pb[j + и]; /* элемент (i + 1, m + 1) +/ s11 += ра[и] + pb[j * и + 1]; & pc[i * и] += s00; pc[i * и+ 1] += sOi; pc[(i + 1) + и] += s10; pc[(i + 1) * и + 1] += я11; & & & & synchronize (total threads); 
202 Глава 10. Синхронизация и взаимодействие задач Таблица 10.3. Относительное время работы хорошей параллельной программы умножения матриц 2 x Pentium Ш 4 x Pentium Pro Число зздз ~ процесс 1 thread процесс 1 thread 1 13.5 (1.00) 13.5 (1.00) 3.10 (1.00) 3.10 (1.00) 2 26.1 (1.93) 26.1 (1.93) 6.20 (1.99) 6.20 (1.99) 4 — — 12.4 (3.99) 12.4 (3.99) Характеристики производительности этой программы при- ведены в табл. 10.3 (обозначения и условия эксперимента те же, что в табл. 10.2). 10.6. Пример multithread-программы, решающей задачу Дирихле для ураВнения Пуассона Рассмотрим решение задачи Дирихле для уравнения Пуассона в двумерной области D = [0,1г] х [О, lg]: одu(x y) одu(x у) х2 у (10.4) (х,y) е D,è(õ,y) = О, (х,y) E ОР. Пусть hg, hg ) 0 — достаточно малы. Тогда — Ьи(х, y) = — (и(х — hq & t; y) Ђ” 2и х, у + ( + г, y))/ — (и(х, y — h2) — 2и(х, y) + и(х, y + h2))/h22. Поэтому рассмотрим следующую конечномерную аппроксима- цию задачи (10.4). Зададимся положительными целочисленны- ми параметрами п1,п2. Положим hg — — гг/п1, hg = lg/n2, х, = ih&g ; = ,1 >. . n gt;, how, г = 0,1 и; = и(х;,y ), г = 0,1,...,n&g ; = 0,1,..., fz — — f(x;>у ) = 0,1,..., ~ = 0,1,..., 
10.6. Решение задачи Дирихле для уравнения Пуассона 203 Вместо задачи (10А) рассмотрим следующую систему линейных уравнений: 2 2» — (и; 1д — 2и; z + и;+1z)/&g ;q Ђ” и y Ђ” и + и; ~+1) 62 Ђ” Ђ” i = 1,2,...,ny — 1, j = 1,2,...,ng — 1, и;о = О, и;„, = 0& t = 0>1&g иод = О, и„, д = О, 1 = О, 1,..., ng. (10.5) Эту систему часто называют разностной аппроксимацией задачи (10.4). Систему (10.5) можно представить в обычном виде: Аи= f, (10.6) где (vij')i=0,1,...,ny, 1=0,1,...,п f (fij )i=0,1,...,п., у=0,1,...,п2 — (п1+1) х (п2+1) векторы, А — матрица (n~+ 1)(n2+1) х (ni+ 1)(п~ + 1), действие которой на вектор задается формулой и=Ли, и; = — (и; 1: — 2и, +- и+1 )/1), — (и;д 1 — 2и; +- и;:+1)/6. 2 ., 2 i = 1,2,...,n~ — 1, j = 1,2,...,и2 — 1, u;,g=0, v;, =О, ж=О,l,...,n~, ио =О, и„, — 0, J =0 l,...,п~. (10.7) В силу большой размерности матрицы А систему (10.6) обычно решают тем или иным итерационным алгоритмом, для проведе- ния которого достато шо уметь вычислять произведение матри- цы на вектор, а. саму матрицу А хранить не требуется. Ъ~!ы рас- смотрим простейпшй из таких алгоритмов — метод Якоби. OH состоит в вычислении последовательности приближений 1и ), 
204 Глава 10. Синхронизация и взаимодействие задач k = 0,1,..., начиная с некоторого начального приближения и~ (мы будем использовать и~ = О), по следующим формулам: ,(~k+1 .~ 1 В +Аи"= (, К=О 1,..., (108) т~ где т~, -- итерационный параметр, В — — диагональная матрица, равная главной диагопа.ш матрицы А. ХХатр1ща В в итерацион- ном процессе вида (10.8) называется предобуславливателем и в силу (10.7) ее действие на вектор задается формулой 2 2 u=Bu и; = —.+ — и;. ~2 ~) 1 '2 г = 1,2,....п1 — 1, J= :1,2,...,и2 — 1, (109) и;о=0. v;„,=О, z=0,1,...,n~, иод — — О, и„,д = " 7 = О, 1,..., и~. Вектор r =f — Аи .k» k называется невя:зкой и его норма характеризует качество полу- ченного приблп>ке пя ~. Обы но вычисле и в проце се (10 про~о..сжают, пока норма ш..вязки не ~надет в заданное число Ъ раз (например, 10 ) по сравн с нормой r . Итерационный параметр т~ в (10.8) мы б~дс.м выбирать методом скорейшего спуска: т~,=(B 'г',r )/(ËB 'г",B 'r ), k=0,1,..., где ( . ) <означ ет евклид ва скаляр ое произведен е. Е ли о зна шть o" = В 'r то в силу симметричности матрицы А ~~,г v, — и,v — u,и (Аи~, ok) (Аи~, vl') (Аи"', vk) т. е. т. = (( f и"') — (Av~, и~))/(Аи~, o~), ok = В т~, k = О, 1,.... (10.10) Расчетные формулы нашего алгоритма, использующие 3 век- тора: и, г, f . имеют вид: 
10.6. Решение задачи Дирихле для уравнения Пуассона 205 1. пусть и содержит приближение на шаге Ус (0 на шаге О); 2. вычисляем r = Аи; 3. вычисляем невязку r = f — r и ее норму; 4. еслй поР~~В неВЯзки 5IPHbIIIP задз неюй, То 33KBH III BBP), Вы- числения; 5. вычисляем r = В т; 6. вычисляем т = ((f, r) — (Ат, п)))(Ат,r); 7. вычисляем приближение на шаге k + 1: 'и = и + тт; 8. переходим к пункту 1. В выражении для т вектор Ar не вычисляется целиком. Опре- деляется очередной его элемент, и он сразу используется для нахождешия значений скалярных произведешш (Ar. и) и (Ат. т). Для отладки программы мы используем в качестве области едипичный квадрат D = [0,1) х [0,1], в качестве правой части фушкцшо f'(õ, g) = 2х(1 — х) + 2д(1 — у), которой соотпетс гвует решепи» u(õ. y) = х(1 — х)у(1 — у). Это позволит вычислять норму ошибки, т. е. норму разности между то шым реи~ешжм и иолучеш~ым В итерационном Iipo- цессе и . Отметим, что метод Якоби для системы (10.5) является весь- ма неэффективным и рассматривается здесь для простоты. Для повьппения скорости работы па многопроцессорной вы- чис..пггельной установке с общей памятью создадим несколько задач (по числу процессоров) и разделим работу между ниии. Фай.1ы проекта: ° get time.ñ, get time.h — исходный текст и соответствую- щий заголовочный файл для функций работы со временем (см. раздел 10.4); ° reduce sum. c. reduce sum. h - — исходе1ый т('..ест и со()тВет- ству1ощий заголовочный файл для функции вычисления суммы массивов, распределенных между процесс'ши; 
206 Глава 10. Синхронизация и взаимодействие задач ° main.с — ввод данных, запуск задач и вывод результатов; ° init . с, init . h — исходный текст и соответствующий заго- лово шый файл для функций, инициализирующих правую часть уравнения (10.4) н вычисляющих ошибку решения; ° operators. с, operators.h — исходный текст и соответству- [oIJ|HfI заголовочный с1)айл 1!ля функций, вычисляющих ))аз- ли шые операторы; ° laplace . с laplace . h — исходный текст и соответствующий заголовочный файл для функции, решающей задачу; ° Makef ile — — для сборки проекта. Заголово шый файл reduce sum. h: void reduce sum (int total threads, double+ à, int k); ¹define synchronize(X) reduce sum (Х, 0, 0) Здесь использовано макроопределение для получения функции synchronize, работающей так )ке, как одноименная функция из раздела 10.4. Файл reduce sum.c: ¹include <pthread ¹include "reduce sum.h" /+ Вычислить сумму элементов массива а длины k во всех потоках и вернуть ее в массиве а в каждом потоке (из общего числа total threads) . +/ void reduce sum (int total threads, double+ à, int k) /+ Объект синхронизации типа mutex +/ static pthread mutex t mutex = PTHREADMUTEXINITIALIZER; /+ Объект синхронизации типа condvar */ statiñ pthread cond t condvar in = PTHREAD COND INITIALIZER; /+ Объект синхронизации типа condvar +/ static pthread cond t condvar out 
10.6. Решение задачи Дирихле для уравнения Пуассона 207 = PTHREAD C0ND INITIALIZER; /+ Число пришедших в функцию задач */ static int threads in = 0; /+ Число ожидающих выхода из функции задач */ static int threads out = 0; /+ Указатель на массив с ответом +/ static double +pres = 0; int i; /+ "захватить" mutex для работы с переменными threads in, threads out, pres +/ pthread mutex lock (kmutex); /+ если текущий поток пришел первым, то установить pres, иначе вычислить сумму с pres +/ if (!pres) & /* первый вошедший поток */ pres = а; } else & /+ вычислить сумму в остальных потоках +/ for (i = 0; i & t; k; i pres[i] += a[i]; } /+ увеличить на 1 количество прибывших в эту функцию задач +/ threadsin++; /+ проверяем количество прибывших задач +/ if (threads in &g ;= to al threa /+ текущий поток пришел последним */ 
208 Глава 10. Синхронизация и взаимодействие задач /+ устанавливаем начальное значение для threads out */ 1.1~еаб~о~1 = О; /+ разрешаем остальным продолжать работу +/ & pthread cond broadcast (kcondvar in); else & /+ есть еще не пришедшие потоки */ /* ожидаем, пока в эту функцию не придут все потоки +/ while (threads in & t; to al threa & /+ ожидаем разрешения продолжить работу: освободить mutex и ждать сигнала от condvar, затем "захватить" mutex опять +/ pthread cond wait (!kcondvar in, !kmutex); & ) /+ в pres находится результат, берем его в каждой задаче +/ if (pres != а) & for (i = 0; i & t; k; i а [i] = pres [i]; & /+ увеличить на 1 количество ожидающих выхода задач +/ threads out++; /+ проверяем количество прибывших задач +/ if (threads out )= total threads) 
10.6. Решение задачи Дирихле для уравнения Пуассона 209 { /* текущий поток пришел в очередь последнии +/ /+ устанавливаек начальное значение для pres +/ pres = 0; /* устанавливаек начальное значение для threads in */ threadsin = 0; /~ разрешаек остальнык продолжать работу +/ pthread cond broadcast (kcondvar out); else /* в очереди ожидания еще есть потоки +/ /+ ожидаек, пока в очередь ожидания не придет последний поток +/ while (threads out & t; to al threa /+ ожидаек разрешения продолжить работу: освободить mutex и ждать сигнала от condvar, затеи "захватить" mutex опять +/ pthread cond wait (Йсопйчаг out, kmutex); & & /+ "освободить" mutex +/ pthread mutex unlock (kmutex); & Функция reduce sum работает аналогично функции synchronize из раздела 10.4, по, в отличие от послелнетт, помимо синхронизации зада~ она в~тчисляет сумму элементов указан- ных в качестве аргументов массивов. Результат записывается на место исходных массивов в каждой из задач. Если длина 
10 Глава 10. Синхронизация и взаимодействие задач массива равна нулю, то функции reduce sum u synchronize совпадают, что использовано в файле reduce sum.h. Файл main. c Do структуре похож на одноименный файл из раздела 10.4: ¹include <stdio ¹include <stdlib ¹include <pthread ¹include "init.h" ¹include "get time.h" ¹include "laplace.h" /~ Аргументы для потока +/ & typedef struct ARGS unsigned int ni; /+ число точек по х +/ unsigned int n2; /+ число точек по у */ double hi /+ шаг сетки.по х */ double h2; /* шаг сетки по у */ unsigned int max it; /+ максимальное число итераций +/ double prec; /* величина падения невязки +/ double +f /+ правая часть */ double +u; /+ решение +/ double ~г; /+ невязка +/ int thread num; /* номер задачи */ int total threads; /+ всего задач +/ ARGS; int get data (char + паше, ARGS +parg); /+ Суммарное время работы всех задач +/ static long int threads total time = О; /* Объект типа mutex для синхронизации доступа к threads total time */ static pthread mutex t threads total time mutex 
10.6. Решение задачи Дирихле для уравнения Пуассона 211 = PTHREADMUTEXINITIALIZER; /+ Умножение матрицы на вектор для одной задачи +/ & void * laplace solve threaded (void ~ра) ARGS *р = (ARGS*)pa; long int ~; printf (" Thread /d startedgn", р->thr ad nu t = get time (); /+ время начала работы +/ laplace solve (р-> i, р-& t;п2, ->h , р-&g ;h2 р->pr c, р- gt;f, р-&gt р->thr ad n m, р-&gt total thr t = get time () — t; /+ время конца работы */ /+ Суммируем времена работы +/ /* "захватить" mutex для работы с threads total time pthread mutex lock (&thr ads t tal ime mut threadstotaltime += t; /+ "освободить" mutex +/ pthread mutex ип1осК (&thr ads t tal ime mut printf (" Thread /d finished, time = /idhn", р->thr ad n m, return О; int main () /+ массив идентификаторов созданных задач +/ pthread t + threads; /+ массив аргументов для созданных задач +/ ARGS + args; 
12 Глава 10. Синхронизация и взаимодействие зад~ /+ астрономическое время работы всего процесса +/ long int t full; /* аргументы */ ARGS arg; int i 1; /+ считываем данные из файла */ if (get data ("а.dat", karg) & t; & fprintf (stderr, "Read еггог!~п"); return i; printf (" Input threads number: "); scanf ("/d", karg.total threads); arg.threadnum = О; if (! (threads = (pthreadt+) malloc (arg.total threads sizeof (pthread t)))) с fprintf (stderr, "Not enough memory!$n"); return 2; & if (!(args = (ARGS*) malloc (arg.total threads sizeof (ARGS)))) & fprintf (stderr, "Not enough memory!$n"); return 3; ) /+ Выделение памяти под массивы +/ 1 = (arg.ni + 1) ~ (arg.n2 + 1) + sizeof (double) if ( !(arg.f = (double+) malloc (1)) !(arg.è = (double+) malloc (1)) 
10.6. Решение задачи Дирихле для уравнения Пуассона 213 !(arg.r = (double+) malloc (1))) fprintf (stderr, "Not enough memory!(n"); & return 3; 1 ~= 3; printf (" Allocated /d bytes (/dKb or /dMb) of тетогу~п", 1, 1 » 10, 1 » 20); /+ Инициализация аргументов задач +/ for (i = 0; i & t; arg.to al threa s; i & args[i] = arg; args[i].thread num = i; & /+ Засекаем астрономическое время начала работы задач~/ t full = get full time (); /+ Запускаем задачи +/ for (i = 0; i & t; arg.to al threa s; i & if (pthread create (threads + i, О, laplace solve threaded, args + i)) & fprintf (stderr, "cannot create thread №/d!(n", i); return 10; } } /+ Ожидаем окончания задач +/ for (i = 0; i & t; arg.to al thre ds i & if (pthread join (threads[i], О)) fprintf (stderr, "cannot wait thread №/d!gn", i) 
14 Глава '10. Синхронизация и взаимодействие задач t full = get full time () - t full. if (t full == О) СХи11 = 1; /~ очень быстрый компьютер... ~/ /+ Здесь можно работать с результатом +/ /+ Воспользуемся тем, что мы знаем ответ +/ print error (arg.ni, arg.n2, arg.hi, arg.h2, arg.u); /+ Освобождаем память +/ free (arg.r); free (arg.u); free (arg.f); free (args); Хree (threads); printf (" Total full time = /ld, total threads time = '/ld (/ld//), per thread = /1&amp й Хи11, threadstotaltime, (threads total time + 100) / t full, threads total time / arg.total threads); & return 0; /+ Считать данные из файла, где находятся: число точек по х число точек по у длина стороны, параллельной оси х длина стороны, параллельной оси у максимальное число итераций величина падения невязки Например: 32 32 1. 1. 1000 1е-б Возвращает &l ; в слу ае ошиб g/ 
10.6. Решение задачи Дирихле для уравнения Пуассона 215 & int get data (char + паше, ARGS ~р) FILE *in = fopen (name, "r"); double 11, 12; if (!in) & fprintf (stderr, "Error opening data file /в~п", name); return -1; } if (Хscanf (in, "'/u/è/lf /lf /u/lf ", &p- gt;ni, &amp li, amp;12, &am ;p->ma i , & fprintf (stderr, "Error reading data file '/в~п", name); return -2; & /+ Вычисляем шаги сетки по направлениям х и у +/ р-&gt h = 1 / р-& р-&gt h = 1 / р-& fclose (in); return О; } В качестве характеристики полученного приближения выдается норма ошибки, вычисляемая главным процессом. Заголовочный файл init.h: void print error (unsigned int ni, unsigned int n2, double hi double h2, double ~и); void init f (unsigned int ni, unsigned int п2, 
16 Глава '10. Синхронизация и взаимодействие задач double hi double h2, double +f int thread num, int total threads); В файле init.ñ находятсл функции: ° solution --- вычисляет то шое решение; ° print error -- вычисляет норму ошибки полученного при- ближения, пользуясь тем, что MbI знаем точное решение; для уменьшения вычислительной погрешности используется тот же прием, что в функции get residual (см. ниже); ° right side — — вычисляет правую часть; ° init f — задает правую часть для каждой из задач. Файл init. с: ¹include <stdio ¹include <math ¹include "init.h" ¹include "reduce sum.h" /+ Инициализация правой части задачи. В тестах будем рассматривать задачу в единичном квадрате [0, 1] х [0, 1] с ответом и(х,у) = x * (1 - х) * у * (1 - у), которому соответствует правая часть f(x,y) = 2 + x + (1 — x) + 2 + y + (1 — у) g/ /* Точный ответ */ statiñ double & solution (double х, double у) return х + (1 — х) * у + (1 — у); & /+ Вычислить и напечатать L2 норму ошибки +/ void 
10.6. Решение задачи Дирихле для уравнения Пуассона 217 print error ( unsigned int ni, /+ число точек по х */ unsigned int n2, /* число точек по у +/ double hi, /+ шаг сетки по х +/ double h2, /+ шаг сетки по у */ double ~и) & /* решение +/ double si, s2, t, error; unsigned int ii, i2, addr; for (i1 = О, addr = О, si = О.; i1 &l ;= 1; i1 & for (i2 = О, s2 = О.; i2 &l ;= 2; i2 & t = u[addr++j — solution (i1 + h1, i2 + h2); s2+= t * t; si += s2; error = sqrt (si + hi + h2); printf (" Error = /1е~п", error); /+ Точная правая часть */ static double right side (double х, double у) & return 2 + х + (1 — х) + 2 + у + (1 — у); & /+ Заполнить правую часть для задачи с номером threadnum из общего количества total threads. +/ void init f ( unsigned int ni, /+ число точек по х */ 
18 Глава '10. Синхронизация и взаимодействие задач unsigned int n2, /+ число точек по у */ double hi /+ шаг сетки по х */ double h2, /+ шаг сетки по у +/ double +f /+ правая часть */ int thread num, /+ номер задачи +/ с int total threads) /* всего задач */ unsigned int first row, last row; unsigned int ii, 1.2, addr; /+ Первая участвующая строка +/ first row = (ni + 1) + thread num; first row /= total threads; /+ Последняя участвующая строка */ last row = (n1 + 1) + (thread num + 1); last row = last row / total threads — 1; for (ii = first row, addr = ii + (n2 + 1); ii &l ;= l st r w; 11 for (i2 = О; i2 &l ;= 2; i2 f[addr++] = right side (i1 + hi i2 + h2) /* подождать всех */ synchronize (total threads); Заголовочн| п1 файл operators.h: double get residual (unsigned int ni, unsigned int n2, double hi double h2, double +r, int threadnum, int totalthreads); void get operator (unsigned int ni, unsigned int n2, double hi double h2, double +u, double +v, int thread num, int total threads); 
10.6. Решение задачи Дирихле для уравнения Пуассона 219 void get preconditioner (unsigned int ni, unsigned int n2, double hi double h2, double +v, int thread num, int total threads); double get optimal tau ( unsigned int ni, unsigned int n2, double hi double h2, double *Х, double *и, double *г, int thread num, int total threads); В файле operators. с находятся функции: ° get residual — возвращает Lq норму невязки; основные осо- бенности функции: — сумма квадратов элементов вычисляетс» в каждой задаче для своего участка массива, затем с помощью reducesum получается сумма квадратов всех элементов массива, ко- тора» используется для вычисления результата, возвра- щаемого функцией в каждой задаче; — граничные точки не учитываются; — для уменьшения вычислительной погрешности, связанной с суммированием чисел, сильно различающихся по поряд- ку, вычисляются суммы квадратов элементов по строкам, которые затем прибавляются к окончательному резуль- тату; ° get operator — вычисляет произведение матрицы А систе- мы (10.6) (т. е. (10.5)) на вектор и: v = Аи. оператор вычис- ляется согласно формулам (10.7) в каждой задаче для своего участка массива, синхронность работы всех задач обеспечи- вается с помощью synchronize; ° get pre condit ioner — вычисляет произведение матрицы В 1 в (10.8) на вектор и: v = В ~и; оператор вычисля- ется согласно формулам (10.9) для матрицы В в каждой задаче для своего участка массива, синхронность работы всех задач обеспечивается с помощью synchronize; 
220 Глава 10. Синхронизация и взаимодействие задач ° get optimal tau — возвращает оптимальное значение итера- ционного параметра тх, в (10.8) согласно формулаи (10.10); основные особенности функции: — скалярные произведения (f'. т), (Ат. хх), (Аг, т) вьх хххсляют- ся l3 каждой задаче для своих участков массивов, затем с помощью reduce sum получаются соответствуюпц~е ска- лярные произведения для всех элементов массива, кото- рые использ~ ются для вычисления результата, возвра~ца- емого функцией в каждой задаче; — значение Ar вы шсляется в точке и сразу исполх зуется прп ххычпслеххии скалярных ххроизведений (Ar, и) хх (Ar, r); — tëÿ уменьшения вычислительной погреп|ности использу- ется тот же прием, что в getresidual. Файл operators с: ¹include <st io ¹include <math ¹include "operators.h" ¹include "reduce sum.h" /+ Вычислить L2 норму невязки для задачи с номером thread num из общего количества total threads. Граничные узлы не входят. +/ double 0;et residual ( unsigned int ni, /+ число точек по х */ unsigned int n2, /* число точек по у */ double hi, /+ шаг сетки по х */ double h2, /+ шаг сетки по у +/ double *r, /+ невязка +/ int thread num, /+ номер задачи */ с int total threads) /+ всего задач */ unsigned int first row, last row; unsigned int ii i2, addr; 
10.6. Решение задачи Дирихле для уравнения Пуассона 221 double s|, s2, /+ Первая участвующая строка +/ first row = (ni + 1) + thread num; first row /= total threads; /+ Последняя участвующая строка +/ last row = (ni + 1) + (thread num + 1); last row = last row / total threads — 1; /+ В силу нулевых краевых условий на границе невязка = О +/ if (first row == О) first row = 1; if (last row == ni) last row = п1 — i; for (ii = firstrow, addr = ii + (n2 + 1), si = О.; & ii &l ;= l st r w; ii /+ в первом элементе невязка = О в силу краевых условий */ аббг++; for (i2 = 1, s2 = О.; i2 & t; 2; i2 & t = г [айаг++]; s2 += t & s| += s2; /+ в последнем элементе невязка = О в силу краевых условий */ аббг++; & /+ Вычислить сумму по всем задачам +/ reduce sum (total threads &amp si, 
222 Глава 10. Синхронизация и взаимодействие задач & return sqrt (si + hi + h2); /+ Вычислить ч = Au, где А — оператор Лапласа для задачи с номером threadnum из общего количества total threads. */ void get operator ( unsigned int ni, /+ число точек по х */ unsigned int n2, /* число точек по у +/ double hi /* шаг сетки по х +/ double h2, /* шаг сетки по у +/ double *u, /+ решение +/ double +v, /* результат */ int thread num, /+ номер задачи +/ int total threads) /+ всего задач */ unsigned int first row, last row; unsigned int ii, i2, addr; double hhi = 1. / (hi + hi), hh2 = 1. / (h2 + h2); /+ Первая участвующая строка +/ first row = (nl + 1) + thread num; first row /= total threads; /& t; Послед яя участвую ая стр ка last row = (n1 + 1) + (thread num + 1); last row = last row / total threads — 1; if (first row == О) & /* нулевые краевые условия */ for (i2 = О, addr = О; i2 &l ;= 2; i2 ч[асЫг++] = О.; first row = i 
10.6. Решение задачи Дирихле для уравнения Пуассона 223 & if (last row == n1) /* нулевые краевые условия */ for (i2 = О, addr = n1 + (n2 + 1); i2 &l ;= 2; i2 v[addr++] = О.; last row = п1 — 1; & for (ii = first row, addr = ii + (n2 + 1); 11 &l ;= l st r w; 11 { /+ первый элемент = О в силу краевых условий +/ v[addr++] = О.; for (i2 = 1; i2 & t; 2; i2 +, addr & /+ v(i1,i2) (u(i1-1,i2)-2и(11,12)+и(11+1,i2))/(hi+hi) (u(ii,i2-1)-2и(11,12)+и(11,i2-1))/(h2+h2)+/ v[addr] = — (u[addr — (n2 + 1)] — 2 * u[addr] + u[addr + (n2 + 1)] ) + hh1 (и[асЫг — 1] — 2 + u[addr] + u[addr + 1]) + hh2; & /+ последний элемент = О в силу краевых условий +/ v[addr++] = О.; & /+ подождать всех */ synchronize (total threads); /+ Вычислить v = В"(-1&gt v, д В Ђ” предобуславливат ль оператора Лапласа для задачи с ноиерои thread num из общего количества total threads. В качестве В используется диагональ матрицы А. */ 
224 Глава 10. Синхронизация и взаимодействие задач void get preconditioner ( unsigned int ni, /+ число точек по х +/ unsigned int n2, /* число точек по у */ double hi /+ шаг сетки по х +/ double h2, /~ шаг сетки по у +/ double ~ч, /+ аргумент/результат */ int thread num, /+ номер задачи +/ int total threads) /+ всего задач */ unsigned int first row, last row; unsigned int ii, i2, addr; double hhi = 1. / (hi + hi), hh2 = 1. / (h2 + h2); double w = 1. / (2 + hhi + 2 + hh2); /+ Первая участвующая строка +/ first row = (ni + 1) + thread num; first row /= total threads; /+ Последняя участвующая строка */ last гои = (ni + 1) + (thread num + 1); last row = last row / total threads — 1; & if (first row == О) /+ нулевые краевые условия */ for (i2 = О, addr = О; i2 &l ;= 2; i2 ч[аййг++] = О.; first row = i; & if (last row == ni) & /+ нулевые краевые условия +/ for (i2 = О, addr = ni * (n2 + 1); i2 &l ;= 2; i2 v[addr++] = О.; last row = п1 — 1; 
10.6. Решение задачи Дирихле для уравнения Пуассона 225 & for (ii = first row, addr = ii + (n2 + 1); ii &l ;= l st r w; ii & /+ первый элемент = О в силу краевых условий +/ ч [асЫг++] = О.; for (i2 = 1; i2 & t; 2; i2 /+ v(ii, i2) = v(i1, i2) /(2/(hi+hi)+2/(h2+h2) ) ч/ v[addr++] ~= w; & /+ последний элемент = О в силу краевых условий +/ v[addr++] = О.; & /+ подождать всех */ synchronize (total threads); /* Вычислить ((f, r) — (Ar, u)) / (Ar, r) где А — оператор Лапласа для задачи с номером thread num из общего количества total threads. Функции и, r удовлетворяют нулевым краевым условиям +/ double get optimal tau ( unsigned int ni, /+ число точек по х +/ unsigned int n2, /* число точек по у +/ double hi, /* шаг сетки по х */ double h2, /* шаг сетки по у +/ double *f /* правая часть +/ double *u, /+ решение +/ double +r, /+ В <-1>(не яз int thread num, /* номер задачи */ int total threads) /+ всего задач +/ с 8 4017 
226 Глава 10. Синхронизация и взаимодействие задач unsigned int first row, last row; unsigned int ii, i2, addr; double hhi = 1. / (hi * hi) hh2 = 1. / (h2 * h2) double si fr, s2fr; /+ суммы для (f, r) +/ double siAru, в2 Аги;/* суммы для (Ar, и) +/ double si Arr, s2 Arr;/+ суммы для (Ar, r) +/ double Ar; /+ значение Ar в точке +/ double a[3]; /+ Первая участвующая строка &l first row = (n1 + 1) + thread num; first row /= total threads; /+ Последняя участвующая строка */ last row = (ni + 1) + (thread num + 1); last row = last row / total threads — 1; /~ В силу нулевых краевых условий на границе, слагаемые, соответствующие граничным узлам = О ~/ if (first row == О) first row = 1; if (last row == ni) last row = ni — i; for (ii = first row, addr = ii + (n2 + 1), sifr = О., з1 Ага = О., з1 Агг = О.; & ii &l ;= l st r w; ii /+ в первом элементе слагаемые = О в силу краевых условий +/ addr++; for (i2 = 1, s2fr = О., s2 Aru = О., s2 Arr = О.; i2 & t; 2; i2 +, addr /+ Вычисляем (f, r) +/ s2fr += f[addr] + r[addr] 
10.6. Решение задачи Дирихле для уравнения Пуассона 227 /+ Вычисляем Ar +/ Ar = — (r[addr — (n2 + 1)] — 2 + г[асЫг] + r[addr + (n2 + 1)]) + hh1 (г [асЫг — 1] — 2 + г [асЫг] + r[addr + 1]) + hh2; /+ Вычисляем (Ar, u) +/ s2Aru += Ar + u[addr]; /* Вычисляем (Az, r) */ & s2Arr += Ar + r[addr]; si fr += s2 fr; si Aru += s2 Aru; з1Агх += s2Arr; /+ в последнем элементе слагаемые = 0 в силу краевых условий +/ асЫг++; & а[0] = в1 Хг; а[1] = s1Aru; а[2] = s1 Arr; /+ Вычислить суммы по всем задачам +/ reduce sum (total threads, а, 3); /+ Вычислить ответ +/ return (а[0] — а[1]) / а[2]; Заголовочный файл laplace h: int laplace solve (unsigned int n1, unsigned int n2, double hi double h2, unsigned int max it double prec, double +f double +u, double +r, int thread num, int total threads) 
228 Глава 10. Синхронизация и взаимодействие задач В файле laplace. с находится функция laplace solve, осу- ществляющая итерационный процесс (10.8), (10.10) с матрица- ми (10.7) и (10.9) по описанным выше расчетным формулам. Файл laplace c: ¹include <stdio ¹include <math ¹include "init.h" ¹include "operators.h" ¹include "reduce sum.h" ¹include "laplace.h" /+ Решить задачу для задачи с номером thread num из общего количества total threads. Возвращает количество потребовавшихся итераций. +/ int 1ар1асе во1че ( unsigned int ni, /* число точек по х */ unsigned int n2, /* число точек по у */ double hi, /* шаг сетки по х */ double h2, /* шаг сетки по у */ unsigned int maxit, /* максимальное число итераций */ double prec, /+ величина падения невязки +/ double *f /+ правая часть +/ double *u, /+ решение +/ double *r, /* невязка */ int thread num, /+ номер задачи +/ int total threads) /* всего задач +/ unsigned int first row, last row; unsigned int first addr, last addr; unsigned int addr; /+ Норма невяэки на первом шаге +/ double norm residual i /+ Норма невяэки на очередном шаге +/ double norm residual; 
10.6. Решение задачи Дирихле для уравнения Пуассона 229 /+ Норма невяэки на предыдущем шаге +/ double norm residual prev; unsigned int it; /+ Номер итерации */ double tau; /+ Итерационный параметр +/ /+ Первая участвующая строка +/ first row = (ni + 1) + thread num; first row /= total threads; /+ Последняя участвующая строка +/ last row = (n1 + 1) + (thread num + 1); last row = lastrow / total threads — 1; /* Соответствующие адреса +/ first addr = first row + (n2 + 1); last addr = last row + (n2 + 1) + n2; /+ Инициализируем правую часть +/ init f (n1, п2, h1 h2 f threadnum, total threads) /+ Начальное приближение = 0 +/ for (addr = first addr; addr &l ;= l st ad r; айаг u[addr] = О.; /+ Невязка при нулевом начальном приближении = f +/ norm residual | = norm residual = norm residual prev = get residual (ni, n2, hi, h2, f, thread num, total threads); if (thread num == О) printf (" Residual = /1е~п", norm residual 1) /+ Итерационный процесс +/ for (it = 1; it & t; max t; it & /+ Шаг итерационного процесса +/ 
230 Глава 10. Синхронизация и взаимодействие задач /+ Вычисляем r = Аи +/ get operator (ni n2, h1 h2, u, r, thread num, totalthreads); /+ Вычисляем невязку r = f — r = f — Au +/ for (addr = first addr addr &l ;= l st a dr addr r[addr] = f[addr] — r[addr]; /* подождать всех */ synchronize (total threads); /+ Вычисляем норму невяэки +/ norm residual = get residual (ni, n2, hi, h2, r, threadnum, total threads); if (thread num == 0) printf (" It № =/2.2d, residual='/11.4е, convergence=/6.3f, average convergence='/6.3fgn", norm residual, norm residual / norm residual prev, pow (normresidual / norm residual i, 1./it)); /+ Проверяем условие окончания процесса +/ if (norm residual & t; n rm resid a + pr break; /+ Обращение предобуславливателя r = В"(-1) r +/ get preconditioner (ni, n2, hi, h2, r, thread num, total threads); /+ Вычисление итерационного параметра tau = ((Ь, r) — (Аг, u)) / (Ar, r) +/ tau = get optimal tau (ni, n2, h1, h2, f, и, r, thread num, total threads); /+ Построение очередного приближения u+=tau + r +/ 
10.6. Решение задачи Дирихле для уравнения Пуассона 231 for (addr = first addr; addr &l ;= l st ad r; addr u[addr] += tau * r [addr]; /* подождать всех +/ synchronize (total threads); normresidual prev = norm residual; return it; 
Интерфейс MPI (Message Passing Interface) В этой главе мы рассмотрим интерфейс МР1 (1VIessage Равв1п Interface), ставший de-facto стандартом для программирования систем с распределенной памятью. МР1 представляет собой набор утилит и библиотечных функций (для языков С/С++, FORTRAN), позволяющих создавать и запускать прило>кен работающие на параллельных вычислительных установках са- мой различной природы. Появившись в 1990-х годах как уни- фицированный подход к программированию для систем с ðàñ- пределенной памятью, МР1 приобрел большую популярность и теперь широко используется также в системах с общей памятью и разнообразных смешанных вычислительных установках. 11.1. Общее устройство МР1-программы Имена всех функций, типов данных, констант и т. д., относя- щигсся к МР1-библиотеке, начинаются с префикса MPI и описа- ны в заголовочном файле mpi.h. Все функции (за исключением MPI Wtime u MPI Wtick) имеют тип возвращаемого значения int и возвращают код ошибки или MPI SUCCESS в случае успеха. Од- нако в случае ошибки перед возвратом из вызвавшей ее функции вызывается стандартный обработчик ошибки, который аварий- но терминирует программу. Поэтому проверять возвращаемое значение не имеет смысла. Стандартный обработчик ошибки можно заменить (с помощью функции MPI Errhandler set), 
11.1. Оощее устройство МР1-программы 233 но стандарт MPI не гарантирует, что программа может про- должать работать после ошибки. Так что это тоже обычно не имеет смысла. До первого вызова любой МР1-функции необходимо вызвать функцию MPI Init. Ее прототип: int MPI Init (int +argc, char +++argv); где argc, argv — указатели на число аргументов программы и на вектор аргументов соответственно (это адреса аргументов функ- ции main программы). Многие реализации MPI требуют, чтобы процесс до вызова MPI Init не делал ничего, что могло бы из- менить его состояние, например открытие или чтение/запись файлов, включая стандартные ввод и вывод. После окончания работы с IvIPI-функциями необходимо вы- звать функцию МРТ Рхпа1хке. Ее прототип: int MPI Finalize (void); Количество работающих параллельных процессов после вызова этой функции не определено, поэтому после ее вызова лучше всего сразу же закончить работу программы. Общий вид MPI-программы: Oinclude "mpi.h" /* Другие описания */ & int main (int argc, char + argv [] ) /+ Описания локальных переменных +/ MPI Init (&a gc, &am /+ Тело программы +/ MPI Finalize (); return О; 
234 Глава 11. Интерфейс MPI (Message Passing Interface) 11.2. Сообщения Параллельно работающие MPI-процессы обмениваются между собой информацией и обеспечивают взаимную синхронизацию посредством сообщений. Различают обмен сообщениями: ° попарный (point-to-point) — сообщение посылается одним процессом другому, в обмене участвуют только два процесса; ° коллективный — сообщение посылается процессом всем процессам из его группы (коммуникатора, см. ниже) и по- лучается всеми процессами из его группы (коммуникатора). Коллективный обмен может быть представлен как последо- вательность попарных обменов, однако специализированные MPI-функции для коллективных обменов учитывают специ- фику построения конкретной вычислительной установки и могут выполняться значительно быстрее соответствующей последовательности попарных обменов. Различают обмен сообщениями: ° синхронный — — процесс — отправитель сообщения переходит в состояние ожидания, пока процесс — получатель не будет го- тов взять сообщение, а процесс — получатель сообщения пере- ходит в состояние ожидания, пока процесс — отправитель не будет готов послать сообщение; ° асинхронный — процесс — отправитель сообщения не ждет готовности процесса — получателя, сообщение копируется под- системой МР1 во внутренний буфер,и отправитель продол- жает работу. В первых реализациях МР1 все обмены были синхронными. Для повышения эффективности работы (за счет параллельности ра- боты процесса и пересылки сообщения) последние реализации МРI стараются сделать как можно больше обменов асинхронны- ми. Например, если размер сообщения небольшой, то его обычно буферизуют и устраивают асинхронный обмен. Длительное вре- мя все коллективные обмены были синхронными. 
11.2. Сообщения 235 Отметим, что поведение некачественной программы может быть разным при использовании синхронного или асинхронного режима. Например, если процесс А посылает сообщение процес- су В, который не вызывает функцию получения сообщения, то это приводит к «зависанию» процесса А в синхронном режиме. В асинхронном режиме такая программа будет работать. Основные составляющие сообщения: 1. Блок данных сообщения — представляется типом void+. 2. Информация о данных сообщения: (а) тип данных — представляется типом MPIDatatype; со- ответствие MPI-типов данных и типов языка С приведе- но в табл. 11.1. (b) количество данных — количество единиц данного типа в блоке сообщения. Представляется типом int. (Причина появления этого поля достаточно очевидна: выгоднее за один раз передать большое сообщение с 10-ю элементами, чем посылать 10 сообщений с одним элементом.) Таблица 11.1. Соответствие типов данных MPI и типов языка С 
236 Глава 11. Интерфейс MPI (Message Passing Intertace) 3. Информация о получателе и отправителе сообще- НИЯ: (а) коммуникатор — идентификатор группы запущенных программой параллельных процессов, которые могут об- мениваться между собой сообщениями. Представляет- ся типом МР1 Соуп. Как получатель, так и отправитель должны принадлежать указанной группе. Процесс мо- жет принадлежать одновременно многим группам, все запущенные процессы принадлежат группе с идентифи- катором MPICOMMWORLD. (b) ранг получателя — номер процесса — получателя в ука- занной группе (коммуникаторе). Представляется типом Ый. Это поле отсутствует при коллективных обменах сообщениями. (с) ранг отправителя — номер процесса — отправителя в указанной группе (коммуникаторе). Представляется ти- пом int. Получатель может указать, что он будет прини- мать только сообщения от отправителя с определенным рангом, или использовать константу МР? «АИУ 800ВСЕ, позволяющую принимать сообщения от любого отпра- вителя в группе с указанным идентификатором. Это поле отсутствует при коллективных обменах сообще- ниями. 4. Тег сообщения — произвольное число типа int (стандарт гарантирует возможность использования чисел от О до 215), приписываемое отправителем сообщению. Получатель мо- жет указать, что он будет принимать только сообщения, имеющие определенный тег, или использовать константу MPIANYTAG, позволяющую принимать сообщения с любым тегом. Это поле отсутствует при коллективных обменах со- общениями, поскольку все процессы обмениваются одинако- выми IIO структуре данными и в первых версиях MPI все коллективные обмены были синхронными. 
11.3. Коммуникаторы 237 11.3. Коммуникаторы Напомним, коммуникатор — это объект типа ИР? Сошш, пред- ставляющий собой идентификатор группы запущенных про- граммой параллельных процессов, которые могут обмениваться сообщениями. Коммуникатор является аргументом всех функ- ций, осуществляющих обмен сообщениями. Программа может разделять исходную группу всех запущенных процессов, иден- тифицируемую коммуникатором MPI COMM WORLD, на подгруппы для: ° организации коллективных обменов внутри этих подгрупп, ° изоляции одних обменов от других (так часто поступают па- раллельные библиотечные функции, чтобы их сообщения не пересекались с сообщениями вызвавшего их процесса), ° учета топологии распределенной вычислительной установ- ки (например, часто распределенный кластер можно пред- ставить в виде пространственной решетки, составленной из скоростных линий связи, в узлах которой находятся рабо- чие станции; скорости обмена данными между узлами тут различны и это можно учесть введением соответствующих коммуникаторов) . Получить количество процессов в группе (коммуникаторе) можно с помощью функции MPI Comm size. Ее прототип: int MPI Comm size (MPI Comm comm, int +size); где comm â коммуникатор (входной параметр), size †указате на результат. Например, общее количество запущенных процес- сов можно получить следующим образом: int р; MPI Comm size (MPI COMM WORLD, &amp Получить ранг (номер) процесса в группе (коммуникаторе) можно с помощью функции MPI Comm rank. Ее прототип: int MPI Comm rank (MPI Comm comm, int +rank); 
238 Глава 11. Интерфейс MPI (Message Passing Interface) где comm — коммуникатор (входной параметр), гBIlk — указатель на результат. Например, номер текущего процесса среди всех запущенных можно получить следующим образом: int my rank; MPI Comm rank (MPI COMM WORLD, &am ;my ra 11.4. Попарный обмен сообщениями Послать сообщение можно с помощью функции ИР? Беппо. Ее прототип: int MPI Send (void +buf, int count, MPI Datatype datatype, int dest, int tag, MPI Comm comm) где входные параметры: ° buf — адрес буфера с данными, ° count †чис элементов (типа йа~айуре) в буфере, ° datatype — тип данных каждого из элементов буфера, ° dest--ранг (номер) получателя в коммуникаторе (группе) comm, ° tag — тег сообщения, ° comm — коммуникатор. 3Та функция может перевести тскущ=й процесс в состояние ожидания, пока получатель с 11омером dest в группе comm н» примет сообщение с тегом tag (если используется синхронный режим обмена). Получить сообщение можно с помощью функции MPI Recv. Ее прототип: int MPI Recv (void +buf, int count, MPI Datatype datatype, int source, int tag;, MPI Comm comm, MPI Status ~в~а~ив); где входные параметры: ° buf — адрес буфера с данными где следует разместить по- лученное сообщение, 
11А. Попарный обмен сообщениями 239 ° count — максимальное число элементов (типа datatype) в бу- фере, ° datatype — тип данных каждого из элементов буфера, ° source — ранг (номер) отправителя в коммуникаторе (груп- пе) comm (может быть MPI ANY SOURCE), ° tag — тег сообщения (может быть ИР?АМУ ТАС), ° comm — коммуникатор, и выходные параметры (результаты): ° buf — полученное сообщение, ° status — информация о полученном сообщении (см. ниже). Эта функция переводит текущий процесс в состояние ожидания, пока отправитель с номером source (или любым номером, если в качестве ранга используется константа МР? АИУЯОУЕСЕ) в груп- пе comm не отправит сообщение с тегом tag (или любым тегом, если в качестве тега используется константа МР1 АИУ ТАС). При попарных обменах сообщениями получатель сообще- ния может получить через переменную status типа MPI Status информацию о по.лученном сообщении. Структура данных MPI Status включает в себя следующие поля: ° MPI SOURCE — реальный ранг отправителя (может потребо- ваться, если в качестве ранга отправителя использована кон- станта MPI ANY SOURCE), ° MPI TAG — реальный тег сообщения (может потребоваться, если в качестве тега сообщения использована константа MPI ANY TAG), ° MPI ERROR — код ошибки, ° дополнительные служебные поля, зависящие от реализации ЫР1. Размер полученного сообщения непосредствешю в структуре данных MPI Status не хранится, но может быть получен из нее с помощью функции MPI Get count. Fe прототип: int MPI Get count (MPI Status +status, MPI Datatype datatype, int +count); 
240 Глава 11. Интерфейс MPI (Message Passing Interface) где входные параметры: ° status — информация о полученном сообщении, ° datatype — тип данных, в единицах которого требуется по- лучить размер сообщения, и выходной параметр (результат): ° count — количество полученных элементов в единицах типа datatype или константа MPI UNDEFINED, если длина данных сообщения ие делится нацело на размер типа datatype. Заметим, что при коллективных обменах получатель инфор- мацию о сообщении не получает, поскольку процессы обменива- ются одинаковыми по структуре данными. Еще раз отметим, что поведение некачественной программы может быть разным при использовании синхронного или асин- хронного режима отправки сообщений. Например, если процесс А последовательно посылает процессу В два сообщения с тегами 0 и 1, а процесс В последовательно вызывает функцию получе- ния сообщения с тегами 1 и О, то это приводит к «зависанию» обоих процессов в синхронном режиме. В асинхронном режиме такая программа будет работать. 11.5. Операции ввода-вывода в МР1-программах Рассмотрим вначале стандартный ввод — вывод (на экран). Здесь ситуация зависит от конкретной реализации МР1: ° Существуют реализации МР1, в которых всем работающим процессам разрешено осуществлять ввод — вывод на экран. В этом случае на экране будет наблюдаться перемешанный вы- вод от всех процессов, в котором часто трудно разобраться. ° Для предотвращения переиешивания вывода разных процес- сов некоторые реализации MPI (и таких большинство) раз- решают осуществлять ввод — вывод на экран только процессу с номером 0 в группе MPI COMM WORLD. В этом случае осталь- ные процессы посылают этому процессу запросы на произ- водство своего ввода-вывода на экран в качестве сообщений. 
11.5. Операции ввода — вывода в МР)-программах 2 1 Этим обеспечивается последовательный вывод на экран (без перемешивания}. ° Существуют реализации MPI, в которых всем работающим процессам запрещено осуществлять ввод — вывод на экран. При выводе на экран создается файл, в котором содержит- ся все выведенное, а при вводе с клавиатуры фиксируется ошибка ввода. Обычно такие реализации работают на рас- пределенных вычислительных установках, где запуск про- граммы на исполнение осуществляется с выделенного ком- пьютера, не входящего в состав узлов, на которых собствен- но исполняется МР1-программа. В таких реализациях весь ввод — вывод осуществляется через файлы. Поэтому для написания переносимой между различными реали- зациями МР1-программы следует придерживаться предположе- ния о том, что программа не может осуществлять стандартный ввод — вывод и должна работать только с файлами (см. ниже). Единственным исключением является вывод на экран, который появится либо на экране, либо в созданном системой файле. На файловый ввод-вывод какие-либо ограничения формаль- но отсутствуют. Однако при одновременной записи данных в файл несколькими процессами может происходить их переме- шивание и даже потеря. Одновременное чтение данных из фай- ла несколькими процессами на распределенной вычислитель- ной установке может приводить к блокировке всех процессов, кроме одного, и их последовательному выполнению. Это свя- зано с работой сетевой файловой системы (NFS, network file system). Поэтому для написания переносимой между различ- ными реализациями MPI-программы следует придерживаться предположения о том, что осуществлять файловый ввод — вывод можно только процессу с номером 0 в группе ИР? СОИМ ИОВЫ. Остальные процессы получают или передают ему свои данные посредством сообщений. Естественно, это не относится к ситу- ации, когда каждый из процессов осуществляет ввод — вывод в свой собственный файл. 
242 Глава 11. Интерфейс MPI (Message Passing Interface) 11.6. Пример простейшей MPI-программы Рассмотрим в качестве примера программу, выводящую на экран сообщение «Не11о» от каждого из процессов. ¹include <stdio ¹include <string ¹include "mpi.h" /+ Ялика буфера сообщений */ ¹define BUF LEN 256 int с main (int argc, char +argv [] ) int myrank; /+ ранг текущего процесса */ int р; /+ общее число процессов +/ int source; /+ ранг отправителя */ int dest; /* ранг получателя */ int ta0; = О; /+ тег сообщений +/ char message[BUF LEN];/+ буфер для сообщения +/ MPI Status status; /+ инфорнация о полученнон сообщении +/ /+ Начать работу с MPI */ MPI Init (&a gc, &am /+ Получить нонер текущего процесса в группе всех процессов +/ MPI Comm rank (MPI COMM WORLD, &myra /+ Получить общее количество запущенных процессов +/ MPI Comm size (MPI COMM WORLD, &amp /~ Посылаен сообщения процессу О, который их выводит на экран +/ 
11.7. Дополнительные функции попарного обмена сообщениями 243 if (myrank ! = О) с /+ Создаем сообщение +/ sprintf (message, "Hello from process '/d!", myrank); /+ Отправляем его процессу 0 +/ dest = 0; MPI Send (message, strlen (message) + 1, MPI CHAR, dest, tag, MPI COMM WORLD); ) else & /+ В процессе 0: получаем сообщение от процесса 1,...,р-1 и выводим его на экран */ for (source = 1; source & t; р; source с MPI Recv (message, BUF LEN, MPI CHAR, source, tag, MPI COMM WORLD, &stat printf ("/в~п", message); ) ) /+ Заканчиваем работу с MPI +/ MPI Finalize (); return 0; ) 11.7. Дополнительные функции для попарного обмена сообщениями Для удобства программиста стандарт МР1 предоставляет ряд дополнительных функций для попарного обмена сообщениями. Лхобая программа может быть написана без их игпользования, но для многих вычислительных установок они могут значитель- но увеличить производительность. 
244 Глава 11. Интерфейс MPI (Message Passing Interface) Если программе важно, чтобы был использован именно синхронный способ посылки сообщений, то вместо функции MPI Send можно использовать функцию MPI Ssend, имеющую те же аргументы и возвращаемое значение (дополнительная буква s в имени — от synchronous). При попарных обменах между процессами очень часто тре- буется именно обменяться данными, т. е. послать и получить разные сообщения. При этом программный код для этих про- цессов получается несимметричным: для одного из них надо вначале использовать МР? Зепи, а затем МР? Весч, для второ- го — эти же функции, но в обратной последовательности. Если же оба процесса вызывают эти функции в одинаковом порядке, то в случае синхронного обмена сообщениями они оба «зави- сают». Для этой ситуации стандарт МР1 предоставляет функ- цию MPI Sendrecv, являющуюся как-бы гибридом MPI Send u MPI Recv. Ее прототип: int MPI Sendrecv (void +sendbuf, int sendcount, MPIDatatype sendtype, int dest, int sendta0;, void +recvbuf, int recvcount, MPIDatatype recvtype, int source, int recvta0;, MPI Comm comm, MPI Status +status); где входные параметры: ° sendbuf — адрес буфера с посылаемыми данными, ° sendcount — число элементов (типа sendtype) в буфере sendbuf, ° sendtype — тип данных каждого из элементов буфера sendbuf, ° dest — ранг (номер) получателя в коммуникаторе (группе) comm, ° sendtag — тег посылаемого сообщения, ° recvbuf — адрес буфера с данными, где следует разместить полученное сообщение, ° recvcount — максимальное число элементов (типа recvtype) в буфере recvbuf, 
11.7. Дополнительные функции попарного обмена сообщениями 245 ° recvtype — тип данных каждого из элементов буфера recvbuf, ° source — ранг (номер) отправителя в коммуникаторе (груп- пе) comm (может быть MPI ANY SOURCE), ° recvtag — тег получаемого сообщения (может быть копстап- той MPI ANY TAG), ° comm — коммуникатор, и выходные параметры (результаты): ° recvbuf — полученное сообщение, ° status — информация о полученном сообщении. Эта функция может перевести текущий процесс в состояние ожидания, пока получатель с номером dest в группе со~~ н© примет сообщение с тегом sendtag (если используется синхрон- ный режим обмена) или пока отправитель с номером source (или любым номером, если в качестве ранга используется KOH- станта MPI ANY SOURCE) в группе comm не отправит сообщение с тегом recvtag (или любым тегом, если в качестве тега использу- ется константа MPI ANY TAG). Посылать сообщения для приема этой функцией можно с помощью любой функции для попарных обменов, например, MPI Send или ее самой, принимать сообще- ния от этой функции можно посредством любой функпии ë» попарных обменов, например, ИР? Н,есч или ее самой. Стандарт МР1 запрещает использовать одинаковые указате- ли для любых двух аргументов любой MPI-функции, если хоТН бы один из них является выходным параметром. Для функций MPISendrecv это означает, что нельзя использовать opvH«o»~© значения для входных и выходных буферов. Если программе э« требуется, то надо применять функцию MPI Sendrecv replace. Ее прототип: int MPI Sendrecv replace (void +buf, int count, MPI Datatype datatype, int dest, int sendtag, int source, int recvtag, MPI Comm comm, MPI Status ив~а~ив); 
246 Глава 11. Интерфейс MPI (Message Passing Interface) где входные параметры: ° buf — адрес буфера с посылаемыми данными (также явля- ется выходным параметром), ° с ount -- число элементов (типа dat atype) в буфере, ° datatype — тип данных каждого из элементов буфера, ° dest — ранг (номер) получателя в коммуникаторе (группе) comm, ° sendtag — тег посылаемого сообщения, ° source — ранг (номер) отправителя в коммуникаторе (груп- пе) comm (может быть MPI ANY SOURCE), ° гесч~ая — тег получаеиого сообщения (может быть констан- той MPI ANY TAG), ° comm — коммуникатор, и выходные параметры (результаты): ° buf — полученное сообщение (также является входным па- раметром), ° status — информация о полученном сообщении. Работа этой функции аналогична MPI Sendrecv, за исключени- елl того, что получеш~ые данные замещают посланные. В некоторых вычислительных установках для обмена сооо- щенияыи существует специальньш коммуникационный процес- сор, способный работать параллельно с основным. Следователь- но, передача сообщений и вычислительная работа могут идти параллельно, если для проведения дальнейших вычислений дан- ««ые сообщения ««е нужны (получателю) и не будут модифици- роваться (отправителси). Послать сообщение без блокирования процесса можно с поиощью функции MPI Isend (дополнитель- ная буква i в имени — от immediate). Ее прототип: int MPI Isend (void +buf, int count, MPIDatatype datatype, int dest, int tag, MPI Comm comm, MPIRequest +request); где входные параметры: 
11.7. Дополнительные функции попарного обмена сообщениями 247 ° buf — адрес буфера с данными, ° count — — число элементов (типа datatype) в буфере, ° datatype — тип данных каждого из элементов буфера, ° dest — ранг (номер) получателя в коммуникаторе (группе) с omm) ° tag — — тег сообщения, ° с omm — — коммуникатор, и выходной параметр (результат): ° request — идентификатор запроса на обслуживание сообще- ния; имеет тип MPIRequest, не доступный пользователю (указатель на объект этого типа возвращается процессу и может быть использован в качестве аргумента для других функций). Принимать сообщения от этой функции можно с помощью лю- бой функции для попарпых обменов, например, ИР? Весч. Получить сообщение без блокирования процесса можно с по- мощью функции MPI Irecv (дополнительная буква i в имени— от immediate). Ее прототип: int MPI Irecv (void +buf, int count, MPI Datatype datatype, int source, int tag, MPI Comm comm, MPI Request +request); где входные параметры: ° buf — адрес буфера с данными, где следует разместить по- лученное сообщение, ° count — максимальное число элементов (типа datatype) в бу- фере, ° datatype — тип данных каждого из элементов буфера, ° source — ранг (номер) отправителя в коммуникаторе (груп- пе) comm (может быть MPI ANY SOURCE), ° tag — тег сообщения (может быть ИР? АЫУ ТАС), ° comm — коммуникатор, и выходные параметры (результаты): 
248 Глава 11. Интерфейс MPI (Message Passing Interface) ° buf — полученное сообщение, ° request — идентификатор запроса на обслуживания сообще- ния. Посылать сообщения для приема этой функцией можно с помощью любой функции для попарных обменов, например, MPISend. Узнать, доставлено ли сообщение (как для MPI Isend, так и для MPI Irecv), можно с помощью функции MPI Test. Ее про- тотип: int MPI Test (MPI Request +request, int +flag, MPI Status ~в1а1ив); где входной параметр: ° request -- идентификатор запроса на обслуживание сообще- ния (также является выходным параметром: если обслужи- вание завершено, то он становится равным специальному значению MPI RE/VEST NULL). и выходные параметры (результаты): ° flag-признак окончания обслуживания, значение +flag становится истинным, если запрос обработан, ° status — если запрос обработан, то содержит информацию о доставленном сообщении (используется только при полу- чении сообщения и в этом случае играет роль одноименного параметра функции ИР? Весч). Функция MPITest не блокирует вызвавший ее процесс, а только устанавливает свои аргументы указанным выше образом. Если требуется проверить доставку одного из нескольких сообщений, то можно использовать функцию ИР? Тевтапу. Ее прототип: int MPI Testany (int count, MPI Request arrayofrequests[], int +index, int +flag, MPI Status +status); где входные параметры 
11.7. Дополнительные функции попарного обмена сообщениями 249 ° count — количество запросов в массиве arrayof requests, ° апау о1гес~иев~в — массив идентификаторов запросов на обслуживание сообщений (также является выходным пара- метром: если обслуживание сообщения с номером +index в массиве arrayofrequests завершено, то соответствую- щий элемент array of requests[+index] становится рав- ным MPI REgUESTNULL) и выходные параметры (результаты): ° flag — признак окончания обслуживания, значение + f lag становится истинным, если один из запросов обработан, ° index — если один из запросов обработан, то содержит номер обработанного сообщения в массиве arrayof requests, ° status — если один из запросов обработан, то содержит ин- формацию о доставленном сообщении (используется только при получении сообщения и в этом случае играет роль одно- именного параметра функции MPI Recv). Дождаться доставки сообщения (как для MPI Isend, так и для MPI Irecv) можно с помощью функции MPI Wait. Ее про- тотип: int MPI Wait (MPIRequest +request, MPI Status ~я1а1ив); где входной параметр ° request — идентификатор запроса на обслуживание сообще- ния (также является выходным параметром: после заверше- ния этой функции он становится равным MPIREAUESTNULL). и выходной параметр (результат): ° status — информация о доставленном сообщении (использу- ется только при получении сообщения и в этом случае играет роль одноименного параметра функции MPI Recv). Функция MPI Wait блокирует вызвавший ее процесс до оконча- ния обслуживания сообщения с идентификатором request. Если 
250 Глава 11. Интерфейс MPI (Message Passing Interface) требуется дождаться доставки одного из нескольких сообщений то можно использовать функцию MPI Waitany. Ее прототип: int MPI Waitany (int count, MPI Request array ofrequests[], int +index, MPI Status +status); где входные параметры: ° count — количество запросов в массиве array of requests, ° array of requests — массив идентификаторов запросов на обслуживание сообщений (также является выходным па- раметром: после завершения этой функции соответствую- щий элемент аггау аХгециевСв[*1пйех] становится рав- ным MPI REgUESTNULL) и выходные параметры (результаты): ° index — содержит номер обработанного сообщения в массиве array of requests, ° status — информация о доставленном сообщении (использу- ется только при получении сообщения и в этом случае играет роль одноименного параметра функции MPI Recv). Функция MPI Waitany блокирует вызвавший ее процесс до окон- чания обработки одного из запросов на обслуживание в массиве array of requests. 11.8. Коллективный обмен сообщениями Все описываемые ниже возможности МР1 являются избыточ- нымш в том смысле, что любая программа может быть написа- На без Нх использования. Действительно, всякий коллективный обмен сообщениями может быть заменен на соответствующий цикл попарных обменов. Однако это приведет к значительно- му снижению скорости работы программы, так как, во-первых, простейшее решение (один из процессов рассылает данные всем остальным) не годится (в каждый момент времени работают только два процесса), а, во-вторых, любое решение (например, 
11.8. Коллективный обмен сообщениями рассылка по принципу бинарного дерева: первый процесс посы- лает данные второму, затем первый и второй посылают данные третьему и четвертому соответственно, и т. д.) не будет учиты- вать специфику конкретной вычислительной установки. Сообщения, посылаемые с помощью функций коллективного обмена, не могут быть получены с помощью функций попарного обмена и наоборот. Напомним также, что в первых версиях МР1 все функции коллективного обмена были синхронными и это наложило отпечаток на их синтаксис. Для синхронизации процессов можно использовать функ- цию ИРТВагг1ег. Ее прототип: int MPI Barrier (MPI Comm comm); где входной параметр ° comm — коммуникатор. Функция MPI Barrier блокирует вызвавший ее процесс до тех пор, пока все процессы в группе с идентификатором comm (коммуникаторе) не вызовут эту функцию (ср. с функцией synchronize в разделе 10.4). Для рассылки данных пз одного процесса всем остальным в его группе можно испольэовать функцию MPI Beast. Ее про- тотип: int MPI Beast (void +buf, int count, MPI Datatype datatype, int root, MPI Comm comm); где входные параметры: ° buf — в процессе с номером root — адрес буфера с посылаемыми данными (входной параметр), — в остальных процессах группы comm — адрес буфера с дан- ными, где следует разместить полученное сообщение (вы- ходной параметр), ° count 
252 Глава 11. Интерфейс MPI (Message Passing Interface) — в процессе с номером гоо~ — число элементов (типа datatype) в буфере, — в остальных процессах группы comm — максимальное чис- ло элементов (типа datatype) в буфере, ° datatype — тип данных каждого из элементов буфера, ° root — ранг (номер) отправителя в коммуникаторе (группе) comm, ° comm — коммуникатор, и выходной параметр (результат): ° buf — в процессах группы comm с номерами, отличными от root, — полученное сообщение. Эта функция должна быть вызвана Во всех процессах группы comm с одинаковыми значениями для аргументов root u comm. Также в большинстве реализаций МР1 требуется, чтобы значе- ния аргументов count u datatype были одинаковыми во всех процессах группы comm. Эта функция рассылает сообщение buf из процесса с номером root всем процессам группы comm. По- скольку все процессы вызывают эту функцию одновременно и с одинаковыми аргументами, то, в отличие от функций MPI Send и MPIRecv, поля tag u status отсутствуют. Часто требуется выполнить в каком-то смысле обратную к производимой функцией MPI Beast операцию — переслать дан- ные одному из процессов группы от всех остальных процессов этой группы; при этом часто нужно получить не сами эти дан- ные, а некоторую функцию от них, например, сумму всех дан- ных. Для этого можно использовать функцию MPI Reduce. Ee прототип: int MPI Reduce (void +sendbuf, void +recvbuf, int count, MPI Datatype datatype, MPI Op op, int root, MPI Comm comm); где входные параметры: ° sendbuf — адрес буфера с данными, ° count — число элементов (типа datatype) в буфере, 
11.8. Коллективный обмен сообщениями 253 Таблица 11.2. Операции над данными в MPI Операция MPI Значение МР1 МАХ максимум МР1 IviIN минимум MPI SUM сумма MPI PROD произведение iPI LAND логическое «и» МР1 BAND побитовое «и» MPI LOR логическое «или» MPI BOR побитовое «или» MPI LXOR логическое «исключающее или» МР1ВХОВ побитовое «исключающее или» МР1 МАХЕОС максимум и его позиция МР1 М?ХЬОС минимум и его позиция ° datatype — тип данных каждого из элементов буфера, ° ор — идентификатор операции (типа MPI Ор), которую нуж- но осуществить над пересланными данными для получения результата в буфере recvbuf; см. в табл. 11.2 возможные значения этого аргумента, ° root — -ранг (номер) получателя в коммуникаторе (группе) comm, ° comm — коммуникатор, и выходной параметр (результат): ° recvbuf — указатель на буфер, где требуется получить ре- зультат; используется только в процессе с номером root в группе comm. Эта функция должна быть вызвана Во всех процессах группы comm с одинаковыми значениями для аргументов root, comm, count, datatype, op. Если требуется, чтобы результат, полученный посредством функции MPI Reduce, стал известен не только одному процессу (с номером root в группе comm), а всем процессам группы, то 
254 Глава 11. Интерфейс MPI (Message Passing Interface) можно использовать MPI Всаяс, но более эффективным реше- нием является функция ИР1 А11гейисе. Ее прототип: int MPI Allreduce (void +sendbuf, void +recvbuf, int count, MPI Datatype datatype, MPI Op op, MPI Comm comm); гце входные параметры: ° sendbuf — адрес буфера с данными, ° count — число элементов (типа datatype) в буфере, ° datatype — тип данных каждого из элементов буфера, ° op — идентификатор операции (типа ИРТ Ор), которую нуж- но осуществить над пе~)есл;цпи.ю~и данными для получения результата в буфере recvbuf; см. в табл. 11.2 возмо>к значения этого аргумента, ° comm — коммуникатор, и выходной параметр (результат): ° recvbuf †указате на буфер. где требуется получить ре- зультат. Функция аналогична MPI Reduce, но результат образуется в бу- фере recvbuf во всех процессах группы comm, поэтому нет ар- гумента root. Если требуется выполнить свою собственную операцию над данными в функциях MPI Reduce u MPI Allreduce, то можно ~н пользовать функцию ИР1 Ор сгеа~е. Ее прототип: int MPI Op create (MPI User function +function, int commute, MPI Op ~ор); гд( входные параметры: ° function — указатель на пользовательскую функцию, зада- ницую операцию и имеющую тип MPI User Юunction, orlpe- д~ ленный как typedef void (MPI User function) (void + а, void + Ь, int + len, MPI Datatype + datatype); 
11.8. Коллективный обмен сообщениями 255 Эта функция выполняет операцию ор над элементами типа datatype массивов а и Ь длины +len и складывает результат в массив Ь: for (i = О; i & t; ~1 п; i Ь[х] = à[i] ор b[i]; ° commute — целое число, истинное, если операция коммутатив- на, и равное О иначе, и выходной параметр (результат): ° ор — идентификатор операции (типа MP I Op) . Полученный иден гификатор операции ор можно использовать в функциях MPIReduce cr MPI Allreduce. После окончания рабо- ты с созданной операцией необходимо освободить используемые для нее ресурсы с помощью функции ИР? Ор аггее. Ее прототип: int MPI Op free (MPI Op ~ор); где входной параметр: ° ор — идентификатор операции, возвращенный функцией MPIOp create, и выходной параметр (результат): ° op — устанавливается в МР1 ОР ИЖС Важным случаем коллективных обменов является запрос на завершение всех процессов, например, в случае ошибки в одном из них. Для этого можно использовать функцию MPI Abort. Ee прототип: int MPI Abort (MPI Comm comm, int errorcode); где входные параметры: ° comm — коммуникатор, описывающий группу задач, которую надо завершить, ° error code — код завершения (аналог аргумента функции exit). 
256 Глава 11. Интерфейс MPI (Message Passing Interface) Функция завершает все процессы в группе comm. В большинстве реализаций завершаются все запущенные процессы. 11.9. Пример MPl-программы, вычисляющей определенный интеграл Рассмотрим в качестве примера программу, вычисляющую определенпьш интеграл. Идея ускорения работы описана в раз- деле 1.2, с. 12. Простейшая реализация этой задачи приведе- на в разделе 1.5, с. 21. Каждый из процессов инициализиру- ет подсистему IPI с помощью MPI Init, получает свой но- мер и общее количество процессов посредством МР1 Соуп rank и MPI Comm size. Основная работа производится в функции process function по окончании которой процесс заканчивает работу с ilIPI с помощью MPI Finalize и завершается. Функция process function вычисляет свою часть интеграла с помощью функции integrate и прибавляет его к ответу total в процессе с номером 0 посредством MPI Reduce. Процесс с номером 0 перед окончанием своей работы выводит переменную total па экран. В этом разделе мы разовьем описанный выше пример так, чтобы входные данные считывались из файла. Этим будет зани- маться функция get data. В процессе с номером 0 она откры- вает указанный файл, считывает из него данные и закрывает. Затем эти данные рассылаются из процесса с номером 0 всем остальным процессам с помощью MPI Beast. Текст программы: ¹include <st io ¹include "mpi.h" ¹include "integral.h" /+ Получить данные +/ void get data (char + name, double +aptr, double +bptr, int +nptr, int myrank); int main (int argc, char ++argv) 
11.9. Пример M Pl-программы, вычисляющей интеграл 257 с int myrank; /+ ранг текущего процесса */ int р; /+ общее число процессов +/ double а; /+ левый конец интервала */ double b; /+ правый конец интервала +/ int n; /+ число точек разбиения +/ /+ длина отрезка интегрирования для текущего процесса~/ double len; /+ левый конец интервала для текущего процесса +/ double lосаl а; /+ правый конец интервала для текущего процесса */ double 1оса1 Ь; /+ число точек разбиения для текущего процесса +/ int local n; /+ значение интеграла в текущем процессе +/ double integral; double total; /+ результат: интеграл +/ /+ Начать работу с MPI +/ MPI Init (&a gc, &am /+ Получить номер текущего процесса в группе всех процессов +/ MPI Comm rank (MPI COMM WORLD, &am ;my ra /+ Получить общее количество запущенных процессов +/ MPI Comm size (MPI COMM WORLD, &amp /+ Получить данные +/ get data ("а.dat", &am ;a, &am ;b & len = (Ь — а) / р; local n = n / р; /+ Вычисляем отрезок интегрирования для текущего 9 — 4017 
258 Глава 11. Интерфейс MPI (Message Passing Interface) процесса +/ 1оса1 а = а + myrank + len; 1оса1 Ь = 1оса1 а + len; /+ Вычислить интеграл в каждом из процессов +/ integral = integrate (locala, localb, local n); /+ Сложить все ответы и передать процессу 0 +/ MPI Reduce (&integ al, &am ;t tal 1, MPI DOU LE, О, MPI COMM WORLD); /+ Напечатать ответ */ if (myrank == О) printf (" Integral from /lf to '/lf = /.181f(n", а, b, total); /+ Заканчиваем работу с MPI */ MPI Finalize (); return О; /+ Прочитать значения à, b, и п из файла name +/ void get data (char + паше, double ~а ptr, double ~Ь ptr, с int +nptr, int myrank) /+ Читаем данные в процессе 0 +/ if (myrank == О) & FILE ~~п = fopen (name, "r"); if (!in) & fprintf (stderr, "Error opening data file /в~п' name); +aptr = О.; ~Ьр~г = О.; 
11.10. Работа с временем 259 +nptr = 1; с else if (fscanf (in, "/lf/lf/d", à ptr, Ь ptr, n ptr) != 3) с fprintf (stderr, "Error reading data file /в~в", name); *aptr = О.; +bptr = О.; +nptr = i; fclose (in); & ) /+ Рассылаем данные из процесса О остальным */ MPI Beast (a ptr, 1, MPI DOUBLE, О, MPI COMM WORLD); MPI Beast (b ptr, 1, MPI DOUBLE, О, MPI COMM WORLD); MPI Beast (n ptr, 1, MPI INT, О, MPI COMM WORLD); ) Текст функции integrate приведен в разделе 1.1. 11.10. Работа с временем В разделе 10.4 мы вычисляли процессорное и астрономическое времена работы программы. Однако для МР1-приложения про- цессорное время не является показателем скорости работы, по- скольку оно не учитывает время передачи сообщений между процессами (которое может быть больше процессорного време- ни!). С другой стороны, и абсолютное значение астрономиче- ского времени может быть различным в разных процессах из-за несинхронности часов на узлах параллельного компьютера. По- этому MPI предоставляет свои функции работы с временем. Ъ 
260 Глава 11. Интерфейс MPI (Message Passing Interface) Узнать астрономическое время можно с помощью функ- ции MPI Wtime. Ее прототип: double MPI Wtime (); Она возвращает для текущего процесса астрономическое время в секундах от некоторого фиксированного момента в прошлом. Это время может быть, а может не быть синхронизированным для всех процессов работающей программы. Точность функции MPI Wtime можно узнать с помощью функции MPI Wtick. Ee прототип: double MPI Wtick (); Она возвращает частоту внутреннего таймера. Например, ес- ли счетчик времени увеличивается 100 раз в секунду, то эта функция вернет 10 2. На всех вычислительных установках, где используется MPI, разрешается запускать только один процесс на каждый процес- сор (иначе очень тяжело балансировать загруженность всех уз- лов параллельного компьютера). В такой ситуации астрономи- ческое время почти совпадает с временем работы задачи. Типичная процедура измерения времени работы программы в МР1 выглядит следующим образом: double t ° ° ° /+ Синхронизация всех процессов +/ MPI Barrier (MPI COMM WORLD); /+ Время начала +/ t = MPI Wtime (); /~Вызов процедуры, время работы которой надо измерить~/ ° ° ° /+ Синхронизация всех процессов ®/ MPI Barrier (MPI COMM WORLD); /+ Время работы +/ t = MPI Wtime () ° ° ° 
11.11. Пример MPI-программы, умножающей матрицу на вектор 261 11.11. Пример MPl-программы, вычисляющей произведение матрицы на вектор Рассмотрим задачу вычисления произведения матрицы на век- тор (см. раздел 10.4). Файлы проекта: ° main.с — ишщиализация данных и вывод результатов; ° matrices c, matrices h — исходный текст и соответствую- щий заголовочный файл для функций, работающих с мат- рицами. ° Makefilå — для сборки проекта. Файл main. c: Oinclude <stdio Oinclude <stdlib Oinclude "mpi.h" Oinclude "matrices h" /+ Получить данные +/ void get data (char + name, int +n ptr, int myrank); /+ Количество тестов (для отладки) */ redefine N TESTS 10 int main (int argc, char ++argv) int myrank; /* ранг текущего процесса */ int р; /* общее число процессов +/ int n; /* размер матрицы и векторов +/ double t; /+ время работы всей задачи +/ int Х1гз1» row, 1аз1» row, rows; int max rows; double +matrix; /+ матрица +/ double +vector; /+ вектор +/ 10 4017 
262 Глава 11. Интерфейс MPI (Message Passing Interface) double +result; /+ результирующий вектор +/ int i /+ Начать работу с MPI +/ MPI Init (&a gc, &am /+ Получить номер текущего процесса в группе всех процессов +/ MPI Comm rank (MPI COMM WORLD, &am ;my ra /+ Получить общее количество запущенных процессов +/ ИРТ Соппп яхье (MPI COMM WORLD, &amp /+ Получить данные +/ get data ("а.dat", &am ;n my ra /+ Первая участвующая строка матрицы +/ first row = n & t; myra first row /= р; /+ Последняя участвующая строка матрицы +/ last row = n + (my rank + 1); last row = last row / р — 1; /+ Количество участвующих строк матрицы +/ rows = last row — first row + 1; /+ Вычисляем максимальное количество строк на процесс~/ max rows = n / р + n /, р; /+ Выделение памяти под массивы +/ if (!(matrix = (double+) malloc (max rows + n + sizeof (double))) fprintf (stderr, "Not enough memory!(n"); MPI Abort (MPI COMM WORLD, 1); 
11.11. Пример MPl-программы, умножающей матрицу на вектор 263 if (!(vector = (double+) malloc (max rows + sizeof (double)))) с fprintf (stderr, "Not enough memory!(n"); MPI Abort (MPI COMM WORLD, 2); } if (!(result = (double+) malloc (max rows ~ sizeof (double)))) fprintf (stderr, "Not enough memory!(n"); MPI Abort (MPI COMM WORLD, 3); & /~ Инициализация массивов ~/ init matrix (matrix, n, first row, last row); init vector (vector, first row, last row); if (my rank == О) int 1; printf (" Matrix: (n"); printmatrix (matrix, n, first row, last row); printf ("Vector:~n"); print vector (vector, first row, last row); 1 = (max rows + n + 2 + max rows) + sizeof (double); printf (" Allocated '/d bytes ('/dKb or '/dMb) of memory per ргосевв~п", 1, 1 &g ;&g ; 10 1 & & /+ Синхронизация всех процессов +/ MPI Barrier (MPI COMM WORLD); /+ Время начала +/ t = MPI Wtime (); 
264 Глава 11. Интерфейс MPI (Message Passing Interface) & for (i = О; i ( N TESTS; i++) /+ Умножить матрицу на вектор для процесса с номером my rank из общего количества р +/ matrix mult vector (matrix, vector, result, n, first row, last row, my rank, р); printf (" Process '/d mult /d times(n", my rank, i); & /+ Синхронизация всех процессов +/ MPI Barrier (MPI COMM WORLD); /+ Время работы +/ t = MPI Wtime () — t; if (my rank == 0) & print vector (result, first row, last row); printf (" Total time = /1е~п", t); /+ Освобождаем память +/ аггее (matrix); free (vector); free (result); /+ Заканчиваем работу с MPI +/ MPI Finalize (); return О; void get data (char ~ паше, int +n ptr, int my rank) /* Читаем данные в процессе О +/ if (my rank == 0) 
11.11. Пример МР!-программы, умножающей матрицу на вектор 265 FILE +in = fopen (name, "r"); if (!in) с fprintf (stderr, "Error opening data file '/sKn", name); +nptr =0; & else if (fscanf (in, "'/d", n ptr) != 1) & fprintf (stderr, "Error reading data file '/я~в", name); +nptr = О; & fclose (in); & & /~ Рассылаем данные из процесса О остальныи ~/ ИРХ Всаяс (n ptr, 1, MPI INT, О, MPI COMM WORLD); & В функции main каждый из процессов инициализирует под- систему Ъ|Р| с помощью MPI Init получает свой номер my rank и общее количество процессов р посредством ИР1 Сопап гапК и ИРТСопып яхге. Затем значение размерности матрицы и с шты- ваетгя из файла функцией get data так, как описано в разде- ле 11.9. В каждом из процессов хранятся строки матрицы а и компоненты «екторо» b и с с номераъш и * my rank / р, ..., n + (my rank + 1) / р — 1. Поскольку эти блоки пересылаются между процессами, то па- мять в каждом из них выделяется иод блок с максимальным числом компонент maxrows = n / р + n '/, р. 
266 Глава 11. Интерфейс MPI (Message Passing Interface) Блоки инициализируются в каждом из процессов, затем про- цесс с номером 0 печатает свою часть. С помощью механизма, описанного в разделе 11.10, замеряется время работы функции matrix mult vector, вычисляющей компоненты произведения матрицы на вектор, имеющие индексы в диапазоне n + my rank / р, ..., n + (my rank + 1) / р — 1. Для целей отладки и более точного замера времени работы функция вычисления т~роизведения матрицы на вектор вызы- вается в цикле N TESTS раз. Заголовочный файл matrices.h: void init matrix (double + matrix, int n, int first row, int last row); void init vector (double + vector, int first row, int last row); void print matrix (double + matrix, int n, int first row, int last row); void print vector (double + vector, int first row, int last row); void matrix mult vector (double ~а, double ~Ь, double ~с, int n, int first row, int last row, int my rank,int р); Файл matrices c: Oinclude <stdio Oinclude "matrices.h" Oinclude "mpi.h" /+ Инициализация матрицы +/ void init matrix (double + matrix, int n, int first row, int last row) int double *а = matrix; 
11.11. Пример MPl-программы, умножающей матрицу на вектор 267 for (i = first row; i &l ;= l st r w; i for (j = 0; j & t; n; j & ~(а++) = (i- & t; j е /+ Инициализация вектора +/ void init vector (double + vector, int first row, int last row) 1П~; 1; double *Ь = vector; for (i = first row; i &l ;= l st r w; i ~(Ь++) = 1.; & redefine ОМАХ 6 /+ Вывод матрицы +/ void printmatrix (double + matrix, int n, int first row int last row) int i, int rows = last row — first row + 1; int mi = (rows & t N A N A : row int mj = (n & t N A N A : for (i = 0; i & t; i; i & for (j = 0; j & t; j; j printf (" '/12.61f", matrix[i + n + j]); printf ("~п"); 
268 Глава 11. Интерфейс MPI (Message Passing Interface) /+ Вывод вектора +/ void print vector (double + vector, int first row, с int last row) 1nt int rows = last row — first row + 1; int m = (rows & t N A N A : row for (i = О; i ( m; i++) printf (" /12.61f", vector[i]); printf ("~п"); /+ Умножить матрицу а на вектор Ь, с = ab для процесса с номером myrank из общего количества р +/ void matrix mult vector (double ~а, double ~Ь, double ~с, int n, int first row, int last row, int myrank, int p & int i, j, К, 1; int rows = last row — first row + i; 1п1 ' maxr ows; int first row k, last row k, rows k; double s; int dest, source, tag = О; MPI Status status; /+ Вычисляем максимальное количество строк на процесс~/ max rows = n / р + n /, р; /+ Обнуляеи результат +/ for (i = 0; i & t; ro s; i c[j] = О.; /+ Вычисляем источник и получатель для текущего 
11.11. Пример MPI-программы, умножающей матрицу на вектор 269 процесса */ source = (myrank + 1) '/ р; if (myrank == О) dest = р — i; else dest = myrank — 1; /+ Цикл IIQ блокам +/ & for (1 = 0; 1 & t; р; 1 /+ Номер процесса, от которого был получен вектор~/ К = (myrank + 1) / р; /+ Первая участвующая строка матрицы в процессе К~/ first row k = n + К; first row k /= р; /+ Последняя участвующая строка в процессе К +/ last row k = n + (k + 1); last row k = last row k / р — 1; /+ Количество участвующих строк в процессе К +/ rows k = last row k — first row k + i; /+ Уиножаеи прямоугольный блок матрицы а в строках first row ... last row и столбцах first row k ... last row k на вектор b, соответствующий компонентам first row k ... first row k +/ for (i = 0; i & t; ro s; i for (s = О., j = 0; j & t; r ws k; j s += a[i + n + j + first row k] + b[j] c[i] += s; & /+ Пересылаем вектор Ь процессу dest и получаем его от source +/ 
270 Глава 11. Интерфейс MPI (Message Passing Interface) MPI Sendrecv replace (Ь, max rows, MPI DOUBLE, & dest, tag, source, tag, MPI COMM WORLD, &stat & Функция matrix mult vector получает указатели на строки матрицы а и компоненты векторов b и с с номерами n + my rank / р, ..., n + (my rank + 1) / р — 1 и вычисляет указанные компоненты ответа с — произведения матрицы а на вектор Ь. При этом память под матрицы и векто- ры выделена для хранения max rows = n / р + n '/, р компонент (строк матрицы пли элементов вектора). Обозначим А = (а; ), i т = О, 1,..., и — 1 — матрица А, Ь = (b;), i = 0,1,...,n — 1, с = (с;), i = 0,1,...,n — 1 — векторы b и с, m = my rank. Функция matrix mult vector в цикле по t = 0, 1,..., p — 1 вычисляет вектор, являющийся произведением блока матрицы А, стоящего в строках ит/р,..., n(m + 1)/р — 1 (11.1) и столбцах (nm/р+ 1) (mod р),..., (n(m + 1)/р — 1 + 1) (mod р), (11.2) и вектора, образованного компонентами вектора b с номера- ми (11.2). Этот вектор прибавляется к вектору, образованно- му компоненгами вектора с с номерами (11.1). Затем вектор, образованный компонентами вектора b с номерами (11.2), пере- сылается процессу с номером (m — 1) (mod р), а на его место помещается вектор, полученный от процесса с номером (m+ 1) (1т1ос1 р), т. е. осуществляется циклическая пересылка доступной каждому процессу части вектора b длиной не более max rows. 
11.11. Пример MPI-программы, умножающей матрицу на вектор 271 С а b b 1 1 1 2 += 2 х 2 3 3 3 а) умножение соответствующих блоков б) пересылки между в соответствующем процессе (I = 0) процессами (I = 0) С а Ь b 1 1 1 2 += 2 х 2 3 3 3 в) умножение соответствующих блоков г) пересылки между в соответствующем процессе (I = 1) процессами (I = 1) С а b b 1 1 1 2 += 2 х 2 3 3 3 д) умножение соответствующих блоков е) пересылки между в соответствующем процессе (1 = 2) процессами (I = 2) Рис. 11.1. Организация вычислений и пересылки данных при умножении матрицы на вектор Иллюстрация этого процесса при р = 3 приведена на рис. 11.1: a) в каждом из процессов 1, 2, 3 (здесь мы для удобства нумеру- ем их, начиная с 1) вычисляем произведения блоков матрицы а и вектора Ь, отмеченных на рисунке соответствующей циф- рой, и прибав.пяем их к результату в векторе с; б) циклически пересылаем блоки вектора b; в) перемножаем соответствующие блоки матрицы а и вектора Ь и прибавляем их к результату в векторе с; 
272 Глава 11. Интерфейс MPI (Message Passing Interface) г) циклически пересылаем блоки вектора b; д) перемножаем соответствующие блоки матрицы а и вектора b и прибавляем их к результату в векторе с, получая при этом окончательный результат; е) циклически пересылаем блоки вектора b, восстанавливая его первоначальное состояние. Файл Makef ile: NAME = вр~пш11» DEBUG СС = mpicc -с LD = mpicc CFLAGS = $(DEBUG) -W -Wall LIBS = -lm LDFLAGS = $(DEBUG) OBJS = main.o matrices.o all : $(NAME) $(NAME) : $(OBJS) $(LD) $(LDFLAGS) $ $(LIBS) -o $6 .с.о: $(CC) $(CFLAGS) $& t; -о clean: rm -f $(ОВОРЯ) $(NAME) main.o main.c matrices.h matrices.o matrices.c matrices.h 
11.12. Дополнительные функции коллективного обмена 273 11.12. Дополнительные функции коллективного обмена сообщениями для работы с массивами В программе, вычисляющей произведение матрицы на вектор (см. раздел 11.11), ни один из процессов не хранит массивы полностью, а в каждый момент времени работает только с их ча; стями. Такая МР1-программа при запуске на установке с общей памятью не использует для хранения массивов памяти больше, чем ее mtiltithread-аналог (см. раздел 10.4). Если некотороиу процессу потребовался весь массив целиком (например, для вы- вода в процессе с номером 0), то можно переслать ему части массива от других процессов с помощью функций попарного обмена сообщениями. Однако удобнее и быстрее это сделать по- средством функции MPI Gather. Ее прототип: int MPI Gather (void +sendbuf, int sendcount, MPI Datatype sendtype, void +recvbuf, int recvcount, МР? ОаСаСуре recvtype, int root, MPI Comm comm); где входные параметры: ° sendbuf — адрес буфера с посылаемыми данными, ° sendcount — число элементов (типа sendtype) в буфере sendbuf, ° sendtype — тип данных каждого из элементов буфера sendbuf, ° recvbuf — адрес буфера с данными, где следует разместить полученное сообщение (используется только в процессе с но- мером root группы comm), ° recvcount — максимальное число элементов (типа recvtype) в буфере recvbuf (используется только в процессе с номером root группы comm), ° recvtype — тип данных каждого из элементов буфера recvbuf (используется только в процессе с номером root группы comm), 
274 Глава 11. Интерфейс MPI (Message Passing Interface) ° root — ранг (номер) получателя в коммуникаторе (группе) с omm) ° comm — коммуникатор, и выходной параметр (результат) в процессе с номером гсов группы comm: ° recvbuf — указатель на полученные данные. Эта функция: ° в процессе с номером root группы comm — принимает recvcount данных типа recvtype от всех остальных процес- сов группы comm и размещает их последовательно в буфере recvbuf: вначале данные от процесса с номером О группы comm, затем дашiые от процесса с номером 1 группы comm и т. д.; от самого себя процесс данные, конечно, не прини- мает, а просто копирует их из sendbuf в соответствующее место recvbuf; ° в процессе группы сопл с номером, отличным от root, — по- сылает sendcount данных типа sendtype из буфера sendbuf процессу с номером root. Эта функция должна быть вызвана во всех процессах груп- пы comm с одинаковыми значениями для аргументов root H comm. Также в большинстве случаев требуется, чтобы значения аргументов sendcount u sendtype были одинаковыми во всех процессах группы comm, причем равными значениям recvcount и recvtype соответственно. Если требуется, чтобы результат, полученный посредством функции MPI Gather, стал известен не только одному процессу (с номером root в группе comm), a, всем процессам группы, то можно использовать MPI Beast, но более эффективным реше- нием является функция MPI Allgather (ср. ситуацию с функ- циями MPI Reduce u MPI Allreduce). Ее прототип: int MPI Allgather (void +sendbuf, int sendcount, MPI Datatype sendtype, void +recvbuf, int recvcount, MPI Datatype recvtype, 
11.12. Дополнительные функции коллективного обмена 275 MPI Comm comm) где входные параметры: ° sendbuf — адрес буфера с посылаемыми данными, ° sendcount — число элементов (типа sendtype) в буфере sendbuf, ° sendtype — тип данных каждого из элементов буфера sendbuf, ° recvbuf — адрес буфера с данными, где следует разместить полученное сообщение, ° recvcount — максимальное число элементов (типа recvtype) в буфере recvbuf, ° recvtype — тип данных каждого из элементов буфера recvbuf, ° с omm — коммуникатор, и выходной параметр (результат): ° recvbuf — указатель на полученные данные. Функция аналогична MPI Gather, но результат образуется в бу- фере recvbuf во всех процессах группы comm, поэтому нет ар- гумента root. МР1 предоставляет функцию MPI Scatter, в некотором смысле обратную к MPI Gather. Ее прототип: int MPI Scatter (void +sendbuf, int sendcount, MPI Datatype sendtype, void +recvbuf, int recvcount, MPI Datatype recvtype, int root, MPI Comm comm) где входные параметры: ° sendbuf — адрес буфера с посылаемыми данными (использу- ется только и процессе с номером root группы comm), ° sendcount — число элементов (типа sendtype) в буфере sendbuf (используется только в процессе с номером root группы comm), 
276 Глава 11. Интерфейс MPI (Message Passing Interface) ° sendtype — тип данных каждого из элементов буфера sendbuf (используется только в процессе с номером root группы comm), ° recvbuf — адрес буфера с данными, где следует разместить полученное сообщение, ° recvcount — максимальное число элементов (типа recvtype) в буфере recvbuf, ° recvtype — — тип данных каждого из элементов буфера recvbuf, ° root — ранг (номер) отххраххххтеххя в коъхмуникаторе (группе) с omm) ° СОП1П1 — КОММУН И KBTOP) и выходной ххарачетр (результат): ° recvbuf -- указатель на полученные даш~ые. Эта функция: ° в процесс» с. номером root группы comm-- посылает первые sendcount элеысххтов типа sendtype в буфере sendbuf про- цсссу с номером О группы comm. следующие sendcount эле- мс ххтов х ххпа sendtype в буфере sendbuf — тхроцессу с номе- ром 1 груххххх х comm и т. д.: сам себе процесс данные, конечно, не посылает а просто копирует HY из соответствующего ме- ста sendbuf в recvbuf; ° в процессе групш r comm с номером, отли иш.~м от root, -- при- нимает recvcount данных типа recvtype от процесса с но- мером root и складыв хст и буфер recvbuf. Эта функция должна быть вызвана во всех процессах груп- пы comm с одинаковыми значениями для аргументов root u comm. Также в бо.|хьихххххстхзс случаев требуется чтобы значения аргументов recvcount u recvtype были одинаковыми во всех процессах групш1 comm, причем равными значениям sendcount и sendtype соответственно. 
11.13. Пересылка структур данных 277 11.13. Пересылка структур данных Рассмотрим задачу пересылки между процессами определенной пользователем структуры данных на примере двух наиболее ча- сто встречающихся задач: 1. пересылка разнородных данных, локализованных в одном блоке памяти (например, пересылка определенного с помо- щью конструкции struct языка С типа данных); 2. пересылка распределенных в памяти однородных данных (например, пересылка столбца матрицы в языке С, где мно- гомерные массивы хранятся по строкам). Описанные ниже способы пересылки можно комбинировать, об- разуя, например, методы пересылки распределенных разнород- ных данных и т. д. 11.13.1. Пересылка локализованных разнородных данных Пусть между процессами требуется пересылать определенную пользователем структуру данных, например, следующего вида: с typedef struct int n; char s [20]; double v; USERTYPE; Для этой задачи можно предложить следующие решения: 1. один раз создать новый MPI-тип, соответствующий C типу USERTYPE, и использовать его в функциях обмена сообще- ниями; 2. при каждой пересылке процесс — отправитель запаковывает объект типа USERTYPE в один объект типа MPIPACKED и пе- ресылает ero процессу — получателю, который его распаковы- вает; 
° I 278 Глава 11. Интерфейс MPl (Message Passing Interface) 3. при любом обмене объект типа USERTYPE рассматривает- ся как массив длины sizeof (USERTYPE) элементов типа MPI BYTE (это решение работает только на однородных вы- числительных установках). Создание нового типа данных. При создании нового ти- па данных подсистеме MPI необходимо указать спецификацию этого типа следующего вида: 1(со, do, < ), ( q, t, <t ,. ., (с 1, d t, 1)), где ° п (типа int) — количество элементов базовых типов в новом типе; ° c; (типа int) — количество элементов типа t; в i-M базовом элементе нового типа; ° d; (типа MPI Aint, «address int») — смещение i-го базового элемента от начала объекта; ° t; (типа MPI Datatype) — тип i-го базового элемента. Например, для С-типа USERTYPE спецификация MPI-типа на 32- битной вычислительной установке будет иметь вид: 3, 1(1, О, MPI INT), (20, 4, MPI CHAR)) (1, 24, MPI DOUBLE)) . Использование типа MPI Aint (равного int или long int) поз- воляет не зависеть от выбранного в языке С размера типа int на 64-битных компьютерах. Получить адрес любого объекта в тер- минах типа MPI Aint можно с помощью функции MPI Address. Ее прототип: int MPI Address (void +location, MPI Aint +address); где входной параметр: ° location — адрес объекта в терминах языка С, и выходной параметр (результат): ° address — адрес объекта как MPI Aint. 
11.13. Пересылка структур данных 229 Функция MPI Address не связана с межпроцессным обменом и выполняется локально, в одном процессе. Построением нового типа по спецификации вида (11.3) зани- мается функция MPI Type struct. Ee прототип: int MPI Type struct (int count, int blocklens[], MPI Aint displacements[], MPI Datatype oldtypes[], MPI Datatype +newtype); где входные параметры: ° count — количество элементов в создаваемом типе, т. е. и в (11.3); ° blocklens — массив длины count, содержащий количество элементов базового типа в каждом поле создаваемого ти- па, т. е. blocklens [] = (со, с1,..., с 1) в терминах описания (11.3); ° displacements — массив длины count, содержащий смеще- ние элементов базовых типов (полей) от начала объекта со- здаваемого типа, т. е. displacements [] = (do, dy,..., d„~) в терминах описания (11.3), ° oldtypes — массив длины count, содержащий типы базовых элементов (полей) создаваемого типа, т. е. oldtypes [] = (tp, ty,..., t„~ ) в терминах описания (11.3); и выходной параметр (результат): ° newtype — идентификатор созданного типа. Функция MPI Type struct не связана с межпроцессным обме- ном и выполняется локально, в одном процессе. Это связано с тем, что мы можем строить новый тип не только для пересылки 
280 Глава 11. Интерфейс MPI (Message Passing Interface) данных этого типа между процессами, а для использования ero в качестве одного из базовых типов Ф, в (11.3). Созданный тип нельзя сразу использовать для передачи со- общений между процессами. Перед этим необходимо вызвать функцию MPI Type commit, которая создает для указанного в качестве аргумента типа все структуры данных, необходимые для его эффективной пересылки. Ее прототип: int MPI Type commit (MPI Datatype +newtype); где единственный входной и выходной аргумент newtype — ука- затель на созданный тип. Эта функция должна быть вызвана всеми процессами, которые будут обмениваться между собой объектами типа newtype. Если построенный тип не требуется для дальнейшей работы программы, то необходимо освободить все выделенные в момент его построения ресурсы с помощью функции MPI Type f ree. Ee прототип: int MPI Type free (MPI Datatype +newtype); где единственный входной и выходной аргумент newtype — ука- затель на созданный тип, который после выполнения этой функ- ции становится равным MPI DATATYPE NULL. Если в качестве ар- гумента этой функции передать стандартный MPI-тип (напри- мер, любой из табл. 11.1), то это приведет к ошибке. Теперь мы можем привести законченный пример построения MPI-типа, соответствующего С-типу USERTYPE: /+ Объект исходного типа USERTYPE +/ USERTYPE t = 4 1, 4'Н', 'е', '1', '1', 'о', 0], 2. ].; /+ Количество элементов в каждом поле USERTYPE +/ int t с[3] = ( 1, 20, 1 ); /+ Смещение каждого поля относительно начала структуры USERTYPE: предстоит вычислить ~/ MPI Aint t d[3]; /4 MPI-тип каждого поля в USERTYPE 4/ MPI Datatype t t[3] = TEMPI INT, MPI CHAR, MPIDOUBLE); 
11.13. Пересылка структур данных 1 /+ Результат: MPI-тип, соответствующий USERTYPE +/ MPI Datatype MPI USERTYPE; /+ Переменные, используемые для вычисления смещений +/ MPI Aint start address, address; /+ Вычисляем адрес начала структуры +/ MPI Address Щ , &s art addre /+ Вычисляем смещение первого поля +/ MPI Address (&a p; n, & t d[0] = address — start address /+ Вычисляем смещение второго поля +/ MPI Address (& .s, &а t d[1] = address — start address; /+ Вычисляем смещение третьего поля +/ MPI Address (& .v, & t d[2] = address — start address; /+ Создаем MPI-тип данных с вычисленными свойствами +/ MPI Type struct (3, t c, t d, t t, &amp MPI USERTY /+ сделаем его доступным при обмене сообщениями +/ MPI Type commit (&amp MPI USERTY /+ Тип MPI USERTYPE можно теперь использовать наряду со стандартными типами */ ° ° ° /+ Например: разослать данные из процесса 0 остальным*/ MPI Beast (&am ;t 1, MPI USERT PE О, MPI OMM WOR ° ° ° /+ Удалить тип, если он больше не понадобится +/ MPI Type free (&amp MPI USERTY Отметим, что процедура построения нового типа является достаточно длительной и оправдана лишь в том случае, если этот тип будет использоваться многократно. 11 4017 
282 Глава 11. Интерфейс MPI (Message Passing Interface) Запаковка/распаковка данных может применяться к лю- бым данным, необязательно локализованным в одном блоке па- мяти. При запаковке данные последовательно записываются подряд в указанном пользователем буфере, а, затем пересыла- ются как один объект типа ИР1 РАСКЕЭ. При распаковке данные последовательно извлекаются из этого буфера. Запаковать данные можно с помощью функции MPI Pack. Ее прототип: int MPI Pack (void +pack data, int pack count, MPIDatatype pack type, void ~ЬиХХег, int buffer size int +position, MPI Comm comm) где «ходш ~ñ параметры: ° pack data — адрес пакуемых дашиных, ° pack count — количество пакуемых элементов (каждый типа pack type), ® pack type — - тип каждого из пакуемых pack count данных, ° buffer адрес буфера, в который склв..чывается запакован- ный рсзультат, ° buffersize — размер буфера buffer, ° position — адрес целого числа., задающего смещение в бай- тах первой свободной позиции в buffer, ° collllll.--коммуникатор, идентифицирующий группу процес- сов, которая будет в последующем использовать buffer для обмена данными, и выходные параметры (результаты): ° ЬиНer — буфер, в котором образуется запакованный резуль- ТО. Т ) ° position — указывает уже на новое значение первой свобод- ной позиции. в буфере. ~ знать размер б~ фера, необходимого для запаковки данных, можно с помощью функции MPI Pack size. Ее прототип: int MPI Pack size (int pack count, 
11.13. Пересылка структур данных 283 MPI Datatype pack type, MPI Comm comm, int +size); где входные параметры: ° pack count — количество пакуемых элементов (каждый типа pack type), ® pack type — тип каждого из пакуемых pack count данных, ° comm — коммуникатор, идентифицирующий группу процес- сов, которая будет в последующем использовать запакован- ные данные, и выходной параметр (результат): ° яхье — размер буфера (в байтах), достаточный для запаков- кп pack count элементов типа pack type. Запакованные данные можно переслать/получить лю- бой функцией передачи с ообщений, указав в качестве типа MPI PACKED, а в качестве размера -- buffer size. Если ис- пользуется попарный обмен сообщениями, то можно избежать передачи неиспользованной части буфера, указав в процессе— отправителе в качестве длины значение position после по- следнего вызова MPI Pack, a, в процессе — получателе — значение buffer size (поскольку значение position известно только отправителю). Распаковать данные можно с помощью функции ИР? УпрасЫ. Ее прототип: int MPI Unpack (void +buffer, int buffer size, int +position, void +unpackdata, int unpack count, NPI Datatype unpack type, MPI Comm comm) г,цс входные параметры: ° buffer — адрес буфера, из которого берется запакованный результат, ° buf f ег вize — размер буфера buf f er, ° position — адрес целого числа. задающего смещение в бай- тах позиции в buffer, с которой на-итнаетси распаковка, 
284 Глава 11. Интерфейс MPI (Message Passing Interface) ° unpack data — адрес буфера, в который следует разместить распаковываемые данные, ® unpack count — количество распаковываемых элементов (типа unpack type). ® unpacktype — тип каждого из unpack count распаковывае- мых данных, ° comm — коммуникатор, идентифицирующий группу процес- сов, которая использовала ЬиХХer для обмена данными, и выходные параметры (результаты): ® unpack data — распакованные данные, ° position — указывает уже на новое значение позиции в бу- фере, с которой начнется распаковка очередного элемента. Приведем закопченный пример пересылки структуры дан- ных типа USERTYPE от процесса О всем остальным процессам: /+ размер буфера +/ ¹define BUFSIZE 128 char buffer[BUF SIZE]; /+ буфер +/ int position; /+ текущая позиция в буфере +/ /+ Объект исходного типа USERTYPE +/ USERTYPE ~; & if (myrank == О) /+ Процесс 0 заносит данные в объект t +/ t.n= 1; strcpy (t.s, "Hello" ); t.v = 2.; /+ Запаковываеи объект t в процессе 0 +/ position = 0; /~ складываеи в начало буфера +/ /+ Пакуеи первое поле +/ MPI Pack (& .n 1, MPI NT, buf er, BUF S &posit on, MPI OMM WOR 
11.13. Пересылка структур данных 285 /* Пакуем второе поле, position указывает на место, где следует разместить результат +/ MPI Pack (& .s, 20, MPI C AR, buf er, BUF S &posit on, MPI OMM WOR /+ Пакуем третье поле, position указывает на место, где следует разместить результат +/ MPI Pack (& .v 1, MPI DOU LE, buf er, BUF S & &posit on, MPI OMM WO /+ Рассылаем буфер из процесса 0 всем остальным */ MPI Beast (buffer, BUF SIZE, MPI PACKED, О, MPI COMM WORLD); if (myrank != 0) & /+ Распаковываем объект t в остальных процессах +/ position = 0; /~начинаем с первой позиции буфера~/ /+ Распаковываем первое поле +/ MPI Unpack (buffer, BUF SIZE, &posit on, & mp MPI INT, MPI COMM WORLD); /~ Распаковываем второе поле, ров1~1оп указывает на место, откуда следует начинать +/ MPI Unpack (buffer, BUF SIZE, &posit on, & mp; MPI CHAR, MPI COMM WORLD); /+ Распаковываем третье поле, position указывает на место, откуда следует начинать */ MPI Unpack (buffer, BUF SIZE, &posit on, & mp MPI DOUBLE, MPI COMM WORLD); & Отметим, что использование MPI Pack. MPI Unpack для пе- редачи информации может приводить к многократному копи- рованию данных: вначале они помещаются в буфер функци- ей ИР1 РасЫ, затем,при отправке, — во внутренний буфер МР1 
286 Глава 11. Интерфейс MPI (Message Passing Interface) для формирования сообщения. При получении сообщения дан- ные копирук> ся из внутренн го буф ра Р в бу ер функ ИР1 УпрасЫ, затеи уже они попадают на свое место. Поэтому этот способ можно рекомендовать лишь для единичных обме- нов. Некоторые реализации 1МР1 для уменьшения лишних пере- сылок данных обрабатывают тип MPI PACKED специальным об- разом: данные этого типа не буферизируются подсистемой IPI, а, сам buffer в MPI Pack, MPI Unpack используется для форми- рова!!!!я сообщения. Это снижает накладные расходы при таком способе обмена. Пересылка неформатированных данных. МР1-подсисте- Ма пересылки сообщений работает с б.!окаи!! да!!и!,!х, о которых ей нужно знать лишь разл!ер блока. Содержимое б.!ока. (т. е. тип данных элементов) при пересылке не важен. Г!!и данных ста- EEQBEET(я Р~я ж н ы м л и Lll ь тол ьео и Ослс дос13 В еи соо~эщРния, ко- гда данные в,[во!! !ном формате процесса-отправителя должш.! быть представлены в двоичном формате !!роцесса — получателя. Если эти процессы работают на процессорах раз!!ой арх!!текту- pbt то дво1!~!!!ы!! формат да!!!!ых ъЪоюкет от1!!!'!ат!ъсг! (см. Гла— ву 4). КаК отл!ечалось в главе 4 все соврс.мен!!ые процессоры имеют единый формат для целочисле!!!!ых данных и данных с плавающей точкой. Отлн иться может лшпь способ н~исра- !!!it! ttatt toB B этих '[а!!!!!~!х, !! тогда ltocjtp ~~оста!!I&lt tt сообще!! необходимо преобразование двоичных да!!!!! !х. Ради поддержки таких гетерогенных в! р!!!слительных установок (т. е. составлен- ных нз процессоров разной арх!!тектуры) система Х!Р1 требует указа!!!!я типа к!!ждого перес!!лчемого объекта. Однако оказалось что разработка МР1-программ являет- ся весьма сложной, tt !тодавля!ощее больппшство программно- го обеспе !е!!!ы строится при ряде упрощающих его разработку предположений. Основным из ННх является предположение о ра- венст~зе производительности всех процессоров на которы& t; полняется МР1-программа. Собственно, это предполагаем и мы 
11.13. Пересылка структур данных 287 во всех примерах, разделяя вычислительную работу поровну между всеми процессами. Следовательно, если, например, один процессор имеет меньшую производительность, чем остальные, то вся программа будет работать именно с его скоростью, по- скольку остальные процессы будут ожидать работающий на нем процесс в точках обмена сообщениями. Поэтому в настоящий момент подавляющее большинство параллельных ЭВЪ~! исполь- зуют не просто процессоры одной архитектуры, а одинаковые процессоры (т. е. одной архитектуры с равной тактовой часто- той . Рассмотрим задачу пересылки структуры данных на одно- родной в слабом смысле вычислительной установке (т. е. ис- пользованы процессоры одной архитектуры, а их частота нам не важна). Эту структуру можно переслать как единый блок элементов типа MPIBYTE размером, равным размеру структуры в байтах. Приведем законченный пример пересылки структуры данных типа USERTYPE от процесса О всем остальным процессам: /+ Объект исходного типа USERTYPE +/ USERTYPE ~; & if (myrank == О) /e Процесс 0 заносит данные в объект t */ t.n= 1; strcpy (t.s, "Hello" ); t.v = 2.; /+ Рассылаем структуру из процесса 0 всем остальным +/ MPI Beast (&am ;t, si eof (USERTY E), MPI B TE MPI COMM WORLD); Этот текст является самым коротким и быстродействующим из всех приведенных выше, но работает не на всех вычнслительны:~ установках. 
288 Глава 11. Интерфейс MPI (Message Passing Interface) 11.13.2. Пересылка распределенных однородных данных Рассмотрим подробнее работу с двумерными массивами. Если мы храним и х и массив а как вектор размера и: double + а; /+ массив и х n +/, то переслать k-ю строку этого массива от процесса src процессу dest можно посредством if (my rank == src) MPI Send (а + k + n, n, MPI DOUBLE, dest, taa;, MPI COMM WORLD); else if (my rank == dest) MPI Recv (а + k + n, n, MPI DOUBLE, src, taa;, MPI COMM WORLD, &sta Если требуется переслать k-й столбец то можно скопировать ero элементы в вектор длины п и затем переслать его; процесс- получатель принимает данные также во вспомогательный век- тор и затем заносит их на место k-ro столбца. Однако может получиться, что данные при отправке будут копироваться мно- гократно: вначале из столбца матрицы в наш вспомогательный вектор, затем — во внутренний буфер МР1 для формирования сообщения. То же, но в обратном порядке, происходит при по- лучении сообщения. Если тане пересылки происходят часто то имеет смысл создать новый тип МР1 для столбца матрицы. Это можно сделать с помощью функции MPI Type struct, как опи- сано в разделе 11.13.1. Но стандарт 5IPI предоставляет для этого случая более удобную функцию MPI Type vector. Ee прототип: int MPI Type vector (int count, int blocklen, int stride, MPIDatatype oldtype, MPI Datatype ~newtype); где входные параметры: ° count — количество элементов в создаваемом векторном ти- пе, 
11.13. Пересылка структур данных 289 ° blocklen †дли одного элемента (в единицах типа oldtype) в создаваемом векторном типе, ° stride — расстояние между элементами (в единицах типа oldtype) в создаваемом векторном типе, ° oldtype — базовый тип элементов в создаваемом векторном типе, и выходной параметр (результат): ° newtype — идентификатор созданного векторного типа. Функция создает новый тип данных: вектор длины count, эле- ментами которого являются массивы длины blocklen объектов типа oldtype; расстояние между элементами — stride объектов типа oldtype. Например, тип, соответствующий столбцу и х и матрицы, можно создать следующим образом: MPIDatatype columnt; MPI Type vector (n, 1, и, MPI DOUBLE, &co umn Созданный тип нельзя сразу использовать для переда- чи сообщений. Перед этим необходимо вы:звать функцию MPI Type commit, см. раздел 11.13.1: MPI Type commit (&co umn После этого переслать k-й столбец массива а от процесса src процессу dest можно посредством if (my rank == src) MPI Send (а + k, 1, column t, dest, tag, MPI COMM WORLD); else if (my rank == dest) MPI Recv (а + k, 1, column t, src, tag, MPI COMM WORLD, &sta Если построенный векторный тип не требуется для дальней- шей работы программы, то необходимо освободить все выде- ленные в момент его построения ресу.рсы с помощью функции MPIType free. 
290 Глава 11. Интерфейс MPI (Message Passing Interface) Напомним, что со:здание нового типа — достаточно дорого- стоящая операция и к ней имеет смысл прибегать, только если тип будет использоваться многократно. 11.14. Ограничение коллективного обмена на подмножество процессов Пусть нам требуется многократно пересылать сообщения между всеми процессами некоторой подгруппы всех процессов. Можно использовать функции попарного обмена сообщениями, но более эффективным является использование функций коллективного обме ta, (см. раздел 11.8) ограниченных на эту подгруппу. Для этого нам необходимо создать новый коммуникатор. С каждым коммуникатором (т. е. идентификатором группы процессов) связана собственно группа — список процессов, вхо- дящих в группу. Структура этого объекта зависит от реализации МР1, и потому недоступна для непосредственного использова- ния в прикладных программах. Группа имет тип MPI Group u может быть получена по коммуникатору с помощью функции MPICommgroup. Ее прототип: int MPI Comm group (MPI Comm comm, MPI Group ~~гоир); где «ходной параметр сопл — коммуникатор, и выходной пара- метр (результат) group — группа. Создать новую гру.ппу, вклю- чающую выбранное подмножество процессов из существующей группы (или все, но перенумерованные lIo-другому), можно с помощью функции MPI Group incl. Ее прототип: int MPI Group incl (MPI Group group, int n, int + ranks, MPI Group +newgroup); где входныс параметры: ® group — существующая группа, ° n — количество элементов в массиве ranks (и тем самым раз- мер новой группы), 
11.14. Ограничение коллективного обмена на часть процессов 291 ° ranks — номера процессов из старой группы, которые войдут в новую, и выходной параметр (результат): ° newgroup — созданная группа, процесс номер К в ней имеет номер ranks [kj в группе group, где k = О, 1,..., n — 1. Создать новый коммуникатор, соответствующий созданной группе, можно с помощью функции MPI Comm create. Ее про- тотип: int MPI Comm create (MPI Comm comm, MPI Group newgroup, MPI Comm +newcomm); где входные параметры: ° comm — существующий коммуникатор, ° пещгоир — группа, являющаяся подмножеством группы с идентификатором comm, и выходной параметр (результат): ° newcomm — созданный коммуникатор, состоящий из процес- сов, входящих в comm и группу newgroup. Например, создать коммуникатор для коллективного обмена между процессами с четным номером можно с помощью следу- ющего фрагмента (здесь р — размер MPI COMM WORLD): MPI Group group world; MPI Group group even; MPIComm comm even; int + ranks; int size; /+ размер новой группы +/ size = (р + р / 2) / 2; ranks = (int+) таllос (size + sizeof (int)); if (!ranks) ... /+ обработка ошибки +/ for (i = О; i & t; si e; i ranks[i] = 2 + 
292 Глава 11. Интерфейс MPI (Message Passing Interface) MPI Comm group (MPI COMM WORLD, &g oup wor MPI Group incl (group world, size, ranks, &g oup ev MPI Comm create (MPI COMM WORLD, group even,& omm ev После окончания работы с созданным коммуникатором его необходимо удалить с помощью функции MPI Comm free. Ee прототип: int MPI Comm free (MPI Comm comm); где входной и одновременно выходной параметр comm — удаляе- мый коммуникатор. 11.15. Пример MPl-программы, решающей задачу Дирихле для уравнения Пуассона Рассмотрим решение задачи Дирихле для уравнения Пуассо- на, (10.4) в двумерной области (см. раздел 10.6). Используем алгоритм, описанный в разделе 10.6. Мы используем такое же, как в разделе 11.11, разделение (nq + 1) х (nq + 1) матриц и, f, r между процессами. Именно, процесс с номером my rank из общего количества р работает со строками матриц с номерами (n1+1)+myrank / р, ..., (n1+1)+(my rank + 1) / р — 1, т. е. с блоком rows x (n2+1), где rows = (ni+1)+(myrank + 1) / р — (ni+1)+myrank / р. Поскольку в силу выражения (10.7) для оператора А при вы- числении i-й строки матрицы Аи (i = 1,2,..., ит — 1) требуется знать предыдущую и последующую строки, то в процессах с но- мером my rank ) 0 мы будем хранить предыдущую к первой строку, т. е. строку с номером (n1 + 1) + my rank / р — 1, а в процессах с номером my rank ( р — 1 мы будем хранить следующую за последней строку, т. е. строку с номером 
11.15. Решение задачи Дирихле для уравнения Пуассона 293 (n1 + 1) + (my rank + 1) / р. Следовательно, распределение данных между процессами будет следующим: ° в первом процессе (my rank = 0) находится блок размером (rows + 1) х (n2 + 1), составленный из строк с номерами (n1+1)+myrank / р, ..., (n1+1)+(myrank+1) / р, ° в «средних» процессах (О & t; my r n lt; р Ђ” 1) нах дитс (rows + 2) х (n2 + 1), составленный из строк с номерами (n1+1)+myrank / р — 1, ..., (n1+1)+(myrank+1) / р, ° в последнем процессе (my rank = р — 1) находится блок (rows + 1) х (n2 + 1), составленный из строк с номерами (n1+1) +myrank/р — 1, ..., (n1+1) + (my rank+1) /р — 1. Следовательно, во всех блоках, кроме первого, в первой стро- ке находится предпоследняя строка предыдущего блока; во всех блоках, кроме последнего, в последней строке находится вторая строка следующего блока. При вычислении любого оператора от матрицы (т. е. вектора) это соотношение между блоками долж- но быть соблюдено в получившемся результате. Этого можно добиться двумя способами: 1. Вычислить ответ, находящийся в строках с номерами (n1+1) +myrank / р, ..., (n1+1) +(myrank+1) / р — 1, а затем: (а) при my rank ) 0: переслать первую из вычисленных строк (т. е. вторую в блоке) предыдущему (по номеру) процессу; (b) при my rank & t р Ђ” 1: пересл ть послед юю из вычисл ных строк (т. е. предпоследшою в блоке) следующему (по номеру) процессу. Этот способ мы будем использовать при вычислении опера- тора А в (10.7). 
294 Глава 11. Интерфейс MPI (Message Passing Interface) 2. Если i-ю строку ответа можно вычислить, зная только i-ю строку аргумента (как, например, в (10.9)), то: вычислить ответ для всех строк блока. При этом значения в дублиру- к~щихся строках (т. е. в первой строке при myrank ) 0 н в последней строке ttpm myrank ( р — 1) будут вычисле- ны дважды, но разными процессами. Если вычисления не очень сложные, то это дает выигрыш в производительности (из-за отсутствия пересылок) и упрощает программу. Мы бу- дем использовать этот способ при вычислении оператора В в (10.9). Для упрощения программы мы будем выделять в каждом про- цессе блоки памяти одинаковой длины, не учитывая, что в пер- вом и последнем (по номеру) процессах количество хранимых строк меныпе. Файлы проекта: ° main. с — ввод данных и вывод результатов; ° init. с, init.h — исходный текст и соответствукнций заго- ловочный файл для функций. инициализирующих правую часть уравнения (10.4) и вычисляющих ошибку решения; ° operators.ñ, operators.h — исходный текст и соответству- ющий заголовочный файл для функций, вычисляющих раз- личные операторы; ° laplace. с, laplace. h — исходный текст и соответствующий заголовочный файл для функции, решающей задачу. ° Makef ile — для сборки проекта. Файл main. с по структуре похож на одноименный файл из раздела 11.11: ¹include <st io ¹include <stdlib ¹include "mpi.h" ¹include "init h" ¹include "laplace.h" 
11.15. Решение задачи Дирихле для уравнения Пуассона 295 /+ Аргументы для задачи +/ & typedef struct ARGS unsigned int ni; /* число точек по х +/ unsigned int n2; /* число точек по у +/ double hi /+ шаг сетки по х +/ double' h2 /* шаг сетки по у +/ unsigned int max it; /+ максимальное число итераций +/ double prec; /+ величина падения невязки +/ ) ARGS; /+ Получить данные +/ int get data (char + паше, ARGS +parg, int myrank); 1ТЙ main (int argc, char ++argv) { int myrank; /+ ранг текущего процесса +/ int р; /+ общее число процессов +/ double с; /+ время работы всей задачи +/ ARGS arg; /+ аргументы */ unsigned int first row, last row, rows, maxrows; double +f /+ правая часть +/ double +u; /& t; реше ие double +r; /+ невязка +/ int 1; /+ Начать работу с ИР1 +/ MPI Init (&a gc, &am /+ Получить номер текущего процесса в группе всех процессов */ MPI Comm rank (MPI COMM WORLD, &myra 
296 Глава 11. Интерфейс MPI (Message Passing Interface) /+ Получить общее количество запущенных процессов +/ MPI Comm size (MPI COMM WORLD, &amp /+ Получить данные +/ { if (get data ("а.dat", & rg, myr n ) if (my rank == 0) fprintf (stderr, "Read еггог!~п"); /+ Заканчиваем работу с MPI +/ MPI Finalize (); return 1; & /+ Первая участвующая строка +/ first row = (arg.ni + 1) + my rank; first row /= р; /+ Последняя участвующая строка +/ last row = (arg.ni + 1) + (my rank + 1); last row = last row / р — 1; /+ Количество участвующих строк +/ last row — first row + 1; /+ Вычисляем максимальное количество строк на процесс~/ maxrows = (arg.ni + 1) / р + (arg.ni + 1) / р + 2; /+ Выделение памяти под массивы +/ 1 = max rows + (arg.n2 + 1) + sizeof (double); if ( !(f = (double+) та11ос (1)) !(u = (double+) malloc (1)) !(r = (double+) та11ос (1))) fprintf (stderr, "Not enough memory!3n"); MPI Abort (MPI COMM WORLD, 1); 
11.15. Решение задачи Дирихле для уравнения Пуассона 297 & if (my rank == О) 1 ~= 3; printf (" Allocated /d bytes (/dKb or '/dMb) of memory per process(n", 1, 1 » 10, 1 » 20); & /+ Синхронизация всех процессов +/ MPI Barrier (MPI COMMWORLD); /+ Время начала +/ t = MPI Wtime (); /+ Решить уравнение +/ laplace solve (arg.ni, arg.n2, arg.hi, arg.h2, arg.max it, arg.prec, f, u, r, myrank, р); /+ Синхронизация всех процессов +/ MPI Barrier (MPI COMM WORLD); /+ Время работы +/ t = MPI Wtime () — t /+ Здесь кожно работать с результатом +/ /~ Воспользуемся тем, что мы знаем ответ +/ print error (arg.ni, arg.n2, arg.hi, arg.h2, и, my rank, р); if (my rank == О) & printf (" Total time = /lehn", t); /+ Освобождаем память +/ free (r); атее (u); free (f); 
298 Глава 11. Интерфейс MPI (Message Passing Interface) /+ Заканчиваем работу с MPI +/ MPI Finalize (); & return 0; /+ Считать данные из файла, где находятся: число точек по х число точек по у длина стороны, параллельной оси х длина стороны, параллельной оси у максимальное число итераций величина падения невязки Например: 32 32 1. 1. 1000 ie-б g/ int get data (char + name, ARGS ~р, int myrank) /+ Количество полей в типе ARGS +/ Odefine N ARGS б /+ Структуры данных, необходимых для создания MPI-типа данных, соответствующего С-типу АВСЯ +/ /+ MPI-тип каждого поля в ARGS +/ MPI Datatype АЕСЯСурев [МАЙСЯ] = ~ MPI INT, MPI INT, MPI DOUBLE, MPI DOUBLE, MPI INT, MPI DOUBLE ); /+ Количество элементов этого типа в каждом поле ARGS*/ int ARGS counts[N ARGSj = ( 1, 1, 1, 1, 1, 1 ); /+ Смещение каждого поля относительно начала структуры */ MPI Aint ARGS disp[N ARGS]; /+ Переменные, используемые для вычисления смещений +/ MPI Aint start address, address; /+ Новый тип +/ MPI Datatype MPI ARGS; /+ Читаем данные в процессе О +/ 
11.15. Решение задачи Дирихле для уравнения Пуассона 299 { if (myrank == О) FILE +in = fopen (name, "r"); double li, 12; static ARGS zero args; /+ инициализируется О +/ if (!in) { fprintf (stderr, "Error opening data file /s(n", name); ~р = zero args; /+ обнулить все поля +/ & else { if (fscanf (in, "/u/è/lf/lf/u/lf", &p- йр-> 2, @ 1, @ 2, йр-> &p-&g ;p ec t fprintf (stderr, "Error reading data file /в~п", паше); ~р = zero args; /+ обнулить все поля +/ & fclose (in); /+ Вычисляем шаги сетки по направлениям х и у~/ р-&gt h = 1 / р-& р-&gt h = 1 / р-& & /~Создаем MPI-тип данных, соответствующий С-типу ARGS+/ /+ Вычисляем начало структуры +/ MPI Address (р, &s art addre /~ Вычисляем смещение первого поля +/ 
300 Глава 11. Интерфейс MPI (Message Passing Interface) MPI Address (&p >n1 &a АЕСЯбхвр[0] = address — start address; /+ Вычисляем смещение второго поля +/ MPI Address (&p- gt;n2, &am АЕСЯйвр[1] = address — start address; /+ Вычисляем смещение третьего поля +/ MPI Address (&p- gt;hi, &am ARGS disp[2] = address — start address /+ Вычисляем смещение четвертого поля +/ MPI Address (&p- gt;h2, &am ARGS disp[3] = address — start address; /+ Вычисляем смещение пятого поля +/ MPI Address (&p- gt; ax it, &am АЕСЯбхвр[4] = address — start address; /+ Вычисляем смещение шестого поля +/ MPI Address (&p-&g ;prec, &am АЕСЯЫвр[5] = address — start address; /+ Создаем MPI-тип данных с вычисленными свойствами +/ MPI Type struct (N ARGS, ARGS counts, ARGS disp, ARGS types, &amp MPI AR /+ Делаем его доступным при обмене сообщениями +/ MPI Type commit (&amp MPI A /+ Рассылаем данные из процесса О остальным +/ MPI Beast (р, 1, MPI ARGS, О, MPI COMM WORLD); /+ Тип нам больше не понадобится +/ MPI Type free (&amp MPI AR /+ Проверяем, не было ли ошибки +/ if (р-&gt ni = О & am ; return -1; /+ ошибка +/ return О; 
11.15. Решение задачи Дирихле для уравнения Пуассона 301 Основные отличия от примера из раздела 11.11: ° По аналогии с разделом 10.6 мы использовали структуру данных для хранения всех входных параметров алгоритма. Для пересылки этой структуры создается IPI-тип данных (см. раздел 11.13.1). Это решение неэффективно для одно- кратной пересылки и выбрано исключительно для еще одной иллюстрации создания нового IPl-типа. ° В случае ошибки чтения из файла все процессы будут корректно завершены с кодом ошибки, поскольку функция get data вернет признак неудачи во всех процессах. ° В качестве характеристики полученного приближения вы- дается норма ошибки. Заголовочный файл init.h: void print error (unsigned int ni, unsigned int n2, double hi double h2, double +u, int myrank, int р); void init f (unsigned int ni, unsigned int n2, double hi double h2, double +f int myrank, int р); В файле init.с находятся функции: ° solution — - вычисляет то шое решение: ° print error — вычисляет норму ошибки полученного при- ближения, пользуясь тем, что мы:шаем точное решение; для уменьшения вычислительной погрешности используется тот же прием, что в функции get residual (см. ниже); ° right side — вычисляет правую часть; ° init f — задает правую часть в каждом процессе. Файл init.c: ¹include <stdio ¹include <math ¹include "mpi.h" 
302 Глава 11. Интерфейс MPI (Message Passing Interface) ¹include "init.h" /+ Инициализация правой части задачи. В тестах будем рассматривать задачу в единичном квадрате [0, 1] х [0, 1] с ответом u(x,у) = х + (1 — х) + у + (1 — у) которому соответствует правая часть f(x,у) = 2 + х 4 (1 — х) + 2 + у + (1 — у) g/ /+ Точный ответ */ static double & solution (double х, double у) return х * (1 — х) * у + (1 — у); /+ Вычислить и напечатать L2 норму ошибки в процессе с номером myrank из общего количества р.+/ void print error ( unsigned int ni, /& t; чи ло то ек п х unsigned int n2, /+ число точек по у +/ double h| /+ шаг сетки по х */ double h2, /* шаг сетки по у */ double *u, /* решение */ int myrank, /+ номер процесса +/ int р) /+ всего процессов +/ & unsigned int first row, last row, rows; double s|, s2, t, error; unsigned int ii, i2, addr; /+ Первая участвующая строка +/ first row = (ni + 1) * myrank; 
11.15. Решение задачи Дирихле для уравнения Пуассона 303 first row /= р; /* Последняя участвующая строка +/ last row = (n1 + 1) + (my rank + 1); last row = last row / р — 1; /+ Количество участвующих строк +/ last row — first row + 1; addr = (first row & t ? n : ); /+ нач ло бл ка & for (ii = О, s1 = О.; i1 ( rows; i1++) for (i2 = О, s2 = О.; i2 (= n2; i2++) & t = и[addr++] solution ((first row + ii) + hi, i2+h2); s2 += s| += s2; /* Сложить все в1 и передать процессу О */ MPI Reduce (&amp si, &am ;e ror 1, MPI DOU LE, PI MPI COMM WORLD); /* Напечатать ошибку +/ if (myrank == 0) & error = sqrt (error + hi + h2); printf (" Error = '/1е~п", error); } } /+ Точная правая часть +/ static double right side (double х, double у) 
304 Глава 11. Интерфейс MPI (Message Passing Interface) return2+x+(1-х)+2*у~(1-y); /+ Заполнить правую часть в процессе с номером my rank из общего количества р. +/ void init f ( unsigned int n1, /~ число точек по х */ unsigned int n2, /* число точек по у */ double hi /* шаг сетки по х */ double h2, /* шаг сетки по у */ double *f /* правая часть */ int myrank, /+ номер процесса +/ int р) /+ всего процессов +/ unsigned int first row, last row, rows; unsigned int i1, i2, addr = О; /+ Первая участвующая строка +/ first row = (n1 + 1) + my rank; first row /= р; /+ Последняя участвующая строка +/ last row = (n1 + 1) + (my rank + 1); last row = last row / р — 1; /+ Количество участвующих строк +/ rows = last row — first row + 1; /+ Первая строка блока +/ & if (first row & t; /* в первом блоке нет дублирования данных в первой строке, в остальных блоках в первой строке находится предпоследняя строка предыдущего блока +/ for (i2 = 0; i2 &l ;= 2; i2 f [addr++] = right side ((first row — 1) + h1 
11.15. Решение задачи Дирихле для уравнения Пуассона 305 ) 12 е h2); /+ Середина блока +/ for (ii = 0; ii & t; ro s; ii for (i2 = 0; i2 &l ;= 2; i2 f[addr++] = right side ((first row + ii) + h1, i2 + h2); /+ Последняя строка блока +/ if (last row & t; { /~ в последнем блоке нет дублирования данных в последней строке, в остальных блоках в последней строке находится вторая строка следующего блока +/ for (i2 = 0; i2 &l ;= 2; i2 f [addr++] = right side ((last row + 1) * h1 12 е h2); ) ) Заголовочный файл operators.h: double get residual (unsigned int ni, unsigned int n2, double h| double h2, double +r, int myrank, int р); void get operator (unsigned int ni, unsigned int n2, double h| double h2, double +u, double ~ч, int myrank, int р); void get preconditioner (unsigned int ni, unsigned int n2, double h| double h2, double ®ч, int myrank, int р); 
306 Глава 11. Интерфейс MPI (Message Passing Interface) double get optimal tau (unsigned int n1, unsigned int n2, double hi double h2, double +f double *и, double +r, int my rank, int р); В файле орегатогв. с находятся функции: ° get residual — возвращает L& t; но му невяз и: основ ые о бх.ххххости функции: — сум~еа квадратов элементов вычисляется в каждом про- цесс(. для строк с номерыюи (n1+1) «myrank/р, ..., (n1+1) «(my rank+1) /р-1, затем с помощью MPI Allreduce получается сумма квад- ратов «сех элементов массива которая используется для вычисления результата возвращаемого функцией в ка&gt ДОМ ll l)OI T.('.ССС; — L ''РЙНИ ' I H hl(! ТОЧКИ Н Е V' I ÈÒb1 Е43 ЮТСЯ; — для $'ме~еыиения Bbl IllcJIII I'ельlioll пое'pf.'11IIIocTII, связаееной с суммирОВанием чисел, сильно pA3ëH'TÇIÎùèõñÿ IIo поряд- ку) Вычи(ля[отся суммы квадратов эл('.миГ1'ОВ IIo строкам, которые затем прибавляются к окончательному резуль- тату; ° get operator — вычисляет произведение матрицы А систе- мхх (10.6) (т. е. (10.5)' иа вектор и: v = Аи оператор вххчххс- ляется согласно формулам (10.7) аписanllblM выше способом 1; схема пер('.сы.1кц дашеык приведена ца рис. 11.2: а) на перххом этапе (рис. 11.2 а): — в первом процессе (my rank = О) пересылаем сле- дующему процессу (ту гаххЫ+1) предпоследнюю стро- ку, имеющую номер rows — 1 (поскольку всего строк rows + 1), с помощью функции MPISend; — в «сре хххххх» процессах (О ( my rank ( р — 1) иересы- л1еч следующему процессу (my rank+1) предпослед- нюю гтроху, имеющую номер гоив (ххоско..хьку всего строк rows + 2), и получаем от ххххедьхдухххего процесса 
11.15. Решение задачи Дирихле для уравнения Пуассона 307 (my rank-1) первую строку, имеющую номер О, с помо- щью функции MPI Sendrecv; — в последнем процессе (my rank = р — 1) получаем от предыдущего процесса (myrank-1) первую строку, имеющую номер О, с помощью функции ИР? Весч; 6) на втором этапе этапе (рис. 11.2 6): — B нервом процессе (my rank = 0) получаем от следу- ющего процесса (my rank+1) последнюю строку, имею- щую номер rows, с помощью функции MPI Recv; — в «средних» процессах (0 & t; my r n lt; р Ђ” 1) п лаем предыдущему процессу (my rank-1) вторую стро- ку, имекнцую номер 1, ~х получаем от следующего нро- первый блок первый блок из rows+i строк из rows+ i строк 1 предпоследняя строка предпоследняя строка последняя строка последняя строка первая строка первая строка вторая строка вторая строка блок из rows+2 строк блок из rows+2 строк предпоследняя строка предпоследняя строка после,.шяя строка последняя строка первая строка первая строка вторая строка вторая строка последний блок последний блок из rows+i строк из rows+1 строк а) б) Рис. 11.2. Схема пересылки данных между процессами при вычислешхи опсратора Лапласа 
308 Глава 11. Интерфейс MPI (Message Passing Interface) цесса (my rank+1) последнюю строку, имеющую номер rows+1, с помощью функции MPISendrecv; — в последнем процессе (my rank = р — 1) пересыла- ем предыдущему процессу (my rank-1) вторую строку, имеющую номер 1, с помощью функции ИР? Яепй; этапы не зависят друг от друга и могут выполняться в произвольном порядке; ° get pre condit ioner — вычисляет произведение матрицы В в (10.8) на вектор и: v = В и; оператор вычисля- ется согласно формулам (10.9) для матрицы В описанным вышс способом 2; ° get optimal tau — возвращает оптимальное значение итера- ционного параметра т~, в (10.8) согласно формулам (10.10); основные особенности функции: — скалярные произведения (f, r), (Ат, и), (Ат, r) вычисляют- ся в каждом процессе для строк с номерами (ni+1) +my rank/р, ..., (n1+1) + (my rank+1) /р-1, O затем с помощью МР? А11гебисе получаются соответству- ющие скалярные произведения для всех элементов мас- сива, которые используются для вычисления результата, возвращаемого функцией в каждой задаче; — значение AT вычисляется в точке и сразу используется прн вычислении скалярных произведений (Ar, и) и (Ar, r); — для уменьшения вычислительной погрешности использу- ется тот &gt ке при м, т в et residu Файл operators. c: ¹include <stdio ¹include <math ¹include "mpi.h" ¹include "operators.h" /+ Вычислить L2 норму невязки в процессе с номером myrank из общего количества р. Граничные узлы не входят. +/ 
11.15. Решение задачи Дирихле для уравнения Пуассона 309 double get residual ( unsigned int ni, /* число точек по х +/ unsigned int n2, /+ число точек по у */ double hi /+ шаг сетки по х */ double h2, /* шаг сетки по у */ double +r, /* невязка */ int myrank, /+ номер процесса +/ int р) & /~ всего процессов +/ unsigned int first row, last row, rows; unsigned int ii, i2, addr; double si s2, t residual /* Первая участвующая строка ~/ first row = (ni + 1) * myrank; first row /= р; /+ Последняя участвующая строка +/ last row = (n1 + 1) + (myrank + 1); last row = last row / р — 1; /+ В силу нулевых краевых условий на границе невязка = О */ /* Адрес начала рассматриваемой части блока +/ addr = n2 + i; if (first row == О) first row = 1; if (last row == ni) last row = ni — i; /+ Количество участвующих строк */ rows = last row — first row + 1; for (ii = О, s1 = О.; i1 & t; ro s; i1 & 
310 Глава 11. Интерфейс MPI (Message Passing Interface) /+ в первом элементе невязка = О в силу краевых условий +/ addr++; { for (i2 = i, s2 = О.; i2 & t; 2; i2 t = r[addr++]; s2 += } s| += s2; /+ в последнем элементе невязка = О в силу краевых условий */ addr++; /+ Сложить все si и передать ответ всем процессам +/ MPI Allreduce (&amp si, &r si ual 1, MPI DOU LE, MPI COMM WORLD); return sqrt (residual + hi + h2); /+ Вычислить v = Au, где А — оператор Лапласа, в процессе с номером myrank из общего количества р.+/ void get operator ( unsigned int ni, /* число точек по х */ unsigned int n2, /* число точек по у +/ double h|, /* шаг сетки по х */ double h2, /+ шаг сетки по у +/ double +u, /* решение */ double ~ч, /* результат */ int myrank, /+ номер процесса +/ int р) /+ всего процессов */ & unsigned int first row, last row, rows; 
11.15. Решение задачи Дирихле для уравнения Пуассона 311 unsigned int ii i2, addr; double hhi = 1. / (hi + hi), hh2 = 1. / (h2 + h2); int tag = О; MPI Status status; /+ Первая участвующая строка +/ first row = (ni + 1) + myrank; first row /= р; /+ Последняя участвующая строка +/ last row = (ni + 1) + (myrank + 1); last row = last row / р — 1; /+ Количество участвующих строк в середине блока +/ rows = last row — first row + i if (first row == 0) rows--; /+ первая строка будет вычислена ниже +/ if (last row == п1) rows--; /+ последняя строка будет вычислена ниже +/ if (first row == О) /+ Первая строка = О в силу краевых условий +/ for (i2 = О, addr = О; i2 &l ;= 2; i2 v[addr++] = О.; else /+ Адрес начала рассматриваемой части блока +/ addr = n2 + i; /* Середина блока */ с for (ii = О; ii & t; ro s; ii /* первый элемент = О в силу краевых условий e/ v[addr++] = О.; 
312 Глава 11. Интерфейс MPI (Message Passing Interface) for (i2 = 1; i2 & t; 2; i2 +, addr с /+ v(ii,i2) =- (u(i1-1, г)-ги(11,i2)+u(i1+1, г))/01*И) (u(i1,i2-1)-2и(11,12)+и(11,i2-1))/(h2+h2)+/ v[addr] = — (u[addr — (n2 + 1)] — 2 + u [addr] + и addr + (n2 + 1)] ) + hhi (и[addr — 1] — 2 * u[addr] + u[addr + 1]) + hh2; } /+ последний элемент = О в силу краевых условий +/ v[addr++] = О.; if (last row == ni) /* Последняя строка = О в силу краевых условий +/ for (i2 = О; i2 &l ;= 2; i2 v[addr++] = О.; if (first row == О &a p;&a p; as ro /* Случай одного процесса: все сделано. */ return; & /+ Количество участвующих строк +/ rows = last row — first row + | /~ В первом блоке первая строка равна О в силу краевых условий, в остальных блоках в первой строке находится предпоследняя строка предыдущего блока. В последнем блоке последняя строка равна О в силу краевых условий, в остальных блоках в последней 
11.15. Решение задачи Дирихле для уравнения Пуассона 313 строке находится вторая строка следующего блока. +/ if (first row == 0) /+ Посылаем предпоследнюю строку следующему. В первом блоке rows + 1 строк +/ MPI Send (v + (rows — 1) + (n2 + 1), n2 + 1, MPI DOUBLE, my rank + i, ta0;, } MPI COMM WORLD); if (last row == ni) /~ Принимаем первую строку от предыдущего. В последнем блоке rows + 1 строк */ MPI Recv (v, n2 + 1, MPI DOUBLE, myrank — 1, tag, MPI COMM WORLD, &stat if (first row & t О &amp &am ; las /+ Посылаем предпоследнюю строку следующему и принимаем первую от предыдущего. В блоке rows + 2 строк +/ MPI Sendrecv (v + rows + (n2 + 1), n2 + 1, MPI DOUBLE, my rank + i, ta0;, v, n2 + i, MPI DOUBLE, my rank — i, tag, MPI COMM WORLD, &stat } if (f irst row == 0) /+ Принимаем последнюю строку от следующего. В первом блоке rows + 1 строк +/ MPI Recv (v + rows + (n2 + 1), n2 + 1, MPI DOUBLE, my rank + i, ta0;, MPI COMMWORLD, &stat 12 4017 
314 Глава 11. Интерфейс MPI (Message Passing Interface) if (last row == ni) /+ Посылаем вторую строку предыдущему. В последнем блоке rows + 1 строк +/ MPI Send (ч + (n2 + 1), n2 + 1, MPI DOUBLE, myrank — 1, tag, MPI COMM WORLD); if (first row & t О &amp &am ; las /+ Посылаем вторую строку предыдущему и принимаем последнюю от следующего. В блоке rows + 2 строк +/ MPI Sendrecv (v + (n2 + 1), n2 + 1, MPI DOUBLE, my rank — i, tag, v, (rows + 1) * (n2 + 1), ИР? ПОЖИВЕЕ my rank + i, tag, MPICOMMWORLD, &stat /+ Вычислить v = В (-1)v, где В — предобуславливатель для оператора Лапласа в процессе с номером my rank из общего количества р. В качестве В используется диагональ матрицы А. +/ void get preconditioner ( unsigned int ni, /* число точек по х */ unsigned int n2, /+ число точек по у +/ double hi /+ шаг сетки по х */ double h2, /* шаг сетки по у */ double ~ч, /+ аргумент/результат +/ int my rank, /+ номер процесса +/ int р) /+ всего процессов +/ unsigned int first row, 1аз1» row, rows; 
11.15. Решение задачи Дирихле для уравнения Пуассона 315 unsigned int ii i2, addr = О; double hhi = 1. / (h1 + h1), hh2 = 1. / (h2 + h2); double w = 1. / (2 + hhi + 2 + hh2); /+ Первая участвующая строка +/ first row = (ni + 1) + myrank; first row /= р; /+ Последняя участвующая строка +/ last row = (n1 + 1) + (myrank + 1); last row = last row / р — 1; /+ Количество участвующих строк */ rows = last row — first row + i; /* Первая строка блока */ if (first row == О) /+ нулевые краевые условия */ for (i2 = О; i2 &l ;= 2; i2 v[addr++] = О.; rows--; /+ первая строка уже вычислена +/ else /+ first row & t О /+ В первой строке находится предпоследняя строка предыдущего блока +/ for (i2 = О; i2 &l ;= 2; i2 /+ v(i1,i2) = v(i1,i2) /(2/(hi+hi)+2/(h2+h2)) +/ v[addr++] ~= w; if (last row == ni) rows--; /+ последняя строка будет вычислена ниже +/ 
316 Глава 11. Интерфейс MPI (Message Passing Interface) /+ Середина блока +/ for (ii = О; ii & t; ro s; ii /+ первый элемент = О в силу краевых условий +/ v[addr++] = О.; for (i2 = 1; i2 & t; 2; i2 /+ v(i1,i2) = v(ii,i2)/(2/(hi+hi)+2/(h2+h2)) +/ .v[addr++] *= w; /+ последний элемент = О в силу краевых условий +/ } v[addr++] = О.; /+ Последняя строка блока +/ if (last row == ni) /* нулевые краевые условия +/ for (i2 = О; i2 &l ;= 2; i2 v[addr++] = О.; else /+ last row & t; ni /+ В последней строке находится вторая строка следующего блока +/ for (i2 = О; i2 &l ;= 2; i2 & /+ v(i1,i2) = v(i1,i2)/(2/(hi+hi)+2/(h2+h2)) +/ v[addr++] *= w; /+ Вычислить ((f, r) — (Ar, и)) / (Ar, r) где А — оператор Лапласа, 
11.15. Решение задачи Дирихле для уравнения Пуассона 317 в процессе с номером myrank из общего количества р. Функции и, r удовлетворяют нулевым краевым условиям +/ double get optimal tau ( unsigned int ni, /* число точек по х */ unsigned int n2, /* число точек по у +/ double hi /* шаг сетки по х +/ double h2, /* шаг сетки по у */ double +f /+ правая часть +/ double +u, /* решение */ double *r, /+ невязка +/ int myrank, /+ номер процесса +/ int р) /* всего процессов */ unsigned int first row, last row, rows; unsigned int ii i2, addr; double hh1 = 1. / (h1 + hi), hh2 = 1. / (h2 + h2); double si fr, s2fr; /+ суммы для (f, r) +/ double si Аги, s2 Аги;/+ суммы для (Аг, u) +/ double siArr, s2Arr;/+ суммы для (Ar, r) +/ double Аг; /+ значение Аг в точке +/ double а[3], Ь[3]; /+ Первая участвующая строка +/ first row = (п1 + 1) + myrank; first row /= р; /+ Последняя участвующая строка +/ last row = (n1 + 1) + (myrank + 1); last row = last row / р — 1; /+ В силу нулевых краевых условий на границе, слагаемые, соответствующие граничным узлам = О +/ /+ Адрес начала рассматриваемой части блока +/ асИг = n2+ 1; if (f irst row == О) 
318 Глава 11. Интерфейс MPI (Message Passing Interface) first row = i; if (lastrow == ni) last row = ni — i; /+ Количество участвующих строк в середине блока +/ rows = last row — first row + i for (ii = О, si fr = О., si Aru = О., si Arr = О.; il & t; ro s; i1 /+ в первом элементе слагаемые = О в силу краевых условий */ addr++; for (i2 = 1, s2fr = О., в2 Аги = О., s2Arr = О.; i2 & t; 2; i2 +, addr /~ Вычисляем (f, r) */ s2 fr += f[addr] + r[addr]; /+ Вычисляем Ar +/ Ar = — (r[addr — (n2 + 1)] — 2 + r [addr] + r[addr + (n2 + 1)] ) + hh1 (r[addr — 1] — 2 + r[addr] + r[addr + 1]) + hh2; /+ Вычисляем (Ar, u) +/ в2 Аги += Ar + u[addr]; /+ Вычисляем (Ar, r) +/ s2Arr += Ar + r[addr]; si fr += s2 fr; siAru += s2Aru; si Arr += s2 Arr; /+ в последнем элементе слагаемые = О в силу краевых условий */ addr++; 
11.15. Решение задачи Дирихле для уравнения Пуассона 319 a[0] = s1fr; а[1] = siAru; a[2] = siArr; /+ Вычислить суммы по всем процессам +/ MPI Allreduce (а, Ь, 3, MPI DOUBLE, MPI SUM, MPI COMM WORLD); /+ Вычислить ответ +/ & return (Ь[0] — Ь[1]) / Ь[2]; Заголовочный файл laplace.h: int laplace solve (unsigned int ni, unsigned int n2, double h1 double h2, unsigned int max it double prec, double +f double ~и, double +r, int my rank, int р); В файле laplace с находится функция laplace solve, осу- ществляющая итерационный процесс (10.8), (10.10) с матрица- ми (10.7) и (10.9) по описанным в разделе 10.6 расчетным фор- мулам. файл 1ар1асе.c: ¹include <stdio ¹include <math ¹include "mpi.h" ¹include "init.h" ¹include "operators h" ¹include "laplace.h" /+ Решить задачу в процессе с номером myrank из общего количества р. Возвращает количество потребовавшихся итераций. +/ int laplace solve ( unsigned int ni, /* число точек по х */ unsigned int n2, /* число точек по у +/ 
320 Глава 11. Интерфейс MPI (Message Passing Interface) double hi /+ шаг сетки по х */ double h2, /* шаг сетки по у */ unsigned int max it, /+ максимальное число итераций +/ double prec, /+ величина падения невязки +/ double +f /+ правая часть +/ double +u, /* решение */ double +r, /* невязка +/ int myrank, /+ номер процесса +/ int р). /+ всего процессов +/ unsigned int Х~гз1» row, 1аз1» row, rows, width, len; unsigned int addr; /+ Норма невязки на первом шаге +/ double погвгезЫца1 1; /+ Норма невязки на очередном шаге +/ double norm residual /+ Норма невязки на предыдущем шаге 4/ double norm residual prev; unsigned int it; /+ Номер итерации */ double tau; /+ Итерационный параметр +/ /+ Первая участвующая строка +/ first row = (ni + 1) + my rank; firstrow /= р; /+ Последняя участвующая строка +/ last row = (n1 + 1) + (myrank + 1); last row = last row / р — 1; /+ Количество участвующих строк +/ rows = last row — first row + i; /+ Первый и последний блоки имеют по rows + 1 строк, остальные по rows + 2 +/ width = rows + 2; if (first row == О) width ††; if (last row == ni) 
11.15. Решение задачи Дирихле для уравнения Пуассона 321 width-— /+ Длина блока +/ len = width + (n2 + 1); /+ Инициализируем правую часть +/ init f (ni, n2, hi, h2, f, my rank, р); /+ Начальное приближение = 0 +/ for (addr = 0; addr & t; l n; addr u[addr] = О.; /+ Невязка при нулевом начальном приближении = f +/ norm residual 1 = norm residual = norm residual prev = get residual (ni, n2, hi, h2, f, my rank, р); if (my rank == О) printf (" Residual = '/le(n" norm residual 1); /+ Итерационный процесс +/ for (it = 1; it & t; ax t; it & /* Шаг итерационного процесса +/ /+ Вычисляем r = Au +/ get operator (ni, n2, hi, h2, u, r, my rank, р); /+ Вычисляем невязку r = f — r = f — Аи +/ for (addr = 0; addr & t; l n; addr r[addr] = f[addr] — r[addr]; /+ Вычисляем норму невязки +/ norm residual = get residual (ni n2, hi h2 r my rank, р); if (my rank == О) printf (" It № =/2.2d, residual=/11.4е, 
322 Глава 11. Интерфейс MPI (Message Passing Interface) convergence=/6.3f, average convergence=/6.3fhn", norm residual, norm residual / norm residual prev, pow (norm residual / norm residual 1, 1./it)); /+ Проверяем условие окончания процесса +/ if (norm residual & t; n rm residu 1 + pr break; /e Обращение предобуславливателя r = В"g-1) r e/ get preconditioner (ni, n2, hi, h2, r, my rank, р); /+ Вычисление итерационного параметра tau = ((Ь, r) — (Ar, и)) / (Ar, r) +/ tau = get optimal tau (n1, п2, h1, h2, f, u, r, myrank, р); /* Построение очередного приближения u+=tau * r */ for (addr = О; addr & t; l n; addr u[addr] += tau +.r[addr]; } norm residual prev = norm residual; return it; 
Источники дополнительной информации Этот раздел является аналогом раздела «список литературы», а несколько необычное название связано с тем что практиче- ски в('я Ip.ppчислеш1зя ешжР цокум(нтск ия сущОстВуОт тОлькО В ЭЛСКТРОННОМ ВИДЕ. Основным исто шиком дополнительной информации о всех описанных функциях, их аргументах, поведении в различных ситуациях о связи с другими функциями можно получить из установленной в UNIX справочной системы (111а11 ра~еи). Для ЭТОГО НаДО НабРатЬ КОМаНДУ: man < мя функц ИЛИ man -а < мя функц если нмя встречается в несгольких разделах справочника (тогда будут выданы все вхождения). Для того чтобы по команде man можно было получать ин- формацию о МР1-функциях, необходимо установить на компью- тере по ~систему IPI. Существует несголько свободно распро- СТРаНЯЕЫЫХ РЕжiИЗаЦИЙ IPI. 1. Реалнзацггя mpich разработанная Argonnc National Lal) H МЬякяррх State Lniversity;rocò~ пна по адрес~ ftp://info.mcs.anl.gov/pub/mpi 
324 Источники дополнительной информации 2. Реализация ЬАМ, разработанная Ohio Supercomputer Center, доступна по адрс.су ftp://ftp.osc.edu/pub/1am 3. Реализация СН1МР, разработанная Edinburgh Рага11е1 Coniputing Center, доступна по адресу ftp://ftp.epcc.ed.ac.uk/ðèb/chimp/release Стандарт МР1 версии 1 доступен по адресу ftp://ftp.netlib.org как Con>pu er Scie ce De t. Techni al Rep rt CS-94-2 Ьп1~'егя~у of Tennessee, Knoxville, TN, #lay 5, 1994. Стандарт МР1 ве1эсии 1.1 доступен по адресу ftp://ftp.mcs.anl.gov 
Программа курса 1. CISC- u RISC-процессоры. Основные черты RISC- архитектуры. 2. Повышение производительности процессоров за счет конвей- еризации. Условия оптимального функционирования конвей- е-'ра- 3. Суперконвейерные и суперскалярные процессоры. Выделе- ние независимо работающих устройств: IU, FPU, ММГ, BU. 4. Методы уменьшения негативного влияни» инструкций пере- хода на производительность процессора. 5. Повьппение производительности процессоров за счет введе- ни» кэш-памяти. Кэши: единый, Гарвардски~~, с прямой за— писью, с обратной записью. б. Организация кэш-памяти. Алгоритмы замены данных в кэш- памяти. Специальные кэши. 7. Согласование кэшей в мультипроцессорных система~с с об- щей памятью. 8. Виды многопроцессорных архитектур. 9. Поддержка многозадачности и многопроцессорности специ- альными инструкциями процессора. Организация данных во внешней памяти. 10. Программа, процессор, процесс. Основные составляющие процесса, состояния процесса. Стек, виртуальная память, механизмы трансляции адреса. 
326 Программа курса 11. Механизмы взаимодействия процессов. Разделяемая память, семафоры. сигналы, почтовые ящики, события. Задачи (thre- ads). Сравнение с процессами. Ресурсы, приоритеты. Парал- лельные процессы. Связывание. Статическое и динамическое сВязывьние. 12. Виды ресурсов: аппаратные, программные, активные, пас- сивные, локальные, разделяемые, постоянные, временные, некритичные, критичные. 13. Типы гзаичодействня процессов: сотрудничающие н конку- рирующие процессы. Критические секции взаимное исклк&g ченне процессов (задач). 14. Проблемы возникающие при синхронизации задач н идеи их разрешеипя. 15. Состояния процесса и механизмы перехода из одного состо- яния В,сц)у Гое. 16. Стандарты на UNIX-системы. 17. Управление процсссамп. Функции fork, execl, execv, waitpid. Примеры использования. 18. Р'1бота с ли наламш. Функция signal. Пример нспользова- H I I ß. 19. Разделяемая намять. Функции shmget, shmat, shmctl. При- меры использоваш~я. 20. Семафоры. Функцнн semget. semop, semctl. Примеры нс- поль юваппя. 1. Собьп нн. Прнмнтнвные операции. Организация взанмодей- ствня клиент--сервер с помощью событий. 22. О н.редн сооб|ценнй. Функции msgget, msgsnd, msgrcv, msgctl. П~~имеры использованп». 23. Управление за ~ачамн (threads). Функции pthread create pthread join, sched yield. Примеры использования. 24. Объекты синхронизации типа пннех. Функции pthread mutex init, pthread mutex lock, pthread mutex trylock, pthread mutex unlock, pthread mutex destroy. Примеры использов'1пия. 
Программа курса 327 25. Объекты синхронизации тина condvar. Функции pthread cond init, pthreadcondsignal, pthread cond broadcast, pthread cond wait, pthread cond destroy. Примеры использования. 26. Пример multithread-программы умножения матрицы на век- тор. 27. Message Passing Interface (MPI). Общая структура MPI-прог- раммы. Функции MPI Init, MPI Finalize. Сообщения и их виды. 28. Коммуникаторы. Функции MPI Comm size, MPI Comm rank. Примеры использования. 29. Попарный обмен сообщениями. Функции ИР1 Яепс1, MPI Recv. Примеры использования. 30. Операции ввода — вывода в МР1-программах. Примеры. 31. Дополнительные возможности для попарного обмена сооб- щениями. Функции MPI Sendrecv, MPI Sendrecv replace. 32. Дополнительные возможности для попарного обмена сообщениями. Функции MPI Isend, MPI Irecv, MPI Test, MPI Testany, MPI Wait, MPI Waitany 33. Коллективный обмен сообщениями. Функции MPI Barrier, MPI Abort, MPI Beast. 34. Коллективный обмен сообщениями. Функции ИР1 Еес1исе, MPI Allreduce, MPI Ор create, MPI Op free. Пример МР1- программы, вычисляющей определенный интеграл. 35. Время в MPI-программах. Функции MPI Wtime, MPI Wtick. Пример использования. 36. Пример МРI-программы умножения матрицы на вектор. 37. Дополнительные возможности для коллективного обмена массивами данных. Функции MPI Gather, MPIAllgather, MPI Scatter, 38. Пересылка структур данных. Создание нового МР1-типа данных с помощью ИР1 Туре вСгисС. Функции MPI Address, MPI Type commit, MPI Type free. Пример использования. 
328 Программа курса 39. Упаковка/распаковка разнородных данных для блочной пе- ресылки с помощью функций MPI Pack, MPI Unpack. Функ- ция MPI Pack size. Пример использования. 40. Гетерогенные и однородные вычислительные установки. Пе- ресылка структур данных в однородных параллельных ЭВМ. Примеры. 41. Пересылки строк и столбцов матриц. Создание нового МР1- типа данных с помощью MPI Type vector. Пример исполь- зования. 42. Ограничение голлективного обмена на подмножество процсссов. Функции MPI Comm group, MPI Group incl, MPI Comm create, MPI Comm free. Примеры использования. 
Список задач 1. Написать программу, вычислякнцую сумму элементов и IIo- следовательностей вещественных чисел, находящихся в и файлах, имена которых заданы массивом а. Ответ должен быть записан в файл res.txt. Программа запускает п про- цессов, вычисляющих ответ для каждого из файлов и при- бавляющих его к результату, находящемуся в выходном фай- ле. Взаимное исключение при доступе к файлу обеспечива- ется с помощью семафора. 2. Написать функцшо, получающую в качестве аргументов целое число и и массив длины и с именами файлов, со- держал~их единую последовательность вещественных чисел неизвестной длины, и возвращающую количество участков постоянства этой последовательности. Функция возвраща- ет — 1, — 2 и т. д., если она H(. смогла открыть какой-либо файл, прочитать элемент и т. д. Функция должна запускать и процессов, обрабатывающих свой файл и передающих ре- зультаты в основной процесс через очередь сообщений для формирования ответа для последовательности в целом. 3. Написать программу, осуществляющую мониторинг и пере- запуск в случае завершения работы заданного количества приложений. Приложения за,вдаются масспвои строк, являн&g щихся их полным путевым именем, и пе имеют аргументов. 4. Написать реализацию стека строк в разделяемой памяти. При запуске программа создает блок разделяемой памяти 
330 Список задач (если его еще нет) или присоединяется к существующему блоку. Программа должна обеспечивать основные функции работы со стеком (добавить и удалить элемент) и возмож- ность запуска себя во многих экземплярах. 5. Написать реализацию набора конфигурационных парамет- ров для многих одновременно работающих экземпляров про- граммы. Набор параметров задается некоторой структурой данных и хранится в разделяемой памяти. Блок разделяе- мой памяти создается при запуске первого экземпляра про- граммы и заполняется из файла. Перед окончанием рабо- ты последнего экземпляра набор параметров сохраняется в файле, а блок разделяемой памяти удаляется. Программа должна обеспечивать основные функции работы с набором параметров (прочптать и изменить элемент), причем в слу- чае изменения даншлх одним из экземпляров он оповещает вс» остальные экземпляры с помощью сигнала. 6. Написать пш1~И1геас1-функцшо, получающую в качестве ар- гументов и х и массив а вещественных чисел, целое число п, номер звдачи (thI cad) k, общее количество задач (Й11геас1в) р и возвращающую ненулевое значение, если массив а сим- метричен (т. е. а; = а~;) 0 в противном случае. При этом должна быть обеспечена равномерная загрузка всех задач. Основная программа должна вводить числа р,п и массив а (из файла или IIo заданной формуле), запускать задачи, вызывать эту функцию и выводить на экран результат ее I) BOOTbI. 7. Написать multithread-подпрограмму, получающую в каче- стве аргументов тс х и массив а вещественных чисел, целое число и, номер задачи (thread) k, общее количество задач (threads) р, и заменяющую матрицу а на ее транспонпрован- í~'ю. При этом должна быть обеспечена равномерная загруз- ка всех задач. Основная программа должна вводить числа р,7l и массив а (из файла или по заданной формуле), за- пускать задачи, вызывать эту подпрограмму и выводить на экран результат ее работы. 
Список задач 1 8. Написать multithread-подпрограмму, получающую в каче- стве аргументов и x n массив а вещественных чисел целое число п, номер задачи (thread) Й, общее количество задач (threads) р, и заменяющую матрицу а на матрицу (а+ а~)/2, где а — транспонированная матрица а. При этом должна f. » быть обеспечена равномерная загрузка всех задач. Основ- ная программа должна вводить числа р,л и массив а (из файла или по заданной формуле), запускать задачи, вызы- вать эту подпрограмму и выводить на экран результат ее работы. 9. Написать multithread-подпрограмму, получающую в каче- стве аргументов массив а вещественных чисел, целое число и, являющееся длиной этого массива, номер задачи ((11геж1) Й, общее количество задач (threads) р, и заменяющую каж- дый элемент массива (для которого это возможно) на сред- нее арифметическое соседних элементов. При этом юл> быть обеспечена равномерная загрузка всех залач. Основная программа должна вводить шсла р,n и массив а, (нз файла или по заданной формуле), запускать зада ш, вызывать эту подпрограмму н выводить на экран результат ее работы. 10. Написать multlthrеа<1-ггодн1>о рамму получ к щую в ства аргументо» а х и масс11» а»еин'.ствеш~ых чисел, вспо- могательный массив Ь вещественнь|х чисел длины и (в каж- дом К1неас1 свой), целое число и,, номер задачи (thread) k, общее количество задач (tlireads) р, и заменяющу1о каж- дый элемент а.;. матрицы а (для которого это иозмо>к на а,+1 + а,, f j + а; +1 + а,д [ — 4а; . При этом должна быть обеспечена равномерная загрузка всех задач. Основ- ная программа должна вводить числа р,н и массив а (нз файла или по заданным формулам), запускать задачи, вы- зывать эту подпрограмму и «ы»одить на экран результат ее работы. 11. Написать rriultitliread-подпрограмму, получающу~о в каче- сгее аргументов и х и массив а вещественных чиса.л всномо- гательный массив б вещестьенных шсел длины 2л, (13 каж- 
332 Список задач дом thread свой), целое число п, номер задачи (thread) k, общее количество задач (threads) р, и заменяющую каж- дый элемент а, матрицы а (для которого это возможно) на Qi+2j. + Qi &g ; + а; + + а 2 Ђ” а . ри э ом дол быть обеспе н.на равномерная загрузка всех задач. Основная программа должна вводить числа р, и и массив а (из файла или по зада>ш >м форм лам), зап скать з дачи, вы эту подпрог1>'& t мму выв ~ ть на экран рез ль ат ее р 12. Наш>с ть МР1-~1>у кцию полу и щую в ка естве ар тов соответствуюн&gt ло ча ть (бл к и масс в а ве стиенных шсел, целое число и„номер процесса k, общее количество процессов р, и возвращающую ненулевое значе- ние, если мас< н и симметри ен т. е. а = а„& t , , О в п ном случае. III&gt ll э ом дол на б ть обеспеч на равномер загрузка. всех процессов. Основная программа .>ол на на HBTh раооту с: МР1, ввод&g ; ь чи л и мас и а из фа ла по заданной формуле), »ьг>ыв ть ту функ и и вывод н3, эе<~ ан pe3ydiь' ят ее ры)о Lj. Написать МР1->>одпрог амму, получ ю ук> в кач гументов соответствующую часть (блок) и х и массива а веществешн&g ;х чис. л. це ое чи л и но ер проце с Й тцее ко.11пчество процессов р и залюн~пощуто матрицу и, на ее т1>ансон>нирова нук gt;. При эт м до жна быть о равномерная загрузка всех процессов. Основная программа должна начинать работу с МР1, вводить число и и мас- сив а (из файла или по задашюй формуле), вызывать эту подпрограмму и иьнюдить на экран результат ее работы. 14. Написать ЬП I-подпрограмму, получающую в качестве ар- гументов соответствую> ую ча ть (бл к и ». масс в вехцествеш~ых шсел целое шсло и номер процесса Й об- щее количество процессов р и заменяютцую матрицу а на матрицу (a + a,')/2 где а~ — транспонировапная матрица а. При этом должна быть обеспечена равномерная загрузка всех процессов. Основная программа должна начинагь ра- боту с МР1. вводить число и и массив а (из файла или по 
Список задач 333 заданной формуле), вызывать эту подпрограмму и выво- дить на экран результат ее работы. 15. Написать ]XIPI-подпрограмму, получающую в ка.хестве аргу- ментов соответствующую часть (блок) массива а веществен- ных чисел, целое число и, являющееся длиной этого массива, номер процесса А:, общее количество процессов р, и заменяю- щ~ ю каждый элемент массива (для которого это возможно) на среднее арххфчетическое соседних элементов. При этом должна быть обеспечена равномерная загрузка всех процес- сов. Основная программа до]]>к ха начин ть раб т с I вводить число и и массив а (нз файла или по заданной формуле), вызывать эту подпрограмму и выводить на экран резуххьтат ее работы. 16. Написать hlI1-гходххрограмму, получахощую в качестве ар- гуме]хтов соответствуюхцие части (блоки) и х и массива а вещественных чисел вспомогательный массив Ь веще- ствеш~ых чисс'л длины и, целое число и, номер процесса А, общее количество процессов р, и замеххяющук& t; каж элемент а; матрицы а (для которого это возможно) на и;+х +а; ..] +а;j+]+а, ] — 4а,; . При этом должна быть обеспечена равномерная загрузка всех процессов. Основная программа должна, начинать работу с Мр?. «водить число и и массив а (из файла или ххх]:хада]хххой форлхуле), вызывать эту подпрограмму и выводить на экран резулыах' ее работы. 17. Написать ]XIPI-подпрограмму, ]холучхххохххую ]х качестве ар- гументов соответствующие части (блок]]) и х и массива а вещественных чисел, вспомогательный массив Ь веще- ственных чисел длиш.~ 2п, целое число и, номер процесса k, общее количество процессов р, и:]аменяющую каждый элемент а; матрицы а (для которого это возможно) на и;] ~j + +i 2,~ + +i j+2+ а,~ 2 — 4а, х. При этом должна быть обеспечена рав]хоиерная загрузка всех процессов. Основная программа должна начинать работу с IPI, вводить число хх и массив а (из файла или по заданной формуле), вызывать эту подпрограмму и выводить на экран р<.] лх ат ее рабо 
Указатель русских терминов Взаимное исключение........ 75 Очереди сообщений..........122 виртуальная память.......... 68 ошибка....... 205 Задача............. 70 Память зада. ы Дирихл».............. 02 виртуальная 68 общая ...... .. 40 Кластеры . '12 ра зд('ляемия .97 к.шент-се1)«ер........... 121 распреде,. ~ешия 40 коммуникатор... 236, 237 поток.................. 70 критическая секция ..... 7,) почтовые ящики..... 122 куш . 3-1 пре,цобу.славливатель........ 204 второго уровня........... 3,) приоритет........... 71 Га~)вардский.............. 35 программа.................... 66 един»~И ................... 35 процесс .......66 первого уровня........... 35 блокировка............. 7,) с обратной записью...... 36 взаимо;~действие ....... 68 ( прямой за,письма)........ Зй почтовые «щики....... 69 p<"r»&l ;'i'о уровня.... .... 3:.) разде яемая амя ь семафоры .. 69 Межпроцессное «заимодей- сигпалы .......... 69 «т«ие .68 застой .. ° 76 метод Якоби....... 203 еопкуриру'ющий.......... 74 метод скорейшего спуска.... 204 параллельный......... 71 состояши».......... 66 Невязка. 201 сотруд~-~ича~ощий......... 74 нить. .70 тупик ............... 75 
Указатель русских терминов 335 процессор..................... 24 пассивный................ 72 CISC......................24 постоянный.............. 72 RISC............... 25 программный............. 72 конвейерный.............. 26 разделяемый............. 72 суперконвейерный........ 29 критичный............ 73 суперскалярный . °... °... ° 30 не критичный. °........ 72 процессоры.................... 39 векторные................ 43 СвЯзывание........... 71 матричные............... 43 динамическое............. 71 распределенные.......... 40 статическое............... 71 сильно связанные........ 39 се»МЩ>ы........ слабо связанные . 40 двоичные................ 100 счетные................. 100 Разделяемая память 97 событие 69, 120 разностная аппроксимация .. 203 стандарт POSIX 1003......... 80 P есурс... ..... ° .......... 70 стек........................... 67 активный............... 72 аппаратный.............. 72 Транспьютеры................ 43 временный..... 72 лок,ьный 72 УраВНеННе Пуассона- ......202 
Указатель английских терминов & t; ptlireac .Ь ..1 6, ЬЗ ..................... .. 157, 161--163, 170--172 TLB...................... 38 schccl.l1 ......157 unified ° 35 =.signal.Ü:=- ... .87, 88 ~vrit,е-ba.ck................. 36 .sos/ipc.Ь& t; .............. .. 8, ~vrite-througli.......... .. 99, 103, 104, 124-126 CISC . 24 <.sys/øsg l» ......... .. 24 Ђ” 26 clo e. . <яуя/ялп l> ...... .... 10 , 104 сопсЬаг................. ... sys/shm.h =... 98, 99 & t; Ьу Яу ьу е %~У™~ 1 У Р ~за~ 11 ) 81 84 87 ИВ........................... 62 ° ° ° ° ° ° ° ° ° м~~.~ м <sys/ivait.h'>... .. .. 81 deadlo k ... ° 7 <unist4 11 =- ........ .. 81 84 33 de ay sl ts ........... БАММ............... 62 API ....... °... 80 ехес1.................. 84 big-епсйаа . ехест................. 84 BSD 4.3 ....................... 79 ВТС...... 38 fork. .81 ]ДЕ ° ° ° ° ° ° ° Я1 FPU ...,...,...,...,...,,,,..., 31 b~irst access.................... 35 IEEE °......... ° .. 80 IPC PRIVATE......... 136, 145 сасне .. ....... 34 ipcrrn................... 120 155 ВТС. °... 38 ipcs.................. 119 155 Нал'агс1 ............... 35 щ 31 Ll . ... 35 L2. . 35 kill . 87 
Указатель английских терминов 337 linkage............. 71 MPI Isend ............ 246 little-endian........ .. 64 МР? LONG ........ 235 lockout .......... 75 MPI ЕОХС DOUBLE.... 235 LRU................38 kIPI Op» create. °......... 254 MPI Op free ........... 255 gg>7IU....................... .. 31 МИ OP ~И ?. 'IPI ...... 232 МР1 Pack.......... 282 шр1.h......................... 232 5IPI Pack size............ 282 :IPI Abort........... 255 МР? PACKED.... 235, 277, 282 МР? Address................ 278 KIPI Recv .................. 238 МР? Aint 278 'IPI R.educe °............... 252 5IPI Allgather..... 274 MPI R.cquest . . 247 МР1 А11гейге.......... 254 IPI REQUEST NULL ... 248 Ъ!Р1 ANY SOURCE...... 236 IPI Scatter........ 275 5 IPI АХЪ TAG.... 236 'IPI Send .. ° 238 IPI Вагпе1................. 251 ЫР1 Sendrec~ . . 244 Ъ! Р? Beast.......... 251 Ъ! Р? Яспс11есч replace..... 245 :IPI BYTE 235 278 МР? ЯНОВТ... 235 MPI CHAR....... 235 MPI SOURCE..... 239 МР? Сотю...... 236 'IPI Ssend..... 244 Х!Р? Comm create........ 291 МР1 Status................. 239 IPI Connn free. 292 МР1 SUCCESS . 232 МР? Сопш1 group .......290 IPI TAG...................239 МР1 Co~nrn rank....... 237 МР? Теь(................... 248 MPI Comm size.... 237 МРI Testany................248 :IPI СОХИ! AVORLD.... 236 МР1 Type con>mit. .. 2 0, IPI Data.tóðå .. 235 IPI» Type free....... 280, 289 :IPI БАТАТ 1'РЕ Ы3ЬЬ .. 280 МР! Туре struct....... 279 МР? DOUBLE......... -35 МР? Туре vector.......... 288 IPI ERROR............... 239 МР? UNDEFINED ......... 240 МР! Finalize................ 233 5;IPI Unpack 283 МР1 FLOAT..... 235 MPI UNSIGNED CHAR .. 235 IPI Gather . ..... 273 МР1 ЩЧЯСМЕ0 ?ХТ.... 235 МР? Get count........ 239 5;IPI UNSIGNED LONG .. 235 IPI» Group................. 290 МР? UNSIGNED» SHORT. 235 O'! Р1 Group incl.... 290 5IPI User function........ 254 IPI Init ................ 233 МР?» Жай................... 249 МР? INT.............. 235 МР? %авралу............... 250 5IPI Irecv............. 247 IPI Жйс1с.............232, 260 
338 Указатель английских терминов МР1 ctime............ 232, 260 read.................. 133 msghuf.............. 124, 125 RISC.......................... 25 томас сг вас'.1........................ 126 ~д®get 124 sched yield. .157 ,д~р-с-~, 125 ЯВКАМ.................. 62 rnsgsnd....................... 124 semctl 104 щц~ех..........75 158 semget........................103 error check............... 161 яетор........................ 103 global ] 59 SETALL...................... 136 1оса1..................... 159 shmat 99 щсщ дщ 161 shmctl 99 inutual ezcliision............... 75 shrnget - .. - - - - .. - - - - - - - . - . - 98 S ц;па1.........................88 NFS.......................... 211 SIGPIPE..................... 134 SI5,! М ...... ° . 62 P ipe...,...,...,...,...,,, 133 ЯМР ° ° ° ° ° ° ° ° ° ° ° ° %У 1/ рост~ ОСТУ I ОSIа ~ ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° snoopi»g....... °............... 38 1003..................... 80 speculative execution.......... 32 1003.1.................... 80 С С1. "7 ° ° ° ° ° ° ° ° ° ° 1003.1b................... 80 empty ассепйщ,".......... 68 1003.Ы................... 80 empty descending......... 67 1003.1с.............. 80 full acccriding... 68 1003.2........ °.......... ° . 80 full descending............ 67 pthread cond» broadcast ° .. 171 ропйе1.................... 67 p<hr ad со с1 destroy... .. 72 starvat on pthread» cond» init..........170 System g Release 4....... 79 РТНВЕА0 СОХО IVIITIALIZER....... 171 thread......................... 70 Р~1~1'еad соnd sig»al.... - 171 ТЬВ....................... 35 38 pthread cond wait......... 172 р(Ь1.еЫ create.............. 156 waitpid........................ 84 pthread detacli.............. 157 XVEXITSTATUS............... 85 pthread join................ 157 WIFEXITED.................. 85 pthreaci mutex. destroy.... 163 WIFSIGNALED............... 95 pthread mutex iiiit........ 161 write......................... 133 PTHREAD» 'A~IUTEX AVTERKISIG............... 95 INITIALIZER....... 162 pthread mutex 1ос1с....... 162 Xi~Open Рог~аЫ11(~ Guide..... 80 Р thread пш~ех trylock.... 162 XPG3 ......................... 80 pthread muted u»lock..... 162 
Список иллюстраций 2.1. Ускорение работы процессора за счет конвейера .... 27 5.1. Виды стеков 5.2. Виды разделяемых ресурсов 73 5.3. Типы взаимодействия процессов: независимые про- ц СССЫ е ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° е ° ° 73 5.4. Типы взаимодействия процессов: синхронизирующи- еся процессы .............................. 74 5.5. Типы взаимодействия процессов: конкурирующие процессы ................................... 74 5.6. Работа с разделяемым ресурсом ... 76 5.7. Состояния процесса ......................... 77 8.1. Пример работы с семафором ................. 102 8.2. Пример работы с событием ........................ 121 8.3. Почтовый ящик ............................ 122 10.1. Организация вычислений при умножении матрицы на матрицу 197 11.1. Организация вычислений и пересылки данных при умножении матрицы на вектор ..................... 271 11.2. Схема пересылки данных между процессами при вы- числении оператора Лапласа ................... 307 
Слисок таблиц 2.1. Соотношение скорости работы различных алгорит- мов умпожеш~я мьтриц . 10.1. Относительное время работы параллельной про- граммы умножения матриц................. 193 10.2. Относительное время работы улучшенной парал- .ле.льной программы умножения матриц ............. 197 10.3. Относительное время работы хорошей параллельной программы умножения матриц ....................... 202 11.1. Соответствие типов бранных МР1 и типов языка С .. 235 11.2. Операции нал лапшыми в IPI .... 253 
Список примеров 1. Вы ч исление интеграла 1 2. Распараллеливание вычисления интеграла с помо- щью процессов 1 3. Рас~~араллеливание вычисления интеграла с помо- щью задач (threads) ....................... 18 4. Распараллеливание вычисления интеграла с помо- щью 1VIPI 21 5. Варианты вычисления произведения матриц ......... 48 6. Варианты вычисления произведения матриц (на язд г- ке FORTRAN) ............................. 54 7. Создание фонового процесса .......................... 82 8. Создание нового процесса ....... ................. 86 9. Создание «отказоустойчивого» фонового процесса .. 88 10. Клиент-серверное взаимодействие процессов, исполь- зующее разделяемую память .......................... 1 11. Клиент-серверное взаимодействие процессов, исполь- зующее очереди сообщений 1 12. Умно>ке ие матр цы на вект р, использую ее п 9Р ц ессы ..................................... ° ° ° ° 1~U 13. Клиент-серверное взаимодействие задач, использую- щее для синхронизации Illlltox 1 14. Клиент-серверное взаимодействие задач, илюльзую- щее для синхронизации condvar ° ... 173 
342 Список примеров 15. Умножение матрицы на вектор, использующее задачи (threads) ................................... 179 16. Умножение матрицы на матрицу, использующее за- да ~и (threads) .......................................... 192 17. Улучшенное умно>ке ие матр цы на матри у, испо зующее задачи (threads) . 194 18. Хорошее умно>ке ие матр цы на матри у, исполь ющее задачи (threads) 198 19. Решение задачи Дирихле для уравнения Пуассона, использующее зада ш (threads) ........................ 206 20. Простешпая МР1 программа ........ 242 21. МГ1 программа, вычисляющая определенный инте- 1 Т1Я ~Т 1Я Р алЛ ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° 256 22. Умно>ке ие матр цы на вект р, использую ее P . 23. Решение зада ы Дирихле для уравнения Пуассона, использующее МР1 ................................... 294 
Минимальные систел«ные требования определяются соответствующими требованиял«и програл«л«ы Adobe Reader версии не ниже 11-й для плат- форл«Windows, Мас OS, Android, iOS, Windows РЬопе и BlackBerrg; экран 10" Учебное электронное издание Серия: «Математика» Богачев Кирилл Юрьевич ОСНОВЫ ПАРАЛЛЕЛЬНОГО ПРОГРАММИРОВАНИЯ Учебное пособие Ведущий редактор М. С. Стригунова Художественный редактор Н. А. Новак Технический редактор Е. B. Депюкова Корректор Н. Н. Ектова Оригинал-макет подготовлен О. Г. Лапко в пакете РТЕХ2е Подписано к использованию 19.03.15. Формат 125 х 200 мм Издательство «ВИНОМ. Лаборатория знаний» 125167, Москва, проезд Аэропорта, д. 3 Телефон: (499) 157-5272 е-mail: info@pilotLZ.ru, http: //www.pilotLZ.ru 
Л ° М ° ° ° ° ° ° М ° ° М ° ° ° ° ° ° ° М ° ° ° ° °: ° -- - ° ° ° ' ° ° ' ° ° ° ° ° ° ° ° ° ° II II ° ° ° ° ° ° М ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° Д ' Д ° ° ° ° II ° ° ° ° ° ° Л ' М ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ° ' ° ° ° ° ° ° ° ° ° ° М ° ° °