Wie deklariere ich eine nicht gestartete Aufgabe, die auf eine andere Aufgabe wartet?

9

Ich habe diesen Unit-Test durchgeführt und verstehe nicht, warum das "Warten auf Task.Delay ()" nicht wartet!

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

Hier ist die Unit Test-Ausgabe:

Unit Test Output

Wie ist das möglich, dass ein "Warten" nicht wartet und der Haupt-Thread fortgesetzt wird?

Elo
quelle
Es ist etwas unklar, was Sie erreichen wollen. Können Sie bitte Ihre erwartete Ausgabe hinzufügen?
OlegI
1
Die Testmethode ist sehr klar - isOK wird voraussichtlich wahr sein
Sir Rufo
Es gibt keinen Grund, eine nicht gestartete Aufgabe zu erstellen. Aufgaben sind keine Threads, sie verwenden Threads. Was versuchst du zu machen? Warum nicht Task.Run()nach dem ersten verwenden Console.WriteLine?
Panagiotis Kanavos
1
@Elo du hast gerade Threadpools beschrieben. Sie benötigen keinen Aufgabenpool, um eine Jobwarteschlange zu implementieren. Sie benötigen eine Warteschlange mit Jobs, z. B. Action <T> -Objekte
Panagiotis Kanavos,
2
@Elo, was Sie versuchen, ist bereits in .NET verfügbar, z. B. über TPL-Datenflussklassen wie ActionBlock oder die neueren System.Threading.Channels-Klassen. Sie können einen ActionBlock erstellen, um Nachrichten mit einer oder mehreren gleichzeitigen Aufgaben zu empfangen und zu verarbeiten. Alle Blöcke haben Eingangspuffer mit konfigurierbarer Kapazität. Der DOP und die Kapazität ermöglichen es Ihnen, die Parallelität zu kontrollieren, Anforderungen zu drosseln und Gegendruck zu implementieren - wenn zu viele Nachrichten in der Warteschlange stehen, wartet der Produzent
Panagiotis Kanavos

Antworten:

8

new Task(async () =>

Eine Aufgabe braucht nicht ein Func<Task>, sondern ein Action. Es ruft Ihre asynchrone Methode auf und erwartet, dass sie bei der Rückkehr endet. Aber das tut es nicht. Es gibt eine Aufgabe zurück. Diese Aufgabe wird von der neuen Aufgabe nicht erwartet. Für die neue Aufgabe ist der Job erledigt, sobald die Methode zurückgegeben wurde.

Sie müssen die bereits vorhandene Aufgabe verwenden, anstatt sie in eine neue Aufgabe einzuschließen:

[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
quelle
4
Task.Run(async() => ... )ist auch eine Option
Sir Rufo
Sie haben genau das Gleiche getan wie ein Autor der Frage
OlegI
Übrigens myTask.Start();wird einInvalidOperationException
Sir Rufo
@OlegI Ich sehe es nicht. Könnten Sie es bitte erklären?
15.
@nvoigt Ich gehe davon aus, dass er meint myTask.Start(), eine Ausnahme für seine Alternative ergeben würde und bei der Verwendung entfernt werden sollte Task.Run(...). Ihre Lösung enthält keinen Fehler.
404
3

Das Problem ist, dass Sie die nicht generische TaskKlasse verwenden, die kein Ergebnis erzeugen soll. Wenn Sie also die TaskInstanz erstellen, die einen asynchronen Delegaten übergibt:

Task myTask = new Task(async () =>

... der Delegierte wird behandelt als async void. An async voidist kein Task, es kann nicht erwartet werden, seine Ausnahme kann nicht behandelt werden, und es ist eine Quelle von Tausenden von Fragen, die von frustrierten Programmierern hier in StackOverflow und anderswo gestellt werden. Die Lösung besteht darin, die generische Task<TResult>Klasse zu verwenden, da Sie ein Ergebnis zurückgeben möchten und das Ergebnis ein anderes ist Task. Sie müssen also Folgendes erstellen Task<Task>:

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

Jetzt, wenn Sie Startdas Äußere Task<Task>, wird es fast sofort abgeschlossen sein, weil seine Aufgabe nur darin besteht, das Innere zu schaffen Task. Sie müssen dann auch auf das Innere warten Task. So kann es gemacht werden:

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

Sie haben zwei Alternativen. Wenn Sie keinen expliziten Verweis auf das Innere benötigen Task, können Sie das Äußere nur Task<Task>zweimal abwarten :

await await myTask;

... oder Sie können die integrierte Erweiterungsmethode verwenden Unwrap, die die äußeren und inneren Aufgaben zu einer Aufgabe kombiniert:

await myTask.Unwrap();

Dieses Auspacken erfolgt automatisch, wenn Sie die viel beliebtere Task.RunMethode verwenden, mit der heiße Aufgaben erstellt werden. Daher wird diese Methode Unwrapheutzutage nicht mehr sehr häufig verwendet.

Wenn Sie entscheiden, dass Ihr asynchroner Delegat ein Ergebnis zurückgeben muss, z. B. a string, sollten Sie die myTaskVariable als vom Typ deklarieren Task<Task<string>>.

Hinweis: Ich unterstütze die Verwendung von TaskKonstruktoren zum Erstellen kalter Aufgaben nicht. Da eine Praxis aus Gründen, die ich nicht wirklich kenne, im Allgemeinen verpönt ist, aber wahrscheinlich, weil sie so selten verwendet wird, dass sie andere ahnungslose Benutzer / Betreuer / Prüfer des Codes überraschen kann.

Allgemeiner Rat: Seien Sie jedes Mal vorsichtig, wenn Sie einen asynchronen Delegaten als Argument für eine Methode angeben. Diese Methode sollte idealerweise ein Func<Task>Argument (dh asynchrone Delegaten) oder zumindest ein Func<T>Argument (dh zumindest das generierte Argument Taskwird nicht ignoriert) erwarten . In dem unglücklichen Fall, dass diese Methode eine akzeptiert Action, wird Ihr Delegierter als behandelt async void. Dies ist selten das, was Sie wollen, wenn überhaupt.

Theodor Zoulias
quelle
Wie eine detaillierte technische Antwort! Vielen Dank.
Elo
@Elo mein Vergnügen!
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");
        }

Geben Sie hier die Bildbeschreibung ein

BASKA
quelle
3
Sie erkennen, dass die Taskverzögerung ohne die awaitAufgabe diese Aufgabe nicht wirklich verzögern wird, oder? Sie haben die Funktionalität entfernt.
Nvoigt
Ich habe gerade getestet, @nvoigt ist richtig: Verstrichene Zeit: 0: 00: 00,0106554. Und wir sehen in Ihrem Screenshot: "Verstrichene Zeit: 18 ms", es sollte> = 1000 ms sein
Elo
Ja du hast recht, ich aktualisiere meine Antwort. Tnx. Nach Ihren Kommentaren löse ich dies mit minimalen Änderungen. :)
BASKA
1
Gute Idee, wait () zu verwenden und nicht innerhalb einer Aufgabe zu warten! Vielen Dank !
Elo