- •Тема 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
- •Властивості та класифікація процесу.
- •Незалежні та взаємодіючі обчислювальні процеси.
- •Види задач синхронізації паралельних процесів.
- •Синхронізація за допомогою блокування пам’яті.
- •Алгоритм деккера.
- •Команда “перевірка” та “встановлення”.
- •Використання семафорів для синхронізації та впорядкування паралельних процесів.
- •Монітороподібні засоби синхронізації паралельних процесів.
- •Поштові ящики.
- •Конвеєри.
- •Черги повідомлень.
Ітеративний паралелізм.
Ітеративна послідовна програма використовує для обробки даних і обчислення результатів цикли типу for і while. Ітеративна паралельна програма містить декілька ітеративних процесів. Кожен процес обчислює результати для підмножини Даних, а потім ці результати збираються разом.
Як простий приклад розглянемо завдання з області наукових обчислень. Припустимо, дані матриці а і b, у кожної по n рядків і стовпців, і обидві ініціалізовані. Мета — обчислити добуток матриць, помістивши результат в матрицю з розміром nхn. Для цього потрібно обчислити п2 проміжних добутків, поодинці для кожної пари рядків і стовпців.
Матриці є розділяємими змінними, оголошеними таким чином.
double а[n,n], b[n,n], с [n,n];
За умови, що n вже оголошене та ініціалізоване, цей вираз резервує пам'ять для трьох масивів дійсних чисел подвійної точності. За замовчуванням індекси рядків і стовпців змінюються від 0 до n-1.
Після ініціалізації масивів а і b можна обчислити добуток матриць за такою послідовною програмою.
for [і = 0 to n-1] {
for [j = 0 to n-1] {
# обчислити добуток а[i,*] і b[*,j]
с[i,j] = 0.0;
for [k = 0 to n-1]
с[i,j] = с[i,j] + а[i,k]*b[k,j];
}
}
Зовнішні цикли (з індексами і і j) повторюються для кожного рядка та стовпця. У внутрішньому циклі (з індексом k) обчислюється проміжний добуток рядка i матриці а та стовпця j матриці b; результат зберігається в комірці с[і,j]. Рядок з символом # на початку є коментарем.
Множення матриць — це приклад програми з масовим паралелізмом, оскільки програма містить велике число операцій, які можуть виконуватися паралельно. Дві операції можуть виконуватися паралельно, якщо вони незалежні. Припустимо, що множина операцій зчитування містить змінні, які вона читає але не змінює, а множина запису — змінні, які вона змінює (і, можливо, читає). Дві операції є незалежними, якщо їх множина запису не перетинається. Кажучи неформально, процеси завжди можуть безпечно читати змінні, які не змінюються. Проте двом процесам в загальному випадку небезпечно виконувати запис в одну і ту ж змінну або одному процесу читати змінну, яка записується іншим.
При множенні матриць обчислення проміжних добутків є незалежними операціями. Зокрема, рядки з 4 по 6 приведеної вище програми виконують ініціалізацію і подальше обчислення елементу матриці с. Внутрішній цикл програми читає рядок матриці а і стовпець матриці b, а потім читає і записує один елемент матриці с. Отже, множина читання для внутрішнього добутку — це рядок матриці а і стовпець матриці b, а множина запису — елемент матриці с.
Оскільки множина запису внутрішніх добутків не перетинається, їх можна виконувати паралельно. Можливі варіанти, коли паралельно обчислюються результуючі рядки, результуючі стовпці або групи рядків і стовпців. Нижче буде показано, як запрограмувати такі паралельні обчислення.
Спочатку розглянемо паралельне обчислення рядків матриці с. Його можна запрограмувати за допомогою оператора со (від "concurrent" — "паралельний"):
со [і = 0 to п-1] { # паралельне обчислення рядків
for [j = 0 to n-1] {
с[i,j] = 0.0;
for [до = 0 to n-1]
с[i,j] = с[i,j] + а[i,k]*b[к,j];
}
}
Між цією програмою і її послідовним варіантом є лише одна синтаксична відмінність — в зовнішньому циклі оператор for замінений оператором со. Але семантична різниця велика: оператор со визначає, що його тіло для кожного значення індексу і виконуватиметься паралельно (якщо не насправді, то, принаймні, теоретично, що залежить від числа процесорів).
Інший спосіб виконання паралельного множення матриць полягає в паралельному обчисленні стовпців матриці с. Його можна запрограмувати таким чином.
со [j = 0 to n-1] { #параллельное обчислення стовпців
for [і = 0 to n-1] {
с[i,j] = 0.0;
for [k = 0 to n-1]
с[i,j] = с[i,j] + а[i,k]*b[k,j];
}
}
Тут два зовнішні цикли (по і та по j) помінялися місцями. Якщо тіла двох циклів незалежні і приводять до обчислення однакових результатів, їх можна безпечно міняти місцями, як це було зроблено тут.
Програму для паралельного обчислення всіх проміжних добутків можна скласти декількома способами. Використовуємо одного оператора з для двох індексів.
со [і = 0 to n-1, j = 0 to n-1] { # всі рядки та
с[ і,j] = 0.0; # всі стовпці
for [до = 0 to n-1]
с[i,j]= с[i,j]+ а[i,k]*b[k,j];
}
Тіло оператора со виконується паралельно для кожної комбінації значень індексів і та j, тому програма задає п2 процесів. (Чи будуть вони виконуватися паралельно, залежить від конкретної реалізації.) Інший спосіб паралельного обчислення проміжних добутків полягає у використанні вкладених операторів со.
со [і = 0 to n-1] { # рядки паралельно, потім
со [j = 0 to n-1] { # стовпці паралельно
с[i,j]= 0.0;
for [k = 0 to n-1]
с[i,j]= с[i,j] + а[i,k]*b[k,j];
}
}
Тут для кожного рядка (зовнішній оператор со ) і потім для кожного стовпця (внутрішній оператор со ) задається по одному процесу. Третій спосіб написати цю програму — поміняти місцями два рядки останньої програми. Результат всіх трьох програм буде однаковим: виконання внутрішнього циклу для всіх п2 комбінацій значень і та j. Різниця між ними — в завданні процесів, а значить, і в часі їх створення.
Слід зазначити, що всі паралельні програми, приведені вище, були одержані заміною оператора for на со. Але ми зробили це тільки для індексів і та j. А як бути з внутрішнім циклом по індексу k? Чи не можна і цього оператора замінити оператором со ? Відповідь — "ні", оскільки тіло внутрішнього циклу як читає, так і записує змінну с[і,j]. Проміжний добуток — цикл for із змінною k — можна обчислити, використовуючи двійковий паралелізм, але для більшості машин це непрактично.
Інший спосіб визначити паралельні обчислення — використовувати декларацію (оголошення) process замість оператора со. По суті, process — це оператор со, виконуваний як "фоновий". Наприклад, перша паралельна програма з показаних вище — та, що паралельно обчислює рядки результату, — може бути записана таким чином:
process row[і = 0 to n-1] { # рядки паралельно
for [j = 0 to n-1] {
с[i,j] = 0.0;
for [k = 0 to n-1]
с[i,j] = с[i,j] + а[i,k]*b[k,j];
}
}
Тут визначений масив процесів — row[1], row[2] і т.д. — по одному для кожного значення індексу і. Ці n процесів створюються і починають виконуватися, коли зустрічається даний рядок опису. Якщо за декларацією процесу слідують оператори, то вони виконуються паралельно з процесом, тоді як оператори, записані після оператора со, не виконуються до його завершення. Декларації процесу, на відміну від операторів со, не можуть бути вкладені в інші декларації або операторів.
У програмах, приведених вище, для кожного елементу, рядка або стовпця результуючої матриці використано по одному процесу. Припустимо, що число процесорів в системі менше за n (так звичайно і буває, особливо коли n велике). Залишається ще очевидний спосіб повного використання всіх процесорів: розділити матрицю на смуги (рядків або стовпців) і для кожної смуги створити робочий процес. Зокрема, кожен робочий процес обчислює результати для елементів своєї смуги. Припустимо, що є Р процесорів та n кратно Р (тобто n ділиться на Р без остачі). Тоді при використанні смуг рядків робочі процеси можна запрограмувати таким чином.
process worker[w = 1 to P] { # смуги паралельно
int first = (w-1) * n/P; # перший рядок смуги
int last = first + n/P - 1; # останній рядок смуги
for [і = first to last] {
for [j = 0 to n-1] {
с[i,j] = 0.0;
for [k = 0 to n-1]
с[i,j] = с[i,j] + а[i,k]*b[k,j];
}
}
}
Відмінність цієї програми від попередньої полягає в тому, що n рядків діляться на Р смуг, по n/Р рядків кожна. Для цього в програму додані оператори, необхідні для визначення першого і останнього рядка кожної смуги. Потім рядки смуги указуються в циклі (по індексу і), щоб обчислити проміжні добутки для цих рядків.
Отже, істотною умовою розпаралелювання програми є наявність незалежних обчислень, тобто обчислень з непересічною множиною запису. Для добутку матриць незалежними обчисленнями є проміжні добуки, оскільки кожен з них записує (і читає) свій елемент с[і,j] результуючої матриці. Тому можна паралельно обчислювати всі проміжні добутки, рядки, стовпці або смуги рядків. Також паралельні програми можна записувати, використовуючи оператори со або оголошення process.
