Использование 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 (), тогда асинхронная часть обработки будет выполняться внутри [потока порта завершения] ». Асинхронная часть обработки ?
Джефф Стернал,
Еще одна вещь - это должно быть достаточно легко протестировать на установке IIS 6.0+ (которой у меня сейчас нет), проверив, являются ли доступные рабочие потоки пула потоков ниже, чем его максимальное количество рабочих потоков, а затем проделав то же самое в очереди. рабочие элементы.
Джефф Стернал,

Ответы:

104

Другие ответы здесь, похоже, не учитывают самый важный момент:

Если вы не пытаетесь распараллелить операцию, интенсивно использующую ЦП, чтобы ускорить ее выполнение на сайте с низкой нагрузкой, нет никакого смысла использовать рабочий поток вообще.

Это касается как бесплатных потоков, созданных new Thread(...), так и рабочих потоков ThreadPool, отвечающих на QueueUserWorkItemзапросы.

Да, это правда, вы можете заморозить ThreadPoolпроцесс ASP.NET, поставив в очередь слишком много рабочих элементов. Это предотвратит обработку дальнейших запросов ASP.NET. Информация в статье верна в этом отношении; тот же пул потоков используется для QueueUserWorkItemобслуживания запросов.

Но если вы действительно ставите в очередь достаточно рабочих элементов, чтобы вызвать голод, тогда вам следует истощить пул потоков! Если вы выполняете буквально сотни операций с интенсивным использованием ЦП одновременно, какой смысл иметь другой рабочий поток для обслуживания запроса ASP.NET, когда машина уже перегружена? Если вы столкнулись с такой ситуацией, вам нужно полностью переделать дизайн!

В большинстве случаев я вижу или слышу о том, что многопоточный код используется ненадлежащим образом в ASP.NET, это не для постановки в очередь работы, интенсивно использующей процессор. Это для постановки в очередь работы, связанной с вводом-выводом. И если вы хотите выполнять операции ввода-вывода, вам следует использовать поток ввода-вывода (порт завершения ввода-вывода).

В частности, вы должны использовать асинхронные обратные вызовы, поддерживаемые любым классом библиотеки, который вы используете. Эти методы всегда очень четко обозначены; они начинаются со слов Beginи End. Как и в Stream.BeginRead, Socket.BeginConnect, WebRequest.BeginGetResponse, и так далее.

Эти методы действительно используют ThreadPool, но они используют IOCP, которые не мешают запросам ASP.NET. Это особый вид облегченного потока, который может быть «разбужен» сигналом прерывания от системы ввода-вывода. А в приложении ASP.NET у вас обычно есть один поток ввода-вывода для каждого рабочего потока, поэтому для каждого отдельного запроса может быть поставлена ​​в очередь одна асинхронная операция. Это буквально сотни асинхронных операций без значительного снижения производительности (при условии, что подсистема ввода-вывода может не отставать). Это намного больше, чем вам когда-либо понадобится.

Просто имейте в виду, что асинхронные делегаты не работают таким образом - они в конечном итоге будут использовать рабочий поток, как и ThreadPool.QueueUserWorkItem. На это способны только встроенные асинхронные методы классов библиотеки .NET Framework. Вы можете сделать это сами, но это сложно и немного опасно и, вероятно, выходит за рамки этого обсуждения.

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

Пожалуйста, если вы обнаружите, что пишете код потоковой передачи в ASP.NET - подумайте, можно ли его переписать для использования уже существующих асинхронных методов, а если нет, то подумайте, действительно ли вам нужен код чтобы вообще работать в фоновом потоке. В большинстве случаев вы, вероятно, будете добавлять сложность без чистой выгоды.

Aaronaught
источник
Спасибо за подробный ответ, и вы правы, я стараюсь по возможности использовать асинхронные методы (в сочетании с асинхронными контроллерами в ASP.NET MVC). В случае моего примера с удаленным регистратором это именно то, что я могу сделать. Однако это интересная проблема дизайна, потому что она подталкивает асинхронную обработку до самого нижнего уровня вашего кода (то есть реализации регистратора), вместо того, чтобы иметь возможность решить ее, скажем, с уровня контроллера (в последнем случае , вам понадобятся, например, две реализации регистратора, чтобы иметь возможность выбирать).
Майкл Харт
@Michael: асинхронные обратные вызовы обычно довольно легко обернуть, если вы хотите поднять их на большее количество уровней; вы можете создать фасад вокруг асинхронных методов и обернуть их, например, одним методом, который использует Action<T>в качестве обратного вызова. Если вы имеете в виду, что выбор между использованием рабочего потока или потока ввода-вывода происходит на самом низком уровне, это сделано намеренно; только этот уровень может решить, нужен ли ему IOCP.
Aaronaught
Хотя, что интересно, только .NET ThreadPoolограничивает вас таким образом, вероятно, потому, что они не доверяли разработчикам, чтобы сделать это правильно. Неуправляемый пул потоков Windows имеет очень похожий API, но на самом деле позволяет вам выбирать тип потока.
Aaronaught
2
Порты завершения ввода-вывода (IOCP). описание IOCP не совсем корректное. в IOCP у вас есть статическое количество рабочих потоков, которые по очереди работают над ВСЕМИ незавершенными задачами. не путать с пулами потоков, которые могут быть фиксированного или динамического размера, НО имеют один поток на задачу - ужасно масштабируются. в отличие от ASYNC, у вас нет одного потока на задачу. поток IOCP может немного поработать над задачей 1, затем переключиться на задачу 3, задачу 2, а затем снова вернуться к задаче 1. состояния сеанса задачи сохраняются и передаются между потоками.
MickyD
1
А как насчет вставок в базу данных? Есть ли команда ASYNC SQL (например, «Выполнить»)? Вставки в базу данных - это самая медленная операция ввода-вывода (из-за блокировки), и ожидание основного потока вставки строки (строк) - это просто пустая трата циклов ЦП.
Ян Томпсон
45

Согласно Томасу Маркуадту из группы ASP.NET в Microsoft, безопасно использовать ASP.NET ThreadPool (QueueUserWorkItem).

Из статьи :

В) Если мое приложение ASP.NET использует потоки CLR ThreadPool, не буду ли я голодать по ASP.NET, которая также использует CLR ThreadPool для выполнения запросов? ..

A) Подводя итог, не беспокойтесь о нехватке потоков ASP.NET, и если вы считаете, что здесь проблема, дайте мне знать, и мы позаботимся об этом.

В) Должен ли я создавать свои собственные темы (новые темы)? Разве это не будет лучше для ASP.NET, поскольку он использует CLR ThreadPool.

A) Пожалуйста, не надо. Или, иначе говоря, нет !!! Если вы действительно умны - намного умнее меня - тогда вы можете создавать свои собственные темы; иначе даже не думай об этом. Вот несколько причин, по которым вам не следует часто создавать новые темы:

  1. Это очень дорого по сравнению с QueueUserWorkItem ... Кстати, если вы можете написать лучший ThreadPool, чем CLR, я рекомендую вам подать заявку на работу в Microsoft, потому что мы определенно ищем таких людей, как вы !.
Тим П.
источник
4

Веб-сайты не должны порождать темы.

Обычно вы переносите эту функцию в службу Windows, с которой затем общаетесь (я использую MSMQ, чтобы общаться с ними).

-- Редактировать

Я описал реализацию здесь: Фоновая обработка на основе очередей в веб-приложении ASP.NET MVC

-- Редактировать

Чтобы объяснить, почему это даже лучше, чем просто потоки:

Используя MSMQ, вы можете общаться с другим сервером. Вы можете писать в очередь между машинами, поэтому, если вы по какой-то причине определите, что ваша фоновая задача слишком сильно использует ресурсы главного сервера, вы можете просто перенести ее довольно тривиально.

Это также позволяет вам выполнять пакетную обработку любой задачи, которую вы пытаетесь выполнить (отправлять электронные письма / что-то еще).

Полдень шелк
источник
4
Я бы не согласился с тем, что это общее заявление всегда верно - особенно для некритических задач. Создание службы Windows только для целей асинхронного ведения журнала определенно кажется излишним. Кроме того, этот вариант не всегда доступен (возможность развернуть MSMQ и / или службу Windows).
Майкл Харт,
Конечно, но это «стандартный» способ реализации асинхронных задач с веб-сайта (тема очереди против какого-либо другого процесса).
Noon Silk
2
Не все асинхронные задачи созданы равными, поэтому, например, в ASP.NET существуют асинхронные страницы. Если я хочу получить результат от удаленной веб-службы для отображения, я не буду делать это через MSMQ. В этом случае я пишу в журнал с помощью удаленного поста. Написание службы Windows или подключение MSMQ для этого не подходит (и я не могу, поскольку это конкретное приложение находится в Azure).
Майкл Харт,
1
Подумайте: вы пишете на удаленный хост? Что, если этот хост не работает или недоступен? Вы хотите еще раз попробовать написать? Может ты будешь, а может и нет. С вашей реализацией трудно повторить попытку. С сервисом это становится довольно тривиальным. Я понимаю, что вы, возможно, не сможете это сделать, и я позволю кому-то другому ответить на конкретные проблемы с созданием потоков с веб-сайтов [например, если ваш поток не был фоновым и т.д.], но я выделяю «правильный» способ сделать это. Я не знаком с azure, хотя использовал ec2 (вы можете установить на нем ОС, так что все в порядке).
Noon Silk
@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! :)
Thomas
1

Эта статья неверна. ASP.NET имеет собственный пул потоков, управляемые рабочие потоки для обслуживания запросов ASP.NET. Этот пул обычно состоит из нескольких сотен потоков и отделен от пула ThreadPool, который несколько меньше процессоров.

Использование ThreadPool в ASP.NET не будет мешать рабочим потокам ASP.NET. Использование ThreadPool - это нормально.

Также было бы приемлемо настроить один поток, который предназначен только для регистрации сообщений и использования шаблона производитель / потребитель для передачи сообщений журнала в этот поток. В этом случае, поскольку поток является долгоживущим, вы должны создать один новый поток для запуска регистрации.

Использование новой ветки для каждого сообщения определенно излишне.

Другой альтернативой, если вы говорите только о ведении журнала, является использование библиотеки вроде log4net. Он обрабатывает ведение журнала в отдельном потоке и заботится обо всех проблемах контекста, которые могут возникнуть в этом сценарии.

Сэмюэл Нефф
источник
1
@Sam, я на самом деле использую log4net и не вижу, чтобы журналы записывались в отдельном потоке - есть ли какая-то опция, которую мне нужно включить?
Майкл Харт,
1

Я бы сказал, что статья неправильная. Если у вас большой магазин .NET, вы можете безопасно использовать этот пул в нескольких приложениях и на нескольких веб-сайтах (используя отдельные пулы приложений), просто на основе одного оператора в документации ThreadPool :

На каждый процесс приходится один пул потоков. Пул потоков имеет размер по умолчанию 250 рабочих потоков на доступный процессор и 1000 потоков завершения ввода-вывода. Количество потоков в пуле потоков можно изменить с помощью метода SetMaxThreads. Каждый поток использует размер стека по умолчанию и работает с приоритетом по умолчанию.

Кристофер Ноблс
источник
Одно приложение, работающее в одном процессе, полностью способно выйти из строя! (Или, по крайней мере, ухудшение собственной производительности настолько, чтобы сделать пул потоков проигрышным.)
Джефф Стернал,
Итак, я предполагаю, что запросы ASP.NET используют потоки завершения ввода-вывода (в отличие от рабочих потоков) - это правильно?
Майкл Харт,
Из статьи Фрица Ониона я дал ссылку в своем ответе: «Эта парадигма меняет [с IIS 5.0 на IIS 6.0] способ обработки запросов в ASP.NET. Вместо отправки запросов из inetinfo.exe в рабочий процесс ASP.NET http. sys напрямую ставит в очередь каждый запрос в соответствующем процессе. Таким образом, все запросы теперь обслуживаются рабочими потоками, взятыми из пула потоков CLR, а не потоками ввода-вывода ». (выделено мной)
Джефф Стернал
Хммм, я все еще не совсем уверен ... Эта статья написана в июне 2003 года. Если вы читаете эту статью из мая 2004 года (по общему признанию, еще довольно старую), в ней говорится: «Тестовая страница Sleep.aspx может использоваться для хранения ASP. Поток ввода-вывода .NET занят », где 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 г. (после которого «ВСЕ запросы теперь выполняются в рабочих потоках»). Что еще более важно, этот пример ясно показывает, что пул потоков ASP.NET - это тот же пул потоков, который предоставляется System.Threading.ThreadPool.
Джефф Стернал,
1

На прошлой неделе мне задали аналогичный вопрос на работе, и я дам вам тот же ответ. Почему вы используете многопоточные веб-приложения по запросу? Веб-сервер - это фантастическая система, сильно оптимизированная для своевременного предоставления множества запросов (например, многопоточность). Подумайте, что происходит, когда вы запрашиваете практически любую страницу в Интернете.

  1. Сделан запрос на какую-то страницу
  2. HTML возвращается
  3. HTML сообщает клиенту, что нужно делать дополнительные запросы (js, css, изображения и т. Д.)
  4. Дополнительная информация возвращается

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

Сэм также прав в том, что использование пула потоков в CLR не вызовет проблем с пулом потоков в IIS. Однако здесь следует обратить внимание на то, что вы не создаете потоки из процесса, вы создаете новые потоки из потоков пула потоков IIS. Есть разница, и различие важно.

Потоки против процесса

И потоки, и процессы - это методы распараллеливания приложения. Однако процессы являются независимыми исполнительными модулями, которые содержат свою собственную информацию о состоянии, используют свои собственные адресные пространства и взаимодействуют друг с другом только через механизмы межпроцессного взаимодействия (обычно управляемые операционной системой). Приложения обычно делятся на процессы на этапе проектирования, и главный процесс явно порождает подпроцессы, когда имеет смысл логически разделить важные функции приложения. Другими словами, процессы - это архитектурная конструкция.

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

Источник

Ty.
источник
3
@Ty, спасибо за ввод, но я хорошо знаю, как работает веб-сервер, и это не имеет отношения к вопросу - опять же, как я сказал в вопросе, я не прошу руководства по этому поводу как архитектурный вопрос. Я прошу конкретную техническую информацию. Что касается того, что это «забота регистратора», у которого уже должен быть асинхронный процесс - как вы думаете, этот асинхронный процесс должен быть написан реализацией регистратора?
Майкл Харт,
0

Я не согласен с указанной статьей (C # feeds.com). Создать новую ветку легко, но опасно. Оптимальное количество активных потоков для запуска на одном ядре на самом деле удивительно мало - менее 10. Слишком легко заставить машину тратить время на переключение потоков, если потоки создаются для второстепенных задач. Потоки - это ресурс, который ТРЕБУЕТ управления. Абстракция WorkItem предназначена для этого.

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

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

Timbo
источник
0

Трудно получить окончательный ответ на вопрос, использует ли IIS один и тот же ThreadPool для обработки входящих запросов, и, похоже, изменился в версиях. Поэтому было бы неплохо не использовать чрезмерно потоки ThreadPool, чтобы в IIS было их много. С другой стороны, создание собственного потока для каждой мелкой задачи кажется плохой идеей. Предположительно, у вас есть какая-то блокировка в вашем журнале, поэтому только один поток может работать за раз, а остальные будут просто по очереди получать запланированные и незапланированные (не говоря уже о накладных расходах на порождение нового потока). По сути, вы сталкиваетесь с теми же проблемами, для предотвращения которых был разработан ThreadPool.

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

bmm6o
источник
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);
                  }
                });
Хайям
источник