Потоки та нитки в Microsoft Windows

Матеріал з Вікі ЦДУ
Перейти до: навігація, пошук

Нить (англ. thread) або повніше нить виконання (англ. thread of execution), часто застосовується потік (програмний потік) та англіцизм тред — в інформатиці так називається спосіб програми розщепити себе на дві чи більше одночасні (чи псевдо-одночасні) задачі. Реалізація нитей та процесів відрізняються в різних операційних системах, але загалом нить міститься всередині процесу і різні ниті одного процесу спільно розподіляють деякі ресурси, в той час як різні процеси ресурси не розподіляють. В системах з одним процесором багатонитевість реалізується загалом поділом часу виконання («кванти часу»), дуже подібно до паралельного виконання багатьох задач: процесор послідовно переключається між різними нитями. Це переключення контексту відбувається настільки швидко, що у кінцевого користувача створюється ілюзія одночасного виконання. На багатопроцесорних чи на багатоядерних системах робота нитей здійснюється справді одночасно, бо різні ниті і процеси виконуються буквально одночасно різними процесорами або ядрами процесора. Особливості квантування в опраційній системі Windows розглянуто у статті Квант (Windows). Багато сучасних операційних систем прямо підтримують квантування часу і багатопроцесорну роботу нитей через планувальник процесів. Ядро операційної системи дозволяє програмісту маніпулювати нитями через інтерфейс системних викликів. Деякі реалізації викликають ниті ядра, оскільки легковагові процеси (англ. lightweight process, LWP) є спеціальним типом нитей ядра, що розподіляють деякі стани і інформацію. Поза тим, програма може емулювати роботу нитей, використовуючи таймер, сигнали або інші методи, щоб перервати власне виконання і послідовно виконувати різні задачі власним квантуванням часу. Такий спосіб іноді зветься нитями користувацького простору (англ. user-space threads) або волокнами.

220px.png

Порівняння нитей з процесами

Ниті відрізняються від традиційних процесів багатозадачних операційних систем, в тому що процеси: на загал, незалежні, дублюють значну частину інформації про стан, мають окремий адресний простір, взаємодіють тільки через системні міжпроцесорні механізми комунікацій. Ниті всередині процесу, з іншої сторони, розподіляють інформацію про стан процесу, і прямо доступаються до спільної пам'яті та інших ресурсів. Переключення контексту між нитями процеса на загал швидше, ніж переключення контексту між процесами. Описуючи ситуацію такі системи, як Windows NT та OS/2, кажуть, що мають «дешеві» ниті та «дорогі» процеси; в інших операційних системах ситуація не дуже відмінна.

Багатонитевість є популярною моделлю програмування і виконання, бо дозволяє багатьом нитям існувати всередині контексту одного процесу, розподіляючи ресурси процесу, але виконуючись незалежно. Нитева модель програмування забезпечує розробникам корисні абстракції для паралельного виконання. Проте, можливо найцікавіше застосування технології в випадку, коли один процес виконується на багатопроцесорній системі. Ця перевага багатонитевих програм дозволяє їм скоріше виконуватися на комп'ютерах з кількома процесорами, процесорами з кількома ядрами, або на кластері машин, бо ниті програми природно надають собі можливість справді паралельного виконання. У такому випадку програміст має бути особливо обережним, щоб уникнути стану гонитви та інших непередбачених поведінок. Для забезпечення коректної роботи з даними, ниті часто потребують процедури доступу до даних в певному порядку. Ниті також вимагають атомарних операцій (часто реалізованих з використанням семафорів), щоб уникнути одночасної неузгодженої модифікації даних або їхнього читання, в той час як процес модифікує дані. Недбале поводження з такими примітивами може призвести до взаємного блокування. Планувальники операційних систем переключають ниті одним з двох можливих способів. Запобіжна або витісняюча багатонитевість загалом розглядається як кращий підхід, бо вона дозволяє операційній системі визначити, коли може статися перемикання контексту. На противагу, кооперативна багатонитевість покладається на самі ниті, щоб вони передали контроль виконання на точках зупину. Це може створити проблеми, якщо нить чекає на ресурс, який став недоступним. Недолік запобіжної багатонитевості в тому, що перемикання контексту може статися в недоречну мить, спричиняючи пріоритетну інверсію або інші погані ефекти, яких можна уникнути в кооперативній багатонитевості. Традиційно базове комп'ютерне апаратне забезпечення не надавало багато підтримку багатонитевості, оскільки переключення між нитями загалом є вже скорішою процедурою порівняно з переключенням контексту процесів. Процесори вбудованих систем, що мають більші вимоги до поведінки в реальному часі, можуть мати апаратну підтримку багатонитевості зменшенням часу перемикання, можливо розміщуючи виділений блок регістрів для кожної ниті, замість того, щоб зберігати/читати загальні регістри. На кінці 1990-х стала відома ідея виконання інструкцій з різних нитей одночасно, це зветься одночасною багатонитевостю. Ця можливість була введена в процесорі Intel Pentium 4 під ім'ям Hyper-threading.

Переваги багатонитевості Багатонитевість програми створює підґрунтя для реалізації реальної багатозадачності — виконання кількох завдань одночасно (якщо обчислювальна система є багатопроцесорною), або «псевдоодночасно» («майже одночасно») на однопроцесорних системах. Наявність кількох нитей дозволяє: Оптимізувати організацію поведінки програми. Часто поведінка програми може бути організована у кілька незалежних паралельних алгоритмів, тоді їх можна винести в окремі ниті. При цьому їм можна задавати різний пріоритет виконання. Обходити критичні до часу операції. Якщо програма має лише одну нить, то вона повинна зупинити все виконання при очікуванні повільних операцій, таких як запис у файл чи відображення засобами мультимедіа. При цьому процесор перебуває у простої, поки ця операція не завершиться. Якщо застосунок складається з кількох нитей, він може продовжувати виконання в окремих нитях, коли одна нить очікуватиме на завершення повільної операції. Реалізувати багатопроцесорну обробку. Якщо система, у якій працює програма, є мультипроцесорною, то можна сповна скористатися наявними обчислювальними ресурсами і підвищити її ефективність використанням кількох нитей. При цьому різні ниті можуть виконуватися одночасно на різних процесорах. Таким чином, доцільне використання нитей може значно поліпшити продуктивність і зручність використання програм. На одному процесорі багатонитевість відбувається шляхом тимчасової активізації різних нитей. Таке перемикання відбувається досить часто, щоб користувач сприймав виконання нитей як одночасне. У багатопроцесорних і багатоядерних системах ниті можуть реально виконуватися одночасно, при цьому кожен процесор або ядро обробляє окрему нить (або кілька нитей).

Загальна характеристика нитей Хоча в кожної ниті свій контекст виконання, кожна нить усередині одного процесу використовує його віртуальний адресний простір (а також інші ресурси, які належать процесу). Це означає, що всі ниті в процесі можуть записувати й зчитувати вміст пам'яті інших нитей цього процесу. Але ниті не можуть посилатися на адресний простір іншого процесу. Виняток може бути в ситуації, коли процес надає частину свого адресного простору як розділ загальної пам'яті через об'єкт «проектований файл» (file mapping object), або коли один із процесів має право на відкриття іншого процесу й використовує функції доступу до пам'яті між процесами. За замовчуванням у ниті немає власного маркера доступу, але він може отримати його, і це дозволить йому підміняти контекст захисту іншого процесу. Нить містить такі важливі елементи: завантажений для виконання код; вміст набору регістрів процесора, що відображають стан процесора; два стеки, один із яких використовується ниттю при виконанні в режимі ядра, а інший — у користувацькому режимі; закриту область пам'яті, яку називають локальною пам'яттю ниті (thread-local storage, TLS); вона використовується підсистемами, бібліотеками виконавчих систем (run-time libraries) і DLL; унікальний ідентифікатор ниті; іноді ниті мають свій контекст захисту, який використовується багатонитевими серверними програмами, що підміняють контекст захисту клієнтів. Стани нитей Initialized (ініціалізована). У цей стан нить входить у процесі свого створення. Ready (готова). Нить у стані готовності очікує виконання. Вибираючи наступну нить для виконання, диспетчер бере до уваги тільки пул нитей, готових до виконання. Standby (простоює). Нить у цьому стані вже обрана наступною для виконання на конкретному процесорі. У підходящий момент диспетчер перемикає контекст на цю нить. У стані Standby може перебувати тільки одна нить для кожного процесора в системі. Проте нить може бути витиснена навіть у цьому стані (якщо, наприклад, до початку виконання такої ниті до виконання буде готова нить з вищим пріоритетом). Running (виконується). Нить переходить у цей стан і починає виконуватися відразу після того, як диспетчер перемикає на неї контекст. Виконання ниті припиняється, коли вона завершується, витісняється ниттю з вищим пріоритетом, перемикає контекст на іншу нить, самостійно переходить у стан очікування або минає виділений йому квант процесорного часу (та інша нить з тим же пріоритетом готова до виконання). Waiting (очікує). Нить входить у цей стан кількома способами. Вона може самостійно почати очікування на синхронізуючому об'єкті або його змушує до цього підсистема оточення. Після закінчення очікування — залежно від пріоритету — або негайно починає виконуватися, або переходить у стан Ready. Transition (перехідний стан). Нить переходить у цей стан, якщо вона готова до виконання, але її стек ядра вивантажений з пам'яті. Щойно цей стек завантажується у пам’ять, нить переходить у стан Ready. Terminated (завершений). Закінчуючи виконання, нить переходить у стан Terminated. Після цього інформація про нить у виконавчій системі може бути знищена. Можливі переходи між станами нитей показано на діаграмі.

Niti.jpg

Планування нитей у Windows Системний алгоритм керування послідовністю виконання нитей та розподілу процесорного часу між ними називають плануванням нитей. У Windows реалізовано підсистему витісняючого планування нитей на основі рівнів пріоритету, у якій завжди виконується готова до виконання нить з найбільшим пріоритетом[2]. Але вибір ниті для виконання може бути обмежений набором процесорів, на яких вона може працювати. Це явище називають прив'язкою до процесорів (processor affinity). За замовчуванням нить виконується на будь-якому доступному процесорі, але можна змінити прив'язку до процесорів через функції планування. Обрана для виконання нить працює протягом певного періоду, який називають квантом. Квант визначає, скільки часу буде виконуватися нить, поки не настане черга іншої ниті з тим же пріоритетом (або вищим). Тривалість квантів залежить від трьох факторів: конфігураційних параметрів системи (довгі або короткі кванти), статусу процесу (активний або фоновий) і використання об'єкта «завдання» для зміни тривалості квантів. Однак нить може не повністю використати свій квант. Оскільки у Windows реалізовано витісняючий планувальник, то відбувається наступне. Щойно інша нить з вищим пріоритетом готова до виконання, поточна нить витісняється, навіть якщо її квант ще не минув. Тому нить може бути обрана для виконання і тут же витиснена, не скориставшись своїм квантом. Код Windows, відповідальний за планування, реалізований у ядрі. Оскільки цей код розосереджений по ядру, єдиного модуля або процедури з назвою "планувальник" немає. Сукупність процедур, що виконують ці обов'язки, називають диспетчером ядра (kernel's dispatcher). Диспетчеризація нитей може бути викликана кожною з наступних подій: нить готова до виконання — наприклад, вона щойно створена або вийшла зі стану очікування. нить виходить зі стану Running (виконується), оскільки її квант минув, або нить завершується, або переходить у стан очікування. пріоритет ниті змінюється в результаті виклику системного сервісу або самої Windows. змінюється прив'язка до процесорів, через що нить більше не може працювати на процесорі, на якому вона виконувався. У будь-якому випадку Windows повинна визначити, яку нить виконувати наступною. Вибравши нову нить, Windows перемикає контекст. Ця операція полягає в збереженні параметрів стану машини, пов'язаних з поточною ниттю, і завантаженні аналогічних параметрів для іншої ниті, після чого починається виконання нової ниті. Планування у Windows здійснюється на рівні нитей. Оскільки рішення, прийняті в ході планування, стосуються винятково нитей, система не звертає уваги на те, якому процесу належить нить. Якщо в процесу А є 10, у процесу В — 2 готові до виконання ниті, і всі 12 мають однаковий пріоритет, кожна з нитей теоретично отримає 1/12 процесорного часу, тому що Windows не стане порівну ділити процесорний час між двома процесами (вважаємо, що класи пріоритету процесів також однакові). Щоб зрозуміти алгоритми планування нитей, слід розглянути рівні пріоритету Windows.

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