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?


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

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.


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


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,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.