Реалізація задачі виробників-споживачів за допомогою монітора
Розглянемо синхронізаційні примітиви, які можуть знадобитися під час реалізації цієї задачі.
· Для того, щоб забезпечити перебування в моніторі тільки одного потоку в конкретний момент часу, використовуватимемо м’ютекс lock. Він буде спільним для всіх функцій семафора, з ним працюватимуть як виробник, так і споживач.
· Для організації очікування виробника у разі повного буфера потрібна умовна змінна, сигналізація якої означатиме, що місце у буфері звільнилося. Назвемо цю змінну not_full. Перед спробою додати новий об’єкт виробник перевіряє, чи буфер повний і, якщо це так, виконує очікування на цій змінній. Споживач, забравши об’єкт із буфера, сигналізує not_full, повідомляючи цим про наявність вільного місця виробникам, які очікують (і перевівши у стан готовності одного з них).
· Для організації очікування споживача під час порожнього буфера потрібна умовна змінна, сигналізація якої означатиме, що у буфері з’явиться об’єкт. Назвемо цю змінну not_empty. Перед спробою забрати об’єкт із буфера споживач перевіряє і, якщо це так, виконує очікування на цій змінній. Виробник, додавши об’єкт у буфер, сигналізує not_empty, повідомляючи цим про наявність об’єктів споживачам, які очікують (і перевівши у стан готовності одного з них).
Завданнями функцій монітора буде забезпечення роботи із буфером. Для кращої організації коду запишемо ці функції окремо від коду виробника і споживача.
|
|
Ось псевдокод розв’язання цієї задачі:
mutex_t lock; //для критичної секції
condition_t not_empty; //сигналізує про те, що буфер непорожній
condition_t not_full; //сигналізує про те, що буфер неповний
int n = 100; //максимально можлива кількість елементів
//виробник
void producer () {
item_t item = produce() //створити об’єкт
put_into_buffer(item);
}
//споживач
void consumer () {
item_t item = obtain_from_buffer();
consume(item); //спожити об’єкт
}
//функції монітора
void put_into_buffer(item_t item) {
mutex_lock(lock); //вхід у критичну секцію
while(count_items_in_buffer() == n)
wait (not_full, lock); //чекати, поки буфер повний
//на цей час зняти блокування
append_to_buffer(item); //додати об’єкт item у буфер
signal(not_empty, lock); //повідомити, що є новий об’єкт
mutex_unlock(lock); //вихід із критичної секції
}
item_t obtain_from_buffer () {
item_t item;
mutex_lock(lock); //вхід у критичну секцію
while (count_items_in_buffer() == 0)
wait (not_empty, lock); //чекати, поки буфер порожній
//на цей час зняти блокування
item = receive_from_buffer(); //забрати об’єкт item із буфера
signal(not_full, lock); //повідомити, що є вільне місце
mutex_unlock(lock); //вихід із критичної секції
|
|
return item;
}
Цей код зрозуміліший, ніж код із використанням семафорів, насамперед тому, що не потрібно здогадуватися, що означає збільшення або зменшення того чи іншого семафора – відразу видно, який примітив відповідає за взаємне виключення, а який – за організацію очікування.
Деякі джерела взагалі не рекомендують користуватися семафорами для синхронізації, обмежуючи себе тільки моніторами (м’ютексами й умовними змінними). Треба, однак, зазначити, що:
· у деяких системах (наприклад, у Linux до появи NPTL) семафори – це єдиний засіб синхронізації потоків різних процесів;
· в інших системах (наприклад, у Win32 API) майже не підтримуються умовні змінні (принаймні, реалізувати їх там складно)ж
· існує великий обсяг коду, написаного із використанням семафорів, який може виявитись необхідним для читання і підтримки.
Загальна стратегія організації паралельного виконання
Для коректної організації виконання багатопотокових програм особливо важливі два із розглянутих раніше правил.
· М’ютекс захищає не код критичної секції, а спільно використовувані дані всередині цієї секції.
· Виклик wait для умовної змінної відбувається тоді, коли не виконується умова, пов’язана зі спільно використовуваними даними всередині критичної секції, виклик signal – коли умова, пов’язана з цими даними, починає виконуватися.
|
|
Як бачимо, м’ютексами і умовними змінними керують дані, що вони захищають. Так само вся концепція монітора побудована навколо спільно використовуваних даних. У разі розробки на С++ є сенс надати самим спільно використовуваним даним право відповідати за свою синхронізацію перетворенням їх в об’єкти класів та інкапсуляцією всіх синхронізаційних примітивів у методах цих класів. Рекомендують такий базовий підхід до розробки багатопотокових програм на С++.
- Виділити одиниці паралельного виконання. Зробити кожну з них потоком. Потоки можуть бути інкапсульовані у класи з методом go(), який виконує функція потоку.
- Виділити спільно використовувані структури даних. Зробити кожну з таких структур класом. Виділити методів класів – дії, які потоки виконуватимуть із цими структурами даних.
- Записати основний цикл виконання кожного потоку.
На цих трьох етапах ми поки що не займаємося синхронізацією – усе відбувається на більш високому рівні. Тепер для кожного класу потрібно виконати такі дії.
Дата добавления: 2018-04-05; просмотров: 411; Мы поможем в написании вашей работы! |
Мы поможем в написании ваших работ!