2009-08-23 20 views
12

Actualmente, tengo una gran cantidad de cálculos de C# (llamadas a métodos) que residen en una cola que se ejecutará secuencialmente. Cada cálculo utilizará un servicio de alta latencia (red, disco ...).Patrón de diseño alternativo a Coroutines

Iba a utilizar Mono coroutines para permitir que el próximo cálculo en la cola de cómputo continúe mientras un cómputo anterior está esperando que regrese el servicio de latencia alta. Sin embargo, prefiero no depender de Mono coroutines.

¿Hay algún patrón de diseño implementable en C# puro que me permita procesar cálculos adicionales mientras espero que vuelvan los servicios de alta latencia?

Gracias

Actualización:

necesito para ejecutar un gran número (> 10.000) de las tareas, y cada tarea va a utilizar algún servicio de alta latencia. En Windows, no puede crear tantos subprocesos.

Actualización:

Básicamente, necesito un patrón de diseño que emula las ventajas (a continuación) de tasklets en Stackless Python (http://www.stackless.com/)

  1. enorme # de tareas
  2. Si una la tarea bloquea la siguiente tarea en la cola se ejecuta
  3. Sin pérdida de ciclo de la CPU
  4. Conmutación mínima de la tara entre las tareas
+0

¿Puede hacer un caso más fuerte para corutinas como una solución aquí? Parece pedir el enhebrado (balanceado), como en la respuesta de dtb. –

+0

Bueno, necesito ejecutar un gran número (> 10000) de tareas, y cada tarea usará un servicio de alta latencia. En Windows, no puede crear tantos subprocesos. – jameszhao00

+0

Suena como un trabajo para ThreadPool, +1 para jscharf –

Respuesta

9

Puede simular la lectura cooperativa de microthreading con IEnumerable. Lamentablemente, esto no funcionará con el bloqueo de API, por lo que debe encontrar API que pueda sondear o que tengan devoluciones de llamada que pueda usar para la señalización.

Considere un método

IEnumerable Thread() 
{ 
    //do some stuff 
    Foo(); 

    //co-operatively yield 
    yield null; 

    //do some more stuff 
    Bar(); 

    //sleep 2 seconds 
    yield new TimeSpan (2000); 
} 

El C# compilador desenvolver esto en una máquina de estado - pero el aspecto es el de una MicroThread cooperativa.

El patrón es bastante sencillo. Implementa un "planificador" que mantiene una lista de todos los Enumeradores activos. A medida que avanza por la lista, "ejecuta" cada uno usando MoveNext(). Si el valor de MoveNext es falso, el subproceso ha finalizado y el planificador lo elimina de la lista. Si es verdadero, el planificador accede a la propiedad Actual para determinar el estado actual del hilo. Si es un TimeSpan, el subproceso desea dormir, y el planificador lo movió a una cola que se puede volver a poner en la lista principal cuando los períodos de suspensión han finalizado.

Puede usar otros objetos de retorno para implementar otros mecanismos de señalización. Por ejemplo, defina algún tipo de WaitHandle. Si el hilo produce uno de estos, se puede mover a una cola de espera hasta que se indique el identificador. O podría soportar WaitAll produciendo una matriz de controladores de espera. Incluso podría implementar prioridades.

Realicé una implementación simple de este planificador en aproximadamente 150LOC, pero aún no he podido rediseñar el código. Fue para nuestro contenedor PhyreSharp PhyreEngine (que no será público), donde parece funcionar bastante bien para controlar un par de cientos de personajes en una de nuestras demostraciones.Tomamos prestado el concepto del motor Unity3D: tienen algunos documentos en línea que lo explican desde el punto de vista del usuario.

+0

Cosas interesantes. Miré esto antes, pero no creo que puedas ceder el código correctamente desde más de 1 nivel. Es decir. si tiene una función de inicio de corrutina que llama a otra función que cede, todo se rompe. – jameszhao00

+0

En la mayoría de los casos, puede implementar las funciones que desea llamar como ienumerables y foreach + ceder sobre ellas, aunque, por supuesto, necesitaría un poco de direccionamiento indirecto para obtener valores de retorno. No recuerdo si los param ref. con un rendimiento, pero * si * no lo hacen, hay muchas otras formas. Puede pasar una referencia a un objeto con campos y usarlo para recuperar los valores de la función, o pasar lambdas que pueden establecer sus locals, o filtrar un cierto tipo de los valores arrojados, etc. ... –

+0

I.e. Cosa devuelta = nula; foreach (var o en Función ((x) => returned = x)) yield return o; –

6
+0

Sí, eso no resuelve el problema de continuar el próximo cálculo cuando hay una operación de latencia-intensiva. El paralelismo de tareas hace que la computación paralela sea más fácil. – jameszhao00

+4

la biblioteca paralela de tarea es libre de usar más hilos que núcleos, por lo tanto, si detecta que los hilos en uso no usan mucho tiempo de CPU, puede programar más tareas ... Esto puede conducir a un exceso de IO, por lo que es necesario ajustarlo, es Espero que la biblioteca haga gran parte de esto por usted, pero la evaluación comparativa y el control siempre es una buena idea ... – ShuggyCoUk

+0

Los hilos verdaderos son un poco demasiado pesados ​​para este proyecto. Necesito 20k-80k tareas de computación ejecutándose a la vez. – jameszhao00

1

No es éste un uso convencional de multiproceso ¿tratamiento?

Tener un vistazo a los patrones como Reactor here

+0

Lo siento. Estoy un poco confundido sobre cómo se puede usar aquí. – jameszhao00

1

Escribirlo utilizar Async IO podría ser suficiente.

Esto puede llevar a un código desagradable y difícil de depurar sin una estructura sólida en el diseño.

+0

En una capa inferior, sí, usaré AsyncIO para enviar/recibir paquetes de red. Sin embargo, en las capas superiores implementaré algún tipo de RPC sincrónico. – jameszhao00

5

le recomiendo usar el Thread Pool para ejecutar múltiples tareas de la cola a la vez en lotes manejables utilizando una lista de tareas activas que se alimenta de la cola de tareas.

En este escenario, su hilo de trabajo principal inicialmente incluiría N tareas de la cola en la lista de tareas activas para enviar al grupo de subprocesos (probablemente usando QueueUserWorkItem), donde N representa una cantidad manejable que no sobrecargará el grupo de subprocesos, empantane su aplicación con costos de sincronización y programación de subprocesos, o absorba la memoria disponible debido a la sobrecarga combinada de memoria de E/S de cada tarea.

Siempre que una tarea indique que se ha completado el hilo del trabajador, puede eliminarlo de la lista de tareas activas y agregar el siguiente de su cola de tareas para su ejecución.

Esto le permitirá tener un conjunto rodante de N tareas de su cola. Puede manipular N para afectar las características de rendimiento y encontrar lo mejor en sus circunstancias particulares.

Dado que, en última instancia, usted está embotellado por las operaciones de hardware (E/S de disco y E/S de red, CPU), imagino que lo más pequeño es mejor. Dos tareas de grupo de subprocesos que trabajan en E/S de disco probablemente no se ejecutarán más rápido que una.

También podría implementar flexibilidad en el tamaño y contenido de la lista de tareas activa restringiéndola a un número determinado de tipo de tarea en particular. Por ejemplo, si está ejecutando en una máquina con 4 núcleos, puede encontrar que la configuración de mayor rendimiento es cuatro tareas vinculadas a la CPU que se ejecutan simultáneamente junto con una tarea vinculada a disco y una tarea de red.

Si ya tiene una tarea clasificada como tarea IO de disco, puede optar por esperar hasta que se complete antes de agregar otra tarea IO de disco, y puede optar por programar una tarea enlazada a la CPU o enlazada a la red en el mientras tanto.

Espero que esto tenga sentido!

PD: ¿Tiene alguna dependencia en el orden de las tareas?

+0

No. No hay ningún requisito sobre el orden de ejecución. – jameszhao00

+0

Déjame ver si tengo esto. Para cada núcleo, un número específico de subprocesos residirá en un grupo de subprocesos. Inicialmente, una tarea se asigna a un hilo y se ejecuta. Cada vez que una tarea bloquea (E/S, ...), la tarea notifica/activa el hilo del controlador para ese núcleo de la CPU y el controlador inicia un nuevo hilo o activa un hilo anterior. Esto continúa hasta que todas las tareas hayan sido procesadas. – jameszhao00

+0

Estás un poco decepcionado, pero creo que tienes lo esencial. Debería leer la documentación de ThreadPool (o Google para algunos tutoriales usando QueueUserWorkItem). Realmente no hay hilos creados para cada núcleo. Piense en ThreadPool como una abstracción independiente de núcleos. Simplemente debe ejecutar varias tareas que debe programar y ejecutar al mismo tiempo siempre que sea posible (lo que generalmente ocurre). – jscharf

2

Definitivamente debe consultar el Concurrency and Coordination Runtime. Una de sus muestras describe exactamente de lo que está hablando: llama a los servicios de latencia larga, y el CCR permite de manera eficiente que se ejecute alguna otra tarea mientras espera. Puede manejar una gran cantidad de tareas porque no necesita generar un hilo para cada una, aunque utilizará todos sus núcleos si así lo solicita.

0

De hecho, si usa un hilo para una tarea, perderá el juego. Piensa en por qué Node.js puede admitir una gran cantidad de conexiones. Usando algunos números de hilo con async IO !!! Async y funciones de espera pueden ayudar en esto.

foreach (var task in tasks) 
{ 
    await SendAsync(task.value); 
    ReadAsync(); 
} 

SendAsync() y ReadAsync() son falsos funciones de llamada IO asíncrono.

Task parallelism es también una buena elección. Pero no estoy seguro de cuál es más rápido. Puede probar ambos en su caso.

0

Sí, por supuesto que puede. Solo necesita crear un mecanismo de despacho que vuelva a llamar a una lambda que usted proporcione y entre en una cola. Todo el código que escribo en unity usa este enfoque y nunca uso corutinas. Envuelvo métodos que usan corotines como WWW para deshacerme de ellos. En teoría, las corutinas pueden ser más rápidas porque hay menos sobrecarga. Prácticamente introducen una nueva sintaxis en un idioma para hacer una tarea bastante trivial y, además, no puedes seguir el seguimiento de la pila correctamente en un error en una co-rutina porque todo lo que verás es -> Siguiente. A continuación, deberá implementar la capacidad de ejecutar las tareas en la cola de otro hilo. Sin embargo, hay funciones paralelas en el último .net y básicamente estarías escribiendo funcionalidades similares. No serían muchas líneas de código realmente.

Si alguien está interesado, le enviaría el código, no lo tengo.