Wzorzec asynchroniczny oparty na zadaniach (TAP) na platformie .NET: Wprowadzenie i omówienie

Na platformie .NET wzorzec asynchroniczny oparty na zadaniach jest zalecanym asynchronicznym wzorcem projektowania dla nowego programowania. Jest on oparty na typach Task i Task<TResult> w System.Threading.Tasks przestrzeni nazw, które są używane do reprezentowania operacji asynchronicznych.

Nazewnictwo, parametry i zwracane typy

We wzorcu TAP do tworzenia wystąpienia i wykonywania operacji asynchronicznej jest używana jedna metoda. Kontrastuje to zarówno ze wzorcem asynchronicznym (APM lub IAsyncResult) jak i wzorcem asynchronicznym opartym na zdarzeniach (EAP). Program APM wymaga Begin metod i End . Protokół EAP wymaga metody, która ma Async sufiks, a także wymaga co najmniej jednego zdarzenia, typów delegatów programu obsługi zdarzeń i EventArgtypów pochodnych. Metody asynchroniczne w interfejsie TAP zawierają Async sufiks po nazwie operacji dla metod, które zwracają oczekiwane typy, takie jak Task, Task<TResult>, ValueTaski ValueTask<TResult>. Na przykład operacja asynchroniczna Get zwracająca Task<String> obiekt może mieć nazwę GetAsync. Jeśli dodasz metodę TAP do klasy, która zawiera już nazwę metody protokołu EAP z sufiksem Async , użyj sufiksu TaskAsync . Jeśli na przykład klasa ma już metodę GetAsync , użyj nazwy GetTaskAsync. Jeśli metoda uruchamia operację asynchroniczną, ale nie zwraca oczekiwanego typu, jej nazwa powinna zaczynać się od Begin, Startlub innego zlecenia, aby zasugerować, że ta metoda nie zwraca ani nie zgłasza wyniku operacji.  

Metoda TAP zwraca wartość lub System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult>, na podstawie tego, czy odpowiednia metoda synchroniczna zwraca wartość void lub typ TResult.

Parametry metody TAP powinny być zgodne z parametrami jego synchronicznego odpowiednika i powinny być podane w tej samej kolejności. out Jednak parametry i ref są zwolnione z tej reguły i należy unikać całkowicie. Wszystkie dane, które zostałyby zwrócone za pomocą parametru out lub ref , powinny zamiast tego zostać zwrócone przez TResultTask<TResult>element , i powinny używać krotki lub niestandardowej struktury danych, aby pomieścić wiele wartości. Ponadto rozważ dodanie parametru CancellationToken , nawet jeśli synchroniczny odpowiednik metody TAP nie oferuje tego parametru.

Metody przeznaczone wyłącznie do tworzenia, manipulowania lub kombinacji zadań (gdzie intencja asynchroniczna metody jest jasna w nazwie metody lub w nazwie typu, do którego należy metoda) nie muszą być zgodne z tym wzorcem nazewnictwa; takie metody są często określane jako kombinatory. Przykłady kombinatorów to WhenAll i WhenAny, i zostały omówione w sekcji Korzystanie z wbudowanych kombinatorów opartych na zadaniach artykułu Korzystanie z asynchronicznego wzorca opartego na zadaniach.

Przykłady różnic składni tap różni się od składni używanej w starszych wzorcach programowania asynchronicznego, takich jak asynchroniczny model programowania (APM) i wzorzec asynchroniczny oparty na zdarzeniach (EAP), zobacz Asynchroniczne wzorce programowania.

Inicjowanie operacji asynchronicznej

Metoda asynchroniczna oparta na wzorcu TAP może wykonać synchronicznie niewielką ilość pracy, na przykład weryfikację argumentów i inicjowanie operacji asynchronicznej, zanim zwróci zadanie wynikowe. Praca synchroniczna powinna być ograniczona do minimum, tak aby metoda asynchroniczna mogła szybko zwracać wynik. Przyczyny szybkiego powrotu obejmują:

  • Metody asynchroniczne mogą być wywoływane z wątków interfejsu użytkownika (UI), a jakakolwiek długotrwała praca synchroniczna może niekorzystnie wpływać na czas reakcji aplikacji.

  • Wiele metod asynchronicznych może zostać uruchomionych jednocześnie. W związku z tym jakakolwiek długotrwała praca synchroniczna metody asynchronicznej może opóźnić zainicjowanie innych operacji asynchronicznych, zmniejszając w ten sposób korzyści płynące ze współbieżności.

W niektórych przypadkach nakład pracy wymagany do ukończenia operacji jest mniejszy niż ilość pracy potrzebna do asynchronicznego uruchomienia operacji. Przykładem takiego scenariusza jest odczyt ze strumienia, gdzie operacja odczytu może się odbyć dzięki danym buforowanym już w pamięci. W takich przypadkach operacja może zakończyć się synchronicznie i może zwracać zadanie, które zostało już zakończone.

Wyjątki

Metoda asynchroniczna powinna zgłosić wyjątek z wywołania metody asynchronicznej tylko w odpowiedzi na błąd użycia. Błędy użycia nie powinny nigdy występować w kodzie produkcyjnym. Jeśli na przykład przekazanie odwołania o wartości null (Nothing w Visual Basic) jako jednego z argumentów metody powoduje wystąpienie błędu (zwykle reprezentowane przez ArgumentNullException wyjątek), można zmodyfikować kod wywołujący, aby upewnić się, że odwołanie o wartości null nigdy nie zostanie przekazane. W przypadku innych błędów wyjątki, które występują, gdy uruchomiona jest metoda asynchroniczna, powinny być przypisane do zwracanego zadania, nawet jeśli metoda asynchroniczna jest wykonywana synchronicznie przed zwróceniem zadania. Zadanie zawiera zazwyczaj co najwyżej jeden wyjątek. Jeśli jednak zadanie reprezentuje wiele operacji (na przykład WhenAll), wiele wyjątków może być skojarzonych z jednym zadaniem.

Środowisko docelowe

Podczas implementacji metody wzorca TAP można określić, gdzie występuje wykonanie asynchroniczne. Możesz wybrać wykonanie obciążenia w puli wątków, zaimplementować je przy użyciu asynchronicznego we/wy (bez powiązania z wątkiem dla większości wykonywania operacji), uruchomić go w określonym wątku (takim jak wątek interfejsu użytkownika) lub użyć dowolnej liczby potencjalnych kontekstów. Metoda TAP może nawet nie mieć nic do wykonania i może po prostu zwrócić element Task reprezentujący wystąpienie warunku w innym miejscu w systemie (na przykład zadanie reprezentujące dane przychodzące do struktury danych w kolejce).

Obiekt wywołujący metodę TAP może zablokować oczekiwanie na ukończenie metody TAP przez synchroniczne oczekiwanie na wynikowe zadanie lub może uruchomić dodatkowy (kontynuacja) kod po zakończeniu operacji asynchronicznej. Twórca kodu kontynuacji ma kontrolę nad tym, gdzie ten kod jest wykonywany. Kod kontynuacji można utworzyć jawnie za pomocą metod w Task klasie (na przykład ContinueWith) lub niejawnie przy użyciu obsługi języka utworzonej na podstawie kontynuacji (na przykład await w języku C# Await , AwaitValue w Visual Basic w języku F#).

Stan zadania

Klasa Task zapewnia cykl życia dla operacji asynchronicznych, a ten cykl jest reprezentowany przez TaskStatus wyliczenie. Aby obsługiwać narożne przypadki typów, które pochodzą z Task i i Task<TResult>i, aby obsługiwać oddzielenie konstrukcji od planowania, Task klasa uwidacznia metodę Start . Zadania tworzone przez konstruktory publiczne Task są nazywane zimnymi zadaniami, ponieważ rozpoczynają swój cykl życia w stanie nieplanowanym Created i są zaplanowane tylko wtedy, gdy Start są wywoływane na tych wystąpieniach.

Wszystkie inne zadania rozpoczynają cykl życia w stanie gorąca, co oznacza, że operacje asynchroniczne, które reprezentują, zostały już zainicjowane, a ich stan zadania to wartość wyliczenia inna niż TaskStatus.Created. Wszystkie zadania, które są zwracane z metod wzorca TAP, muszą być aktywowane. Jeśli metoda TAP wewnętrznie używa konstruktora zadania do utworzenia wystąpienia zadania, które ma zostać zwrócone, metoda TAP musi wywołać StartTask obiekt przed zwróceniem go. Konsumenci metody TAP mogą bezpiecznie założyć, że zwrócone zadanie jest aktywne i nie powinno próbować wywołać Start żadnego Task zwracanego z metody TAP. Wywołanie Start aktywnego zadania powoduje wyjątek InvalidOperationException .

Anulowanie (opcjonalnie)

We wzorcu TAP anulowanie jest opcjonalne dla implementatorów metody asynchronicznej i konsumentów metody asynchronicznej. Jeśli operacja zezwala na anulowanie, uwidacznia przeciążenie metody asynchronicznej, która akceptuje token anulowania (CancellationToken wystąpienie). Zgodnie z konwencją parametr ma nazwę cancellationToken.

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

Operacja asynchroniczna monitoruje ten token pod kątem żądań anulowania. Jeśli odbierze żądanie anulowania, może je zaakceptować i anulować operację. Jeśli żądanie anulowania zakończy się przedwcześnie, metoda TAP zwróci zadanie kończące się w Canceled stanie; nie ma dostępnego wyniku i nie zostanie zgłoszony żaden wyjątek. Stan Canceled jest uznawany za stan końcowy (ukończony) dla zadania wraz z stanami Faulted i RanToCompletion . W związku z Canceled tym, jeśli zadanie jest w stanie, jego IsCompleted właściwość zwraca wartość true. Po zakończeniu Canceled zadania w stanie wszelkie kontynuacje zarejestrowane w zadaniu są zaplanowane lub wykonywane, chyba że określono opcję kontynuacji, taką jak NotOnCanceled określono, aby zrezygnować z kontynuacji. Każdy kod, który asynchronicznie oczekuje na anulowane zadanie za pomocą funkcji językowych, będzie nadal uruchamiany, ale otrzymuje OperationCanceledException od niego wyjątek lub. Kod, który jest blokowany synchronicznie czekając na zadanie za pomocą metod, takich jak Wait i WaitAll nadal działa z wyjątkiem.

Jeśli token anulowania zażądał anulowania przed wywołaną metodą TAP, która akceptuje ten token, metoda TAP powinna zwrócić Canceled zadanie. Jeśli jednak żądanie anulowania zostanie zgłoszone, gdy trwa operacja asynchroniczna, operacja asynchroniczna nie musi akceptować żądania anulowania. Zwrócone zadanie powinno zakończyć się w Canceled stanie tylko wtedy, gdy operacja kończy się w wyniku żądania anulowania. Jeśli żądanie anulowania jest wymagane, ale wynik lub wyjątek jest nadal generowany, zadanie powinno zakończyć się w RanToCompletion stanie lub Faulted .

W przypadku metod asynchronicznych, które chcą uwidocznić możliwość anulowania przede wszystkim, nie trzeba podawać przeciążenia, które nie akceptuje tokenu anulowania. W przypadku metod, które nie mogą być anulowane, nie należy dostarczać przeciążeń, które akceptują token anulowania. Pomaga to wskazać obiektowi wywołującemu, czy metodę docelową można w rzeczywistości anulować. Kod odbiorcy, który nie wymaga anulowania, może wywołać metodę, która akceptuje CancellationToken element i podaje None jako wartość argumentu. None jest funkcjonalnie odpowiednikiem domyślnego CancellationTokenelementu .

Raportowanie postępu (opcjonalnie)

Niektóre operacje asynchroniczne korzystają z dostarczania powiadomień na temat postępu. Zazwyczaj są one używane do aktualizowania interfejsu użytkownika za pomocą informacji o postępie operacji asynchronicznej.

W interfejsie TAP postęp jest obsługiwany za pośrednictwem interfejsu IProgress<T> , który jest przekazywany do metody asynchronicznej jako parametru, który zwykle nosi nazwę progress. Dostarczenie interfejsu postępu, gdy wywoływana jest metoda asynchroniczna, pomaga wyeliminować sytuację wyścigu, która wynika z niepoprawnego użycia (tzn. kiedy obsługa zdarzeń, która jest niepoprawnie rejestrowana po rozpoczęciu operacji, może pominąć aktualizacje). Co więcej, interfejs postępu obsługuje różne implementacje postępu, co zostało określone przez kod konsumencki. Na przykład kod zużywający może dbać tylko o najnowszą aktualizację postępu lub może chcieć buforować wszystkie aktualizacje lub może chcieć wywołać akcję dla każdej aktualizacji lub chcieć kontrolować, czy wywołanie jest wywoływane do określonego wątku. Wszystkie te opcje można osiągnąć przy użyciu innej implementacji interfejsu dostosowanego do potrzeb określonego konsumenta. Podobnie jak w przypadku anulowania implementacje interfejsu TAP powinny podać IProgress<T> parametr tylko wtedy, gdy interfejs API obsługuje powiadomienia o postępie.

Jeśli na przykład ReadAsync metoda omówiona wcześniej w tym artykule jest w stanie zgłosić pośredni postęp w postaci liczby odczytanych bajtów do tej pory, wywołanie zwrotne postępu może być interfejsem IProgress<T> :

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

FindFilesAsync Jeśli metoda zwraca listę wszystkich plików spełniających określony wzorzec wyszukiwania, wywołanie zwrotne postępu może zapewnić oszacowanie procentu wykonanej pracy i bieżącego zestawu wyników częściowych. Te informacje mogą zawierać krotkę:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

lub z typem danych specyficznym dla interfejsu API:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

W tym drugim przypadku specjalny typ danych jest zwykle sufiksem ProgressInfo.

Jeśli implementacje tap zapewniają przeciążenia, które akceptują progress parametr, muszą zezwolić na argument null, w takim przypadku żaden postęp nie jest zgłaszany. Implementacje tap powinny zgłaszać postęp do Progress<T> obiektu synchronicznie, co umożliwia metodę asynchroniczną w celu szybkiego zapewnienia postępu. Umożliwia również konsumentowi postępów określenie, jak i gdzie najlepiej obsługiwać informacje. Na przykład wystąpienie postępu może wybrać przekazywanie wywołań zwrotnych i generowanie zdarzeń w kontekście przechwyconych synchronizacji.

Implementacje języka T> protokołu IProgress<

Platforma .NET udostępnia klasę Progress<T> , która implementuje IProgress<T>element . Klasa Progress<T> jest zadeklarowana w następujący sposób:

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Wystąpienie Progress<T> uwidacznia ProgressChanged zdarzenie, które jest wywoływane za każdym razem, gdy operacja asynchroniczna zgłasza aktualizację postępu. Zdarzenie ProgressChanged jest wywoływane na SynchronizationContext obiekcie przechwyconym podczas Progress<T> tworzenia wystąpienia. Jeśli kontekst synchronizacji nie był dostępny, używany jest domyślny kontekst ukierunkowany na pulę wątków. Z tym zdarzeniem można zarejestrować programy obsługi. Pojedynczą procedurę obsługi można również udostępnić konstruktorowi Progress<T> dla wygody i zachowuje się tak samo jak procedura obsługi zdarzeń dla ProgressChanged zdarzenia. Aktualizacje postępu są wywoływane asynchronicznie, aby unikać opóźniania operacji asynchronicznej podczas wykonywania obsługi zdarzeń. Inna IProgress<T> implementacja może zdecydować się na zastosowanie różnych semantyki.

Wybieranie przeciążeń, które mają być podane

Jeśli implementacja interfejsu TAP używa zarówno opcjonalnych CancellationToken , jak i opcjonalnych IProgress<T> parametrów, potencjalnie może wymagać maksymalnie czterech przeciążeń:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Jednak wiele implementacji interfejsu TAP nie zapewnia możliwości anulowania ani postępu, dlatego wymagają jednej metody:

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

Jeżeli implementacja wzorca TAP obsługuje anulowanie lub postęp, ale nie obie te możliwości naraz, może ona dostarczać dwa przeciążenia:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

Jeśli implementacja wzorca TAP obsługuje zarówno anulowanie, jak i postęp, może uwidaczniać wszystkie cztery przeciążenia. Może jednak dostarczyć tylko następujące dwa:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Aby zrekompensować dwie brakujące kombinacje pośrednie, deweloperzy mogą przekazać None lub wartość domyślną CancellationToken parametru cancellationToken i null parametru progress .

Jeśli oczekujesz, że każde użycie metody TAP będzie obsługiwać anulowanie lub postęp, możesz pominąć przeciążenia, które nie akceptują odpowiedniego parametru.

Jeśli zdecydujesz się uwidocznić wiele przeciążeń w celu anulowania lub postępu opcjonalnego, przeciążenia, które nie obsługują anulowania lub postępu, powinny zachowywać się tak, jakby zostały przekazane None do anulowania lub null postępu do przeciążenia, które je obsługuje.

Nazwa opis
Wzorce programowania asynchronicznego Wprowadza trzy wzorce do wykonywania operacji asynchronicznych: asynchroniczny wzorzec oparty na zadaniach (TAP), asynchroniczny model programowania (APM) i asynchroniczny wzorzec oparty na zdarzeniach (EAP).
Implementowanie wzorca asynchronicznego opartego na zadaniach Opis sposobu implementacji asynchronicznego wzorca opartego na zadaniach (TAP), którą można przeprowadzić na trzy sposoby: za pomocą kompilatorów języków C# i Visual Basic w programie Visual Studio, ręcznie lub za pomocą kombinacji metod kompilatora i manualnych.
Wykorzystywanie wzorca asynchronicznego opartego na zadaniach Opis sposobu używania zadań i wywołań zwrotnych w celu osiągnięcia oczekiwania bez blokowania.
Współdziałanie z innymi wzorcami asynchronicznymi i typami Opis sposobu używania asynchronicznego wzorca opartego na zadaniach (TAP) w celu implementacji asynchronicznego modelu programowania (APM) i asynchronicznego wzorca opartego na zdarzeniach (EAP).