Теорія і практика Java: Пули потоків і черга дій (исходники), Різне, Програмування, статті

Чому потік пулів?


Робота багатьох серверних додатків, таких як Web-сервери, сервери бази даних, сервери файлів або поштові сервери, пов’язана з вчиненням великої кількості коротких завдань, що надходять від будь-якого віддаленого джерела. Запит прибуває на сервер певним чином, наприклад, через мережеві протоколи (такі як HTTP, FTP або POP), через чергу JMS, або, можливо, шляхом опитування бази даних. Незалежно від того, як запит надходить, в серверних додатках часто буває, що обробка кожної індивідуальної завдання короткочасна, а кількість запитів велике.


Однією з спрощених моделей для побудови серверних додатків є створення нового потоку кожен раз, коли запит прибуває і обслуговування запиту в цьому новому потоці. Цей підхід в дійсності хороший для розробки прототипу, але має значні недоліки, що стало б очевидним, якщо б вам знадобилося розгорнути серверний додаток, що працює таким чином. Один з недоліків підходу “Потік-на-запит” полягає в тому, що системні витрати створення нового потоку для кожного запиту значні; a сервер, який створив новий потік для кожного запиту, буде витрачати більше часу і споживати більше системних ресурсів, створюючи і руйнуючи потоки, ніж він би витрачав, обробляючи фактичні запити користувачів.


На додаток до витрат створення та руйнування потоків, активні потоки споживають системні ресурси. Створення занадто великої кількості потоків в одній JVM (віртуальної Java-машині) може привести до нестачі системної пам’яті або пробуксовці через надмірне споживання пам’яті. Для запобігання пробуксовки ресурсів, серверних додатків потрібні деякі заходи з обмеження кількості запитів, що обробляються в заданий час.


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


Альтернативи пулів потоків


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


Інша поширена модель організації потокової обробки – наявність єдиного фонового потоку і черги завдань для задач певного типу. AWT (набір інструментальних засобів для абстрактних вікон) і Swing використовують цю модель, в якій є потік подій GUI (графічного інтерфейсу користувача), і вся робота, яка викликає зміни в інтерфейсі, повинна виконуватися в цьому потоці. Однак, оскільки існує тільки один AWT-потік, небажано виконувати завдання в потоці AWT, завершення якого може зайняти значну кількість часу. В результаті, додатки Swing часто вимагають додаткових потоків “виконавця” для вирішення довгострокових, пов’язаних з призначеним для користувача інтерфейсом (UI) завдань.


Підходи “потік-на-задачу” і “єдиний фоновий потік” можуть досить добре функціонувати в певних ситуаціях. Підхід “потік-на-задачу” добре работаeт з невеликою кількістю довгострокових завдань. Підхід “Єдиний фоновий потік” функціонує досить добре, якщо не важлива передбачуваність розподілу (scheduling predictability), як у випадку низькопріоритетних фонових (low-priority background) завдань. Однак більша частина серверних додатків орієнтовані на обробку великої кількості короткострокових завдань чи подзадач, і потрібно мати механізм для ефективного здійснення цих завдань з невеликими витратами, а також будь-яку міру управління ресурсами і передбачуваністю часу виконання. Пули потоку дають наступні переваги.


Черги дій


З точки зору того, як застосовуються пули потоків, термін “пул потоків” кілька оманливий, “очевидне” застосування пулу потоків в більшості випадків дає не той результат, який ми хотіли б. Термін “Пул потоків” пов’язаний з платформою Java і, можливо, є артефактом менш об’єктно-орієнтованого підходу. Проте термін, як і раніше широко використовується.


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


Те, що нам зазвичай потрібно – це робоча чергу в поєднанні з фіксованою групою робочих потоків, в якій використовуються wait (очікування) () і notify (повідомлення) (), Щоб сигналізувати очікують потокам, що прибула нова робота. Черга дій головним чином застосовується як зв’язаний список з приєднаним об’єктом монітора. Лістинг 1 показує приклад простий, об’єднаної в пул черги. Ця модель, використовуючи чергу Runnable (працездатних) об’єктів, є звичайною для планувальників і черг дій, хоча немає особливої ​​необхідності через Thread API використовувати інтерфейс Runnable.






public class WorkQueue
{
private final int nThreads;
private final PoolWorker[] threads;
private final LinkedList queue;
public WorkQueue(int nThreads)
{
this.nThreads = nThreads;
queue = new LinkedList();
threads = new PoolWorker[nThreads];
for (int i=0; i<nThreads; i++) {
threads[i] = new PoolWorker();
threads[i].start();
}
}
public void execute(Runnable r) {
synchronized(queue) {
queue.addLast(r);
queue.notify();
}
}
private class PoolWorker extends Thread {
public void run() {
Runnable r;
while (true) {
synchronized(queue) {
while (queue.isEmpty()) {
try
{
queue.wait();
}
catch (InterruptedException ignored)
{
}
}
r = (Runnable) queue.removeFirst();
}
// If we don”t catch RuntimeException,
// the pool could leak threads
try {
r.run();
}
catch (RuntimeException e) {
// You might want to log something here
}
}
}
}
}

Можливо, ви помітили, що в реалізації завдань у лістингу 1 використовується notify() замість notifyAll(). Більшість експертів радять використовувати notifyAll() замість notify(), І не даремно: є деякі моменти ризику, що асоціюються з notify(), Його використання є вірним в певних специфічних умовах. З іншого боку, в разі належного використання, notify() має більш бажані робочі характеристики, ніж notifyAll() ; В особливо те, що notify() викликає набагато менше перемикань процесів, що є важливим в роботі серверних додатків.


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


Можливий ризик при використанні пулів потоків


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


Взаімоблокіровка


У будь-якому багатопотоковому додатку є ризик взаимоблокировки. Кажуть, що набір процесів або потоків взаімоблокірован, коли кожен очікує події, яка може бути викликане іншим процесом. Найпростіший випадок взаимоблокировки – коли потік A повністю блокує об’єкт X і чекає блокування об’єкта Y, в той час як потік B повністю блокує об’єкт Y і чекає блокування об’єкта X. І якщо немає будь-якого способу вирватися з очікування блокування (що блокувальний пристрій Java не підтримує), взаімоблокірованние потоки будуть чекати вічно.


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


Пробуксовка ресурсів


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


Якщо пул потоків занадто великий, ресурси, що споживаються цими потоками, можуть значною мірою вплинути на роботу системи. Час буде марно витрачено на перемикання між потоками, і якщо у вас потоків більше, ніж необхідно, це може викликати проблеми ресурсного голодування, так кака потоки пулів споживають ресурси, які могли б бути більш ефективно використані іншими завданнями. На додаток до ресурсів, що використовуються самими потоками, робота, виконувана з обслуговування запитів, може також вимагати додаткових ресурсів, таких як з’єднання JDBC, сокети, або файли. Ці ресурси також обмежені, і занадто багато паралельних запитів можуть викликати збої, такі як неможливість визначенні місця JDBC-з’єднання.


Паралельні помилки


Пули потоків і інші механізми ведення черг спираються на методи wait() і notify(), Які можуть бути підступні. Повідомлення, якщо їх неправильно закодувати, можуть бути загублені, в результаті потоки залишаються в стані бездіяльності, навіть якщо в черзі є робота, яка повинна бути виконана. При використанні цих коштів необхідно дотримуватися великої обережності; навіть експерти роблять помилки при роботі з ними. Ще краще використовувати перевірену в роботі реалізацію, наприклад пакет util.concurrent, Який обговорюється в розділі Немає необхідності писати своє.


Витік потоку


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


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


Перевантаження запитами


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


Керівництво з ефективного використання пулів потоків


Пули потоків можуть бути надзвичайно ефективним способом структурування серверних додатків, за умови, що ви прямуєте деяким простим правилам:



Налаштування розміру пулу


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


Якщо ви пам’ятаєте, є дві основні переваги в організації потокової обробки повідомлень в додатках: можливість продовження процесу під час очікування повільних операцій, таких, як I / O (введення – висновок), і використання можливостей декількох процесорів. У додатках з обмеженням по швидкості обчислень, що функціонують на N-процесорної машині, додавання додаткових потоків може поліпшити пропускну здатність, у міру того як кількість потоків підходить до N, але додавання додаткових потоків понад N не виправдано. Дійсно, дуже багато потоків руйнують якість функціонування через додаткових витрат перемикання процесів


Оптимальний розмір пулу потоків залежить від кількості доступних процесорів і природи завдань в робочій черги. На N-процесорної системі для робочої черги, яка буде виконувати виключно завдання з обмеженням по швидкості обчислень, ви досягнете максимального використання CPU з пулом потоків, в якому міститься N або N +1 потік.


Для задач, які можуть чекати здійснення I / O (введення – виведення) – наприклад, завдання, що зчитує HTTP-запит з сокета – вам може знадобитися збільшення розміру пулу понад кількості доступних процесорів, тому, що не всі потоки будуть працювати весь час. Використовуючи профілювання, ви можете оцінити відношення часу очікування (WT) до часу обробки (ST) для типового запиту. Якщо назвати це співвідношення WT / ST, для N-процесорної системі вам знадобиться приблизно N * (1 + WT / ST) потоків для повної завантаженості процесорів.


Використання процесора – не єдиний фактор, важливий при налаштуванні розміру пулу потоків. У міру зростання пулу потоків, можна зіткнутися з обмеженнями планувальника, доступної пам’яті, або інших системних ресурсів, таких, як кількість сокетів, дескриптори відкритого файлу, або канали зв’язку бази даних


Немає необхідності писати своє


Даг Лі створив відмінну відкриту бібліотеку утиліт паралельності, util.concurrent, Яка включає об’єкти-мьютекс, семафори, колекції, такі як черги і хеш-таблиці, добре працюють при паралельному доступі, і кілька реалізацій робочої черги. Клас PooledExecutor з цього пакета – ефективна, широко використовується, правильна реалізація пулу потоків, заснованого на робочій черги. Перш ніж намагатися писати власне програмне забезпечення, яке цілком може виявитися неправильним, ви можете розглянути використання деяких утиліт в util.concurrent.


Бібліотека util.concurrent також служить натхненником для JSR 166, робочої групи Java Community Process (JCP), яка буде проводити набір паралельних утиліт для включення до бібліотеки класів Java в пакеті java.util.concurrent, І яка готує випуск Java Development Kit 1.5.


Висновок


Пул потоку – корисний інструмент для організації серверів додатків. Він досить простий по суті, але є деякі моменти, з якими слід бути обережними під час застосування та використання, такі як взаімоблокіровка, пробуксовка ресурсів, і складнощі, пов’язані з wait() і notify(). Якщо вам буде потрібно пул потоків для вашого додатки, розгляньте використання одного з класів Executor з util.concurrent, Такий як PooledExecutor, Замість створення нового з нуля. Якщо вам потрібно створити потоки для вирішення короткострокових завдань, вам безумовно слід розглянути використання замість цього пулу потоків.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*