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


111

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

Así que así es como he estado haciendo pequeñas tareas asincrónicas 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 la posibilidad (si el artículo es correcto) de que las tareas en segundo plano compitan por subprocesos con los controladores de solicitudes ASP.NET. El segundo método libera ThreadPool, pero a costa de no estar limitado y, por lo tanto, consumir potencialmente demasiados recursos.

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

Si su sitio estaba recibiendo 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 usted ¿No deberías intentar iniciar tus propios hilos?

Aclaración: solo pregunto en el ámbito de pequeñas tareas asincrónicas no críticas (por ejemplo, registro remoto), no elementos de trabajo costosos que requerirían un proceso separado (en estos casos, estoy de acuerdo en que necesitará una solución más sólida).


La trama se complica: encontré este artículo ( blogs.msdn.com/nicd/archive/2007/04/16/… ), que no puedo decodificar del todo. Por un lado, parece estar diciendo que IIS 6.0+ siempre procesa solicitudes en subprocesos de trabajo del grupo de subprocesos (y que las versiones anteriores podrían hacerlo), pero luego está esto: "Sin embargo, si está utilizando nuevas páginas asíncronas .NET 2.0 ( Async = "true") o ThreadPool.QueueUserWorkItem (), entonces la parte asincrónica del procesamiento se realizará dentro de [un hilo de puerto de finalización] ". ¿La parte asincrónica del procesamiento ?
Jeff Sternal

Otra cosa: esto debería ser lo suficientemente fácil de probar en una instalación de IIS 6.0+ (que no tengo en este momento) al inspeccionar si los subprocesos de trabajo disponibles del grupo de subprocesos son más bajos que su número máximo de subprocesos de trabajo, y luego hacer lo mismo dentro de la cola artículos de trabajo.
Jeff Sternal

Respuestas:


104

Otras respuestas aquí parecen estar omitiendo el punto más importante:

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

Eso se aplica tanto a los hilos libres, creados por new Thread(...), como a los hilos de trabajo en el ThreadPoolque responden a las QueueUserWorkItemsolicitudes.

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

Pero si realmente está haciendo cola suficientes elementos de trabajo para causar esta inanición, ¡entonces debería estar privando al grupo de subprocesos! Si está ejecutando literalmente cientos de operaciones con uso intensivo de la CPU al mismo tiempo, ¿de qué le serviría tener otro hilo de trabajo para atender una solicitud ASP.NET, cuando la máquina ya está sobrecargada? Si se encuentra en esta situación, ¡debe rediseñarlo por completo!

La mayoría de las veces que veo o escucho que el código multiproceso se usa de manera inapropiada en ASP.NET, no es para poner en cola el trabajo intensivo de la CPU. Es para poner en cola trabajos vinculados a E / S. Y si desea realizar un trabajo de E / S, debería utilizar un hilo de E / S (puerto de finalización de E / S).

Específicamente, debería usar las devoluciones de llamada asíncronas compatibles con cualquier clase de biblioteca que esté usando. Estos métodos siempre están etiquetados con mucha claridad; comienzan con las palabras Beginy End. Al igual que en Stream.BeginRead, Socket.BeginConnect, WebRequest.BeginGetResponse, y así sucesivamente.

Estos métodos hacen uso de la ThreadPool, pero utilizan IOCPs, que no no interfieren con las solicitudes de ASP.NET. Son un tipo especial de hilo ligero que se puede "despertar" mediante una señal de interrupción del sistema de E / S. Y en una aplicación ASP.NET, normalmente tiene un subproceso de E / S para cada subproceso de trabajo, por lo que cada solicitud puede tener una operación asíncrona en cola. Eso es literalmente cientos de operaciones asíncronas sin ninguna degradación significativa del rendimiento (asumiendo que el subsistema de E / S puede mantenerse al día). Es mucho más de lo que necesitará.

Solo tenga en cuenta que los delegados asíncronos no funcionan de esta manera; terminarán usando un hilo de trabajo, como ThreadPool.QueueUserWorkItem. Solo los métodos asíncronos integrados de las clases de biblioteca de .NET Framework son capaces de hacer esto. Puede hacerlo usted mismo, pero es complicado y un poco peligroso y probablemente más allá del alcance de esta discusión.

La mejor respuesta a esta pregunta, en mi opinión, es no usar ThreadPool o una Threadinstancia en segundo plano en ASP.NET . No es en absoluto como hacer girar un hilo en una aplicación de Windows Forms, donde lo hace para mantener la interfaz de usuario receptiva y no le importa cuán eficiente sea. En ASP.NET, su preocupación es el rendimiento , y todo ese cambio de contexto en todos esos subprocesos de trabajo va a matar absolutamente su rendimiento, ya sea que use ThreadPoolo no.

Por favor, si se encuentra escribiendo código de subprocesamiento en ASP.NET, considere si se puede reescribir o no para usar métodos asincrónicos preexistentes, y si no puede, considere si realmente necesita el código o no para ejecutarse en un hilo de fondo. En la mayoría de los casos, probablemente agregará complejidad sin ningún beneficio neto.


Gracias por esa respuesta detallada y tiene razón, trato de usar métodos asíncronos cuando sea posible (junto con controladores asíncronos en ASP.NET MVC). En el caso de mi ejemplo, con el registrador remoto, esto es exactamente lo que puedo hacer. Sin embargo, es un problema de diseño interesante porque empuja el manejo asíncrono hasta el nivel más bajo de su código (es decir, la implementación del registrador), en lugar de poder decidirlo desde, digamos, el nivel del controlador (en el último caso , necesitaría, por ejemplo, dos implementaciones de registrador para poder elegir).
Michael Hart

@Michael: Las devoluciones de llamada asíncronas son generalmente bastante fáciles de ajustar si quieres subir más niveles; podría crear una fachada alrededor de los métodos asíncronos y envolverlos con un solo método que usa Action<T>como devolución de llamada, por ejemplo. Si quiere decir que la elección de utilizar un hilo de trabajo o un hilo de E / S ocurre en el nivel más bajo, eso es intencional; solo ese nivel puede decidir si necesita o no un IOCP.
Aaronaught

Aunque, como punto de interés, es solo .NET lo ThreadPoolque te limita de esta manera, probablemente porque no confiaban en que los desarrolladores lo hicieran bien. El grupo de subprocesos de Windows no administrado tiene una API muy similar, pero en realidad le permite elegir el tipo de subproceso.
Aaronaught

2
Puertos de finalización de E / S (IOCP). la descripción de IOCP no es del todo correcta. en IOCP, tiene un número estático de subprocesos de trabajo que se turnan para trabajar en TODAS las tareas pendientes. no debe confundirse con los grupos de subprocesos que pueden ser de tamaño fijo o dinámico PERO tienen un subproceso por tarea: se escala terriblemente. a diferencia de ASYNC, no tiene un hilo por tarea. un subproceso IOCP puede funcionar un poco en la tarea 1, luego cambiar a la tarea 3, la tarea 2 y luego volver a la tarea 1 nuevamente. los estados de la sesión de tareas se guardan y se pasan entre subprocesos.
MickyD

1
¿Qué pasa con las inserciones de la base de datos? ¿Existe un comando ASYNC SQL (como Ejecutar)? Las inserciones de bases de datos son la operación de E / S más lenta (debido al bloqueo) y hacer que el hilo principal espere a que se inserten las filas es solo una pérdida de ciclos de CPU.
Ian Thompson

45

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 usa subprocesos CLR ThreadPool, ¿no privaré a ASP.NET, que también usa CLR ThreadPool para ejecutar solicitudes? ..

A) Para resumir, no se preocupe por privar a ASP.NET de subprocesos, y si cree que hay un problema aquí, avíseme y nos encargaremos de ello.

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

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

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

4

Los sitios web no deberían dar vueltas en torno a los hilos de generación.

Por lo general, mueve esta funcionalidad a un servicio de Windows con el que luego se comunica (yo uso 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 expandir por qué esto es incluso mejor que solo hilos:

Con MSMQ, puede comunicarse con otro servidor. Puede escribir en una cola en todas las máquinas, por lo que si determina, por alguna razón, que su tarea en segundo plano está consumiendo demasiado los recursos del servidor principal, puede cambiarla de manera bastante trivial.

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


4
No estaría de acuerdo en que esta afirmación general sea siempre cierta, especialmente para tareas no críticas. La creación de un servicio de Windows, solo para fines de registro asincrónico, definitivamente parece una exageración. Además, esa opción no siempre está disponible (pudiendo desplegar MSMQ y / o un Servicio de Windows).
Michael Hart

Claro, pero es la forma 'estándar' de implementar tareas asincrónicas desde un sitio web (tema de cola contra algún otro proceso).
Mediodía Silk

2
No todas las tareas asincrónicas se crean de la misma manera, por lo que, por ejemplo, existen páginas asincrónicas en ASP.NET. Si quiero obtener un resultado de un servicio web remoto para mostrarlo, no lo haré a través de MSMQ. En este caso, estoy escribiendo en un registro usando una publicación remota. No encaja con el problema de escribir un servicio de Windows, ni conectar MSMQ para eso (y tampoco puedo, ya que esta aplicación en particular está en Azure).
Michael Hart

1
Considere: ¿está escribiendo a un host remoto? ¿Qué pasa si ese host está inactivo o inaccesible? ¿Quieres volver a intentar tu escritura? Tal vez lo hagas, tal vez no. Con su implementación, es difícil volver a intentarlo. Con el servicio, se vuelve bastante trivial. Aprecio que es posible que no pueda hacerlo, y dejaré que otra persona responda a los problemas específicos con la creación de subprocesos desde sitios web (es decir, si su subproceso no era de fondo, etc.), pero estoy describiendo el "adecuado" forma de hacerlo. No estoy familiarizado con azure, aunque he usado ec2 (puedes instalar un sistema operativo en eso, así que cualquier cosa está bien).
Mediodía Silk

@silky, gracias por los comentarios. Dije "no crítico" para evitar esta solución más pesada (pero duradera). Aclaré la pregunta para que quede claro que no estoy pidiendo mejores prácticas en torno a elementos de trabajo en cola. Azure admite este tipo de escenario (tiene su propio almacenamiento en cola), pero la operación de puesta en cola es demasiado cara para el registro sincrónico, por lo que necesitaría una solución asincrónica de todos modos. En mi caso, soy consciente de los peligros de las fallas, pero no voy a agregar más infraestructura en caso de que este proveedor de registros en particular falle; también tengo otros proveedores de registros.
Michael Hart

4

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 .NET, particularmente para escenarios de alto tráfico, ya que desea que sus recursos estén limitados.

Además, la implementación de subprocesos está oculta: si comienza a generar sus propios subprocesos, también debe administrarlos correctamente. No digo que no puedas 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 las conexiones de la base de datos, las conexiones de red salientes, la memoria, los tiempos de espera de la página, etc.), entonces modifica 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, elegir generar nuevos subprocesos para reducir la contención con la cola de solicitudes ASP.NET es una optimización prematura clásica.

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


2

Debe usar QueueUserWorkItem y evitar la creación de nuevos hilos como evitaría la plaga. Para una imagen que explique por qué no dejará pasar hambre a ASP.NET, ya que usa el mismo ThreadPool, imagine a un malabarista muy hábil que usa las dos manos para mantener media docena de bolos, espadas o lo que sea en vuelo. Para tener una idea visual 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 muy utilizadas permiten que los vehículos ingresen al tráfico de inmediato en lugar de usar una luz y limitar el número de entradas a una cada pocos segundos. . Finalmente, para obtener una explicación detallada, consulte este enlace:

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

Gracias Thomas


Ese enlace es muy útil, gracias por eso Thomas. También me interesaría saber qué piensas de la respuesta de @ Aaronaught.
Michael Hart

Estoy de acuerdo con Aaronaught y dije lo mismo en la publicación de mi blog. Lo digo de esta manera: "En un intento de simplificar esta decisión, solo debe cambiar [a otro hilo] si de lo contrario bloquearía el hilo de solicitud ASP.NET mientras no hace nada. Esto es una simplificación excesiva, pero estoy intentando para simplificar la decisión ". En otras palabras, no lo haga para un trabajo computacional sin bloqueo, pero hágalo si está realizando solicitudes de servicio web asíncronas a un servidor remoto. ¡Escuche a Aaronaught! :)
Thomas

1

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

El uso de ThreadPool en ASP.NET no interferirá con los subprocesos de trabajo de ASP.NET. Usar ThreadPool está bien.

También sería aceptable configurar un solo hilo que sea solo para registrar mensajes y usar el 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.

Definitivamente, usar un hilo nuevo para cada mensaje es excesivo.

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


1
@Sam, en realidad estoy usando log4net y no veo que los registros se escriban en un hilo separado, ¿hay algún tipo de opción que deba habilitar?
Michael Hart

1

Yo diría que el artículo está mal. Si está ejecutando una gran tienda .NET, puede usar el grupo de manera segura en múltiples aplicaciones y múltiples sitios web (usando grupos de aplicaciones separados), simplemente según una declaración en la documentación de ThreadPool :

Hay un grupo de subprocesos 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 mediante el método SetMaxThreads. Cada subproceso usa el tamaño de pila predeterminado y se ejecuta con la prioridad predeterminada.


¡Una aplicación que se ejecuta en un solo proceso es completamente capaz de dejar de funcionar! (O al menos degradando su propio rendimiento lo suficiente como para hacer que el grupo de subprocesos sea una propuesta perdedora)
Jeff Sternal

Así que supongo que las solicitudes ASP.NET utilizan los subprocesos de finalización de E / S (a diferencia de los subprocesos de trabajo), ¿es correcto?
Michael Hart

Del artículo de Fritz Onion enlacé en mi respuesta: "Este paradigma cambia [de IIS 5.0 a IIS 6.0] la forma en que se manejan las solicitudes en ASP.NET. En lugar de enviar solicitudes de inetinfo.exe al proceso de trabajo de ASP.NET, http. sys pone en cola directamente cada solicitud en el proceso apropiado. Por lo tanto, todas las solicitudes ahora son atendidas por subprocesos de trabajo extraídos del grupo de subprocesos CLR y nunca en subprocesos de E / S ". (énfasis mío)
Jeff Sternal

Hmmm, todavía no estoy del todo seguro ... Ese artículo es de junio de 2003. Si lees este de mayo de 2004 (es cierto que todavía es bastante antiguo), dice "La página de prueba Sleep.aspx se puede usar para mantener un ASP .NET I / O thread busy ", donde Sleep.aspx solo hace que el subproceso de ejecución actual se suspenda: msdn.microsoft.com/en-us/library/ms979194.aspx - Cuando tenga la oportunidad, veré si puedo codificar ese ejemplo y probar en IIS 7 y .NET 3.5
Michael Hart

Sí, el texto de ese párrafo es confuso. Más adelante en esa sección, se vincula a un tema de soporte ( support.microsoft.com/default.aspx?scid=kb;EN-US;816829 ) que aclara las cosas: ejecutar solicitudes en subprocesos de finalización de E / S era un .NET Framework 1.0 problema que se solucionó en ASP.NET 1.1 de junio de 2003 Hotfix Rollup Package (después del cual "TODAS las solicitudes ahora se ejecutan en subprocesos de trabajo"). Más importante aún, ese ejemplo muestra con bastante claridad que el grupo de subprocesos de ASP.NET es el mismo grupo de subprocesos expuesto por System.Threading.ThreadPool.
Jeff Sternal

1

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

  1. Se realiza una solicitud para alguna página
  2. Html se devuelve
  3. El Html le dice al cliente que realice más solicitudes (js, css, imágenes, etc.)
  4. Se devuelve más información

Usted da el ejemplo del registro remoto, pero eso debería ser una preocupación para 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 admitir esto.

Sam también tiene razón en que el uso del grupo de subprocesos en CLR no causará problemas con el grupo de subprocesos en IIS. Sin embargo, lo que debe preocuparse aquí es que no está generando subprocesos de un proceso, está generando nuevos subprocesos a partir de subprocesos de IIS. Hay una diferencia y la distinción es importante.

Hilos vs proceso

Tanto los subprocesos 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 generalmente se dividen 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 codificación que no afecta la arquitectura de una aplicación. Un solo proceso puede contener varios subprocesos; todos los subprocesos 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


3
@Ty, gracias por la entrada, pero soy muy consciente de cómo funciona un servidor web y no es realmente relevante para la pregunta; nuevamente, como dije en la pregunta, no estoy pidiendo orientación sobre esto como arquitectura problema. Estoy solicitando información técnica específica. En cuanto a que es "la preocupación del registrador" que ya debería tener un proceso asincrónico en su lugar, ¿cómo cree que la implementación del registrador debería escribir ese proceso asíncrono?
Michael Hart

0

No estoy de acuerdo con el artículo de referencia (C # feeds.com). Es fácil crear un hilo nuevo pero peligroso. El número óptimo de subprocesos activos para ejecutar en un solo núcleo es sorprendentemente bajo: menos de 10. Es demasiado fácil hacer que la máquina pierda tiempo cambiando subprocesos 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 reducir la cantidad de subprocesos disponibles para las solicitudes y crear demasiados subprocesos para permitir que cualquiera de ellos se procese de manera eficiente. Esta es una situación muy dinámica, pero creo que debería ser administrada activamente (en este caso por el grupo de subprocesos) en lugar de dejar que el procesador se mantenga por delante de la creación de subprocesos.

Finalmente, el artículo hace algunas declaraciones bastante amplias sobre los peligros de usar ThreadPool, pero realmente necesita algo concreto que las respalde.


0

Si IIS usa 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, parecería una buena idea no usar excesivamente los subprocesos ThreadPool, para que IIS tenga 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 bloqueo en su registro, por lo que solo un hilo podría progresar a la vez, y el resto solo se turnaría para programar y no programar (sin mencionar la sobrecarga de generar un nuevo hilo). Esencialmente, 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 único hilo de registro al que pueda pasar mensajes. Debería tener cuidado de que el envío de mensajes sea lo más rápido posible para no ralentizar su aplicación.


0

Puede usar Parallel.For o Parallel.ForEach y definir el límite de posibles subprocesos que desea asignar para que se ejecuten sin problemas y evitar la falta de grupos.

Sin embargo, al ejecutarlo en segundo plano, deberá utilizar el estilo TPL puro a continuación en la aplicación web 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);
                  }
                });
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.