Працюємо з Mono: Частина 6. Розробка багатопоточних додатків

  1. Серія контенту:
  2. Цей контент є частиною серії: Працюємо з Mono
  3. Створення та управління потоками
  4. Лістинг 1. Створення і запуск потоків
  5. Малюнок 1. Результат виконання програми
  6. Проблеми, що виникають при розробці багатозадачних додатків
  7. Синхронізація потоків через стандартну блокування
  8. Лістинг 2. Використання оператора lock
  9. Лістинг 3. Конфлікт внутрішньої і зовнішньої блокування
  10. Лістинг 4. Синхронізація доступу до загального ресурсу з різних потоків
  11. Малюнок 2. Результат виконання програми з синхронізацією доступу
  12. Лістинг 4. Синхронізація доступу до загального ресурсу за допомогою подій
  13. Блокування за допомогою мютексов
  14. Блокування за допомогою семафорів
  15. висновок
  16. Ресурси для скачування

Працюємо з Mono

Серія контенту:

Цей контент є частиною # з серії # статей: Працюємо з Mono

https://www.ibm.com/developerworks/ru/library/?series_title_by=**auto**

Слідкуйте за виходом нових статей цієї серії.

Цей контент є частиною серії: Працюємо з Mono

Слідкуйте за виходом нових статей цієї серії.

Сучасні багатопроцесорні системи і багатозадачні середовища надають програмістам величезна кількість можливостей для паралельного виконання завдань. Однак складна архітектура багатопоточних додатків призводить до виникнення нових проблем. У даній статті розглядаються можливості платформи Mono в області реалізації багатозадачності і синхронізації потоків і проблеми, що виникають при розробці подібних додатків.

Створення та управління потоками

За роботу з потоками в Mono відповідає простір імен System.Threading. Для створення потоку потрібно створити екземпляр класу Thread і передати в конструктор в якості параметра ім'я методу, який буде виконуватися в створюваному потоці. За замовчуванням потік створюється в зупиненому стані, і для його запуску потрібно викликати метод Start, як показано в лістингу 1.

Лістинг 1. Створення і запуск потоків

using System; using System.Threading; namespace Mono6_1 {class MainClass {static public int i = 0; public static void Main (string [] args) {Thread t1 = new Thread (T1_Run); Thread t2 = new Thread (T2_Run); t1.Start (); t2.Start (); } Public static void T1_Run () {while (true) {Console.WriteLine ( "T1, i = {0}", i); i ++; if (i> 50) break; }} Public static void T2_Run () {while (true) {Console.WriteLine ( "T2, i = {0}", i); i ++; if (i> 50) break; }}}}

Для компіляції і запуску в консолі необхідно ввести наступні команди:

gmsc Mono6_1.cs mono Mono6_1.exe

Малюнок 1. Результат виконання програми
Працюємо з Mono   Серія контенту:   Цей контент є частиною # з серії # статей: Працюємо з Mono   https://www

Нижче наводиться аналіз інформації, виведеної додатком. У момент запуску програми обидва потоку зчитують значення змінної i, потім виконання потоку T2 переривається і зі змінною працює тільки потік T1. У момент, коли управління було передано потоку T2 (i = 24), потік T1 встиг збільшити значення змінної, але не вивів рядок на екран. Потік T2 вивів на екран старе прочитане значення (i = 0), потім прочитав поточне значення (i = 25), збільшив його і вивів на екран (i = 26). Надалі така ж ситуація повторилася в момент перемикання з потоку T2 на потік T1.

Ця проблема виникає через те, що операційна система перериває роботу потоку в довільний момент, а переривання роботи потоку в середині циклу "читання-> ізмененіе-> запис" може привести до збою в роботі програми. Вирішити цю проблему можна за допомогою методів синхронізації, які розглядаються нижче.

Проблеми, що виникають при розробці багатозадачних додатків

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

Проблема «гонка за даними» (data race) виникає при спробі доступу до загального ресурсу з різних потоків. При цьому система може перервати виконання потоку в процесі взаємодії з пам'яттю, а потім відновити його в той момент, коли інший потік вже змінив вміст пам'яті. Так як перший потік нічого не знає про дії другого, його подальші дії з даними, швидше за все, будуть неправильними. Саме ця проблема і виникла в прикладі з лістингу 1. Для її вирішення використовуються різні способи синхронізації потоків.

Проблема «взаємне блокування» (dead lock) може виникнути від зайвого або неправильного застосування синхронізації. Наприклад, потік 1 призупиняє свою роботу і чекає якихось дій від потоку 2, а потік 2 призупиняє свою роботу і чекає дій від потоку 1. Для продовження роботи хоча б один з потоків повинен пройти далі, однак вони обидва заблоковані. З боку це виглядає як зависання потоків. Ця проблема вирішується шляхом перегляду алгоритмів роботи з об'єктами синхронізації.

Синхронізація потоків через стандартну блокування

У стандарті мови C # передбачена можливість синхронізації потоків без використання будь-яких додаткових класів. Для цього використовується оператор lock, що має такий синтаксис:

lock (змінна) {блок операторів}

В якості змінної може виступати будь-яка змінна посилального типу для об'єктів або вираз typeof (ім'я класу) для статичних змінних або для синхронізації всередині статичних методів класу. Приклад використання lock продемонстрований в лістингу 2.

Лістинг 2. Використання оператора lock

private void ThreadFunc1 () {lock (this) {// виконується доступ до загального ресурсу}} private void ThreadFunc2 () {lock (this) {// виконується доступ до загального ресурсу}}

Поки один потік знаходиться всередині блоку операторів lock, інший потік, дійшовши до блоку lock, буде змушений призупинити свою роботу. Використання this в якості змінної, що відповідає за блокування, - один з найпоширеніших випадків, рекомендований навіть в .NET SDK від Microsoft. Однак в інших джерелах (наприклад, стаття lock (this): do not ) Використовувати this (а точніше будь-public поле класу) не рекомендується, так як в цьому випадку із зовнішнього коду може бути виконана небажана блокування. Це може статися, якщо всередині класу блокування проводиться за this, а поза класом - по змінної-об'єкту класу, як показано в лістингу 3.

Лістинг 3. Конфлікт внутрішньої і зовнішньої блокування

// блокування всередині класу lock (this) {// доступ до загального ресурсу} // ... // блокування поза класом CClassWithBlocking cClassVar = new CClassWithBlocking (); lock (cClassVar) {// доступ до того ж ресурсу}

В даному випадку блокування lock (this) і lock (cClassVar) можуть заблокувати один одного. Клас, який використовує CClassWithBlocking, може нічого не знати про внутрішній устрій CClassWithBlocking і ненавмисно використовувати для блокування змінну cClassVar.

У лістингу 4 наведена змінена версія коду з лістингу 1 з додаванням синхронізації доступу до змінної i.

Лістинг 4. Синхронізація доступу до загального ресурсу з різних потоків

public static void T1_Run () {while (true) {lock (typeof (MainClass) {Console.WriteLine ( "T1, i = {0}", i); i ++; if (i> 50) break;}}} public static void T2_Run () {while (true) {lock (typeof (MainClass) {Console.WriteLine ( "T2, i = {0}", i); i ++; if (i> 50) break;}}}

На малюнку 2 наведено результат запуску зміненої програми. Як видно, тепер потоки не перериваються в процесі "чтенія-> зміни-> записи" змінної.

Малюнок 2. Результат виконання програми з синхронізацією доступу

Блокування за допомогою подій

Об'єкт EventWaitHandle є об'єктом синхронізації і може перебувати в одному з двох станів - нормальному і сигнальному. При синхронізації за допомогою даного об'єкта потік зупиняється, якщо EventWaitHandle знаходиться в нормальному стані, і продовжує свою роботу, якщо EventWaitHandle знаходиться в сигнальному стані.

Як правило, даний об'єкт синхронізації використовується для контролю доступу до загального ресурсу або для контролю проходження певної ділянки коду одним з потоків. Об'єкт-подія може бути безіменним (тоді область його використання обмежена потоками одного процесу) і іменованих (можливо управління об'єктом-подією з різних процесів).

Конструктор для створення безіменного об'єкта-події приймає на вхід два параметри:

  • bool bInitialState - початковий стан об'єкта; якщо значення цього параметра дорівнює true, то об'єкт створюється в сигнальному стані, якщо false - об'єкт створюється в нормальному стані.
  • EventResetMode mode - визначає спосіб переказу події з сигнального в нормальний стан і може приймати одне з двох значень: EventResetMode.ManualReset - переклад вручну або EventResetMode.AutoReset - автоматичний переклад.

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

Якщо потрібно звертатися до події за межами процесу, де воно було створено, то використовується іменоване подія, при створенні якого вказується третій параметр:

  • string name - ім'я створюваного події; якщо подія з такою назвою вже існує, то повертається об'єкт, прив'язаний до існуючого події, а зазначені в конструкторі параметри bInitialState і mode ігноруються.

Подія створюється наступним чином:

EventWaitHandle wh = new EventWaitHandle (true, EventResetMode.ManualReset);

Для перекладу події з сигнального стану в нормальне (для подій з ручним скиданням) використовується метод Reset:

wh.Reset ();

Для перекладу події з нормального в сигнальне використовується метод Set:

wh.Set ();

Очікування сигнального стану події здійснюється за допомогою методу WaitOne:

wh.WaitOne ();

Також існують дві перевантажені версії методу WaitOne з параметрами типу Int32 або типу TimeSpan, які задають інтервал часу очікування в мілісекундах. Час очікування використовується в ситуаціях, коли подія може ніколи (принаймні, в розумне з точки зору програми час) не перейти в сигнальний стан. Якщо не вказати час очікування, то зупинені потоки ніколи не будуть запущені знову.

Всі версії методу WaitOne () повертають значення типу bool, що дорівнює true, якщо подія перейшло в сигнальний стан, і false, якщо не перейшло (актуально для методів з періодом очікування). Після закінчення роботи з EventWaitHandle необхідно закрити об'єкт:

wh.Close ();

У лістингу 4 наведена ще одна модифікована версія приклади з лістингу 1, на цей раз з використанням об'єктів-подій.

Лістинг 4. Синхронізація доступу до загального ресурсу за допомогою подій

private EventWaitHandle wh; public static void Main (string [] args) {wh = new EventWaitHandle (true, EventResetMode.AutoReset); Thread t1 = new Thread (T1_Run); Thread t2 = new Thread (T2_Run); t1.Start (); t2.Start (); t1.Join (); t2.Join (); wh.Close (); } Public static void T1_Run () {while (true) {wh.WaitOne (); // wh.Reset (); Console.WriteLine ( "T1, i = {0}", i); i ++; wh.Set (); if (i> 50) break; }} Public static void T2_Run () {while (true) {wh.WaitOne (); // wh.Reset (); Console.WriteLine ( "T2, i = {0}", i); i ++; wh.Set (); if (i> 50) break; }}

У лістингу 4 створюється об'єкт-подія з автоматичним скиданням, так як для подібної ситуації використання ручного скидання не підходить. Щоб перевірити це твердження, слід змінити тип скидання з AutoReset на ManualReset і прибрати коментарі з рядків wh.Reset (). Якщо запустити таку версію програми, то синхронізація потоків працювати не буде, так як потік може бути перерваний між викликами WaitOne () і Reset (), в результаті чого іншого потік також може пройти WaitOne ().

Бувають ситуації, коли потрібно очікувати переходу в сигнальний стан не одного, а відразу декількох подій або одного з декількох. В даному випадку використовуються статичні методи класу WaitHandle - WaitAll і WaitAny.

Методу WaitAll як параметр передається масив об'єктів WaitHandle. Потік, зупинений методом WaitAll, продовжить свою роботу, тільки якщо всі об'єкти із заданого масиву перейдуть в сигнальний стан одночасно. Також, за аналогією з WaitOne, можна вказати значення часу очікування. Метод WaitAny має той же набір параметрів, що і WaitAll, проте дозволяє потоку продовжити роботу, якщо хоча б один елемент з масиву об'єктів перейде в сигнальний стан.

Також слід зазначити наявність двох об'єктів синхронізації, успадкованих від EventWaitHandle. Це об'єкти класів AutoResetEvent і ManualResetEvent. Об'єкт AutoResetEvent повністю аналогічний EventWaitHandle, створеному із зазначенням EventResetMode.AutoReset, а ManualResetEvent - EventWaitHandle, створеному із зазначенням EventResetMode.ManualReset. Ці два об'єкти не можуть бути іменованими і доступні тільки в межах їх створила процесу.

Блокування за допомогою мютексов

Об'єкт синхронізації мютекс (клас Mutex, від англ. MUTually EXclusive (взаємно виключає)) схожий на EventWaitHandle за деякими винятками:

  1. Мютекс оперує станами «захоплений» / «звільнений» замість «нормальне» / «сигнальне».
  2. Якщо мютекс захоплений потоком, то всі інші потоки при спробі захоплення мютекса будуть припинені до його звільнення;
  3. У разі, якщо потік спробує повторно захопити мютекс, то він не буде зупинений (інакше це призвело б до блокування роботи програми, так як потік вже захопив мютекс і не може його звільнити), але внутрішній лічильник захоплень мютекса буде збільшений. Таким чином, для звільнення мютекса, потік повинен буде звільнити його стільки разів, скільки захоплював.
  4. Після закінчення потрібно синхронізувати ділянки програми мютекс повинен бути звільнений. Якщо мютекс, захоплений потоком, не буде звільнений до його закінчення, то це вважається помилковою ситуацією, і при спробі іншого потоку захопити подібний мютекс виникне виключення AbandonedMutexException.

Іменований мютекс може використовуватися для синхронізації потоків, що належать різним процесам. Для створення мютекса можуть використовуватися кілька конструкторів.

Найпростіший конструктор не вимагає параметрів і створює вільний мютекс. Якщо передати в конструктор один параметр типу bool зі значенням true, то буде створено мютекс, спочатку захоплений створює потоком. Другим параметром в конструктор можна передати ім'я, і ​​якщо мютекс з таким ім'ям вже існує, то він буде повернутий і може використовуватися для синхронізації потоків з різних процесів.

Проте, щоб уникнути проблем з правами доступу до об'єктів операційної системи рекомендується відкривати існуючий мютекс за допомогою статичного методу OpenExisting, в який передається ім'я мютекса і звідки повертається об'єкт типу Mutex або виникає виняток WaitHandleCannotBeOpenedException, якщо такого мютекса в системі не існує.

Захоплення мютекса виконується при виклику методу WaitOne (аналогічний методу EventWaitHandle.WaitOne). При цьому якщо мютекс вже захоплена іншою потоком, то поточний потік припиняється. Звільнити мютекс можна викликом методу ReleaseMutex, а після закінчення використання закрити, викликавши метод Close.

Блокування за допомогою семафорів

Семафор (клас Semaphore) - це ще один об'єкт синхронізації, частково схожий на мютекс з тією різницею, що він може бути захоплений відразу декількома потоками одночасно. Хоча складно знайти ситуацію, коли використання семафорів на 100% необхідно і коли не можна замінити їх на інший тип синхронізації, іноді семафори виявляються єдиним можливим варіантом. Йдеться про ситуації, коли є обмежений (але не в однині) ресурс, яким можуть скористатися, припустимо, не більше, ніж п'ять потоків, а за володіння цим ресурсом можуть боротися десять потоків. Семафор має вбудований лічильник, який зменшується кожного разу при захопленні семафора потоком і збільшується до заданого при створенні значення при звільненні. Коли лічильник буде дорівнює нулю, потік, який спробує захопити семафор, буде зупинений до тих пір, поки хоча б один з потоків, вже захопили даний семафор, не звільнить його.

При створенні об'єкта типу Semaphore в його конструктор передаються два параметри типу Int32. Перший визначає поточне значення лічильника, а другий - його максимальне значення. У нормальній ситуації ці значення повинні збігатися, однак можна створити семафор, в якому частина ресурсів вже буде зарезервована (перший параметр буде менше другого). Для створення іменованого семафора в конструктор передається третій параметр - ім'я семафора. Як завжди, якщо семафор з вказаним ім'ям вже існує в системі, то він і буде повернений.

Щоб перевірити, чи існує вже семафор з вказаним ім'ям, можна скористатися статичним методом OpenExisting, в який передається ім'я семафора, а повертається об'єкт типу Semaphore або виникає виняток WaitHandleCannotBeOpenedException, якщо такого семафора не існує.

Захоплення семафора проводиться при виклику методу WaitOne. При цьому, якщо лічильник семафора дорівнює нулю, потік припиняється, в іншому випадку лічильник семафора зменшується на одиницю, і потік продовжує виконання.

Звільнити семафор можна викликом Release, при цьому лічильник семафора збільшується на одиницю і повертається попереднє значення лічильника. Існує ще один перевантажений метод Release, який приймає параметр типу Int32, в якому можна вказати на скільки необхідно зменшити значення лічильника. Наприклад, виклик:

s.Release (2);

звільнить семафор два рази (мається на увазі, що він вже був двічі захоплений в даному потоці). При спробі звільнити семафор більше число раз, ніж він був захоплений, виникне виключення SemaphoreFullException. Дане виняток сигналізує про помилку в коді програми, але ця помилка не обов'язково знаходиться в потоці, де виникло виключення, так як семафор міг бути помилково звільнений зайвий раз в іншій ділянці програми. Після закінчення використання семафор необхідно закрити, викликавши метод Close ().

висновок

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

Ресурси для скачування

Схожі теми

Підпишіть мене на повідомлення до коментарів

Com/developerworks/ru/library/?

Дополнительная информация

rss
Карта