Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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 są konfigurowane do uruchamiania jako zadanie .NET. Zadanie jest uruchamiane natychmiast po wykonaniu tego kodu i działa w bieżącym wątku do momentu wykonania pierwszej operacji asynchronicznej (na przykład asynchronicznego uśpienia, asynchronicznego wejścia/wyjścia 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 let!. Efektem let! jest umożliwienie kontynuowania wykonywania innych obliczeń lub wątków jednocześnie z realizacją obliczeń. Po powrocie prawej strony let! powiązania, wznowione zostaje wykonywanie pozostałej części zadania.
Poniższy kod przedstawia różnicę między let i let!. Wiersz kodu, który używa let, jedynie tworzy zadanie jako obiekt, na który można później czekać, używając na przykład task.Wait() lub task.Result. Linia kodu, która 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:
- Zadania Task<TResult> platformy .NET i nieogólne Task.
- Zadania .NET typu wartościowego ValueTask<TResult> i nieogólne ValueTask.
- Asynchroniczne obliczenia języka F#
Async<T>. - Każdy obiekt zgodny z wzorcem "GetAwaiter" określonym w F# RFC FS-1097.
return Wyrażenia
W wyrażeniach zadań, return expr służy do zwracania wyniku zadania.
return! Wyrażenia
return! expr w wyrażeniach zadań jest używane do zwracania wyniku innego zadania. Jest ona równoważna użyciu let! , a następnie natychmiast zwraca wynik.
Sterowanie przebiegiem
Wyrażenia zadań mogą obejmować konstrukcje przepływu sterowania, takie jak for .. in .. do, while .. do, try .. with .., try .. finally .., if .. then .. else i if .. then ... Mogą one z kolei obejmować kolejne konstrukcje zadań, z wyjątkiem obsługujących with i finally , które są wykonywane synchronicznie. Jeśli potrzebujesz try .. finally .. w trybie asynchronicznym, stosuj powiązanie use w połączeniu z obiektem typu IAsyncDisposable.
use i use! wiązania
W wyrażeniach use zadań wiązania mogą łączyć się z wartościami typu IDisposable lub IAsyncDisposable. W przypadku tego ostatniego operacja związana z usuwaniem jest wykonywana asynchronicznie.
Oprócz let!, 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 zostaje usunięty po zamknięciu bieżącego zakresu. Należy pamiętać, że w języku F# 6 use! nie zezwala na zainicjowanie wartości null, chociaż use na to pozwala.
open System
open System.IO
open System.Security.Cryptography
task {
// use IDisposable
use httpClient = new Net.Http.HttpClient()
// use! Task<IDisposable>
use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
// use IDisposable
use aes = Aes.Create()
aes.KeySize <- 256
aes.GenerateIV()
aes.GenerateKey()
// do! Task
do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")
// use IAsyncDisposable
use outputStream = File.Create "secret.enc"
// use IDisposable
use encryptor = aes.CreateEncryptor()
// use IAsyncDisposable
use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
// do! Task
do! exampleDomain.Content.CopyToAsync cryptoStream
}
Zadania wartości
Zadania wartości to struktury używane do unikania alokacji w programowaniu opartym na zadaniach. Zadanie wartości to przemijająca wartość, która przekształca się w rzeczywiste zadanie dzięki użyciu .AsTask().
Aby utworzyć zadanie wartości na podstawie wyrażenia zadania, użyj polecenia |> ValueTask<ReturnType> lub |> ValueTask. Przykład:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and! powiązania (począwszy od języka F# 10)
W wyrażeniach zadań można równocześnie oczekiwać na wiele operacji asynchronicznych (Task<'T>, ValueTask<'T>, Async<'T>, itp). Porównaj:
// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
task {
let! x = getX()
let! y = getY()
return x, y
}
// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
task {
let! x = getX()
and! y = getY()
return x, y
}
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. 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żyjesz tego przeciążenia, tej konkretnej operacji 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 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 są obecne, kontynuacje zadań zostaną przydzielone do puli wątków platformy .NET.
W praktyce często pożądane jest, aby kod biblioteki, który generuje zadania ignorował kontekst synchronizacji i zamiast tego, jeśli jest to konieczne, zawsze korzystał z puli wątków .NET. 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 / Notatka
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 po zakończeniu zadania w tle.
Ograniczenia zadań dotyczących wywołań ogonowych
W przeciwieństwie do asynchronicznych wyrażeń w F#, wyrażenia zadaniowe nie obsługują tailcalls. Oznacza to, że gdy return! jest wykonywane, bieżące zadanie jest rejestrowane jako oczekujące na zadanie, którego wynik ma być zwrócony. Oznacza to, że funkcje rekursywne i metody zaimplementowane przy użyciu wyrażeń zadań mogą tworzyć nieograniczone łańcuchy kolejnych zadań, a te mogą używać nieograniczonego stosu lub steru. 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ż zmianę tego kodu, aby użyć jawnej pętli, 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 wywołania ogonowe, użyj wyrażenia asynchronicznego języka F#, które obsługuje wywołania ogonowe. 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#.