2012-01-05 18 views
148

Después de esta pregunta, me siento cómodo cuando uso las operaciones async en ASP.NET MVC. Por lo tanto, he escrito dos entradas de blog en que:Realice operaciones asíncronas en ASP.NET MVC use un hilo de ThreadPool en .NET 4

tengo demasiados malentendidos en mi mente acerca de las operaciones asincrónicas en ASP.NET MVC.

siempre escucho esta frase: La aplicación puede escalar mejor si las operaciones se ejecutan de forma asíncrona

Y oí este tipo de frases mucho también: si tiene un gran volumen de tráfico, Es mejor que no realice sus consultas de forma asíncrona, ya que consume 2 hilos adicionales para atender una solicitud que quita recursos de otras solicitudes entrantes.

Creo que esas dos oraciones son inconsistentes.

No tengo mucha información sobre cómo funciona threadpool en ASP.NET, pero sé que threadpool tiene un tamaño limitado para los subprocesos. Entonces, la segunda oración tiene que estar relacionada con este tema.

Y me gustaría saber si las operaciones asincrónicas en ASP.NET MVC utilizan un hilo de ThreadPool en .NET 4?

Por ejemplo, cuando implementamos AsyncController, ¿cómo se estructura la aplicación? Si recibo mucho tráfico, ¿es una buena idea implementar AsyncController?

¿Hay alguien afuera que pueda quitarme esta cortina negra delante de mis ojos y explicarme el trato sobre la asincronía en ASP.NET MVC 3 (NET 4)?

Editar:

He leído este documento a continuación casi cientos de veces y entiendo que el acuerdo principal, pero aún tengo la confusión porque hay demasiado inconsistente comentario por ahí.

Using an Asynchronous Controller in ASP.NET MVC

Editar:

Supongamos que tengo acción del controlador, como a continuación (no una implementación de AsyncController sin embargo):

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
     //Do an advanced looging here which takes a while 
    }); 

    return View(); 
} 

Como se ve aquí, enciendo una operación y olvídate de eso. Luego, regreso de inmediato sin esperar a que se complete.

En este caso, ¿esto tiene que usar un hilo de threadpool? Si es así, después de que se complete, ¿qué le sucede a ese hilo? ¿GC entra y se limpia justo después de que se completa?

Editar:

Por toda respuesta, el @ de Darin, aquí es una muestra de código asíncrono, que habla con la base de datos:

public class FooController : AsyncController { 

    //EF 4.2 DbContext instance 
    MyContext _context = new MyContext(); 

    public void IndexAsync() { 

     AsyncManager.OutstandingOperations.Increment(3); 

     Task<IEnumerable<Foo>>.Factory.StartNew(() => { 

      return 
       _context.Foos; 
     }).ContinueWith(t => { 

      AsyncManager.Parameters["foos"] = t.Result; 
      AsyncManager.OutstandingOperations.Decrement(); 
     }); 

     Task<IEnumerable<Bars>>.Factory.StartNew(() => { 

      return 
       _context.Bars; 
     }).ContinueWith(t => { 

      AsyncManager.Parameters["bars"] = t.Result; 
      AsyncManager.OutstandingOperations.Decrement(); 
     }); 

     Task<IEnumerable<FooBar>>.Factory.StartNew(() => { 

      return 
       _context.FooBars; 
     }).ContinueWith(t => { 

      AsyncManager.Parameters["foobars"] = t.Result; 
      AsyncManager.OutstandingOperations.Decrement(); 
     }); 
    } 

    public ViewResult IndexCompleted(
     IEnumerable<Foo> foos, 
     IEnumerable<Bar> bars, 
     IEnumerable<FooBar> foobars) { 

     //Do the regular stuff and return 

    } 
} 
+0

No estoy seguro de la respuesta, pero vale la pena señalar asincrónico y multi-threading son cosas diferentes. Entonces, sería posible tener un número fijo de hilos con manejo asíncrono. Lo que sucedería es cuando una página tiene que bloquear, por ejemplo, E/S, otra página tendría la oportunidad de ejecutarse en el mismo hilo. Así es como ambas afirmaciones pueden ser ciertas, async puede hacer que las cosas sean más rápidas pero demasiados hilos es un problema. –

+0

@ChrisChilvers Sí, el multihilo no siempre es necesario en la operación asincrónica. Ya me lo imaginé, pero creo que no tengo ningún controlador sobre eso hasta donde sé. AsyncController crea cuántos hilos desea desde mi punto de vista pero tampoco está seguro de eso. ¿Hay una noción de threadpool en aplicaciones de escritorio como WPF también? Creo que el número de subprocesos no es un problema en ese tipo de aplicaciones. – tugberk

+5

Eche un vistazo al video [Enhebrado con Jeff Richter] (http://channel9.msdn.com/Shows/AppFabric-tv/AppFabrictv-Threading-with-Jeff-Richter) – oleksii

Respuesta

159

Aquí hay un excellent article Le recomiendo que lea para comprender mejor el procesamiento asíncrono en ASP.NET (que es lo que básicamente representan los controladores asíncronos).

Consideremos primero una acción síncrona estándar:

public ActionResult Index() 
{ 
    // some processing 
    return View(); 
} 

Cuando se hace una solicitud a esta acción de un hilo se extrae de la agrupación de hebras y el cuerpo de esta acción se ejecuta en este hilo. Entonces, si el procesamiento dentro de esta acción es lento, está bloqueando este hilo para todo el proceso, por lo que este hilo no puede reutilizarse para procesar otras solicitudes. Al final de la ejecución de la solicitud, el hilo se devuelve al grupo de subprocesos.

Ahora vamos a tomar un ejemplo del modelo asincrónico:

public void IndexAsync() 
{ 
    // perform some processing 
} 

public ActionResult IndexCompleted(object result) 
{ 
    return View(); 
} 

Cuando se envía una solicitud a la acción Index, un hilo se extrae de la agrupación de hebras y se ejecuta el cuerpo del método IndexAsync. Una vez que el cuerpo de este método termina de ejecutarse, el hilo se devuelve al grupo de subprocesos. Luego, usando el estándar AsyncManager.OutstandingOperations, una vez que indica la finalización de la operación asincrónica, se extrae otro subproceso del grupo de subprocesos y se ejecuta el cuerpo de la acción IndexCompleted y el resultado se presenta al cliente.

Entonces, lo que podemos ver en este patrón es que una solicitud HTTP de cliente único podría ejecutarse mediante dos subprocesos diferentes.

Ahora la parte interesante ocurre dentro del método IndexAsync. Si tiene una operación de bloqueo dentro, está desperdiciando totalmente el propósito de los controladores asíncronos porque está bloqueando el hilo de trabajo (recuerde que el cuerpo de esta acción se ejecuta en un hilo extraído del grupo de subprocesos).

Entonces, ¿cuándo podemos aprovechar las ventajas de los controladores asíncronos?

En mi humilde opinión, podemos obtener más beneficios cuando tenemos operaciones intensivas de E/S (como las llamadas de bases de datos y de red a servicios remotos). Si tiene una operación intensiva de CPU, las acciones asincrónicas no le brindarán muchos beneficios.

Entonces, ¿por qué podemos beneficiarnos de las operaciones intensivas de E/S? Porque podríamos usar I/O Completion Ports. IOCP es extremadamente poderoso porque no consume ningún hilo o recurso en el servidor durante la ejecución de toda la operación.

¿Cómo funcionan?

Supongamos que queremos descargar los contenidos de una página web remota utilizando el método WebClient.DownloadStringAsync. Llama a este método que registrará un IOCP dentro del sistema operativo y lo devolverá de inmediato. Durante el procesamiento de toda la solicitud, no se consumen hilos en su servidor. Todo sucede en el servidor remoto. Esto podría llevar mucho tiempo, pero no le importa, ya que no está poniendo en peligro sus hilos de trabajo. Una vez que se recibe una respuesta, se señala el IOCP, se extrae un subproceso del grupo de subprocesos y se ejecuta la devolución de llamada en este subproceso. Pero como puede ver, durante todo el proceso, no hemos monopolizado ningún hilo.

Los mismos sigue siendo verdad, con métodos como FileStream.BeginRead, SqlCommand.BeginExecute, ...

¿Qué pasa con la paralelización de varias llamadas de base de datos? Supongamos que tiene una acción de controlador síncrono en la que realizó 4 llamadas a la base de datos de bloqueo en secuencia. Es fácil calcular que si cada llamada a la base de datos tarda 200 ms, la acción de su controlador tardará aproximadamente 800ms en ejecutarse.

Si no necesita ejecutar esas llamadas secuencialmente, ¿la paralelización mejoraría el rendimiento?

Esa es la gran pregunta, que no es fácil de responder. Tal vez sí tal vez no. Dependerá completamente de cómo implemente esas llamadas a la base de datos. Si utiliza los controladores asíncronos y los puertos de finalización de E/S como se explicó anteriormente, aumentará el rendimiento de esta acción del controlador y de otras acciones, ya que no monopolizará los subprocesos de trabajo.

Por otro lado, si los implementa mal (con una llamada de base de datos bloqueada realizada en un hilo del grupo de subprocesos), básicamente reducirá el tiempo total de ejecución de esta acción a aproximadamente 200ms pero habrá consumido 4 subprocesos de trabajo, por lo que puede haber degradado el rendimiento de otras solicitudes que podrían pasar hambre debido a que faltan subprocesos en el grupo para procesarlas.

Por lo tanto, es muy difícil y si no se siente listo para realizar pruebas exhaustivas en su aplicación, no implemente controladores asíncronos, ya que es probable que haga más daño que beneficio. Impleméntelos solo si tiene una razón para hacerlo: por ejemplo, ha identificado que las acciones del controlador síncrono estándar son un cuello de botella para su aplicación (después de realizar extensas pruebas de carga y mediciones, por supuesto).

Ahora vamos a considerar su ejemplo:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
     //Do an advanced looging here which takes a while 
    }); 

    return View(); 
} 

Cuando se recibe una solicitud de la acción Índice de un hilo se extrae del grupo de subprocesos para ejecutar su cuerpo, pero su cuerpo sólo programa una nueva tarea usando TPL. Por lo tanto, la ejecución de la acción finaliza y el hilo se devuelve al grupo de subprocesos. Excepto que, TPL usa subprocesos del grupo de subprocesos para realizar su procesamiento. Por lo tanto, aunque el hilo original haya sido devuelto al grupo de hilos, habrá dibujado otro hilo de este grupo para ejecutar el cuerpo de la tarea. De modo que ha puesto en peligro 2 hilos de su valioso grupo.

Ahora vamos a considerar lo siguiente:

public ViewResult Index() { 

    new Thread(() => { 
     //Do an advanced looging here which takes a while 
    }).Start(); 

    return View(); 
} 

En este caso estamos desove manualmente un hilo. En este caso, la ejecución del cuerpo de la acción Index podría tomar un poco más de tiempo (porque generar un nuevo hilo es más costoso que dibujar uno de un grupo existente). Pero la ejecución de la operación de registro avanzada se realizará en un hilo que no es parte del conjunto. Por lo tanto, no estamos poniendo en peligro los hilos del grupo que permanecen libres para atender otras solicitudes.

+1

Realmente detallado, gracias! Supongamos que tenemos 4 tareas asincrónicas ('System.Threading.Task') ejecutándose dentro del método' IndexAsync'. Dentro de esas operaciones, estamos haciendo llamadas de DB a un servidor. Entonces, todas ellas son operaciones intensivas de E/S, ¿verdad? En ese caso, ¿creamos 4 hilos separados (u obtenemos 4 hilos separados del grupo de hilos)? Asumiendo que tengo una máquina multi-core, también se ejecutarán en paralelo, ¿verdad? – tugberk

+10

@tugberk, las llamadas a bases de datos son operaciones de E/S, pero todo dependerá de cómo las implemente. Si utiliza una llamada a una base de datos de bloqueo, como 'SqlCommand.ExecuteReader', está perdiendo todo, ya que esta es una llamada de bloqueo. Estás bloqueando el hilo en el que se ejecuta esta llamada y si este hilo pasa a ser un hilo del grupo es muy malo. Se beneficiará solo si utiliza Puertos de finalización de E/S: 'SqlCommand.BeginExecuteReader'. Si no usa IOCP sin importar lo que haga, no use controladores asíncronos, ya que hará más daño que beneficio en el rendimiento general de su aplicación. –

+1

Bueno, la mayoría de las veces utilizo el código EF primero. No estoy seguro si encaja. Puse una muestra que muestra lo que generalmente hago. Actualicé la pregunta, ¿puedes echarle un vistazo? – tugberk

2

Sí, utilizan un hilo desde el grupo de subprocesos. En realidad, existe una excelente guía de MSDN que abordará todas sus preguntas y más. He encontrado que es bastante útil en el pasado. ¡Echale un vistazo!

http://msdn.microsoft.com/en-us/library/ee728598.aspx

Mientras tanto, los comentarios + sugerencias que se escuchan sobre el código asíncrono se deben tomar con un grano de sal. Para empezar, el hecho de hacer algo asincrónico no necesariamente lo hace escalar mejor, y en algunos casos puede empeorar su aplicación. El otro comentario que publicó sobre "un gran volumen de tráfico ..." también es correcto solo en ciertos contextos. Realmente depende de lo que estén haciendo sus operaciones y de cómo interactúan con otras partes del sistema.

En resumen, muchas personas tienen muchas opiniones sobre async, pero es posible que no estén fuera de contexto. Diría que se centren en sus problemas exactos y realicen pruebas básicas de rendimiento para ver qué controladores asíncronos, etc. realmente manejan con su aplicación.

+0

He leído ese documento quizás cientos de veces y todavía tengo tanta confusión (tal vez el problema es yo, quién sabe). Cuando miras alrededor, ves tantos comentarios inconsistentes sobre la asincronía en ASP.NET MVC como puedes ver en mi pregunta. – tugberk

+0

para la última frase: dentro de una acción de controlador, estaba consultando una base de datos 5 veces por separado (tenía que hacerlo) y todo tardaba aproximadamente 400 ms. Luego, implementé AsyncController y los ejecuté en paralelo. El tiempo de respuesta se redujo drásticamente a aprox. 200 ms. Pero no tengo idea de cuántos hilos crea, qué sucede con esos hilos una vez que los he terminado, 'GC' viene y los limpio justo después de que haya terminado para que mi aplicación no tenga una pérdida de memoria, etc. etcétera. Alguna idea sobre esa parte – tugberk

+0

adjuntar un depurador y averiguarlo. –

6

Aplicaciones puede escala mejor si las operaciones se ejecutan de forma asíncrona, pero sólo si hay recursos disponibles para atender las operaciones adicionales.

Las operaciones asincrónicas garantizan que nunca se bloquee una acción porque una existente está en progreso. ASP.NET tiene un modelo asincrónico que permite que varias solicitudes se ejecuten una al lado de la otra. Sería posible poner en cola las solicitudes y procesarlas FIFO, pero esto no se escalaría bien cuando tenga cientos de solicitudes puestas en cola y cada solicitud requiera 100 ms para procesarse.

Si usted tiene un gran volumen de tráfico, se puede ser mejor no realizar sus consultas de forma asíncrona, ya que puede haber recursos adicionales para dar servicio a las peticiones. Si no hay recursos sobrantes, sus solicitudes se ven obligadas a hacer cola, demorarse exponencialmente o fallar por completo, en cuyo caso la sobrecarga asíncrona (mutexes y operaciones de cambio de contexto) no le da nada.

Por lo que respecta a ASP.NET, no tiene otra opción: utiliza un modelo asincrónico, porque eso es lo que tiene sentido para el modelo de servidor-cliente. Si tuviera que escribir internamente su propio código que utiliza un patrón asíncrono para intentar escalar mejor, a menos que intente administrar un recurso compartido entre todas las solicitudes, en realidad no verá ninguna mejora porque ya está envuelto. en un proceso asincrónico que no bloquea nada más.

En última instancia, todo es subjetivo hasta que realmente observas lo que está causando un cuello de botella en tu sistema. A veces es obvio dónde un patrón asíncrono ayudará (evitando un bloqueo de recursos en cola). En última instancia, solo medir y analizar un sistema puede indicar dónde puede obtener eficiencias.

Editar:

En su ejemplo, la llamada será Task.Factory.StartNew cola una operación en el .NET hilo piscina.La naturaleza de los subprocesos de Thread Pool se debe reutilizar (para evitar el costo de crear/destruir muchos subprocesos). Una vez que la operación finaliza, el hilo se libera al grupo para ser reutilizado por otra solicitud (el recolector de basura no se involucra realmente a menos que haya creado algunos objetos en sus operaciones, en cuyo caso se recopilan como de costumbre alcance).

En lo que respecta a ASP.NET, no hay ninguna operación especial aquí. La solicitud ASP.NET se completa sin respeto a la tarea asincrónica. La única preocupación podría ser si su grupo de subprocesos está saturado (es decir, no hay subprocesos disponibles para atender la solicitud en este momento y la configuración del grupo no permite crear más subprocesos), en cuyo caso la solicitud está bloqueada esperando para comenzar la tarea hasta que esté disponible un hilo de grupo.

+0

Gracias! Después de leer su respuesta, edité la pregunta con una muestra de código. ¿Puedes echarle un vistazo? – tugberk

+0

Espero que mi actualización responda a sus preguntas adicionales. –

+0

Tiene una oración mágica allí para mí: ** llamada 'Task.Factory.StartNew' pondrá en cola una operación en .NET thread-pool. **. En este contexto, cuál es el correcto aquí: ** 1 -) ** Crea un nuevo hilo y, cuando lo hace, ese hilo vuelve a threadpool y espera allí para ser reutilizado nuevamente. ** 2 -) ** Obtiene un hilo de threadpool y ese hilo vuelve a threadpool y espera allí para ser reutilizado de nuevo. ** 3 -) ** Toma el enfoque más eficiente y puede hacer cualquiera de ellos. – tugberk

42

Sí, todos los hilos proceden del grupo de subprocesos. Su aplicación MVC ya tiene varios hilos, cuando llega una solicitud, se toma un nuevo hilo del grupo y se utiliza para dar servicio a la solicitud. Ese hilo estará 'bloqueado' (a partir de otras solicitudes) hasta que se complete el servicio y se complete la solicitud. Si no hay ningún subproceso disponible en el grupo, la solicitud deberá esperar hasta que esté disponible.

Si tiene controladores asíncronos, aún reciben un hilo del grupo, pero mientras atienden la solicitud, pueden abandonar el hilo, mientras esperan que algo suceda (y ese hilo se puede dar a otra solicitud) y cuando el original la solicitud necesita un hilo nuevamente, obtiene uno del grupo.

La diferencia es que si tiene muchas solicitudes de larga ejecución (donde el hilo espera una respuesta de algo) es posible que se le agoten los hilos del grupo para atender incluso las solicitudes básicas. Si tiene controladores asíncronos, no tiene más subprocesos, pero esos subprocesos que están en espera se devuelven al grupo y pueden atender otras solicitudes.

Un ejemplo casi la vida real ... Piense en ello como subir a un autobús, hay cinco personas esperando para entrar en la primera entra en contacto, paga y se sienta (el conductor atendida su petición), se continuar (el conductor está atendiendo su pedido) pero no puede encontrar su dinero; mientras hurga en sus bolsillos, el conductor se da por vencido y obtiene a las dos personas siguientes (atendiendo sus solicitudes), cuando encuentra su dinero, el conductor comienza a tratar con usted nuevamente (completando su pedido) - la quinta persona tiene que esperar hasta Ya terminaste, pero la tercera y la cuarta persona fueron atendidas mientras estabas a la mitad del tiempo para que te atendieran. Esto significa que el controlador es el único hilo del grupo y los pasajeros son las solicitudes. Era demasiado complicado escribir cómo funcionaría si hubiera dos controladores, pero se puede imaginar ...

Sin un controlador asíncrono, los pasajeros detrás de usted tendrían que esperar años mientras buscaba su dinero, mientras tanto el autobús el conductor no estaría trabajando.

Así que la conclusión es que si muchas personas no saben dónde está su dinero (es decir, requieren mucho tiempo para responder algo que el conductor les haya preguntado), los controladores asíncronos podrían ayudar a procesar solicitudes, acelerando el proceso de algunos. Sin un controlador de aysnc, todos esperan hasta que la persona de enfrente haya sido completamente tratada. PERO no olvide que en MVC tiene muchos conductores de autobús en un solo bus, por lo que la sincronización no es una opción automática.

+6

Muy buena analogía. Gracias. –

+0

Me gustó la descripción. Gracias –

+0

Excelente manera de explicarlo. Gracias, –

9

Aquí hay dos conceptos en juego. En primer lugar, podemos hacer que nuestro código se ejecute en paralelo para ejecutarlo más rápido o programar código en otro hilo para evitar que el usuario espere. El ejemplo que tuvo

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
     //Do an advanced looging here which takes a while 
    }); 

    return View(); 
} 

pertenece a la segunda categoría. El usuario obtendrá una respuesta más rápida, pero la carga de trabajo total en el servidor es mayor porque tiene que hacer el mismo trabajo + manejar el subproceso.

Otro ejemplo de esto sería:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
     //Make async web request to twitter with WebClient.DownloadString() 
    }); 

    Task.Factory.StartNew(() => { 
     //Make async web request to facebook with WebClient.DownloadString() 
    }); 


    //wait for both to be ready and merge the results 

    return View(); 
} 

Debido a que las solicitudes se desarrollan en paralelo el usuario no tendrá que esperar tanto como si fueran hechas en serie. Pero deberías darte cuenta de que utilizamos más recursos aquí que si ejecutáramos en serie porque ejecutamos el código en muchos hilos mientras tenemos el hilo esperando también.

Esto está perfectamente bien en un escenario de cliente.Y es bastante común que para envolver código sincrónico de larga ejecución en una nueva tarea (ejecutarlo en otro subproceso) también mantenga la interfaz de usuario sensible o paralice para hacerlo más rápido. Sin embargo, un hilo todavía se usa para toda la duración. En un servidor con alta carga, esto podría ser contraproducente porque realmente usa más recursos. Esto es lo que la gente le ha advertido acerca de

Controladores asíncronos en MVC tienen otro objetivo. El punto aquí es evitar tener subprocesos sin hacer nada (lo que puede perjudicar la escalabilidad). Realmente solo importa si las API que está llamando tienen métodos asíncronos. Como WebClient.DowloadStringAsync().

El punto es que puede permitir que su hilo sea devuelto para manejar nuevas solicitudes hasta que finalice la solicitud web donde lo llamará de regreso que obtiene el mismo o un nuevo hilo y finaliza la solicitud.

Espero que comprenda la diferencia entre asíncrono y paralelo. Piense en el código paralelo como el código donde se encuentra el hilo y espere el resultado. Mientras que el código asíncrono es un código donde se le notificará cuando el código esté listo y pueda volver a trabajar en él, mientras tanto, el hilo puede hacer otro trabajo.

0

Lo primero es que no es MVC sino el IIS que mantiene el grupo de subprocesos. Por lo tanto, cualquier solicitud que llegue a la aplicación MVC o ASP.NET se publica desde hilos que se mantienen en el grupo de subprocesos. Solo al hacer la aplicación Asynch invoca esta acción en un hilo diferente y lanza el hilo inmediatamente para que se puedan realizar otras solicitudes.

He explicado lo mismo con un video de detalles (http://www.youtube.com/watch?v=wvg13n5V0V0/ "controladores MVC Asynch y falta de hilo") que muestra cómo se produce la falta de hilo en MVC y cómo se minimiza utilizando controladores MVC Asynch. También he medido las colas de solicitud usando perfmon para que pueda ver cómo se disminuyen las colas de solicitudes para el asincron MVC y cómo es peor para las operaciones de Sincronización.

Cuestiones relacionadas