Come dichiarare un'attività non avviata in attesa di un'altra attività?


9

Ho fatto questo test unitario e non capisco perché "waitit Task.Delay ()" non aspetta!

   [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");
    }

Ecco l'output del test unitario:

Uscita test unitario

Come è possibile che un "wait" non aspetti e il thread principale continua?


Non è chiaro cosa stai cercando di ottenere. Potete per favore aggiungere l'output previsto?
OlegI,

1
Il metodo di prova è molto chiaro - isOK dovrebbe essere vero
Sir Rufo,

Non c'è motivo di creare un'attività non avviata. Le attività non sono thread, usano thread. Cosa stai cercando di fare? Perché non usare Task.Run()dopo il primo Console.WriteLine?
Panagiotis Kanavos,

1
@Elo hai appena descritto i threadpools. Non è necessario un pool di attività per implementare una coda lavori. Hai bisogno di una coda di lavori, ad esempio oggetti Action <T>
Panagiotis Kanavos

2
@Elo, ciò che si tenta di fare è già disponibile in .NET, ad esempio tramite le classi TPL Dataflow come ActionBlock o le nuove classi System.Threading.Channels. È possibile creare un ActionBlock per ricevere ed elaborare i messaggi utilizzando una o più attività simultanee. Tutti i blocchi hanno buffer di input con capacità configurabile. Il DOP e la capacità consentono di controllare la concorrenza, limitare le richieste e implementare la contropressione - se vengono messi in coda troppi messaggi, il produttore attende
Panagiotis Kanavos

Risposte:


8

new Task(async () =>

Un'attività non richiede un Func<Task>, ma un Action. Chiamerà il tuo metodo asincrono e si aspetta che finisca quando ritorna. Ma non lo fa. Restituisce un'attività. Quel compito non è atteso dal nuovo compito. Per la nuova attività, il lavoro viene eseguito una volta restituito il metodo.

È necessario utilizzare l'attività già esistente invece di racchiuderla in una nuova attività:

[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");
}

4
Task.Run(async() => ... )è anche un'opzione
Sir Rufo,

Hai appena fatto lo stesso di un autore della domanda
OlegI

BTW myTask.Start();rilancierà unInvalidOperationException
Sir Rufo il

@OlegI Non lo vedo. Potresti spiegarlo per favore?
nvoigt,

@nvoigt Presumo che significhi myTask.Start()produrre un'eccezione per la sua alternativa e dovrebbe essere rimosso durante l'utilizzo Task.Run(...). Non ci sono errori nella tua soluzione.
404

3

Il problema è che stai usando la Taskclasse non generica , che non ha lo scopo di produrre un risultato. Quindi quando si crea l' Taskistanza passando un delegato asincrono:

Task myTask = new Task(async () =>

... il delegato viene trattato come async void. Un async voidnon è una Task, non può essere atteso, la sua eccezione non può essere gestito, ed è una fonte di migliaia di domande fatte dai programmatori frustrati qui in StackOverflow e altrove. La soluzione è utilizzare la Task<TResult>classe generica , perché si desidera restituire un risultato e il risultato è un altro Task. Quindi devi creare un Task<Task>:

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

Ora quando sei Startesterno Task<Task>sarà completato quasi istantaneamente perché il suo compito è solo quello di creare l'interno Task. Dovrai quindi aspettare anche l'interno Task. Ecco come si può fare:

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

Hai due alternative. Se non hai bisogno di un riferimento esplicito all'interiore Task, puoi semplicemente attendere l'esterno Task<Task>due volte:

await await myTask;

... oppure puoi utilizzare il metodo di estensione incorporato Unwrapche combina le attività esterne e interne in una:

await myTask.Unwrap();

Questo scartamento si verifica automaticamente quando si utilizza il Task.Runmetodo molto più popolare che crea attività a caldo, quindi al Unwrapgiorno d'oggi non viene utilizzato molto spesso.

Nel caso in cui decidi che il tuo delegato asincrono deve restituire un risultato, ad esempio a string, devi dichiarare che la myTaskvariabile è di tipo Task<Task<string>>.

Nota: non approvo l'uso di Taskcostruttori per la creazione di attività a freddo. Dato che una pratica è generalmente disapprovata, per ragioni che non conosco davvero, ma probabilmente perché è usata così raramente che ha il potenziale di sorprendere di sorpresa altri utenti / manutentori / revisori del codice.

Consiglio generale: fare attenzione ogni volta che si fornisce un delegato asincrono come argomento a un metodo. Questo metodo dovrebbe idealmente aspettarsi un Func<Task>argomento (nel senso che comprende i delegati asincroni), o almeno un Func<T>argomento (nel senso che almeno il generato Tasknon verrà ignorato). Nel malaugurato caso in cui questo metodo accetti un Action, il tuo delegato verrà trattato come async void. Questo è raramente quello che vuoi, se mai.


Come una risposta tecnica dettagliata! grazie.
Elo,

@Elo mio piacere!
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");
        }

inserisci qui la descrizione dell'immagine


3
Ti rendi conto che senza il await, il ritardo dell'attività non ritarderà effettivamente tale attività, giusto? Hai rimosso la funzionalità.
nvoigt,

Ho appena provato, @nvoigt ha ragione: tempo trascorso: 0: 00: 00,0106554. E vediamo nel tuo screenshot: "tempo trascorso: 18 ms", dovrebbe essere> = 1000 ms
Elo

Sì, hai ragione, aggiorno la mia risposta. Tnx. Dopo aver commentato, lo risolvo con modifiche minime. :)
BASKA

1
Buona idea usare wait () e non attendere dall'interno di un'attività! Grazie !
Elo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.