Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Programowanie asynchroniczne to mechanizm, który jest niezbędny dla nowoczesnych aplikacji z różnych powodów. Istnieją dwa podstawowe przypadki użycia, które napotyka większość deweloperów:
- Prezentacja procesu serwera zdolnego do obsługi dużej liczby równoczesnych nadchodzących żądań, minimalizując jednocześnie wykorzystanie zasobów systemowych w czasie oczekiwania na dane wejściowe z zewnętrznych systemów lub usług.
- Utrzymywanie dynamicznego interfejsu użytkownika lub głównego wątku podczas równoczesnego postępu pracy w tle
Chociaż praca w tle często wiąże się z wykorzystaniem wielu wątków, ważne jest, aby oddzielnie rozważać pojęcia asynchroniczności i wielowątkowości. W rzeczywistości są one oddzielnymi obawami, a jeden nie oznacza drugiego. W tym artykule opisano bardziej szczegółowo oddzielne pojęcia.
Zdefiniowano asynchronię
Poprzedni punkt - że asynchronia jest niezależna od wykorzystania wielu wątków - warto wyjaśnić nieco bardziej. Istnieją trzy koncepcje, które czasami są powiązane, ale ściśle niezależne od siebie:
- Współbieżności; gdy wiele obliczeń jest wykonywanych w nakładających się okresach czasu.
- Równoległość; gdy wiele obliczeń lub kilka części pojedynczego obliczenia jest wykonywanych dokładnie w tym samym czasie.
- Asynchrony; gdy co najmniej jedno obliczenie może być wykonywane oddzielnie od głównego przepływu programu.
Wszystkie trzy są pojęciami ortogonalnymi, ale można je łatwo pomylić, zwłaszcza gdy są używane razem. Na przykład może być konieczne równoległe wykonywanie wielu obliczeń asynchronicznych. Ta relacja nie oznacza, że równoległość ani asynchronia implikują siebie nawzajem.
Jeśli rozważysz etymologię słowa "asynchroniczne", istnieją dwa elementy:
- "a", oznacza "nie".
- "synchroniczny", czyli "w tym samym czasie".
Po połączeniu tych dwóch terminów zobaczysz, że "asynchroniczne" oznacza "nie w tym samym czasie". To wszystko! W tej definicji nie ma oznaczeń współbieżności ani równoległości. Jest to również prawdą w praktyce.
W praktyce obliczenia asynchroniczne w języku F# mają być wykonywane niezależnie od głównego przepływu programu. To niezależne wykonanie nie oznacza współbieżności ani równoległości ani nie oznacza, że obliczenia zawsze odbywają się w tle. W rzeczywistości obliczenia asynchroniczne mogą nawet wykonywać synchronicznie, w zależności od charakteru obliczeń i środowiska wykonywanego przez obliczenia.
Głównym wnioskiem jest to, że obliczenia asynchroniczne są niezależne od głównego przepływu programu. Chociaż istnieje kilka gwarancji dotyczących czasu lub sposobu wykonywania obliczeń asynchronicznych, istnieją pewne podejścia do organizowania i planowania ich. W pozostałej części tego artykułu omówiono podstawowe pojęcia dotyczące asynchronii języka F# oraz sposób używania typów, funkcji i wyrażeń wbudowanych w język F#.
Podstawowe pojęcia
W języku F# programowanie asynchroniczne koncentruje się wokół dwóch podstawowych pojęć: obliczeń asynchronicznych i zadań.
- Typ
Async<'T>
zasync { }
wyrażeniami, który reprezentuje komponowalne obliczenia asynchroniczne, które można rozpocząć w celu utworzenia zadania. - Typ
Task<'T>
ztask { }
wyrażeniami reprezentującymi wykonywanie zadania platformy .NET.
Ogólnie rzecz biorąc, należy rozważyć użycie task {…}
zamiast async {…}
w nowym kodzie, jeśli pracujesz z bibliotekami .NET, które korzystają z zadań, i jeżeli nie polegasz na asynchronicznych wywołaniach zwrotnych kodu ani na propagacji tokenu anulowania.
Podstawowe pojęcia dotyczące asynchroniczności
Podstawowe pojęcia programowania asynchronicznego można zobaczyć w poniższym przykładzie:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
W przykładzie funkcja printTotalFileBytesUsingAsync
jest typu string -> Async<unit>
. Faktyczne wywołanie funkcji nie powoduje wykonania obliczeń asynchronicznych. Zamiast tego zwraca element Async<unit>
, który działa jako specyfikacja pracy, która ma być wykonywana asynchronicznie. Wywołuje Async.AwaitTask
w swojej treści, co konwertuje wynik ReadAllBytesAsync na odpowiedni typ.
Innym ważnym wierszem jest wywołanie metody Async.RunSynchronously
. Jest to jedna z funkcji uruchamiania modułu asynchronicznego, które należy wywołać, jeśli chcesz faktycznie wykonać obliczenia asynchroniczne języka F#.
Różnica w stylu programowania w C#/Visual Basic jest zasadnicza. W języku F# obliczenia asynchroniczne można traktować jako zadania zimne. Aby rzeczywiście zostały wykonane, muszą być jawnie uruchomione. Ma to pewne zalety, ponieważ umożliwia łączenie i sekwencjonowanie pracy asynchronicznej znacznie łatwiej niż w języku C# lub Visual Basic.
Łączenie obliczeń asynchronicznych
Oto przykład, który opiera się na poprzednim, łącząc obliczenia:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
Jak widać, main
funkcja ma jeszcze kilka elementów. Koncepcyjnie wykonuje następujące czynności:
- Przekształć argumenty wiersza polecenia w sekwencję
Async<unit>
obliczeń za pomocą poleceniaSeq.map
. - Utwórz obiekt
Async<'T[]>
, który planuje i uruchamiaprintTotalFileBytes
obliczenia równolegle, kiedy działa. - Utwórz obiekt
Async<unit>
, który uruchomi obliczenia równoległe i zignoruje jego wynik (czyliunit[]
). - Jawnie uruchom ogólne skomponowane obliczenia z wartością
Async.RunSynchronously
, blokując ją do momentu ukończenia.
Po uruchomieniu tego programu, printTotalFileBytes
jest uruchamiany równolegle dla każdego argumentu wiersza polecenia. Ponieważ obliczenia asynchroniczne są wykonywane niezależnie od przepływu programu, nie ma zdefiniowanej kolejności, w której wyświetlają informacje i kończą wykonywanie. Obliczenia będą zaplanowane równolegle, ale ich kolejność wykonywania nie jest gwarantowana.
Sekwencjonowanie obliczeń asynchronicznych
Ponieważ Async<'T>
jest to specyfikacja pracy, a nie już uruchomionego zadania, można łatwo wykonywać bardziej skomplikowane przekształcenia. Oto przykład, który sekwencjonuje zestaw obliczeń asynchronicznych, aby wykonywać je jedno po drugim.
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
To zorganizuje printTotalFileBytes
do wykonania według kolejności elementów argv
, zamiast planowania ich równolegle. Ponieważ każda kolejna operacja zostanie zaplanowana dopiero po zakończeniu wykonywania poprzednich obliczeń, obliczenia są sekwencjonowane tak, aby nie nakładały się w czasie ich wykonywania.
Ważne funkcje modułu asynchronicznego
Podczas pisania kodu asynchronicznego w języku F#zwykle będziesz korzystać z platformy obsługującej planowanie obliczeń. Jednak nie zawsze jest tak, więc dobrze jest zrozumieć różne funkcje, które mogą służyć do planowania pracy asynchronicznej.
Ponieważ obliczenia asynchroniczne języka F# są specyfikacją pracy, a nie reprezentacją już wykonywanej pracy, muszą być jawnie uruchomione z funkcją początkową. Istnieje wiele metod początkowych asynchronicznych , które są przydatne w różnych kontekstach. W poniższej sekcji opisano niektóre z bardziej typowych funkcji początkowych.
Async.StartChild
Uruchamia obliczenia podrzędne w ramach obliczeń asynchronicznych. Dzięki temu można wykonywać jednocześnie wiele obliczeń asynchronicznych. Obliczenia potomne dzielą token anulowania z obliczeniami nadrzędnymi. Jeśli obliczenia nadrzędne zostaną anulowane, obliczenia podrzędne również zostaną anulowane.
Podpis:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
Kiedy należy użyć:
- Kiedy chcesz wykonywać wiele obliczeń asynchronicznych jednocześnie, zamiast jednego po drugim, ale bez ich planowania równolegle.
- Jeśli chcesz powiązać czas trwania obliczeń podrzędnych z czasem trwania obliczeń nadrzędnych.
Co należy uważać na:
- Uruchamianie wielu obliczeń za pomocą
Async.StartChild
nie jest tym samym co planowanie ich równocześnie. Jeśli chcesz zaplanować obliczenia równolegle, użyj poleceniaAsync.Parallel
. - Anulowanie obliczeń nadrzędnych spowoduje automatyczne anulowanie wszystkich obliczeń podrzędnych, które zostały uruchomione.
Async.StartImmediate
Uruchamia obliczenia asynchroniczne, które natychmiast rozpoczynają się na bieżącym wątku systemu operacyjnego. Jest to przydatne, jeśli musisz zaktualizować coś w wątku wywołującym podczas obliczeń. Na przykład, jeśli obliczenia asynchroniczne muszą zaktualizować interfejs użytkownika (taki jak pasek postępu), wtedy należy użyć Async.StartImmediate
.
Podpis:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Kiedy należy użyć:
- Jeśli musisz zaktualizować element w wątku wywołującym w trakcie asynchronicznych obliczeń.
Co należy uważać na:
- Kod w obliczeniach asynchronicznych będzie uruchamiany na każdym wątku, na którym akurat jest zaplanowany. Może to być problematyczne, jeśli ten wątek jest w jakiś sposób poufny, na przykład wątek interfejsu użytkownika. W takich przypadkach
Async.StartImmediate
może być nieodpowiednie do użycia.
Async.StartAsTask
Wykonuje obliczenia w puli wątków. Zwraca wartość Task<TResult>, która zostanie zakończona w odpowiednim momencie po zakończeniu obliczeń (produkuje wynik, rzuci wyjątek lub zostanie anulowana). Jeśli nie podano tokenu anulowania, zostanie użyty domyślny token anulowania.
Podpis:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
Kiedy należy użyć:
- Jeśli musisz wywołać interfejs API platformy .NET, który zwraca Task<TResult> w celu reprezentacji wyniku obliczeń asynchronicznych.
Co należy uważać na:
- To wywołanie przydzieli dodatkowy
Task
obiekt, który może zwiększyć obciążenie, jeśli jest często używany.
Async.Parallel
Planuje równoległe wykonywanie sekwencji obliczeń asynchronicznych, co daje tablicę wyników w kolejności ich dostarczenia. Stopień równoległości można opcjonalnie dostroić/ograniczyć, określając maxDegreeOfParallelism
parametr .
Podpis:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Kiedy należy go używać:
- Jeśli musisz uruchomić zestaw obliczeń w tym samym czasie i nie musisz się martwić o ich kolejność wykonywania.
- Jeśli nie potrzebujesz wyników z obliczeń zaplanowanych równolegle do momentu ich ukończenia.
Co należy uważać na:
- Dostęp do wynikowej tablicy wartości można uzyskać tylko po zakończeniu wszystkich obliczeń.
- Obliczenia będą uruchamiane, gdy tylko zostaną zaplanowane. To zachowanie oznacza, że nie można polegać na ich kolejności wykonywania.
Async.Sequential
Planuje sekwencję obliczeń asynchronicznych, które mają być wykonywane w kolejności, w której są przekazywane. Pierwsze obliczenie zostanie wykonane, a następnie następne itd. Nie będą wykonywane równolegle żadne obliczenia.
Podpis:
computations: seq<Async<'T>> -> Async<'T[]>
Kiedy należy go używać:
- Jeśli musisz wykonać wiele obliczeń w kolejności.
Co należy uważać na:
- Dostęp do wynikowej tablicy wartości można uzyskać tylko po zakończeniu wszystkich obliczeń.
- Obliczenia będą uruchamiane w kolejności przekazywania ich do tej funkcji, co może oznaczać, że więcej czasu upłynie przed zwróceniem wyników.
Async.AwaitTask
Zwraca asynchroniczne obliczenie, które oczekuje na ukończenie danej Task<TResult> operacji i zwraca jego wynik jako Async<'T>
Podpis:
task: Task<'T> -> Async<'T>
Kiedy należy użyć:
- Podczas korzystania z interfejsu API platformy .NET, który zwraca wartość Task<TResult> w asynchronicznym obliczeniu w języku F#.
Co należy uważać na:
- Wyjątki są opakowane w AggregateException, zgodnie z konwencją Task Parallel Library; to zachowanie różni się od sposobu, w jaki asynchroniczne środowisko F# zazwyczaj przedstawia wyjątki.
Async.Catch
Tworzy asynchroniczne obliczenie, które wykonuje podany Async<'T>
, zwracając Async<Choice<'T, exn>>
. Jeśli dane Async<'T>
zakończy się pomyślnie, zostanie zwrócony Choice1Of2
z wartością wynikową. Jeśli wyjątek zostanie zgłoszony przed ukończeniem zadania, zostanie Choice2of2
zwrócony razem z podniesionym wyjątkiem. Jeśli jest on używany w obliczeniach asynchronicznych, które same są złożone z wielu obliczeń, a jedno z tych obliczeń zgłasza wyjątek, obejmujące obliczenie zostanie całkowicie zatrzymane.
Podpis:
computation: Async<'T> -> Async<Choice<'T, exn>>
Kiedy należy użyć:
- W przypadku wykonywania pracy asynchronicznej, która może zakończyć się niepowodzeniem z powodu wyjątku i chcesz obsłużyć ten wyjątek w wywołującym.
Co należy uważać na:
- W przypadku używania złożonych lub sekwencyjnych obliczeń asynchronicznych otaczające obliczenia zatrzymają się w pełni, jeśli jedno z jego "wewnętrznych" obliczeń wyrzuci wyjątek.
Async.Ignore
Tworzy asynchroniczne obliczenia, które wykonuje podane obliczenia, ale pomija jego wynik.
Podpis:
computation: Async<'T> -> Async<unit>
Kiedy należy użyć:
- Jeśli masz obliczenia asynchroniczne, których wynik nie jest potrzebny. Jest to analogia do
ignore
funkcji dla kodu innego niż asynchroniczny.
Co należy uważać na:
- Jeśli musisz użyć
Async.Ignore
, ponieważ chcesz użyćAsync.Start
lub innej funkcji, która wymagaAsync<unit>
, rozważ czy odrzucenie wyniku jest w porządku. Unikaj odrzucania wyników tylko w celu dopasowania sygnatury typu.
Async.RunSynchronously
Uruchamia obliczenia asynchroniczne i oczekuje na jego wynik w wątku wywołującym. Propaguje wyjątek, jeśli obliczenie da jedną wartość. To wywołanie jest blokujące.
Podpis:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Kiedy należy go używać:
- Jeśli jest ona potrzebna, użyj jej tylko raz w aplikacji — w punkcie wejścia pliku wykonywalnego.
- Jeśli nie interesuje Cię wydajność i chcesz jednocześnie wykonać zestaw innych operacji asynchronicznych.
Co należy uważać na:
- Wywołanie
Async.RunSynchronously
blokuje wątek wywołujący do momentu zakończenia jego wykonywania.
Async.Start
Uruchamia asynchroniczne obliczenia zwracające unit
w puli wątków. Nie czeka na ukończenie ani nie zauważa wyniku wyjątku. Zagnieżdżone obliczenia rozpoczęte z Async.Start
są uruchamiane niezależnie od obliczeń nadrzędnych, które je wywołały; ich czas trwania nie jest związany z obliczeniami nadrzędnymi. Jeśli obliczenia nadrzędne zostaną anulowane, nie zostaną anulowane żadne obliczenia podrzędne.
Podpis:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Użyj tylko wtedy, gdy:
- Masz obliczenie asynchroniczne, które nie daje wyniku i/lub wymaga przetworzenia wyniku.
- Nie musisz wiedzieć, kiedy kończy się obliczenia asynchroniczne.
- Nieważne jest dla ciebie, na którym wątku jest uruchamiane obliczenie asynchroniczne.
- Nie musisz być świadomym wyjątków ani zgłaszać wyjątków powstałych podczas wykonania.
Co należy uważać na:
- Wyjątki zgłaszane przez obliczenia rozpoczęte z
Async.Start
nie są propagowane do wywołującego. Stos wywołań będzie całkowicie unwound. - Każda praca (na przykład wywołanie
printfn
) rozpoczęta przy użyciuAsync.Start
nie spowoduje efektu w głównym wątku wykonywania programu.
Współdziałanie z platformą .NET
W przypadku korzystania z async { }
programowania może być konieczne współdziałanie z biblioteką .NET lub bazą kodu w języku C#, która korzysta ze stylu asynchronicznego programowania z użyciem async/await. Ponieważ język C# i większość bibliotek platformy .NET używają Task<TResult> typów i Task jako ich podstawowych abstrakcji, może to zmienić sposób pisania kodu asynchronicznego języka F#.
Jedną z opcji jest przejście na pisanie zadań platformy .NET bezpośrednio przy użyciu polecenia task { }
. Alternatywnie możesz użyć funkcji Async.AwaitTask
, aby poczekać na asynchroniczne obliczenia platformy .NET.
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
Za pomocą funkcji Async.StartAsTask
można przekazać obliczenia asynchroniczne do wywołującego .NET.
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Aby pracować z interfejsami API, które używają Task (czyli obliczeń asynchronicznych platformy .NET, które nie zwracają żadnej wartości), może być konieczne dodanie dodatkowej funkcji, która przekonwertuje element Async<'T>
na wartość Task:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Istnieje już element Async.AwaitTask
, który akceptuje Task jako dane wejściowe. Dzięki temu i wcześniej zdefiniowanej startTaskFromAsyncUnit
funkcji można uruchamiać typy i czekać Task na nie na podstawie obliczeń asynchronicznych języka F#.
Pisanie zadań platformy .NET bezpośrednio w języku F#
W języku F#można pisać zadania bezpośrednio przy użyciu polecenia task { }
, na przykład:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
W przykładzie funkcja printTotalFileBytesUsingTasks
jest typu string -> Task<unit>
. Wywołanie funkcji rozpoczyna wykonywanie zadania.
Wywołanie task.Wait()
czeka na ukończenie zadania.
Relacja do wielowątkowości
Mimo że wątkowanie zostało wspomniane w tym artykule, należy pamiętać o dwóch ważnych kwestiach:
- Nie ma związku między obliczeniami asynchronicznymi a wątkiem, chyba że zostanie explicytnie uruchomione na bieżącym wątku.
- Programowanie asynchroniczne w języku F# nie jest abstrakcją dla wielowątkowości.
Na przykład obliczenia mogą być faktycznie wykonywane w wątku obiektu wywołującego, w zależności od charakteru pracy. Obliczenia mogą również "przeskoczyć" między wątkami, pożyczając je na chwilę, aby wykonać między okresami "oczekiwania" przydatną pracę (np. w trakcie przesyłania połączenia sieciowego).
Chociaż język F# zapewnia pewne możliwości uruchamiania obliczeń asynchronicznych w bieżącym wątku (lub jawnie nie w bieżącym wątku), asynchronia zazwyczaj nie jest skojarzona z określoną strategią wątkowania.