Udostępnij za pomocą


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 z asynchronicznym modelem programowania (APM lub IAsyncResult), jak i asynchronicznym wzorcem opartym na zdarzeniach (EAP). Program APM wymaga metod Begin 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ą sufiks Async po nazwie operacji dla metod, które zwracają typy oczekujące, takie jak Task, Task<TResult>, ValueTask i ValueTask<TResult>. Na przykład operacja asynchroniczna Get, która zwraca Task<String>, 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 typu oczekującego, jej nazwa powinna zaczynać się od Begin, Start lub innego czasownika, aby zasugerować, że ta metoda nie zwraca ani nie zgłasza wyniku operacji.  

Metoda TAP zwraca albo System.Threading.Tasks.Task, albo System.Threading.Tasks.Task<TResult>, na podstawie tego, czy odpowiednia metoda synchroniczna zwraca void czy typ TResult.

Parametry metody TAP powinny być zgodne z parametrami jego synchronicznego odpowiednika i powinny być podane w tej samej kolejności. Jednak parametry out i ref są zwolnione z tej reguły i należy ich całkowicie unikać. Wszystkie dane, które zostały zwrócone za pomocą parametru out lub ref , powinny zamiast tego być zwracane jako część TResult zwracanego przez Task<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.

Aby zapoznać się z przykładami różnic w składni TAP w porównaniu do 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 interfejsie TAP może wykonać niewielką ilość pracy synchronicznie, na przykład weryfikację argumentów i zainicjowanie operacji asynchronicznej, zanim zwróci wynikowe zadanie. Praca synchroniczna powinna być zachowana do minimum, aby metoda asynchroniczna mogła zostać szybko zwrócona. Przyczyny szybkiego powrotu obejmują:

  • Metody asynchroniczne mogą być wywoływane z wątków interfejsu użytkownika, a każda długotrwała praca synchroniczna może zaszkodzić reakcji aplikacji.

  • Wiele metod asynchronicznych można uruchamiać współbieżnie. W związku z tym każda długotrwała praca w synchronicznej części metody asynchronicznej może opóźnić inicjowanie innych operacji asynchronicznych, zmniejszając w ten sposób korzyści wynikające ze współbieżności.

W niektórych przypadkach ilość pracy wymaganej do ukończenia operacji jest mniejsza niż ilość pracy wymaganej do asynchronicznego uruchomienia operacji. Odczyt ze strumienia, w którym operacja odczytu może być spełniona przez dane, które są już buforowane w pamięci, jest przykładem takiego scenariusza. W takich przypadkach operacja może zostać wykonana synchronicznie i może zwrócić zadanie, które zostało już ukończone.

Wyjątki

Metoda asynchroniczna powinna zgłosić wyjątek, który ma zostać wyrzucony z wywołania metody asynchronicznej tylko w odpowiedzi na błąd użycia. Błędy użycia nigdy nie powinny 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 wszystkich innych błędów, wyjątki występujące podczas działania metody asynchronicznej powinny zostać przypisane do zwróconego zadania, nawet jeśli metoda asynchroniczna zakończy się synchronicznie przed zwróceniem zadania. Zazwyczaj zadanie zawiera 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 implementowania metody TAP można określić, gdzie występuje wykonywanie asynchroniczne. Możesz wybrać wykonanie zadania w puli wątków, zaimplementować je przy użyciu asynchronicznego wejścia/wyjścia (bez konieczności utrzymywania powiązania z wątkiem przez większość czasu wykonywania operacji), uruchomić je 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 miejscem wykonywania tego kodu. 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ć skrajne przypadki typów, które pochodzą z Task i Task<TResult>, oraz aby wspierać oddzielenie konstrukcji od planowania, klasa Task udostępnia 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ą swój cykl życia w stanie aktywnym, co oznacza, że operacje asynchroniczne, które reprezentują, zostały już zainicjowane oraz ich status zadania jest wartością z wykazu inną niż TaskStatus.Created. Wszystkie zadania zwracane z metod TAP muszą być aktywowane. Jeśli metoda TAP wewnętrznie używa konstruktora zadania do utworzenia instancji zadania, które ma zostać zwrócone, metoda TAP musi wywołać Start na obiekcie Task przed jego zwróceniem. Konsumenci metody TAP mogą bezpiecznie założyć, że zwrócone zadanie jest aktywne i nie powinni próbować wywołać Start na żadnej Task, która jest zwracana przez metodę TAP. Wywołanie Start na aktywnym zadaniu skutkuje wyjątkiem InvalidOperationException.

Anulowanie (opcjonalnie)

W modelu TAP anulowanie jest opcjonalne zarówno dla implementujących asynchroniczne metody, jak i dla ich odbiorców. 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 otrzyma żądanie anulowania, może on wybrać honorację tego żądania i anulować operację. Jeśli żądanie anulowania zakończy się przedwcześnie, metoda TAP zwraca zadanie, które kończy się w stanie Canceled; wynik nie jest dostępny i nie jest zgłaszany żaden wyjątek. Stan Canceled jest uznawany za stan końcowy (ukończony) dla zadania wraz z stanami Faulted i RanToCompletion . Jeśli zadanie jest w stanie Canceled, jego właściwość IsCompleted zwraca true. Po zakończeniu zadania w stanie Canceled, wszelkie kontynuacje zarejestrowane dla zadania są planowane lub wykonywane, chyba że określono opcję kontynuacji, taką jak NotOnCanceled, aby zrezygnować z kontynuacji. Każdy kod, który oczekuje na anulowane zadanie asynchronicznie za pomocą funkcji językowych, nadal działa, ale otrzymuje OperationCanceledException lub wyjątek pochodny. 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 zażądano anulowania podczas wykonywania operacji asynchronicznej, operacja asynchroniczna nie musi zaakceptować żądania anulowania. Zadanie, które zostało zwrócone, powinno zakończyć się w Canceled stanie tylko wtedy, gdy operacja kończy się wskutek żądania anulowania. Jeśli anulowanie jest żądane, ale wynik lub wyjątek nadal powstaje, zadanie powinno zakończyć się w stanie RanToCompletion 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órych nie można anulować, nie udostępniaj przeciążeń akceptujących token anulowania; pomaga to wywołującemu ustalić, czy metoda docelowa jest rzeczywiście anulowalna. Kod użytkownika, który nie potrzebuje anulowania, może wywołać metodę, która akceptuje CancellationToken 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ń o postępie; są one zwykle używane do aktualizowania interfejsu użytkownika przy użyciu 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. Udostępnienie interfejsu postępu, gdy wywoływana jest metoda asynchroniczna, pomaga wyeliminować warunki wyścigu wynikające z nieprawidłowego użycia (czyli gdy programy obsługi zdarzeń, które są niepoprawnie zarejestrowane po rozpoczęciu operacji, mogą przegapić aktualizacje). Co ważniejsze, interfejs aktualizacji obsługuje różne implementacje aktualizacji, określone przez kod korzystający. 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.

Na przykład, jeśli wspomniana wcześniej w tym artykule metoda ReadAsync jest w stanie zgłosić postęp pośredni w formie liczby bajtów odczytanych 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 do specjalnego typu danych zazwyczaj dodawany jest sufiks ProgressInfo.

Jeśli implementacje TAP zapewniają przeciążenia, które akceptują parametr progress, muszą zezwolić na argument null, w takim przypadku nie zgłasza się żadnego postępu. Implementacje TAP powinny zgłaszać postęp do Progress<T> obiektu synchronicznie, co umożliwia metodzie asynchronicznej szybkie zapewnienie postępu. Umożliwia również konsumentowi postępów określenie, jak i gdzie najlepiej obsługiwać informacje. Na przykład instancja postępu może zdecydować się na zarządzanie wywołaniami zwrotnymi oraz uruchomienie zdarzeń w przechwyconym kontekście synchronizacji.

Implementacje IProgress<T>

Platforma .NET udostępnia klasę Progress<T>, która implementuje IProgress<T>. 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 generowane na obiekcie SynchronizationContext, który został przechwycony podczas inicjalizacji instancji Progress<T>. Jeśli nie jest dostępny kontekst synchronizacji, jest używany domyślny kontekst przeznaczony dla puli wątków. Programy obsługi mogą być zarejestrowane w tym zdarzeniu. 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 uniknąć opóźnienia operacji asynchronicznej podczas wykonywania procedur 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 TAP nie zapewnia możliwości anulowania ani śledzenia postępu, dlatego wymagają użycia jednej metody.

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

Jeśli implementacja interfejsu TAP obsługuje anulowanie lub postęp, ale nie oba, może zapewnić 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 interfejsu TAP obsługuje zarówno anulowanie, jak i postęp, może uwidocznić wszystkie cztery przeciążenia. Może jednak zawierać 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 zastosowanie metody TAP będzie wspierać anulowanie lub śledzenie postępu, możesz pominąć te przeciążenia, które nie przyjmują odpowiedniego parametru.

Jeśli zdecydujesz się zapewnić możliwość wyboru spośród wielu przeciążeń w celu umożliwienia opcjonalnego anulowania lub śledzenia postępu, przeciążenia, które nie obsługują anulowania ani postępu, powinny zachowywać się tak, jakby przekazały None do anulowania lub null postępu do przeciążenia, które te funkcje obsługuje.

Nazwa Opis
Wzorce programowania asynchronicznego Wprowadza trzy wzorce do wykonywania operacji asynchronicznych: wzorzec asynchroniczny oparty na zadaniach (TAP), asynchroniczny model programowania (APM) i asynchroniczny wzorzec oparty na zdarzeniach (EAP).
Implementowanie wzorca asynchronicznego opartego na zadaniach Opisuje sposób implementowania asynchronicznego wzorca opartego na zadaniach (TAP) na trzy sposoby: przy użyciu kompilatorów języka C# i Visual Basic w programie Visual Studio, ręcznie lub za pomocą kombinacji metod kompilatora i metod ręcznych.
Korzystanie ze wzorca asynchronicznego opartego na zadaniach Opisuje sposób użycia zadań i wywołań zwrotnych w celu osiągnięcia oczekiwania bez blokowania.
Współdziałanie z innymi wzorcami i typami asynchronicznymi Opisuje sposób użycia asynchronicznego wzorca opartego na zadaniach (TAP) do implementowania asynchronicznego modelu programowania (APM) i asynchronicznego wzorca opartego na zdarzeniach (EAP).