НОУ ІНТУІТ, Лекція, Потоки
Критичні секції. Блокування. Оператор lock
Критичною секцією коду багатопотокового додатка називатимемо той фрагмент коду, в якому ведеться робота із загальним ресурсом потоків. Щоб вирішити проблему "перегонки даних", достатньо, щоб у критичній секції коду одночасно міг працювати тільки один потік, інші потоки повинні стояти в черзі, очікуючи, коли потік, що виконує критичну секцію, закінчить свою роботу. Після чого почати виконувати критичну секцію зможе інший потік. Така схема роботи називається "блокуванням" потоків. Поки один потік працює у критичній секції, робота всіх інших потоків, які претендують на загальний ресурс, блокується. Існують різні механізми блокування.
Розглянемо найпростіший механізм блокування, що ґрунтується на використанні оператора мови С# - оператора lock . Нехай у нашому додатку існує кілька критичних секцій, які використовують один і той самий ресурс. Різні потоки можуть входити до різних секцій. Проте, необхідно їх блокувати крім тієї, де працює активний потік , вже встиг захопити ресурс . Вирішення проблеми полягає в тому, що створюється певний об'єкт, видимий у всіх критичних секціях. Зазвичай це об'єкт універсального типу об'єкта з ім'ям, наприклад, locker . Потім кожна критична секція закривається оператором lock із ключем locker. Синтаксично конструкція блокування виглядає так:
Семантика конструкції така. Вважатимемо, що об'єкт locker може бути у двох станах - " відкритий " чи " закритий " . У вихідному стані об'єкт відкритий. Коли деякий потік досягає критичної секції з ключовим об'єктом locker , то якщо об'єкт відкритий, потік починає виконувати критичну секцію, попередньоперемикаючи об'єкт locker у стан "закритий". При завершенні критичної секції об'єкт locker перетворюється на стан "відкритий". Коли потік досягає критичної секції із закритим об'єктом locker , він стає в чергу виконання критичної секції. Як тільки locker відкривається, перший по черзі потік входить до критичної секції, блокуючи ресурс для інших потоків з черги.
Такий спосіб блокування дозволяє впоратися з проблемою "перегонки даних" та написати потокобезпечний додаток для роботи з банківськими рахунками.
Безпечна робота з банківськими рахунками
Прочитавши вищенаведений текст, програмісти Послідовників та Паралельнів зрозуміли свою помилку і досить швидко внесли до свого проекту необхідні зміни. Перш ніж ознайомитися з їх рішенням, спробуйте відповісти на запитання, як модифікувати існуюче рішення:
- Внести зміни до класу Account ;
- Створити новий клас Account_new;
- Змінити логіку роботи клієнтів із банківським рахунком;
- Інший варіант вирішення проблеми.
Перший варіант порушує один із основних принципів ОВП - ніколи не вносите зміни до вже працюючого класу при появі нових потреб. Клас Account добре справляється зі своїми обов'язками при послідовному програмуванні, коли немає потоків та паралельних обчислень.
Створювати новий клас є недоцільним, оскільки в існуючому класі багато корисного і не хочеться повторювати вже виконану роботу. Це знову порушує принципи ОВП.
Логіку роботи клієнтів міняти не слід. Гонка даних виникає у критичних секціях, пов'язаних із методами класу Account.
Правильним вирішенням проблеми є створення класу Safe_Account, що успадковує від класу Account. В нашоїЗавдання загальним ресурсом є об'єкт account, що представляє банківський рахунок, з яким працюють кілька клієнтів. Критичні секції виникають у всіх методах класу, що змінюють значення полів об'єкта. Ці методи вимагають блокування і відповідно перевизначення у класі нащадку. Ось як виглядає клас Safe_Account:
У класі вводиться статичний об'єкт Locker, який використовується для блокування критичних секцій, представлених методами Add та Sub - поповнення та зняття грошей з рахунку.
При роботі з клієнтами тепер необхідно використовувати об'єкт класу Safe_Account:
Тепер робота із сімейним рахунком виконується коректно. Ось як виглядають результати роботи після оновлення:
Як бачите, тепер усе гаразд. Перегонка даних усунена. Грошей знято рівно стільки, скільки належить.