Secuenciación de tareas y reentrada

Tengo el siguiente escenario, que creo que podría ser bastante común:

  1. Hay una tarea (un controlador de comandos de UI) que puede completarse de forma sincrónica o asíncrona.

  2. Los comandos pueden llegar más rápido de lo que se procesan.

  3. Si ya hay una tarea pendiente para un comando, la nueva tarea del controlador de comandos debe ponerse en cola y procesarse secuencialmente.

  4. El resultado de cada tarea nueva puede depender del resultado de la tarea previa.

La cancelación debe ser observada, pero me gustaría dejarla fuera del scope de esta pregunta por simplicidad. Además, la seguridad de subprocesos (concurrencia) no es un requisito, pero debe admitirse la reentrada.

Aquí hay un ejemplo básico de lo que estoy tratando de lograr (como una aplicación de consola, por simplicidad):

using System; using System.Threading.Tasks; namespace ConsoleApp { class Program { static void Main(string[] args) { var asyncOp = new AsyncOp(); Func<int, Task> handleAsync = async (arg) => { Console.WriteLine("this task arg: " + arg); //await Task.Delay(arg); // make it async return await Task.FromResult(arg); // sync }; Console.WriteLine("Test #1..."); asyncOp.RunAsync(() => handleAsync(1000)); asyncOp.RunAsync(() => handleAsync(900)); asyncOp.RunAsync(() => handleAsync(800)); asyncOp.CurrentTask.Wait(); Console.WriteLine("\nPress any key to continue to test #2..."); Console.ReadLine(); asyncOp.RunAsync(() => { asyncOp.RunAsync(() => handleAsync(200)); return handleAsync(100); }); asyncOp.CurrentTask.Wait(); Console.WriteLine("\nPress any key to exit..."); Console.ReadLine(); } // AsyncOp class AsyncOp { Task _pending = Task.FromResult(default(T)); public Task CurrentTask { get { return _pending; } } public Task RunAsync(Func<Task> handler) { var pending = _pending; Func<Task> wrapper = async () => { // await the prev task var prevResult = await pending; Console.WriteLine("\nprev task result: " + prevResult); // start and await the handler return await handler(); }; _pending = wrapper(); return _pending; } } } } 

La salida:

 Prueba n. ° 1 ...

 Resultado de la tarea anterior: 0
 esta tarea arg: 1000

 Resultado de la tarea anterior: 1000
 esta tarea arg: 900

 Resultado de la tarea anterior: 900
 esta tarea arg: 800

 Presione cualquier tecla para continuar con la prueba # 2 ...


 Resultado de la tarea anterior: 800

 Resultado de la tarea anterior: 800
 esta tarea arg: 200
 esta tarea arg: 100

 Presiona cualquier tecla para salir...

Funciona de acuerdo con los requisitos, hasta que se vuelva a introducir en la prueba n. ° 2:

 asyncOp.RunAsync(() => { asyncOp.RunAsync(() => handleAsync(200)); return handleAsync(100); }); 

El resultado deseado debe ser 100 , 200 , en lugar de 200 , 100 , porque ya hay una tarea externa pendiente para 100 . Eso es obviamente porque la tarea interna se ejecuta de forma síncrona, rompiendo la lógica var pending = _pending; /* ... */ _pending = wrapper() var pending = _pending; /* ... */ _pending = wrapper() para la tarea externa.

¿Cómo hacer que funcione para la prueba n. ° 2 también?

Una solución sería aplicar asincronía para cada tarea, con Task.Factory.StartNew(..., TaskScheduler.FromCurrentSynchronizationContext() . Sin embargo, no quiero imponer la ejecución asincrónica en los controladores de comandos que podrían ser sincrónicos internamente. , No quiero depender del comportamiento de un contexto de sincronización en particular (es decir, confiar en que Task.Factory.StartNew debería regresar antes de que la tarea creada realmente se haya iniciado).

En el proyecto de la vida real, soy responsable de lo que AsyncOp está arriba, pero no tengo control sobre los manejadores de comandos (es decir, lo que sea que esté dentro de handleAsync ).