Jak zadeklarować nie rozpoczęte zadanie, które będzie oczekiwać na inne zadanie?

9

Zrobiłem ten test jednostkowy i nie rozumiem, dlaczego „czekaj na Task.Delay ()” nie czeka!

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

Oto wynik testu jednostkowego:

Wyjście testu jednostkowego

Jak to możliwe, że „czekanie” nie czeka, a główny wątek trwa?

Elo
źródło
Nie jest jasne, co próbujesz osiągnąć. Czy możesz dodać oczekiwaną wydajność?
OlegI
1
Metoda testu jest bardzo jasna - oczekuje się, że isOK będzie prawdziwy
Sir Rufo,
Nie ma powodu, aby utworzyć niezainicjowane zadanie. Zadania nie są wątkami, używają wątków. Co próbujesz zrobić? Dlaczego nie użyć Task.Run()po pierwszym Console.WriteLine?
Panagiotis Kanavos
1
@ Elo właśnie opisałeś pule wątków. Państwo nie potrzebują pulę zadań zaimplementować kolejkę zadań. Potrzebujesz kolejki zadań, np. Obiektów Action <T>
Panagiotis Kanavos
2
@Elo to, co próbujesz zrobić, jest już dostępne w .NET, np. Za pośrednictwem klas TPL Dataflow, takich jak ActionBlock, lub nowszych klas System.Threading.Channels. Możesz utworzyć ActionBlock do odbierania i przetwarzania wiadomości za pomocą jednego lub więcej równoczesnych zadań. Wszystkie bloki mają bufory wejściowe o konfigurowalnej pojemności. DOP i pojemność pozwalają kontrolować współbieżność, żądania przepustnicy i wdrażać przeciwciśnienie - jeśli w kolejce jest zbyt wiele wiadomości, producent czeka
Panagiotis Kanavos

Odpowiedzi:

8

new Task(async () =>

Zadanie nie wymaga Func<Task>, ale Action. Wywoła metodę asynchroniczną i oczekuje, że zakończy się po powrocie. Ale tak nie jest. Zwraca zadanie. To zadanie nie jest oczekiwane przez nowe zadanie. W przypadku nowego zadania zadanie jest wykonywane po zwróceniu metody.

Musisz użyć zadania, które już istnieje, zamiast owijać je w nowe zadanie:

[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
źródło
4
Task.Run(async() => ... )jest również opcją
Sir Rufo
Zrobiłeś to samo, co autor pytania
OlegI
BTW myTask.Start();podniesieInvalidOperationException
Sir Rufo
@OlegI Nie widzę tego. Czy mógłbyś to wyjaśnić?
nvoigt
@nvoigt Zakładam, że ma na myśli, że myTask.Start()dałby wyjątek dla swojej alternatywy i powinien zostać usunięty podczas używania Task.Run(...). W twoim rozwiązaniu nie ma błędu.
404
3

Problem polega na tym, że używasz nie-ogólnej Taskklasy, która nie ma na celu uzyskania wyniku. Kiedy tworzysz Taskinstancję, przekazując delegata asynchronicznego:

Task myTask = new Task(async () =>

... delegat jest traktowany jako async void. async voidNie jest Task, to nie może być oczekiwany, jego wyjątek nie może być obsługiwane, i to jest źródłem tysięcy pytań złożonych przez sfrustrowanych programistów tutaj w StackOverflow i gdzie indziej. Rozwiązaniem jest użycie Task<TResult>klasy ogólnej , ponieważ chcesz zwrócić wynik, a wynik jest inny Task. Musisz więc stworzyć Task<Task>:

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

Teraz, kiedy jesteś Startzewnętrzny Task<Task>, zostanie on ukończony niemal natychmiast, ponieważ jego zadaniem jest po prostu stworzenie wnętrza Task. Będziesz musiał także poczekać na wnętrze Task. Oto jak można to zrobić:

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

Masz dwie możliwości. Jeśli nie potrzebujesz wyraźnego odniesienia do wnętrza Task, możesz po prostu Task<Task>dwukrotnie poczekać na zewnętrzny :

await await myTask;

... lub możesz użyć wbudowanej metody rozszerzenia, Unwrapktóra łączy zadania zewnętrzne i wewnętrzne w jedno:

await myTask.Unwrap();

To rozpakowywanie odbywa się automatycznie, gdy korzystasz ze znacznie popularniejszej Task.Runmetody, która tworzy gorące zadania, więc Unwrapnie jest obecnie często używana.

Jeśli zdecydujesz, że delegat asynchroniczny musi zwrócić wynik, na przykład a string, powinieneś zadeklarować myTaskzmienną typu Task<Task<string>>.

Uwaga: Nie popieram używania Taskkonstruktorów do tworzenia zimnych zadań. Jako, że praktyka jest ogólnie niezadowolona, ​​z powodów, których tak naprawdę nie wiem, ale prawdopodobnie dlatego, że jest używana tak rzadko, że może potencjalnie zaskoczyć innych nieświadomych użytkowników / opiekunów / recenzentów kodu.

Porady ogólne: Uważaj za każdym razem, gdy podajesz delegata asynchronicznego jako argument metody. Ta metoda powinna idealnie oczekiwać Func<Task>argumentu (co oznacza, że ​​rozumie delegatów asynchronicznych) lub przynajmniej Func<T>argumentu (co oznacza, że ​​przynajmniej wygenerowany Tasknie zostanie zignorowany). W niefortunnym przypadku, że ta metoda akceptuje Action, twój delegat będzie traktowany jako async void. To rzadko jest to, czego chcesz, jeśli w ogóle.

Theodor Zoulias
źródło
Jaka szczegółowa odpowiedź techniczna! Dziękuję Ci.
Elo,
@Elo moja przyjemność!
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");
        }

wprowadź opis zdjęcia tutaj

BASKA
źródło
3
Zdajesz sobie sprawę, że bez tego awaitopóźnienie zadania nie opóźni tego zadania, prawda? Usunąłeś funkcjonalność.
nvoigt
Właśnie przetestowałem, @nvoigt ma rację: Upłynął czas: 0: 00: 00,0106554. I widzimy na twoim zrzucie ekranu: „czas, który upłynął: 18 ms”, powinien wynosić> = 1000 ms
Elo
Tak, masz rację, aktualizuję moją odpowiedź. Tnx. Po komentarzach rozwiązuję to przy minimalnych zmianach. :)
BASKA,
1
Dobrym pomysłem jest użycie funkcji wait () i nie czekanie z wewnątrz zadania! dzięki !
Elo,