Використання ThreadPool.QueueUserWorkItem в ASP.NET в сценарії високого трафіку


111

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

Ось ось, як я до цього часу робив невеликі асинхронні завдання:

ThreadPool.QueueUserWorkItem(s => PostLog(logEvent))

А стаття пропонує замість цього створити потік явно, подібний до:

new Thread(() => PostLog(logEvent)){ IsBackground = true }.Start()

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

Отже, моє запитання: чи правильна порада в статті?

Якщо на вашому веб-сайті потрапляє стільки трафіку, що ваш ThreadPool набирається, то краще вийти з поза діапазону, або якщо повний ThreadPool має на увазі, що ви все-таки доходите до межі своїх ресурсів, у такому випадку ви не слід намагатися запустити власні теми?

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


Сюжет згущується - я знайшов цю статтю ( blogs.msdn.com/nicd/archive/2007/04/16/… ), яку я не можу повністю розшифрувати. З одного боку, схоже, йдеться про те, що IIS 6.0+ завжди обробляє запити в потокових робочих потоках пулу (і що це можна зробити раніше ), але тоді є таке: "Однак якщо ви використовуєте нові сторінки асинхронізації .NET 2.0 ( Async = "true") або ThreadPool.QueueUserWorkItem (), тоді асинхронна частина обробки буде виконана всередині [потоку порту завершення]. " Асинхронна частина обробки ?
Jeff Sternal

Ще одне - це повинно бути досить простим для тестування на установці IIS 6.0+ (чого я зараз не маю), перевіривши, чи наявні робочі потоки пулу потоків нижче, ніж його максимальні робочі потоки, а потім зробити те ж саме в черзі робочі предмети.
Jeff Sternal

Відповіді:


104

Інші відповіді тут, здається, не залишають найважливішого моменту:

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

Це стосується як вільних потоків, створених new Thread(...), так і робочих потоків у ThreadPoolвідповіді на QueueUserWorkItemзапити.

Так, це правда, ви можете поголодувати ThreadPoolпроцесом ASP.NET, поставивши в чергу занадто багато елементів роботи. Це не дозволить ASP.NET обробляти подальші запити. Інформація у статті в цьому відношенні є точною; той самий пул потоків, який використовується QueueUserWorkItem, також використовується для обслуговування запитів.

Але якщо ви насправді в черзі вистачаєте робочих предметів, щоб викликати це голодування, то вам слід голодувати пулом ниток! Якщо ви одночасно виконуєте сотні операційних процесорів, яка користь буде мати ще один робочий потік для обслуговування запиту ASP.NET, коли машина вже перевантажена? Якщо ви стикаєтеся з цією ситуацією, вам потрібно повністю переробити дизайн!

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

Зокрема, ви повинні використовувати зворотні дзвінки async, підтримувані будь-яким класом бібліотеки, який ви використовуєте. Ці методи завжди дуже чітко марковані; вони починаються зі слів Beginі End. Як і в Stream.BeginRead, Socket.BeginConnect, WebRequest.BeginGetResponse, і так далі.

Ці методи роблять використовувати ThreadPool, але вони використовують IOCPs, у яких НЕ втручаються запитів ASP.NET. Вони являють собою особливий вид легкої нитки, який може бути "пробуджений" сигналом переривання від системи вводу / виводу. І в додатку ASP.NET у вас зазвичай є один потік вводу / виводу для кожного робочого потоку, тому кожен запит може мати одну чергу з асинхронною чергою. Це буквально сотні операцій асинхронізації без істотного зниження продуктивності (якщо припустити, що підсистема вводу / виводу може йти в ногу). Це набагато більше, ніж вам коли-небудь знадобиться.

Пам’ятайте лише про те, що делегати асинхронізу не працюють таким чином - вони в кінцевому підсумку використовують робочу нитку, як і раніше ThreadPool.QueueUserWorkItem. Це робити здатні лише вбудовані методи асинхрування класів бібліотеки .NET Framework. Можна зробити це самостійно, але це складно і трохи небезпечно і, ймовірно, виходить за рамки цієї дискусії.

Найкращою відповіддю на це питання, на мою думку, є не використання екземпляра ThreadPool або фонового Threadекземпляра в ASP.NET . Це зовсім не так, як закручувати нитку в додатку Windows Forms, де ви це робите, щоб інтерфейс інтерфейсу відповідав і не хвилювався, наскільки він ефективний. В ASP.NET ваша проблема полягає в пропускній спроможності , і все те, що контекстне перемикання всіх цих робочих потоків абсолютно знищує вашу пропускну здатність, використовуєте ви ThreadPoolчи ні.

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


Дякую за детальну відповідь, і ви маєте рацію, я намагаюся використовувати методи асинхронізації, коли це можливо (у поєднанні з контролерами async в ASP.NET MVC). У випадку мого прикладу, з віддаленим реєстратором, це саме те, що я можу зробити. Це цікава проблема дизайну, оскільки вона підштовхує керування асинхронією до самого низького рівня вашого коду (тобто, реалізація реєстратора), замість того, щоб мати можливість вирішити це з, скажімо, рівня контролера (в останньому випадку , вам знадобиться, наприклад, дві реалізації реєстратора, щоб мати можливість вибору).
Майкл Харт

@Michael: Асинхронні виклики Async, як правило, досить легко завершити, якщо ви хочете підняти його на більше рівнів; ви можете створити фасад навколо методів асинхронізації та обгорнути їх єдиним методом, який, наприклад, використовує Action<T>зворотний виклик. Якщо ви маєте на увазі, що вибір використання робочої нитки або потоку вводу / виводу відбувається на найнижчому рівні, це навмисно; тільки цей рівень може вирішити, чи потрібен він IOCP чи ні.
Aaronaught

Хоча, як цікаво, ThreadPoolтаким чином обмежує вас лише .NET , ймовірно, тому, що вони не довіряли розробникам, щоб це правильно встановили. У некерованого пулу Windows Thread Pool є дуже схожий API, але насправді дозволяє вибрати тип потоку.
Aaronaught

2
Порти завершення вводу / виводу (IOCP). опис МОКП не зовсім коректний. в IOCP у вас є статична кількість робочих ниток, які по черзі працюють над ВСІМИ відкладеними завданнями. не плутати з пулами ниток, які можуть бути фіксованими або динамічними за розміром, АЛЕ мають по одній нитці на кожне завдання - масштабно шалі. на відміну від ASYNC, у вас немає жодного потоку на кожне завдання. потік IOCP може трохи попрацювати над завданням 1, потім перейти до завдання 3, завдання 2, а потім знову повернутися до завдання 1. Стани сеансу завдання зберігаються і передаються між потоками.
MickyD

1
Що з вставками в базу даних? Чи існує команда ASYNC SQL (наприклад, Виконати)? Вставки в базу даних - це про найповільнішу операцію вводу / виводу навколо (через блокування), і тому, щоб основний потік чекати, щоб вставити рядок (ів), - це лише витрата циклів процесора.
Іван Томпсон

45

За Томасом Марквадом з команди ASP.NET в Microsoft, безпечно використовувати ASP.NET ThreadPool (QueueUserWorkItem).

Зі статті :

Q) Якщо мій додаток ASP.NET використовує потоки CLR ThreadPool, чи не голодуватиму ASP.NET, який також використовує CLR ThreadPool для виконання запитів? ..

A) Підводячи підсумок, не хвилюйтесь про голодування ASP.NET ниток, і якщо ви думаєте, що тут є проблема, повідомте мене про це, і ми подбаємо про це.

Q) Чи повинен я створити власні теми (нова тема)? Чи не буде це краще для ASP.NET, оскільки він використовує CLR ThreadPool.

А) Будь ласка, не варто. Або по-іншому, ні !!! Якщо ви справді розумні - набагато розумніші за мене, - ви можете створити власні теми; інакше навіть не думай про це. Ось кілька причин, чому не слід часто створювати нові теми:

  1. Це дуже дорого, порівняно з QueueUserWorkItem ... До речі, якщо ви можете написати кращий ThreadPool, ніж CLR, я рекомендую подати заявку на роботу в Microsoft, тому що ми точно шукаємо таких людей, як ви!

4

Веб-сайти не повинні обходити нитки нересту.

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

- Редагувати

Я описав реалізацію тут: Обробка фонової обробки на основі черг у веб-додатку ASP.NET MVC

- Редагувати

Щоб розширити, чому це навіть краще, ніж просто нитки:

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

Це також дозволяє пакетно обробляти будь-які завдання, які ви намагалися виконати (надсилайте електронні листи / що завгодно).


4
Я не погоджуюся, що ця заготовка завжди є вірною, особливо для некритичних завдань. Створення сервісу Windows, лише для асинхронного ведення журналу, безумовно, здається непосильним. Крім того, ця опція не завжди доступна (можливість розгортати MSMQ та / або службу Windows).
Майкл Харт

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

2
Не всі асинхронні завдання створюються рівними, саме тому, наприклад, асинхронні сторінки існують в ASP.NET. Якщо я хочу отримати результат від віддаленої веб-служби для відображення, я не збираюся робити це через MSMQ. У цьому випадку я пишу до журналу за допомогою віддаленої публікації. Це не відповідає проблемі написати службу Windows, а також підключити MSMQ для цього (і я не можу, оскільки саме ця програма в Azure).
Майкл Харт

1
Поміркуйте: ви пишете на віддалений хост? Що робити, якщо цей хост не працює або недоступний? Чи хочете ви повторно спробувати свої записи? Можливо, ви хочете, можливо, не будете. З вашою реалізацією важко спробувати. З сервісом він стає досить банальним. Я вдячний, що ви, можливо, не зможете цього зробити, і я дозволю комусь іншому відповісти на конкретні проблеми зі створенням тем з веб-сайтів [тобто, якщо ваша тема не була тлом тощо], але я окреслю "належне" спосіб це зробити. Я не знайомий з блакитністю, хоча я використовував ec2 (ви можете встановити ОС на цьому, так що все нормально).
полудень Шовк

@silky, дякую за коментарі. Я сказав "не критично", щоб уникнути цього більш важкого (але довговічного) рішення. Я уточнив це питання, щоб було зрозуміло, що я не прошу кращої практики навколо чергових предметів роботи. Azure підтримує такий тип сценаріїв (у нього є власне сховище для черги) - але операція черги занадто дорога для синхронного ведення журналів, тому мені все одно знадобиться асинхронне рішення. У моєму випадку я знаю про неполадки відмови, але я не збираюся додавати більше інфраструктури на випадок, якщо цей конкретний постачальник лісозаготівень не вдасться - у мене є й інші постачальники лісозаготівлі.
Майкл Харт

4

Я безумовно думаю, що загальною практикою для швидкої асинхронної роботи з низьким пріоритетом в ASP.NET було б використання пулу потоків .NET, особливо для сценаріїв з високим трафіком, оскільки ви хочете, щоб ваші ресурси були обмежені.

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

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

Якщо у вас немає проблеми з продуктивністю, тоді вибір нерестування нових потоків для зменшення суперечок із чергою запитів ASP.NET є класичною передчасною оптимізацією.

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


2

Ви повинні використовувати QueueUserWorkItem і уникати створення нових тем, як ви б уникнули чуми. Для наочності, яка пояснює, чому ви не будете голодувати ASP.NET, оскільки він використовує той же ThreadPool, уявіть дуже кваліфікованого жонглера двома руками, щоб тримати півтузина шпильок для боулінгу, мечів чи будь-чого іншого в польоті. Для наочності того, чому створювати власні теми, погано, уявіть, що відбувається в Сіетлі в годину пік, коли сильно використовувані вхідні пандуси до шосе дозволяють транспортним засобам негайно входити в рух, а не використовувати світло та обмежувати кількість під’їздів до одного кожні кілька секунд. . Нарешті, для детального пояснення перейдіть за цим посиланням:

http://blogs.msdn.com/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx

Спасибі, Томасе


Це посилання дуже корисне, дякую за це Томас. Мені буде цікаво почути, що ви також думаєте про відповідь @ Aaronaught.
Майкл Харт

Я погоджуюся з Aaronaught, і те саме сказав у своїй публікації в блозі. Я кажу так: "Намагаючись спростити це рішення, вам слід переключитися [на інший потік] лише якщо ви інакше заблокуєте потік запиту ASP.NET, поки нічого не зробите. Це надмірне спрощення, але я намагаюся щоб зробити рішення простим ». Іншими словами, не робіть цього для неблокуючих обчислювальних робіт, але робіть це, якщо ви робите запити на асинхронну веб-службу на віддалений сервер. Слухай Aaronaught! :)
Томас

1

Ця стаття не є правильною. ASP.NET має власний пул потоків, керовані робочі потоки для обслуговування запитів ASP.NET. Цей пул, як правило, становить кілька сотень потоків і є окремим від пулу ThreadPool, який є деяким меншим кратним процесором.

Використання ThreadPool в ASP.NET не заважатиме робочим потокам ASP.NET. Використання ThreadPool чудово.

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

Використання нової нитки для кожного повідомлення, безумовно, зайве.

Іншою альтернативою, якщо ви говорите лише про ведення журналів, є використання бібліотеки типу log4net. Він обробляє журнал в окремому потоці та опікується всіма питаннями контексту, які можуть виникнути в цьому сценарії.


1
@Sam, я фактично використовую log4net і не бачу, щоб журнали записувалися в окрему нитку - чи є якийсь варіант, який мені потрібно включити?
Майкл Харт

1

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

Є один пул потоків на один процес. Пул потоків має розмір за замовчуванням 250 робочих ниток на доступний процесор та 1000 потоків завершення вводу / виводу. Кількість потоків у пулі потоків можна змінити за допомогою методу SetMaxThreads. Кожен потік використовує стандартний розмір стека та працює за пріоритетом за замовчуванням.


Одна програма, що працює в одному процесі, цілком здатна знизити себе! (Або, принаймні, приниження власної продуктивності, щоб зробити нитку пул програшною пропозицією.)
Jeff Sternal

Тому я здогадуюсь, що запити ASP.NET використовують потоки завершення вводу / виводу (на відміну від робочих потоків) - це правильно?
Майкл Харт

З статті Фріца Онона я пов’язав у своїй відповіді: "Ця парадигма змінює [від IIS 5.0 до IIS 6.0] спосіб обробки запитів в ASP.NET. Замість того, щоб відправляти запити з inetinfo.exe в робочий процес ASP.NET, http. систематизує безпосередньо черги кожного запиту у відповідному процесі. Таким чином, всі запити тепер обслуговуються робочими потоками, витягнутими з пулу потоків CLR і ніколи в потоках вводу / виводу . " (мій наголос)
Jeff Sternal

Гммм, я все ще не зовсім впевнений ... Ця стаття з червня 2003 року. Якщо ви читаєте цю травню з травня 2004 року (правда, вона ще досить стара), в ній написано: "Тестову сторінку Sleep.aspx можна використовувати для збереження ASP Нитка .NET I / O зайнята ", де Sleep.aspx просто приводить до сну поточного виконуваного потоку: msdn.microsoft.com/en-us/library/ms979194.aspx - Коли я отримаю можливість, я побачу якщо я можу зашифрувати цей приклад та протестувати на IIS 7 та .NET 3.5
Майкл Харт

Так, текст цього абзацу заплутаний. Далі в цьому розділі він посилається на тему підтримки ( support.microsoft.com/default.aspx?scid=kb;EN-US;816829 ), яка пояснює речі: запуск запитів у потоках завершення вводу / виводу був .NET Framework 1.0 Проблема, яка була виправлена ​​в пакеті ASP.NET 1.1 червня 2003 року. Виправлення пакетного виправлення (після якого "ВСІ запити тепер виконуються на потоках Worker") Що ще важливіше, цей приклад досить чітко показує, що пул потоків ASP.NET - це той же пул потоків, на який піддається System.Threading.ThreadPool.
Jeff Sternal

1

На минулому тижні мені було задано подібне питання на роботі, і я дам вам таку ж відповідь. Чому ви багатопроменеві веб-програми на запит? Веб-сервер - це фантастична система, яка оптимізована для того, щоб своєчасно надавати безліч запитів (тобто багатопоточна передача). Подумайте, що відбувається, коли ви запитаєте майже будь-яку сторінку в Інтернеті.

  1. Здійснюється запит на деяку сторінку
  2. Html подається назад
  3. Html пропонує клієнту робити подальші реквіти (js, css, зображення тощо).
  4. Подальша інформація подається назад

Ви наводите приклад віддаленого ведення журналу, але це має стосуватися вашого реєстратора. Асинхронний процес повинен мати місце для своєчасного отримання повідомлень. Сем навіть зазначає, що ваш реєстратор (log4net) повинен це вже підтримувати.

Сем також правильний у тому, що використання пулу потоків у CLR не спричинить проблем із пулом потоків у IIS. Тут слід потурбуватись про те, що ви не породжуєте нитки в процесі, ви нерестуєте нові нитки з потоків ниток ниток IIS. Є різниця, і відмінність важлива.

Нитки проти обробки

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

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

Джерело


3
@ Дякую, за вклад, але я добре знаю, як працює веб-сервер, і це не дуже важливо для цього питання - знову ж таки, як я вже говорив у запитанні, я не прошу вказівки щодо цього як архітектури проблема. Я прошу конкретну технічну інформацію. Що стосується того, що це "стурбованість реєстратора", яка вже повинна мати асинхронний процес - як ви вважаєте, що асинхронний процес повинен бути записаний реалізацією реєстратора?
Майкл Харт

0

Я не згоден із посиланням на цю статтю (C # feeds.com). Створити нову нитку легко, але небезпечно. Оптимальна кількість активних потоків для запуску на одному ядрі насправді напрочуд мала - менше 10. Не надто просто змусити машину витрачати час на перемикання потоку часу, якщо нитки створені для другорядних завдань. Нитки - це ресурс, який ЗАПОВІДЬ управління. Абстракція WorkItem є для вирішення цього питання.

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

Нарешті, у статті викладені досить прискіпливі твердження про небезпеку використання ThreadPool, але дійсно потрібно щось конкретне, щоб їх створити.


0

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

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


0

Ви можете використовувати Parallel.For або Parallel.ForEach та визначити межу можливих потоків, які ви хочете виділити, щоб плавно запускатись та запобігати голодуванню в пулі.

Однак для запуску у фоновому режимі вам потрібно використовувати чистий стиль TPL нижче в веб-додатку ASP.Net.

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;

ParallelOptions po = new ParallelOptions();
            po.CancellationToken = ts.Token;
            po.MaxDegreeOfParallelism = 6; //limit here

 Task.Factory.StartNew(()=>
                {                        
                  Parallel.ForEach(collectionList, po, (collectionItem) =>
                  {
                     //Code Here PostLog(logEvent);
                  }
                });
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.