¿Cómo declarar una tarea no iniciada que esperará otra tarea?

9

¡He hecho esta Prueba de Unidad y no entiendo por qué el "esperar Task.Delay ()" no espera!

   [TestMethod]
    public async Task SimpleTest()
    {
        bool isOK = false;
        Task myTask = new Task(async () =>
        {
            Console.WriteLine("Task.BeforeDelay");
            await Task.Delay(1000);
            Console.WriteLine("Task.AfterDelay");
            isOK = true;
            Console.WriteLine("Task.Ended");
        });
        Console.WriteLine("Main.BeforeStart");
        myTask.Start();
        Console.WriteLine("Main.AfterStart");
        await myTask;
        Console.WriteLine("Main.AfterAwait");
        Assert.IsTrue(isOK, "OK");
    }

Aquí está la salida de Prueba de Unidad:

Salida de prueba unitaria

¿Cómo es posible que un "esperar" no espere y el hilo principal continúe?

Elo
fuente
No está claro qué está tratando de lograr. ¿Puedes agregar tu salida esperada?
OlegI
1
El método de prueba es muy claro: se espera que isOK sea cierto
Sir Rufo, el
No hay razón para crear una tarea no iniciada. Las tareas no son hilos, usan hilos. ¿Que estás tratando de hacer? ¿Por qué no usar Task.Run()después del primero Console.WriteLine?
Panagiotis Kanavos
1
@Elo, acabas de describir los grupos de subprocesos. Usted no necesita un grupo de tareas para implementar una cola de trabajos. Necesita una cola de trabajos, p. Ej. Objetos Action <T>
Panagiotis Kanavos
2
@Elo, lo que intenta hacer ya está disponible en .NET, por ejemplo, a través de clases de flujo de datos TPL como ActionBlock o las clases más recientes System.Threading.Channels. Puede crear un ActionBlock para recibir y procesar mensajes utilizando una o más tareas simultáneas. Todos los bloques tienen memorias intermedias de entrada con capacidad configurable. El DOP y la capacidad le permiten controlar la concurrencia, acelerar las solicitudes e implementar la contrapresión, si hay demasiados mensajes en cola, el productor espera
Panagiotis Kanavos

Respuestas:

8

new Task(async () =>

Una tarea no requiere un Func<Task>, sino un Action. Llamará a su método asincrónico y esperará que termine cuando regrese. Pero no lo hace. Devuelve una tarea. Esa tarea no es esperada por la nueva tarea. Para la nueva tarea, el trabajo se realiza una vez que se devuelve el método.

Debe usar la tarea que ya existe en lugar de envolverla en una nueva tarea:

[TestMethod]
public async Task SimpleTest()
{
    bool isOK = false;

    Func<Task> asyncMethod = async () =>
    {
        Console.WriteLine("Task.BeforeDelay");
        await Task.Delay(1000);
        Console.WriteLine("Task.AfterDelay");
        isOK = true;
        Console.WriteLine("Task.Ended");
    };

    Console.WriteLine("Main.BeforeStart");
    Task myTask = asyncMethod();

    Console.WriteLine("Main.AfterStart");

    await myTask;
    Console.WriteLine("Main.AfterAwait");
    Assert.IsTrue(isOK, "OK");
}
nvoigt
fuente
44
Task.Run(async() => ... )también es una opción
Sir Rufo
Usted acaba de hacer lo mismo como autor de la pregunta
OlegI
Por cierto myTask.Start();levantará unInvalidOperationException
Sir Rufo
@OlegI No lo veo. ¿Podría explicarlo por favor?
nvoigt
@nvoigt Supongo que quiere decir myTask.Start()que produciría una excepción para su alternativa y debería eliminarse cuando se usa Task.Run(...). No hay error en su solución.
404
3

El problema es que está utilizando la Taskclase no genérica , que no está destinada a producir un resultado. Entonces, cuando crea la Taskinstancia pasando un delegado asíncrono:

Task myTask = new Task(async () =>

... el delegado es tratado como async void. Un async voidno es un Task, no se puede esperar, su excepción no se puede manejar, y es una fuente de miles de preguntas hechas por programadores frustrados aquí en StackOverflow y en otros lugares. La solución es usar la Task<TResult>clase genérica , porque desea devolver un resultado, y el resultado es otro Task. Entonces tienes que crear un Task<Task>:

Task<Task> myTask = new Task<Task>(async () =>

Ahora, cuando lo Startexterno Task<Task>se completará casi instantáneamente porque su trabajo es solo crear lo interno Task. Entonces también tendrás que esperar lo interno Task. Así es como se puede hacer:

myTask.Start();
Task myInnerTask = await myTask;
await myInnerTask;

Tienes dos alternativas. Si no necesita una referencia explícita al interior Task, puede esperar Task<Task>dos veces al exterior :

await await myTask;

... o puede usar el método de extensión incorporado Unwrapque combina las tareas externas e internas en una sola:

await myTask.Unwrap();

Este desenvolvimiento ocurre automáticamente cuando usa el Task.Runmétodo mucho más popular que crea tareas activas , por lo Unwrapque no se usa con mucha frecuencia en la actualidad.

En caso de que decida que su delegado asíncrono debe devolver un resultado, por ejemplo a string, entonces debe declarar que la myTaskvariable es de tipo Task<Task<string>>.

Nota: No apruebo el uso de Taskconstructores para crear tareas en frío. Como una práctica generalmente está mal vista, por razones que realmente no conozco, pero probablemente porque se usa tan raramente que tiene el potencial de atrapar a otros usuarios / mantenedores / revisores del código por sorpresa.

Consejo general: tenga cuidado cada vez que proporcione un delegado asíncrono como argumento para un método. Idealmente, este método debería esperar un Func<Task>argumento (lo que significa que entiende a los delegados asíncronos), o al menos un Func<T>argumento (lo que significa que al menos lo generado Taskno será ignorado). En el desafortunado caso de que este método acepte un Action, su delegado será tratado como async void. Esto rara vez es lo que quieres, si es que alguna vez.

Theodor Zoulias
fuente
¡Cómo una respuesta técnica detallada! gracias.
Elo
@Elo un placer!
Theodor Zoulias
1
 [Fact]
        public async Task SimpleTest()
        {
            bool isOK = false;
            Task myTask = new Task(() =>
            {
                Console.WriteLine("Task.BeforeDelay");
                Task.Delay(3000).Wait();
                Console.WriteLine("Task.AfterDelay");
                isOK = true;
                Console.WriteLine("Task.Ended");
            });
            Console.WriteLine("Main.BeforeStart");
            myTask.Start();
            Console.WriteLine("Main.AfterStart");
            await myTask;
            Console.WriteLine("Main.AfterAwait");
            Assert.True(isOK, "OK");
        }

ingrese la descripción de la imagen aquí

BASKA
fuente
3
Te das cuenta de que sin el await, el retraso de la tarea no retrasará esa tarea, ¿verdad? Eliminaste la funcionalidad.
nvoigt
Acabo de probar, @nvoigt tiene razón: Tiempo transcurrido: 0: 00: 00,0106554. Y vemos en su captura de pantalla: "tiempo transcurrido: 18 ms", debería ser> = 1000 ms
Elo
Sí, tienes razón, actualizo mi respuesta. Tnx. Después de tus comentarios, lo resuelvo con cambios mínimos. :)
BASKA
1
¡Es una buena idea usar wait () y no esperar desde dentro de una tarea! Gracias !
Elo