Wyrażenia zadań

W tym artykule opisano obsługę języka F# dla wyrażeń zadań, które są podobne do wyrażeń asynchronicznych, ale umożliwiają bezpośrednie tworzenie zadań platformy .NET. Podobnie jak wyrażenia asynchroniczne, wyrażenia zadań wykonują kod asynchronicznie, czyli bez blokowania wykonywania innych zadań.

Kod asynchroniczny jest zwykle utworzony przy użyciu wyrażeń asynchronicznych. Używanie wyrażeń zadań jest preferowane w przypadku współpracy w szerokim zakresie z bibliotekami platformy .NET, które tworzą lub używają zadań platformy .NET. Wyrażenia zadań mogą również poprawić wydajność i środowisko debugowania. Jednak wyrażenia zadań są dostarczane z pewnymi ograniczeniami, które zostały opisane w dalszej części artykułu.

Składnia

task { expression }

W poprzedniej składni obliczenia reprezentowane przez expression program są konfigurowane do uruchamiania jako zadanie platformy .NET. Zadanie jest uruchamiane natychmiast po wykonaniu tego kodu i jest uruchamiane w bieżącym wątku do momentu wykonania pierwszej operacji asynchronicznej (na przykład asynchronicznego uśpienia, asynchronicznego we/wy lub innej pierwotnej operacji asynchronicznej). Typ wyrażenia to Task<'T>, gdzie 'T jest typem zwracanym przez wyrażenie, gdy return słowo kluczowe jest używane.

Wiązanie przy użyciu let!

W wyrażeniu zadania niektóre wyrażenia i operacje są synchroniczne, a niektóre są asynchroniczne. Jeśli oczekujesz na wynik operacji asynchronicznej, zamiast zwykłego let powiązania, użyj polecenia let!. Efektem let! jest umożliwienie wykonywania w celu kontynuowania wykonywania na innych obliczeniach lub wątkach podczas wykonywania obliczeń. Po prawej stronie let! powiązania zostanie wznowione wykonywanie pozostałej części zadania.

Poniższy kod przedstawia różnicę między let i let!. Wiersz kodu, który używa let tylko tworzy zadanie jako obiekt, który można czekać później, używając na przykład task.Wait() lub task.Result. Wiersz kodu, który używa let! , uruchamia zadanie i oczekuje na jego wynik.

// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int)  = stream.ReadAsync(buffer, offset, count, cancellationToken)

Wyrażenia języka F# task { } mogą oczekiwać na następujące rodzaje operacji asynchronicznych:

return Wyrażenia

W wyrażeniach return expr zadań służy do zwracania wyniku zadania.

return! Wyrażenia

W wyrażeniach return! expr zadań służy do zwracania wyniku innego zadania. Jest ona równoważna użyciu let! , a następnie natychmiast zwraca wynik.

Przepływ sterowania

Wyrażenia zadań mogą obejmować konstrukcje for .. in .. doprzepływu sterowania , , while .. do, try .. with ..try .. finally .., if .. then .. else, i if .. then ... Mogą one z kolei obejmować kolejne konstrukcje zadań, z wyjątkiem with programów obsługi i finally , które są wykonywane synchronicznie. Jeśli potrzebujesz asynchronicznego try .. finally .., użyj use powiązania w połączeniu z obiektem typu IAsyncDisposable.

usepowiązania i use!

W wyrażeniach use zadań powiązania mogą wiązać się z wartościami typu IDisposable lub IAsyncDisposable. W przypadku tego ostatniego operacja oczyszczania usuwania jest wykonywana asynchronicznie.

Oprócz let!programu można używać use! do wykonywania powiązań asynchronicznych. Różnica między elementami let! i use! jest taka sama jak różnica między let i use. W przypadku use!elementu obiekt jest usuwany na zamknięciu bieżącego zakresu. Należy pamiętać, use! że w języku F# 6 nie zezwala na zainicjowanie wartości null, mimo że use nie.

Zadania wartości

Zadania wartości to struktury używane do unikania alokacji w programowaniu opartym na zadaniach. Zadanie wartości to efemeryczna wartość, która zamienia się w rzeczywiste zadanie przy użyciu polecenia .AsTask().

Aby utworzyć zadanie wartości na podstawie wyrażenia zadania, użyj polecenia |> ValueTask<ReturnType> lub |> ValueTask. Na przykład:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

Dodawanie tokenów anulowania i sprawdzanie anulowania

W przeciwieństwie do wyrażeń asynchronicznych języka F# wyrażenia zadań nie przekazują niejawnie tokenu anulowania i nie wykonują niejawnie kontroli anulowania. Jeśli kod wymaga tokenu anulowania, należy określić token anulowania jako parametr. Na przykład:

open System.Threading

let someTaskCode (cancellationToken: CancellationToken) =
    task {
        cancellationToken.ThrowIfCancellationRequested()
        printfn $"continuing..."
    }

Jeśli zamierzasz poprawnie anulować kod, dokładnie sprawdź, czy przekazujesz token anulowania do wszystkich operacji bibliotek platformy .NET, które obsługują anulowanie. Na przykład Stream.ReadAsync ma wiele przeciążeń, z których jeden akceptuje token anulowania. Jeśli nie używasz tego przeciążenia, ta konkretna operacja odczytu asynchronicznego nie będzie można anulować.

Zadania w tle

Domyślnie zadania platformy .NET są zaplanowane przy użyciu funkcji SynchronizationContext.Current , jeśli są obecne. Dzięki temu zadania mogą służyć jako kooperacyjni agenci wykonujący wątek interfejsu użytkownika bez blokowania interfejsu użytkownika. Jeśli nie ma, kontynuacje zadań zostaną zaplanowane do puli wątków platformy .NET.

W praktyce często pożądane jest, aby kod biblioteki, który generuje zadania ignoruje kontekst synchronizacji, a zamiast tego zawsze przełącza się do puli wątków platformy .NET, jeśli jest to konieczne. Można to osiągnąć przy użyciu polecenia backgroundTask { }:

backgroundTask { expression }

Zadanie w tle ignoruje dowolne SynchronizationContext.Current w następującym sensie: jeśli rozpoczęto w wątku z wartością inną niż null SynchronizationContext.Current, przełącza się do wątku w tle w puli wątków przy użyciu polecenia Task.Run. Jeśli wątek został uruchomiony z wartością null SynchronizationContext.Current, jest wykonywany w tym samym wątku.

Uwaga

W praktyce oznacza to, że wywołania ConfigureAwait(false) nie są zwykle potrzebne w kodzie zadania języka F#. Zamiast tego zadania, które mają być uruchamiane w tle, powinny być tworzone przy użyciu polecenia backgroundTask { ... }. Każde powiązanie zadania zewnętrznego z zadaniem w tle zostanie ponownie zsynchronizowane SynchronizationContext.Current z po zakończeniu zadania w tle.

Ograniczenia zadań dotyczących tailcalls

W przeciwieństwie do wyrażeń asynchronicznych języka F# wyrażenia zadań nie obsługują poleceń tailcalls. Oznacza to, że po return! wykonaniu bieżące zadanie jest rejestrowane jako oczekujące na zadanie, którego wynik jest zwracany. Oznacza to, że funkcje rekursywne i metody zaimplementowane przy użyciu wyrażeń zadań mogą tworzyć niezwiązane łańcuchy zadań, a te mogą używać niezwiązanego stosu lub sterta. Rozważmy na przykład następujący kod:

let rec taskLoopBad (count: int) : Task<string> =
    task {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! taskLoopBad (count-1)
    }

let t = taskLoopBad 10000000
t.Wait()

Ten styl kodowania nie powinien być używany z wyrażeniami zadań — spowoduje utworzenie łańcucha 100000000 zadań i wywołanie elementu StackOverflowException. Jeśli na każdym wywołaniu pętli zostanie dodana operacja asynchroniczna, kod będzie używać zasadniczo niezwiązanej sterty. Rozważ przełączenie tego kodu, aby użyć pętli jawnej, na przykład:

let taskLoopGood (count: int) : Task<string> =
    task {
        for i in count .. 1 do
            printfn $"looping... count = {count}"
        return "done!"
    }

let t = taskLoopGood 10000000
t.Wait()

Jeśli wymagane są asynchroniczne tailcalls, użyj wyrażenia asynchronicznego języka F#, które obsługuje tailcalls. Na przykład:

let rec asyncLoopGood (count: int) =
    async {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! asyncLoopGood (count-1)
    }

let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()

Implementacja zadania

Zadania są implementowane przy użyciu kodu z możliwością wznowienia— nowej funkcji w języku F# 6. Zadania są kompilowane w "Reumable State Machines" przez kompilator języka F#. Opisano je szczegółowo w dokumentach RFC z możliwością wznowienia kodu oraz w sesji społeczności kompilatora języka F#.

Zobacz też