Używanie Task lub klas Task ma wąskie gardło wydajności, o którym nie wspomnieliśmy w poprzednich artykułach. Krótko mówiąc, te zajęcia prowadzą, gdy wyniki są natychmiast dostępneNiepotrzebne przydzielanie。 Oznacza to, że nowe Zadanie lub obiekt Zadania zawsze zostanie utworzony, nawet jeśli wynik jest już dostępny. Wspomnieliśmy, że koncepcja async/await, której używaliśmy w poprzednich artykułach, istnieje od czasu wydania .NET 4.5. Funkcja ta została rozszerzona od czasów C# 7 o wersję .NET 4.7 ze strukturą ValueTask, która może być używana jako powrót dla funkcji asynchronicznych.
Struktura ValueTask
Struktura ValueTask po raz pierwszy pojawiła się w repozytorium corefxlab w 2015 roku. To repozytorium służy do eksperymentowania i eksplorowania nowych pomysłów, które mogą, ale nie muszą trafić do głównego repozytorium corefx. Repozytorium Corefx to repozytorium, w którym znajdują się wszystkie bazowe biblioteki rdzenia .NET. Został opracowany i zaproponowany przez Stephena Tauba dla biblioteki System.Threading.Tasks.Channels. W tym czasie Stephen udzielił krótkiego wyjaśnienia:
Adres biblioteki corefxlab:Logowanie do linku jest widoczne.
ValueTask to odrębna kombinacja T i Task, pozwalająca ReadAsync na swobodne przydzielanie zadań synchronicznie zwracających dostępne wartości T (w przeciwieństwie do Task.FromResult, które wymaga alokacji instancji Task). ValueTask jest oczekowalny, więc zużycie większości instancji jest nie do odróżnienia od konsumpcji zadań. Wiele osób dostrzega zalety stosowania tej struktury, która jest zawarta w C# 7 jako część pakietu System.Threading.Tasks.Extensions NuGet. Zanim więc przejdziemy do struktury ValueTask, przyjrzyjmy się problemowi, którego używa do rozwiązania. Ponieważ Task(Task) jest typem odniesienia, zacznijmy odMetoda asynchroniczna zwracająca obiekt Task oznacza, że jest on alokowany na stosie za każdym razem。 Jest to konieczne w wielu przypadkach.
Jednak w niektórych przypadkach metody asynchroniczne zwracają wyniki natychmiast lub całkowicie synchronicznie. W takich przypadkach taka alokacja jest niepotrzebna i może stać się kosztowna w krytycznych dla wydajności częściach kodu. Do czasu wydania .NET 4.7 nie było sposobu, by tego uniknąć, ponieważ metody asynchroniczne musiały zwracać Zadanie, Zadanie <T>lub unieważnienie (to ostatnie zwykle było niepożądane). W tej wersji .NET jest to rozszerzone, co oznacza, że metoda asynchroniczna może zwracać dowolny typ, o ile posiada dostępną metodę GetAwaiter. ValueTask jest konkretnym przykładem tego typu i został również dodany do tej wersji.
Możesz przeglądać repozytorium corefx i zobaczyć pełną implementację ValueTask, oto sekcja API, która nas interesuje:
Jako struktura, ValueTask pozwala na pisanie asynchronicznych metod, które nie alokują pamięci podczas synchronicznego czasu działania. Spójność API koncepcji async/await nie jest w ten sposób zagrożona. Dodatkowo ta konstrukcja czeka samodzielnie, co czyni ją łatwą w obsłudze. Na przykład, jeśli uruchomimy ten prosty kod:
W metodzie MultiplyAsync symulujemy sytuację, w której chcemy uniknąć użycia Zadania i zwracamy tylko prostą liczbę całkowitą. Robi się to w instrukcji if metody, gdzie zasadniczo sprawdzamy, czy przekazany parametr jest równy zeru. Problem polega na tym, żeNawet jeśli nasz warunek w instrukcji if jest prawdziwy, powyższy kod tworzy obiekt Task。 Rozwiązujemy ten problem w następujący sposób:
ValueTask i Task
Jak wspomniano wcześniej, istnieją dwie główne korzyści z korzystania z ValueTask:
- Poprawa wydajności
- Zwiększenie elastyczności wdrożenia
Jakie są więc liczby stojące za poprawą wydajności? Obserwuj ten kod:
Jeśli uruchomimy ten kod, wykonanie JIT zajmuje 120 ns. Jeśli zastąpimy Zadanie Wartościowym Zadaniem w ten sposób:
Dzięki JIT uzyskamy czas wykonania 65 ns. Oczywiście z powodu Task.Delay nie wykonujemy się synchronicznie, ale widzimy poprawę czasu wykonywania.
Kolejną korzyścią, o której wspomnieliśmy, jest większa elastyczność w implementacji. Co to dokładnie oznacza? Implementacje interfejsów asynchronicznych, które powinny być zsynchronizowane, będą zmuszone do używania Task.Run lub Task.FromResult. Oczywiście prowadzi to do problemów z wydajnością, o których rozmawialiśmy wcześniej. Korzystając z ValueTask, częściej wybieramy między implementacjami synchronicznymi a asynchronicznymi. Pamiętaj, że może to być znak, że Twój kod może być źle zaprojektowany, jeśli to się przydarzy Tobie.
Na przykład, obserwuj ten interfejs:
Załóżmy, że chcesz wywołać to z kodu w ten sposób:
Ponieważ używamy ValueTask w interfejsie, implementacja interfejsu może być synchroniczna lub asynchroniczna. Możemy uzyskać tę korzyść, pomijając dodawanie do IThing niektórych funkcji obsługujących zachowanie synchronizacji. W ten sposób korzystanie z tego interfejsu jest znacznie łatwiejsze. Oto synchroniczna implementacja powyższego interfejsu:
Oto asynchroniczna implementacja tego samego interfejsu:
Jednak musimy rozważyć pewne kompromisy przed użyciem ValueTask. Łatwo pomyśleć, że ValueTask powinno być używane domyślnie zamiast Task, co z pewnością nie jest prawdą. Na przykład, chociaż ValueTask pomaga unikać niepotrzebnych przypisań, gdy dostępna jest synchronizacja wyników, zawiera także dwa pola.
Ważne jest, aby pamiętać, że używamy tutaj takiej struktury, czyli używamy typów wartości i wszystkich ich obciążeń. Zadanie natomiast jest typem odniesienia z tylko jednym polem.Kiedy korzystasz z ValueTask, mamy więcej danych do przetworzenia i przetwarzania. Jeśli taka metoda jest oczekiwana w metodzie asynchronicznej, to metoda asynchroniczna jestMaszyna stanów również będzie większa, ponieważ przechowywanie całej struktury zwykle zajmuje więcej miejsca niż przechowywanie pojedynczej referencji.
Dlatego ludzie z Microsoftu faktycznie zalecają używanie Task lub Task jako domyślnego typu return dla metod asynchronicznych. Dopiero po analizie wydajności powinieneś rozważyć przejście na ValueTask.
streszczenie
ValueTask to struktura wprowadzona w .NET 4.7, która daje nam wiele możliwości stosowania metod asynchronicznych w .NET. Jednak nie jest to pozbawione ceny. Jest to przydatne dla metod krytycznych dla wydajności, które są wykonywane synchronicznie. Dzięki nim możemy uniknąć przypisywania niepotrzebnych przedmiotów. Mimo to, jako typ wartości, niesie ze sobą wszystkie problemy, które zwykle mają typy wartości. Dlatego możemy skorzystać z tej struktury, ale musimy być ostrożni.
Oryginalny adres:Logowanie do linku jest widoczne.
|