- •Тема 1. Вступ в паралельні та розподілені обчислення.
- •Поняття паралелізму.
- •Два узагальнені підходи до досягнення паралельності.
- •3. Переваги паралельних обчислень
- •4. Найпростіша модель розпаралелення
- •Переваги розподілених обчислень.
- •6. Найпростіші моделі розподілених обчислень.
- •7. Мульти-агентні розподілені обчислення.
- •8. Основні етапи проектування паралельних та розподілених алгоритмів.
- •8.1. Декомпозиція
- •8.2. Зв'язок
- •8.3. Синхронізація
- •9. Базові рівні програмного паралелізму
- •9.1. Паралелізм на рівні інструкцій
- •9.2. Паралелізм на рівні підпрограм
- •9.3. Паралелізм на рівні об'єктів
- •9.4. Паралелізм на рівні програм
- •Тема 2. Архітектура паралельних обчислювальних систем.
- •Класифікація паралельних комп’ютерів та систем
- •Векторно-конвеєрні комп’ютери
- •Паралельні комп’ютери з спільною пам’яттю
- •Паралельні комп’ютери з розподіленою пам’яттю
- •Концепція grid або метакомп’ютеринг
- •Тема 3. Основні моделі паралельних та розподілених обчислень.
- •Основні типи паралельних програм.
- •Ітеративний паралелізм.
- •3. Рекурсивний паралелізм
- •4. Модель „виробники-споживачі"
- •5. Паралельна парадигма „клієнт - сервер"
- •6. Паралельна модель „взаємодіючі рівні"
- •Тема 4: Засоби розробки паралельних програм.
- •1.Основні підходи до розробки паралельних програм
- •2.Використання бібліотеки паралельного програмування pthreads
- •Навчальний приклад: Pthreads
- •3. Бібліотека паралельного програмування OpenMp
- •4. Бібліотека паралельного програмування мрі
- •Система програмування mpi
- •Властивості та класифікація процесу.
- •Незалежні та взаємодіючі обчислювальні процеси.
- •Види задач синхронізації паралельних процесів.
- •Синхронізація за допомогою блокування пам’яті.
- •Алгоритм деккера.
- •Команда “перевірка” та “встановлення”.
- •Використання семафорів для синхронізації та впорядкування паралельних процесів.
- •Монітороподібні засоби синхронізації паралельних процесів.
- •Поштові ящики.
- •Конвеєри.
- •Черги повідомлень.
3. Рекурсивний паралелізм
Програма вважається рекурсивною, якщо вона містить процедури, які викликають самі себе — прямо або опосередковано. Рекурсія дуальна ітерації в тому сенсі, що рекурсивні програми можна перетворити в ітеративні і навпаки. Проте кожен стиль програмування має своє застосування, оскільки одні завдання за своєю природою рекурсивні, а інші — ітеративні.
У тілі багатьох рекурсивних процедур звернення до самого себе зустрічається більше одного разу. Наприклад, алгоритм quicksort часто використовується для сортування. Він розбиває масив на дві частини, а потім двічі викликає себе: перший раз для сортування лівої частини, а другий — для правої. Багато алгоритмів для роботи з деревами і графами мають подібну структуру.
Рекурсивну програму можна реалізувати за допомогою паралелізму, якщо вона містить декілька незалежних рекурсивних викликів. Два виклики процедури (або функції) є незалежними, якщо їх множини запису не перетинаються. Ця умова виконується, якщо: 1) процедура не звертається до глобальних змінних або тільки читає їх; 2) аргументи і результуючі змінні (якщо є) є різними змінними. Наприклад, якщо процедура не звертається до глобальних змінних і має тільки параметри-значення, то будь-який її виклик буде незалежним. (Добре, якщо процедура читає і записує тільки локальні змінні, тоді кожен екземпляр процедур має локальну копію змінних.) Відповідно до цих вимог можна запрограмувати і алгоритм швидкого сортування. Розглянемо один цікавий приклад.
З
адача
квадратури
полягає в апроксимації інтеграла
безперервної функції. Припустимо, що
це функція f(х).
Як показано на мал.1,
інтеграл функції f(х)
від а
до b
— це площа між графіком f (х) і віссю
абсцис від прямої х
= а
до прямої х = b.
Існує два основні способи апроксимації значення інтеграла. Перший — розділити інтеграл від а до b на фіксоване число відрізків, а потім апроксимувати площу на кожному з них за правилом трапецій або за правилом Сімпсона.
double fleft = f(a), fright, area = 0.0;
double width = (b-a) / INTERVALS;
for [x = (а + width) to b by width] {
fright = f(x);
area = area + (fleft + fright) * width / 2;
fleft = fright;
}
Кожна ітерація обчислює площу малої фігури за правилом трапецій і додає її до загального значення площі. Змінна width — ширина кожної трапеції. Відрізки перебираються зліва направо, тому праве значення кожної ітерації стає лівим значенням наступної ітерації.
Другий спосіб апроксимації інтеграла— використовувати парадигму "розділяй і володарюй" та змінне число інтервалів. Зокрема, спочатку обчислюють значення m — середину відрізка між а та b. Потім апроксимують площу трьох областей під кривою, певною функцією f(): від а до m, від m до b та від а до b. Якщо сума менших площ рівна більшій площі з деякою заданою точністю EPSILON, то апроксимацію можна вважати достатньою. Якщо ні, то велика задача — від а до b — ділиться на дві підзадачі — від а до m та від m до b, і процес повторюється. Цей спосіб називається адаптивною квадратурою, оскільки алгоритм адаптується до форми кривої. Його можна запрограмувати так.
double quad(double left,right,fieft,fright,lrarea) {
double mid = (left + right) / 2;
double fmid = f(mid);
double larea = (fleft+fmid) * (mid-left) / 2;
double rarea = (fmid+fright) * (right-mid) / 2;
if (abs((larea+rarea) - lrarea) > EPSILON) {
# рекурсія для інтеграції обох значень
larea = quad(left, mid, fleft, fmid, larea);
rarea = quadfmid, right, fmid, fright, rarea);
}
return (larea + rarea);
}
Інтеграл функції f(x) від а до b апроксимується таким викликом функції:
area = quad(а, b, f(a), f(b) (f(a)+f(b))*(b-a)/2);
У функції знову використовується правило трапеції. Значення функції f() у крайніх точках відрізка і наближена площа цього інтервалу передаються в кожен виклик функції quad, щоб не обчислювати їх більше одного разу.
Ітеративну програму не можна розпаралелювати, оскільки тіло циклу зчитує і записує значення змінної area. Проте в рекурсивній програмі виклики функції quad незалежні за умови, що обчислення функції f(х) не дає побічних ефектів. Зокрема, аргументи функції quad передаються по значенню, і в її тілі немає присвоєння глобальним змінним. Таким чином, для завдання паралельного виконання рекурсивних викликів функції можна використовувати оператор со.
со larea = quad(left, mid, fleft, fmid, larea);
// rarea = quad(mid, right, fmid, fright, rarea);
со
Ця єдина зміна необхідна для того, щоб зробити дану програму рекурсивною. Оскільки оператор со не закінчується до тих пір, поки не будуть завершені обидва виклики функцій, значення змінних larea і rarea обчислюються до того, як функція quad поверне їх суму.
У операторах со програм множення матриць містяться списки інструкцій, що виконуються для кожного значення лічильників (і та j). У операторі со, містяться два виклики функцій, розділених знаками //. Перша форма оператора со використовується для виразу ітеративного паралелізму, друга — рекурсивного.
Отже, програму з декількома рекурсивними викликами функцій можна легко перетворити в паралельну рекурсивну програму, якщо виклики незалежні. Проте існує чисто практична проблема: паралельно виконуваних операцій може стати дуже багато. Кожен оператор со в приведеній вище програмі створює два процеси, поодинці для кожного виклику функції. Якщо глибина рекурсії велика, то з'явиться велике число процесів, можливо, дуже велике для паралельного виконання. Рішення цієї проблеми полягає в скороченні, або відсіканні, дерева рекурсії при достатній кількості процесів, тобто перемиканні з паралельних рекурсивних викликів на послідовні.
