Uso de ThreadPool.QueueUserWorkItem en ASP.NET en un escenario de alto tráfico

Siempre he tenido la impresión de que usar ThreadPool para tareas de segundo plano (digamos no críticas) de corta vida se consideraba la mejor práctica, incluso en ASP.NET, pero luego me encontré con este artículo que parece sugerir lo contrario: el El argumento es que debe abandonar el ThreadPool para tratar las solicitudes relacionadas con ASP.NET.

Así que aquí es cómo he estado haciendo pequeñas tareas asíncronas hasta ahora:

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

Y el artículo sugiere, en cambio, crear un hilo explícitamente, similar a:

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

El primer método tiene la ventaja de ser administrado y limitado, pero existe el potencial (si el artículo es correcto) de que las tareas en segundo plano compiten por hilos con los manejadores de solicitudes de ASP.NET. El segundo método libera el ThreadPool, pero a costa de no tener límites y, por lo tanto, puede consumir demasiados recursos.

Entonces mi pregunta es, ¿el consejo en el artículo es correcto?

Si su sitio recibía tanto tráfico que su ThreadPool se estaba llenando, entonces es mejor salir de banda, o un ThreadPool completo implicaría que está llegando al límite de sus recursos de todos modos, en cuyo caso no debería estar tratando de iniciar sus propios hilos?

Aclaración: solo pregunto en el scope de pequeñas tareas asincrónicas no críticas (p. Ej., Registro remoto), no elementos de trabajo costosos que requerirían un proceso separado (en estos casos, acepto que necesitará una solución más robusta).

Otras respuestas aquí parecen estar dejando de lado el punto más importante:

A menos que esté tratando de paralelizar una operación intensiva de CPU para hacerla más rápido en un sitio de baja carga, no tiene sentido utilizar un hilo de trabajo en absoluto.

Esto se ThreadPool tanto a los hilos libres, creados por el new Thread(...) , como a los hilos de trabajo en el ThreadPool que responden a QueueUserWorkItem peticiones QueueUserWorkItem .

Sí, es cierto, puede matar de hambre a ThreadPool en un proceso ASP.NET haciendo cola en demasiados elementos de trabajo. Evitará que ASP.NET procese más solicitudes. La información en el artículo es precisa al respecto; el mismo grupo de subprocesos utilizado para QueueUserWorkItem también se utiliza para atender solicitudes.

Pero si realmente está haciendo cola con suficientes elementos de trabajo para causar esta inanición, ¡entonces debería estar muriendo de hambre en el grupo de hilos! Si está ejecutando literalmente cientos de operaciones que consumen mucha CPU al mismo tiempo, ¿de qué le serviría tener otra cadena de trabajo para atender una solicitud ASP.NET, cuando la máquina ya está sobrecargada? Si te encuentras en esta situación, ¡necesitas rediseñar por completo!

La mayoría de las veces que veo o escucho que el código de subprocesos múltiples se utiliza de manera inapropiada en ASP.NET, no es para hacer cola en el trabajo intensivo de la CPU. Es para poner en cola el trabajo de E / S. Y si desea hacer un trabajo de E / S, entonces debe usar un hilo de E / S (puerto de finalización de E / S).

Específicamente, debe usar las devoluciones de llamada asíncronas compatibles con cualquier clase de biblioteca que esté utilizando. Estos métodos siempre están claramente etiquetados; comienzan con las palabras Begin y End . Como en Stream.BeginRead , Socket.BeginConnect , WebRequest.BeginGetResponse , y así sucesivamente.

Estos métodos usan ThreadPool , pero usan IOCP, que no interfieren con las solicitudes de ASP.NET. Son un tipo especial de hilo ligero que puede ser “despertado” por una señal de interrupción del sistema de E / S. Y en una aplicación ASP.NET, normalmente tiene una cadena de E / S para cada cadena de trabajo, por lo que cada solicitud puede tener una operación asincrónica en cola. Eso es literalmente cientos de operaciones asincrónicas sin ninguna degradación significativa del rendimiento (suponiendo que el subsistema de E / S pueda seguir el ritmo). Es mucho más de lo que nunca necesitarás.

Solo tenga en cuenta que los delegates asíncronos no funcionan de esta manera: terminarán usando un hilo de trabajo, al igual que ThreadPool.QueueUserWorkItem . Solo los métodos asíncronos incorporados de las clases de la biblioteca .NET Framework son capaces de hacerlo. Puede hacerlo usted mismo, pero es complicado y un poco peligroso, y probablemente esté fuera del scope de esta discusión.

La mejor respuesta a esta pregunta, en mi opinión, es no utilizar ThreadPool o una instancia ThreadPool en ASP.NET . No es para nada como hacer girar un hilo en una aplicación de Windows Forms, donde lo haces para mantener la UI receptiva y no importa qué tan eficiente sea. En ASP.NET, su preocupación es el rendimiento , y todo ese contexto que enciende todos esos hilos de trabajo absolutamente va a matar su rendimiento, ya sea que use el ThreadPool o no.

Por favor, si se encuentra escribiendo código de enhebrado en ASP.NET, considere si podría reescribirse para utilizar métodos asíncronos preexistentes, y si no puede, entonces considere si realmente necesita el código o no. para ejecutar en un hilo de fondo en absoluto. En la mayoría de los casos, probablemente agregará complejidad sin ningún beneficio neto.

Según Thomas Marquadt del equipo ASP.NET de Microsoft, es seguro utilizar ASP.NET ThreadPool (QueueUserWorkItem).

Del artículo :

P) Si mi aplicación ASP.NET utiliza subprocesos CLR ThreadPool, ¿no voy a privar de ASP.NET, que también usa CLR ThreadPool para ejecutar solicitudes? Blockquote

A) [T] o resum, no se preocupe por hambrientos ASP.NET de hilos, y si cree que hay un problema aquí hágamelo saber y nos ocuparemos de ello.

P) ¿Debo crear mis propios hilos (nuevo hilo)? ¿No será esto mejor para ASP.NET, ya que usa el CLR ThreadPool?

A) Por favor no. O para decirlo de otra manera, ¡no! Si eres realmente inteligente, mucho más inteligente que yo, puedes crear tus propios hilos; de lo contrario, ni siquiera lo pienses. Aquí hay algunas razones por las que no debe crear con frecuencia nuevos temas:

1) Es muy caro, comparado con QueueUserWorkItem … Por cierto, si puedes escribir un ThreadPool mejor que el CLR, te animo a que solicites un trabajo en Microsoft, ¡porque definitivamente estamos buscando gente como tú! .

Los sitios web no deberían rodear a los hilos de desove.

Por lo general, transfiere esta funcionalidad a un servicio de Windows con el que luego se comunica (utilizo MSMQ para hablar con ellos).

– Editar

Describí una implementación aquí: Procesamiento en segundo plano basado en cola en la aplicación web ASP.NET MVC

– Editar

Para ampliar por qué esto es incluso mejor que solo los hilos:

Usando MSMQ, puede comunicarse con otro servidor. Puede escribir en una cola en todas las máquinas, de modo que si determina, por algún motivo, que su tarea en segundo plano consume demasiado los recursos del servidor principal, puede cambiarla de manera bastante trivial.

También le permite procesar por lotes cualquier tarea que esté tratando de hacer (enviar correos electrónicos / lo que sea).

Definitivamente creo que la práctica general para el trabajo asincrónico rápido y de baja prioridad en ASP.NET sería usar el grupo de subprocesos de .NET, particularmente para escenarios de alto tráfico, ya que quiere que sus recursos estén limitados.

Además, la implementación de subprocesos está oculta: si comienzas a generar tus propios subprocesos, también debes administrarlos correctamente. No dice que no podrías hacerlo, pero ¿por qué reinventar esa rueda?

Si el rendimiento se convierte en un problema y puede establecer que el grupo de subprocesos es el factor limitante (y no conexiones de base de datos, conexiones de red salientes, memoria, tiempos de espera de página, etc.), ajuste la configuración del grupo de subprocesos para permitir más subprocesos de trabajo, solicitudes en cola más altas , etc.

Si no tiene un problema de rendimiento, la opción de generar nuevos hilos para reducir la contención con la cola de solicitudes de ASP.NET es la clásica optimización prematura.

Sin embargo, idealmente no necesitaría usar un hilo separado para hacer una operación de registro: simplemente habilite el hilo original para completar la operación lo más rápido posible, que es donde MSMQ y un hilo / proceso de consumidor separado entran en la imagen. Estoy de acuerdo en que esto es más pesado y más trabajo de implementar, pero realmente necesita la durabilidad aquí: la volatilidad de una cola compartida en memoria se agotará rápidamente.

Debería usar QueueUserWorkItem y evitar crear nuevos hilos como evitaría la peste. Para obtener un efecto visual que explique por qué no morirá de hambre a ASP.NET, ya que utiliza el mismo ThreadPool, imagine a un malabarista muy habilidoso que use dos manos para guardar media docena de bolos, espadas o lo que sea que esté volando. Para tener una idea de por qué crear sus propios hilos es malo, imagine lo que sucede en Seattle en la hora pico cuando las rampas de entrada a la autopista permiten a los vehículos ingresar al tráfico inmediatamente en lugar de usar una luz y limitar el número de entradas a uno cada pocos segundos . Finalmente, para una explicación detallada, vea este enlace:

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

Gracias, Thomas

Ese artículo no es correcto. ASP.NET tiene su propio conjunto de subprocesos, hilos de trabajo administrados, para atender solicitudes ASP.NET. Este conjunto suele tener unos cientos de subprocesos y está separado del grupo ThreadPool, que es un múltiplo más pequeño de procesadores.

Usar ThreadPool en ASP.NET no interferirá con los hilos de trabajo de ASP.NET. Usar ThreadPool está bien.

También sería aceptable configurar un único hilo que sea solo para registrar mensajes y usar un patrón productor / consumidor para pasar mensajes de registro a ese hilo. En ese caso, dado que el hilo es de larga duración, debe crear un único hilo nuevo para ejecutar el registro.

Usar un nuevo hilo para cada mensaje definitivamente es excesivo.

Otra alternativa, si solo está hablando sobre el registro, es usar una biblioteca como log4net. Maneja el registro en un hilo separado y se encarga de todos los problemas de contexto que podrían surgir en ese escenario.

Yo diría que el artículo está mal. Si está ejecutando una gran tienda .NET, puede usar la agrupación de forma segura en múltiples aplicaciones y múltiples sitios web (usando grupos de aplicaciones independientes), simplemente basándose en una statement en la documentación de ThreadPool :

Hay un grupo de hilos por proceso. El grupo de subprocesos tiene un tamaño predeterminado de 250 subprocesos de trabajo por procesador disponible y 1000 subprocesos de finalización de E / S. El número de subprocesos en el grupo de subprocesos se puede cambiar utilizando el método SetMaxThreads. Cada subproceso usa el tamaño de stack predeterminado y se ejecuta con la prioridad predeterminada.

Me hicieron una pregunta similar en el trabajo la semana pasada y le daré la misma respuesta. ¿Por qué estás aplicando aplicaciones web multihilo por solicitud? Un servidor web es un fantástico sistema optimizado en gran medida para proporcionar muchas solicitudes de manera oportuna (es decir, multi-threading). Piense en lo que sucede cuando solicita casi cualquier página en la web.

  1. Se realiza una solicitud para alguna página
  2. Html se sirve de nuevo
  3. El Html le dice al cliente que haga más repeticiones (js, css, imágenes, etc.)
  4. Más información se sirve de nuevo

Usted da el ejemplo del registro remoto, pero eso debería ser una preocupación de su registrador. Debe existir un proceso asincrónico para recibir mensajes de manera oportuna. Sam incluso señala que su registrador (log4net) ya debería soportar esto.

Sam también tiene razón en que usar el Thread Pool en el CLR no causará problemas con el grupo de subprocesos en IIS. Sin embargo, lo que debe preocuparnos aquí es que no está generando hilos de un proceso, está generando hilos nuevos fuera de los hilos de subprocesos de IIS. Hay una diferencia y la distinción es importante.

Hilos vs Proceso

Tanto los hilos como los procesos son métodos para paralelizar una aplicación. Sin embargo, los procesos son unidades de ejecución independientes que contienen su propia información de estado, usan sus propios espacios de direcciones y solo interactúan entre sí a través de mecanismos de comunicación entre procesos (generalmente administrados por el sistema operativo). Las aplicaciones se suelen dividir en procesos durante la fase de diseño, y un proceso maestro genera explícitamente subprocesos cuando tiene sentido separar lógicamente la funcionalidad significativa de la aplicación. Los procesos, en otras palabras, son una construcción arquitectónica.

Por el contrario, un hilo es una construcción de encoding que no afecta la architecture de una aplicación. Un solo proceso puede contener múltiples hilos; todos los hilos dentro de un proceso comparten el mismo estado y el mismo espacio de memoria, y pueden comunicarse entre sí directamente, porque comparten las mismas variables.

Fuente

No estoy de acuerdo con el artículo al que se hace referencia (C # feeds.com). Es fácil crear un nuevo hilo pero peligroso. El número óptimo de subprocesos activos para ejecutarse en un solo núcleo es realmente sorprendentemente bajo, menos de 10. Es demasiado fácil hacer que la máquina pierda tiempo cambiando hilos si los subprocesos se crean para tareas menores. Los hilos son un recurso que REQUIERE gestión. La abstracción WorkItem está ahí para manejar esto.

Aquí hay una compensación entre la reducción del número de subprocesos disponibles para las solicitudes y la creación de demasiados subprocesos para permitir que cualquiera de ellos procese de manera eficiente. Esta es una situación muy dinámica, pero creo que debería gestionarse activamente (en este caso, por el grupo de subprocesos) en lugar de dejar que el procesador se adelante a la creación de subprocesos.

Finalmente, el artículo hace algunas declaraciones bastante extensas sobre los peligros de usar ThreadPool, pero realmente necesita algo concreto para respaldarlas.

Ya sea que IIS use o no el mismo ThreadPool para manejar las solicitudes entrantes parece difícil obtener una respuesta definitiva, y también parece haber cambiado en las versiones. Por lo tanto, parece una buena idea no utilizar hilos ThreadPool en exceso, por lo que IIS tiene muchos de ellos disponibles. Por otro lado, generar tu propio hilo para cada pequeña tarea parece una mala idea. Presumiblemente, tiene algún tipo de locking en su registro, por lo que solo un hilo podría progresar a la vez, y el rest se turnaría para progtwigrse y no progtwigrse (sin mencionar la sobrecarga de generar un nuevo hilo). Básicamente, te encuentras con los problemas exactos para los que se diseñó ThreadPool.

Parece que un compromiso razonable sería que su aplicación asigne un solo hilo de registro al que pueda pasarle mensajes. Debería tener cuidado de que el envío de mensajes sea lo más rápido posible para que no disminuya la velocidad de su aplicación.