La mejor forma en .NET para administrar la cola de tareas en un hilo separado (único)

Sé que la progtwigción asincrónica ha visto muchos cambios en los últimos años. Estoy algo avergonzado de que me haya dejado oxidado con solo 34 años, pero cuento con StackOverflow para ponerme al día.

Lo que trato de hacer es administrar una cola de “trabajo” en un hilo separado, pero de tal manera que solo se procesa un elemento a la vez. Deseo publicar el trabajo en este hilo y no es necesario que devuelva nada al llamador. Por supuesto, podría simplemente girar un nuevo objeto Thread y hacer que circule sobre un objeto Queue compartido, usando duerme, interrumpe, maneje la espera, etc. Pero sé que las cosas han mejorado desde entonces. Tenemos BlockingCollection , Task , async / await, por no mencionar los paquetes NuGet que probablemente abstraen mucho de eso.

Sé que las preguntas “¿Qué es lo mejor …?” Generalmente son mal vistas así que lo expressé de otra manera diciendo “¿Cuál es la manera recomendada actualmente?” Para lograr algo como esto utilizando mecanismos .NET incorporados preferiblemente. Pero si un paquete NuGet de terceros simplifica un poco las cosas, es igual de bueno.

Consideré una instancia de TaskScheduler con una concurrencia máxima fija de 1, pero parece que hay una forma mucho menos torpe de hacerlo ahora.

Fondo

Específicamente, lo que bash hacer en este caso es poner en cola una tarea de geolocalización IP durante una solicitud web. La misma IP puede terminar en cola para la geolocalización varias veces, pero la tarea sabrá cómo detectar eso y saltearse temprano si ya se ha resuelto. Pero el manejador de solicitudes simplemente lanzará estas llamadas () => LocateAddress(context.Request.UserHostAddress) a una cola y permitirá que el método LocateAddress maneje la detección de trabajo duplicado. La API de geolocalización que estoy usando no le gusta ser bombardeada con solicitudes, por lo que quiero limitarla a una única tarea simultánea a la vez. Sin embargo, sería bueno si el enfoque se permitiera escalar fácilmente a más tareas simultáneas con un cambio de parámetro simple.

Para crear un solo grado asíncrono de cola de paralelismo de trabajo, puede simplemente crear un SemaphoreSlim , inicializado en uno, y luego tener el método de enrutamiento await en la adquisición de ese semáforo antes de comenzar el trabajo solicitado.

 public class TaskQueue { private SemaphoreSlim semaphore; public TaskQueue() { semaphore = new SemaphoreSlim(1); } public async Task Enqueue(Func> taskGenerator) { await semaphore.WaitAsync(); try { return await taskGenerator(); } finally { semaphore.Release(); } } public async Task Enqueue(Func taskGenerator) { await semaphore.WaitAsync(); try { await taskGenerator(); } finally { semaphore.Release(); } } } 

Por supuesto, para tener un grado fijo de paralelismo distinto a uno simplemente inicialice el semáforo a algún otro número.

Tu mejor opción es ver el ActionBlock TPL Dataflow :

 var actionBlock = new ActionBlock(address => { if (!IsDuplicate(address)) { LocateAddress(address); } }); actionBlock.Post(context.Request.UserHostAddress); 

TPL Dataflow es un framework robusto, seguro para subprocesos, async y muy configurable basado en actores (disponible como nuget)

Aquí hay un ejemplo simple para un caso más complicado. Supongamos que quieres:

  • Habilite la concurrencia (limitado a los núcleos disponibles).
  • Limite el tamaño de la cola (para que no se quede sin memoria).
  • Haga que tanto LocateAddress como la inserción de la cola sean async .
  • Cancele todo después de una hora.
 var actionBlock = new ActionBlock(async address => { if (!IsDuplicate(address)) { await LocateAddressAsync(address); } }, new ExecutionDataflowBlockOptions { BoundedCapacity = 10000, MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = new CancellationTokenSource(TimeSpan.FromHours(1)).Token }); await actionBlock.SendAsync(context.Request.UserHostAddress); 

Use BlockingCollection para crear un patrón productor / consumidor con un consumidor (solo una cosa que se ejecute a la vez como desee) y uno o varios productores.

Primero defina una cola compartida en alguna parte:

 BlockingCollection queue = new BlockingCollection(); 

En tu Thread o Task consumo, tomas de ella:

 //This will block until there's an item available Action itemToRun = queue.Take() 

Luego, desde cualquier número de productores en otros hilos, simplemente agregue a la cola:

 queue.Add(() => LocateAddress(context.Request.UserHostAddress)); 

En realidad, no necesita ejecutar tareas en un hilo, necesita que se ejecuten en serie (una tras otra) y FIFO. TPL no tiene clase para eso, pero aquí está mi implementación muy ligera con pruebas. https://github.com/Gentlee/SerialQueue

También tengo la implementación @Servy allí, las pruebas muestran que es dos veces más lenta que la mía y no garantiza FIFO.

Ejemplo:

 private readonly SerialQueue queue = new SerialQueue(); async Task SomeAsyncMethod() { var result = await queue.Enqueue(DoSomething); }