Приклад розробки простого многопоточного мережевого сервера частина 5

Цей контент є частиною серії: Приклад розробки простого многопоточного мережевого сервера

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

Після знайомства в попередній частині циклу з організацією мережевих з'єднань по TCP логічно подумати про реакцію програми на мережеві запити. На нинішньому етапі розвитку технологій підходів, як зазвичай, кілька, що дозволяє нам створювати гнучкі програми, в хорошій ступеня відповідні передбачуваним використання і навантаженні. Традиційно, немає кращого підходу, є доречний в кожному конкретному випадку. Існуючі методи реагування на вхідні запити можна скомбінувати, наприклад, в такому списку:

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

проста обробка

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

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

многопроцессность обробка

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

Новий процес породжується за допомогою функції fork (2). При цьому немає потреби щось явно передавати нащадку, оскільки на момент породження нащадок є копією батька (при цьому дескриптори з'єднань "множаться"). Після успішного породження нащадка нове з'єднання в контексті батька можна закрити і повернутися до очікування наступного. Перед цим в контексті батька варто якось запам'ятати ідентифікатор (pid) породженого процесу щоб пізніше, після його завершення, по цьому ідентифікатору можна було прибрати інформацію про завершився нащадку з таблиці процесів, щоб уникнути її захаращення і звалювання цієї діяльності на системний процес init. Як саме ми дізнаємося про завершення нащадка?

При зміні стану процесу-нащадка (припинений, відновлений або завершений) ядро ​​відправляє батькові сигнал SIGCHLD, який можна обробляти в контексті батька і робити відповідні дії. Обробку цього сигналу треба включати явно, оскільки за замовчуванням він ігнорується. По приходу сигналу можна запускати обхід списку збережених pid нащадків, на кожен викликати, наприклад, функцію waitpid (2) в неблокірующіх режимі і дивитися на результат. Можна уникнути такого перебору списку передачею батькові з нащадка (перед його завершенням) pid цього нащадка через будь-якої з методів IPC (Inter Process Communication) - канал (pipe), сокету, файл або разделямую пам'ять. В цьому випадку є "можливість" отримати відразу кілька pid в результаті майже одночасного завершення усіх нащадків, через що кілька сигналів можуть "злитися" в один. Проте, якщо ваше додаток працює відразу з безліччю нащадків, навіть таке "пакетне" отримання pid краще перебору всього списку на кожну появу сигналу. Побічним ефектом використання процесів для розпаралелювання обробки є відносна незалежність цієї самої обробки між процесами, так як у кожного процесу своє власне ізольований простір пам'яті і свої помилки. Все це означає, що критичний збій в одному з процесів зазвичай не призводить до краху всієї програми. Залежно від ваших уподобань і досвіду ізольованість пам'яті процесів може бути і мінусом, оскільки завдяки їй у процесів спочатку немає загальних змінних або якихось ще структур даних, і для обміну інформацією між ними доведеться докладати додаткові зусилля і організовувати його за допомогою методів IPC. При цьому потрібно приділити увагу і синхронізації (для обходу проблеми «письменників» і «читачів»), щоб дані при такому обміні не псувалися. Простим вирішенням цієї проблеми може бути використання каналів (pipes), але і у них є свої обмеження. Найбільш продуктивним при істотних обсягах (сотні кілобайт і більше) виглядає несинхронний обмін даними через пам'ять, що розділяється, але за умови, що дані використовуються за місцем зберігання, без додаткового копіювання з пам'яті, що кудись ще, а це зажадає уважного і обережного проектування і, швидше за все, ускладнить додаток.

багатопотокова обробка

В даний час в GNU / Linux потоки відрізняються від процесів в основному набором властивостей і речами на кшталт виклику функцій сімейства execve (2), exec (3) в одному з потоків. Тобто і процеси, і потоки є об'єктами планування для планувальника ядра, можуть незалежно один від одного блокуватися, отримувати сигнали і т.д. Власне, навіть породження процесів і потоків відбувається схожим чином за допомогою функції clone (2) перерахуванням потрібних прапорів, що визначають властивості породжується об'єкта (докладніше можна подивитися в мануалі до цієї функції).

Тим не менш, це все-таки деталі конкретної реалізації, а взаємини між процесами і потоками залишилися колишніми:

Відреагувати на вхідні повідомлення породженням потоку можна, наприклад, так (це теж тільки ілюстрація, а не повноцінний код):

Новий потік виконання створюється за допомогою pthread_create (3), яка, в разі успіху, розмістить в пам'яті за вказівником threadId ідентифікатор породженого потоку. Серед аргументів функції pthread_create (3) є один (з типом void *) для передачі чогось в потокову функцію (яка і буде точкою початку потоку). Передати можна і одну змінну (наприклад, дескриптор сокети з новим з'єднанням в нашому випадку), спочатку явно привівши його до типу void *, а потім виконавши зворотне приведення вже в потокової функції. Якщо потрібно передати якийсь блок (в тому числі і "різношерстих") даних, то передається покажчик на цей блок з тим же дворазовим явним приведенням типів.

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

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

Окремо варто зупинитися на завершенні потоку. У цьому контексті потоки можуть бути двох видів - приєднані (attached) і не приєднані (від'єднаний, відключені і т.д. detached). Перше для потоків означає приблизно те ж, що і для процесів - після завершення потоку-нащадка батьку необхідно "прибрати" за ним за допомогою функції pthread_join (3), для чого і потрібно зберегти ідентифікатор потоку (threadId в прикладі). Якщо ж вам не цікаво повертається потокової функцією значення або вона нічого не повертає, то можна створити потік відразу від'єднаний і після його завершення система не буде нічого зберігати і, відповідно, витрачати на це ресурси.

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

  • повторновходімость (re-entrancy): в загальному випадку неможливо передбачити як буде спланована робота процесів і потоків планувальником ядра (в якій послідовності, на який час) і може статися так, що один потік буде припинений під час виконання коду якоїсь функції і управління буде передано іншому потоку, який виконає ту ж функцію, а потім буде повернуто назад призупинення потоку. Повторновходімимі є функції, при виконанні коду яких потоки не помітять такий "підміни". З іншого боку, неповторновходімие функції можна використовувати як "індикатори фактів планування" (хоч і не зі 100% спрацьовуванням). Домогтися повторновходімості можна використанням тільки локальних змінних і винятком роботи з глобальними змінними на зміну.
  • локальне зберігання: перед роботою з усім потрібними даними вже в контексті потоку потрібно спочатку зробити їх копії в локальні для потокової функції змінні, які для кожного потоку свої власні.
  • атомарность: є ряд операцій, при виконанні яких ядро ​​гарантує безперервність, т. е. якщо вони розпочаті, то вони обов'язково будуть завершені в контексті поточного потоку без його припинення; як правило це елементарні операції на кшталт инкремента / декремента.

Процесно-потокові комбінації

Є об'єднанням двох вищезгаданих способів, при цьому зазвичай уникають породження процесів з потоків - навіщо вам копія процесу з купою займаються своїми справами потоків? Замість цього або все потрібні процеси породжуються заздалегідь, до створення з'єднань і породження потоків, або породжуються "між справою" в міру необхідності (тобто для відкладеного поповнення запасу процесів "на пару" при його вичерпання) з окремого, "зразкового" і / або "чистого" процесу. Подібний підхід виправданий в ситуаціях, коли час, що витрачається на породження процесу або потоку для обробки з'єднання має важливе значення і його необхідно скоротити до мінімуму. Розробка таких програм є досить складним заняттям в основному через необхідність ретельно продумати і налагодити межпроцессное і міжпоточної взаємодії. Тут також може знадобитися розробка якихось внутрішніх команд і / або процедур для більш гнучкого і керованого взаємодії процесів і потоків між собою.

висновок

Змішаний підхід можна застосувати коли потрібно об'єднати обидва згаданих варіанту в одному додатку.

У статті теж не порушена тема скасування потоків. Крім того, ця частина, як і інші, як і раніше має на увазі значний обсяг самостійної роботи.

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

Схожі теми

  • Приклад розробки простого многопоточного мережевого сервера з підтримкою призначених для користувача сесій на мові C в ОС GNU / Linux: Частина 1. Знайомство з оточенням розробки. Розбір параметрів командного рядка, підтримка вбудованої довідки, "демонізація" програми.
  • Приклад розробки простого многопоточного мережевого сервера з підтримкою призначених для користувача сесій на мові C в ОС GNU / Linux: Частина 2. Повноцінний розбір параметрів командного рядка.
  • Приклад розробки простого многопоточного мережевого сервера з підтримкою призначених для користувача сесій на мові C в ОС GNU / Linux: Частина 3. Робота з конфігураційних файлів, ініціалізація внутрішніх структур програми.
  • Приклад розробки простого многопоточного мережевого сервера з підтримкою призначених для користувача сесій на мові C в ОС GNU / Linux: Частина 4. Огляд методик введення / виведення в застосуванні до мережевим з'єднанням.
  • Приклад розробки простого многопоточного мережевого сервера з підтримкою призначених для користувача сесій на мові C в ОС GNU / Linux: Частина 5. Способи паралельної обробки мережевих запитів (процеси, потоки і їх комбінації).
  • Приклад розробки простого многопоточного мережевого сервера з підтримкою призначених для користувача сесій на мові C в ОС GNU / Linux: Частина 6. Механізми перевірки автентичності.