Como declarar uma tarefa não iniciada que aguardará outra tarefa?

9

Eu fiz esse teste de unidade e não entendo por que o "aguardar Task.Delay ()" não 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");
    }

Aqui está a saída do teste de unidade:

Saída do teste de unidade

Como isso é possível, um "aguardar" não espera e o thread principal continua?

Elo
fonte
Não está claro o que você está tentando alcançar. Você pode adicionar sua saída esperada?
OlegI
11
O método de teste é muito claro - espera-se que o isOK seja verdadeiro #
Sir Rufo
Não há motivo para criar uma tarefa não iniciada. Tarefas não são threads, elas usam threads. O que você está tentando fazer? Por que não usar Task.Run()após o primeiro Console.WriteLine?
Panagiotis Kanavos
11
@ Elo, você acabou de descrever as roscas. Você não precisa de um conjunto de tarefas para implementar uma fila de trabalhos. Você precisa de uma fila de trabalhos, por exemplo, objetos Action <T>
Panagiotis Kanavos
2
@ O que você tenta fazer já está disponível no .NET, por exemplo, através das classes TPL Dataflow, como ActionBlock, ou as mais recentes classes System.Threading.Channels. Você pode criar um ActionBlock para receber e processar mensagens usando uma ou mais tarefas simultâneas. Todos os blocos possuem buffers de entrada com capacidade configurável. O DOP e capacidade de permitir que você controle de concorrência, solicitações do acelerador e implementar contrapressão - se muitas mensagens são enfileiradas, os aguarda produtores
Panagiotis Kanavos

Respostas:

8

new Task(async () =>

Uma tarefa não leva a Func<Task>, mas sim Action. Ele chamará seu método assíncrono e esperará que ele termine quando retornar. Mas isso não acontece. Retorna uma tarefa. Essa tarefa não é aguardada pela nova tarefa. Para a nova tarefa, o trabalho é realizado assim que o método é retornado.

Você precisa usar a tarefa que já existe, em vez de agrupá-la em uma nova tarefa:

[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
fonte
4
Task.Run(async() => ... )também é uma opção
Sir Rufo
Você acabou de fazer o mesmo que um autor da pergunta
OlegI
BTW myTask.Start();irá aumentar umInvalidOperationException
Sir Rufo
@ OlegI eu não vejo isso. Você poderia explicar isso por favor?
Nvoigt 15/11/19
@ Nvoigt Eu suponho que ele significa que myTask.Start()traria uma exceção para sua alternativa e deve ser removido ao usar Task.Run(...). Não há erro na sua solução.
404
3

O problema é que você está usando a Taskclasse não genérica , que não pretende produzir um resultado. Portanto, quando você cria a Taskinstância passando um delegado assíncrono:

Task myTask = new Task(async () =>

... o delegado é tratado como async void. Um async voidnão é um Task, não pode ser esperado, sua exceção não pode ser tratada e é uma fonte de milhares de perguntas feitas por programadores frustrados aqui no StackOverflow e em outros lugares. A solução é usar a Task<TResult>classe genérica , porque você deseja retornar um resultado, e o resultado é outro Task. Então você tem que criar um Task<Task>:

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

Agora, quando você for Startexterno Task<Task>, será concluído quase instantaneamente, porque seu trabalho é apenas criar o interior Task. Você terá que aguardar o interior Tasktambém. É assim que isso pode ser feito:

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

Você tem duas alternativas. Se você não precisar de uma referência explícita ao interno Task, poderá aguardar o externo Task<Task>duas vezes:

await await myTask;

... ou você pode usar o método de extensão Unwrapinterno que combina as tarefas externa e interna em uma:

await myTask.Unwrap();

Esse desempacotamento acontece automaticamente quando você usa o Task.Runmétodo muito mais popular que cria tarefas quentes, portanto, ele Unwrapnão é usado com muita frequência atualmente.

Caso você decida que seu representante assíncrono deve retornar um resultado, por exemplo string, a , declare que a myTaskvariável é do tipo Task<Task<string>>.

Nota: Eu não apoio o uso de Taskconstrutores para criar tarefas frias. Como uma prática geralmente é desaprovada, por motivos que eu realmente não conheço, mas provavelmente porque é usada tão raramente que tem o potencial de capturar outros usuários / mantenedores / revisores desconhecidos do código de surpresa.

Conselho geral: tenha cuidado sempre que estiver fornecendo um delegado assíncrono como argumento para um método. Idealmente, esse método deve esperar um Func<Task>argumento (o que significa que compreende delegados assíncronos) ou pelo menos um Func<T>argumento (o que significa que pelo menos o gerado Tasknão será ignorado). No infeliz caso em que esse método aceite um Action, seu delegado será tratado como async void. Isso raramente é o que você deseja, se é que alguma vez.

Theodor Zoulias
fonte
Como uma resposta técnica detalhada! obrigado.
Elo
@ Elo meu prazer!
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");
        }

insira a descrição da imagem aqui

BASKA
fonte
3
Você percebe que sem o awaitatraso da tarefa, na verdade, não atrasará essa tarefa, certo? Você removeu a funcionalidade.
Nvoigt 15/11/19
Acabei de testar, @nvoigt está certo: Tempo decorrido: 0: 00: 00,0106554. E vemos na sua captura de tela: "tempo decorrido: 18 ms", deve ser> = 1000 ms
Elo
Sim, você está certo, eu atualizo minha resposta. Tnx. Depois de comentar, eu resolvo isso com alterações mínimas. :)
BASKA
11
Boa ideia usar wait () e não esperar de dentro de uma tarefa! obrigado !
Elo