Конкуренція потоків

Конфлікт (або конкуренція) потоків (ниток)— це ситуація під час роботи програми, коли різні нитки при доступі до спільного ресурсу порушують очікувану логіку виконання програми[1]. При цьому, коли кожна з цих ниток виконуватиметься сама (тобто інші нитки відсутні), то порушення виконання програми не спостерігатиметься.

Складності використання кількох нитей у програмах

[ред. | ред. код]
Кожна з нитей збільшує значення змінної А на 1, і в результаті отримуємо правильне значення змінної А=2
Кожна з нитей збільшує значення змінної А на 1, але через перемикання ниток у "несподіваний момент" отримуємо неочікуване значення змінної А=1

У зв’язку з тим, що Windows використовує витісняючу багатозадачність, при якій виконання будь-якої нитки може бути перерване в будь-який момент часу, доступ кількох ниток до однієї ділянки пам’яті процесу може спричинити суттєві проблеми[2]. Справа у тому, що багато операцій при виконанні програми повинні бути атомарними, тобто неподільними: коли нитка виконує таку дію, інші нитки повинні бачити її або ще непочатою, або вже завершеною. Якщо нитки не синхронізовані, то всі операції аж до рівня процесорних інструкцій є неатомарні.

Приклад конфлікту ниток

[ред. | ред. код]

Розглянемо найпростіший приклад збільшення спільної для кількох ниток змінної A (фрагмент програми подано мовою Delphi)[2].

var A: integer;    // Оголошення глобальної змінної .................... // Код нитки -------------------------------------- Inc(A); 

Цей оператор (Inc) на асемблерному рівні транслюється у три різних дії для процесора (завантаження числа в регістр eax, його збільшення на 1, та збереження у змінній A). Оскільки перемикання ниток може відбутися в будь-який момент, у тому числі між згаданими процесорними інструкціями, це може привести до непередбачуваного значення змінної A.

Розглянемо дві нитки, які виконують наведений вище код (Inc(A)). За сприятливих обставин ніякого конфлікту може не відбутися, і результат буде очікуваним: значення A збільшиться на 2 (це ілюструє перший рисунок).

Проте перемикання ниток зручним для нас чином — випадковість. Все може бути по-іншому, і тоді змінна A збільшиться на 1 та міститиме невірний результат (другий рисунок). Таку ситуацію і називають конфліктом (або конкуренцією) ниток.

Може здатися, що подібні ситуації настільки рідкісні, що згаданим ефектом можна знехтувати. Проте це не так. Розглянемо три однакових нитки, кожна з яких здійснює циклічний інкремент спільної змінної A. Функція нитки може бути такою:

function ThreadFunc(Ptr: Pointer): LongInt; var I : integer; begin  for I := 1 to 1000000 do begin    // Якийсь код    Inc(A);    // Якийсь код  end;   end; 

Логічно було б припустити, що після виконання всіх трьох ниток змінна A дорівнюватиме 3000000. Проте внаслідок згаданої конкуренції ниток це значення буде суттєво відрізнятися (на практиці, для системи на основі двоядерного процесора Intel Pentium Dual-Core з частотою 1.73ГГц результат коливається в межах 2600000 та 2800000).

Слід зауважити, що кількість помилок у підрахунку залежить від обсягу циклу у функції нитки: при малій кількості ітерацій вони всі можуть реалізуватися за один квант часу без перемикання ниток, і описаної помилки не виникне. В загальному можна констатувати, що чим більше часу займають операції зі спільною змінною, тим частішими будуть помилки. Тому значення змінної А з попереднього прикладу стає непередбачуване і може описуватися лише ймовірнісними категоріями.

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

Синхронізація ниток

[ред. | ред. код]

Детальніше : Синхронізація

Практично в кожній багатонитевій програмі потрібно узгоджувати між собою роботу ниток. Такі дії називають синхронізацією ниток. Необхідність у цьому може виникнути при доступі до спільних змінних (як демонструє останній приклад). В іншому випадку нитка не повинна продовжувати роботу, поки інша нитка не досягне певного етапу свого виконання. Завжди потрібно бути впевненим, що кілька ниток одночасно не записують дані в одну і ту ж ділянку пам’яті.

Для запобігання помилкам, які можуть виникати при узгодженні роботи кількох ниток у Windows, використовують засоби синхронізації: критичні секції, м’ютекси, семафори та події. Об’єкти синхронізації забезпечують доступ до системних ресурсів, які можуть перебувати під керуванням одного чи кількох процесів. Об’єкт синхронізації може перебувати у двох станах: сигнальному та несигнальному. Коли об’єкт перебуває у стані зайнятості, або несигнальному стані, очікуюча нитка не може працювати. Сигнальний стан об’єкта синхронізації дозволяє продовжити роботу нитки.

Для очікування на сигнальний стан об'єкта синхронізації використовують функції очікування[3]. Функції очікування (синхронізації) — це набір функцій API, які зупиняють виконання нитки, доки не буде досягнуто заданих умов. Такі функції перевіряють стан об’єктів синхронізації, і якщо вони перебувають у сигнальному стані, робота нитки відновлюється. В іншому випадку функція призупинить нитку, постійно опитуючи об’єкт синхронізації, поки він не перейде у сигнальний стан або не закінчиться заданий час очікування.

Джерела

[ред. | ред. код]
  1. Руссинович М. Внутреннее устройство Microsoft Windows : Windows Server 2003, Windows XP и Windows 2000. Мастер-класс / М.Руссинович, Д.Соломон ; пер. с англ. – 4-е изд. – М : Издательско-торговый дом "Русская редакция" ; СПб : Питер, 2005.
  2. а б Коноваленко І.В., Федорів П.С. Системне програмування у Windows з прикладами на Delphi, Т:ТНТУ.- 2012 [Архівовано 8 грудня 2012 у Wayback Machine.].
  3. Харт Джонсон М. Системное программирование в среде Windows / Джонсон М. Харт ; пер. с англ. – М. : Издательский дом "Вильямс", 2005. – 592с.

Дивись також

[ред. | ред. код]