Реалізація задачі виробників-споживачів за допомогою монітора



Розглянемо синхронізаційні примітиви, які можуть знадобитися під час реалізації цієї задачі.

· Для того, щоб забезпечити перебування в моніторі тільки одного потоку в конкретний момент часу, використовуватимемо м’ютекс 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 – коли умова, пов’язана з цими даними, починає виконуватися.

Як бачимо, м’ютексами і умовними змінними керують дані, що вони захищають. Так само вся концепція монітора побудована навколо спільно використовуваних даних. У разі розробки на С++ є сенс надати самим спільно використовуваним даним право відповідати за свою синхронізацію перетворенням їх в об’єкти класів та інкапсуляцією всіх синхронізаційних примітивів у методах цих класів. Рекомендують такий базовий підхід до розробки багатопотокових програм на С++.

  1. Виділити одиниці паралельного виконання. Зробити кожну з них потоком. Потоки можуть бути інкапсульовані у класи з методом go(), який виконує функція потоку.
  2. Виділити спільно використовувані структури даних. Зробити кожну з таких структур класом. Виділити методів класів – дії, які потоки виконуватимуть із цими структурами даних.
  3. Записати основний цикл виконання кожного потоку.

На цих трьох етапах ми поки що не займаємося синхронізацією – усе відбувається на більш високому рівні. Тепер для кожного класу потрібно виконати такі дії.


Дата добавления: 2018-04-05; просмотров: 411; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!