SwiftprogramowanieProgramista Swift

Jaką strategię rejestrowania stosuje środowisko uruchomieniowe **Swift** w **TaskGroup**, aby utrzymać relacje parent-child między zadaniami i jak to ułatwia propagację anulacji?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie

Model współbieżności Swift przeszedł znaczną zmianę wraz z Swift 5.5, wprowadzając uporządkowaną współbieżność, aby zastąpić przestarzałe wzorce Grand Central Dispatch, które często prowadziły do porzuconych zadań i wycieków zasobów. Przedtem programiści ręcznie zarządzali instancjami DispatchGroup do śledzenia równoległej pracy, co wymagało jawnej synchronizacji, aby zapobiec warunkom wyścigu podczas anulacji. Abstrakcja TaskGroup została zaprojektowana do native'owego kapsułkowania hierarchii relacji parent-child, zapewniając, że środowisko uruchomieniowe utrzymuje metadane cyklu życia, a nie programista.

Głównym problemem jest utrzymanie deterministycznej hierarchii, w której zadania nadrzędne mogą niezawodnie sygnalizować anulację wszystkim potomkom bez przeszukiwania globalnych rejestrów lub ręcznych tablic słabych referencji. Tradycyjne podejścia z użyciem OperationQueue wymagają jawnej rejestracji i deregistracji handlerów zakończenia, co tworzy kruchą obsługę stanu, która zawodzi, jeśli handler zakończenia zostanie pominięty z powodu wcześniejszego zakończenia. Ponadto propagowanie anulacji wymaga złożonego sprawdzania flag atomowych, co często prowadzi do opóźnionej reakcji lub nadmiernego obciążenia CPU.

Swift rozwiązuje to, osadzając rekord zadania w kontekście każdego zadania, który wskazuje na jego rodzica, tworząc inwazyjną listę połączoną osadzoną w TaskGroup. Kiedy wywoływana jest addTask, środowisko uruchomieniowe wstawia rekord zadania potomnego do tej listy, atomowo rejestrując go przy handlerze anulacji rodzica. Mechanizm anulacji wykorzystuje maszyny stanowe: gdy wywoływana jest cancelAll(), środowisko uruchomieniowe przechodzi przez tę listę, ustawiając flagę isCancelled w metadanych każdego zadania potomnego i budzi zawieszone wykonawce. To zapewnia propagację O(n), gdzie n to głębokość drzewa, unikając globalnych blokad.

import Foundation func downloadImages(urls: [URL]) async throws -> [Data] { try await withThrowingTaskGroup(of: Data.self) { group in for url in urls { group.addTask { // Potomne zadanie automatycznie sprawdza anulację rodzica let (data, _) = try await URLSession.shared.data(from: url) return data } } // Symulowanie anulacji przez użytkownika group.cancelAll() var results: [Data] = [] for try await data in group { results.append(data) } return results } }

Sytuacja z życia

Aplikacja do przetwarzania mediów musiała generować miniatury dla 10 000 obrazów, jednocześnie pozwalając użytkownikom na anulowanie w dowolnym momencie. Zespół inżynieryjny początkowo użył podejścia DispatchGroup, śledząc aktywne obiekty URLSessionDataTask w wątkowo bezpiecznej NSHashTable, aby umożliwić anulację.

Pierwsze rozwiązanie wykorzystywało DispatchGroup z DispatchSemaphore, aby ograniczyć równoległość. Choć funkcjonalne, wymagało skomplikowanej logiki do usuwania zadań zakończonych z zestawu anulacyjnego. Występowały warunki wyścigu, gdy zadania kończyły się między sygnałem anulacji a enumeracją zestawu, co powodowało, że aplikacja odwoływała się do zwolnionych obiektów. To podejście również powodowało wycieki pamięci, gdy kontroler widoku był zamykany, ponieważ powiadomienia DispatchGroup silnie utrzymywały delegata.

Drugie podejście przyjęło FlatMap z Combine i PassthroughSubject do anulacji. To zapewniało lepszą kompozycję, ale wprowadzało znaczący narzut pamięci z powodu alokacji ciągu publikatora. Propagacja anulacji wymagała przechowywania tokenów AnyCancellable w kolekcji wymagającej ręcznego czyszczenia. Deklaratywna abstrakcja ukrywała rzeczywistą hierarchię zadań, co utrudniało debugowanie, gdy sygnały anulacji nie propagowały się przez łańcuch operatorów.

Zespół przeszedł na TaskGroup w Swift. To wyeliminowało ręczne zarządzanie NSHashTable, ponieważ środowisko uruchomieniowe automatycznie powiązało każde zadanie generowania miniatury z dziedziną anulacji grupy. Gdy użytkownik naciskał anuluj, kontroler widoku wywoływał group.cancelAll(), co atomowo sygnalizowało wszystkim uruchomionym zadaniom zakończenie przy następnym punkcie zawieszenia await. To rozwiązanie zapewniło, że żadne porzucone zadania nie będą kontynuowały przetwarzania po zwolnieniu widoku, a deterministyczne zakresy withThrowingTaskGroup zapewniły automatyczne czyszczenie, nawet jeśli funkcja zgłosiła błąd.

Opóźnienie anulacji spadło z średnio 500 ms (czekanie na ręczną enumerację zestawu) do poniżej 10 ms (bezpośrednie przechodzenie przez listę połączoną). Profilowanie pamięci wykazało zerowe wycieki obiektów Task po anulacji, a baza kodu zmniejszyła się o 40 linii nadmiarowej synchronizacji.

Co często umyka kandydatom

Jak TaskGroup radzi sobie z sytuacją, w której zadanie potomne ignoruje anulację i kontynuuje wykonywanie bez końca?

Kandydaci często wierzą, że TaskGroup przymusowo kończy zadania lub wstrzykuje wyjątki. W rzeczywistości anulacja w Swift jest współpraca: środowisko ustawia flagę isCancelled w kontekście zadania, ale zadanie kontynuuje, aż napotka punkt zawieszenia lub jawnie sprawdzi Task.isCancelled. Potomne zadanie musi okresowo sprawdzać Task.checkCancellation() lub polegać na API świadomych anulacji. Jeśli zadanie wykonuje szczelną pętlę CPU bez punktów zawieszenia, blokuje zakończenie grupy na czas nieokreślony. Aby tego uniknąć, długoterminowe obliczenia powinny korzystać z Task.yield() lub dzielić pracę na fragmenty sprawdzające flagi anulacji.

Dlaczego dodanie zadania do TaskGroup po wywołaniu cancelAll() nadal skutkuje natychmiastową anulacją tego nowego zadania?

Wielu zakłada, że cancelAll() to sygnał jednorazowy wysłany tylko do istniejących dzieci. Jednak implementacja Swift oznacza samą TaskGroup jako anulowaną w swoim rejestrze stanu. Kiedy wywoływana jest addTask później, środowisko unika sprawdzenia stanu anulacji grupy atomowo podczas tworzenia zadania; jeśli anulowane, nowe zadanie potomne tworzone jest z flagą isCancelled ustawioną na wartość true. To zapewnia, że później dodane zadania nie mogą uciec z dziedziny anulacji, zachowując strukturalną gwarancję, że anulowany zakres nie może generować nowych ważnych wyników. To zapobiega warunkom wyścigu, w których zadania dodane podczas wycofywania anulacji umykają.