Użyj grupy zadań w Swift, aby zebrać konkurujące obliczenia w kolejności ich ukończenia, pomijać nil i anulować pozostałe zadania potomne, gdy pojawi się wartość. Ten przykład programu wiersza poleceń na Macu wyszukuje użyteczny układ prostokąta dla syntetycznych kafelków i wyjaśnia, kiedy wynik może faktycznie zostać zwrócony.
Określ, co uznajesz za pierwszy użyteczny wynik
Załóżmy, że niewielkie narzędzie do układania elementów na pulpicie ma rozmieścić 221 syntetycznych kafelków w prostokąt. Kilka lokalnych obliczeń przeszukuje różne możliwe liczby wierszy. Akceptowalny jest dowolny nietrywialny dzielnik: sprawdzi się układ 13 wierszy i 17 kolumn, podobnie jak 17 wierszy i 13 kolumn. Zadaniem odbiorcy jest zaakceptowanie tego poprawnego obliczenia, które zakończy się jako pierwsze.
To kontrakt oparty na kolejności ukończenia. Nie wybiera najmniejszego dzielnika, najbardziej zrównoważonego prostokąta ani obliczenia wymienionego jako pierwsze. Apple dokumentuje, że kolejne wyniki grupy zadań przychodzą w kolejności ukończenia, niezależnie od kolejności dodania. TaskGroup.next(isolation:)
Zdefiniuj zachowanie przed napisaniem funkcji pomocniczej:
| Sytuacja | Wymagane zachowanie |
|---|---|
Obliczenie kończy się wynikiem nil |
Nadal czekaj na inne obliczenia. |
| Obliczenie kończy się wartością | Wybierz ją i zażądaj anulowania pozostałych zadań. |
Wszystkie obliczenia zwracają nil |
Zwróć nil. |
| Lista obliczeń jest pusta | Zwróć nil. |
| Wykryto anulowanie zadania nadrzędnego | Przestań wybierać wyniki i zwróć nil. |
Ostatni wiersz to celowy wybór API na potrzeby tego przykładu. Tutaj nil oznacza, że wywołanie nie dostarczyło użytecznej wartości; nie rozróżnia nieudanego wyszukiwania od anulowania. Jeśli wywołujący musi rozróżniać te przypadki, użyj jawnego typu wyniku albo kontraktu opartego na zgłaszaniu błędów.
Zbuduj kompletny lokalny przykład na Macu
Użyj toolchaina Swift 6 lub nowszego. W Terminalu sprawdź jego wersję poleceniem swift --version. Jeśli Swift nie jest dostępny, skorzystaj z oficjalnych instrukcji Install Swift — macOS. Program używa Foundation i interfejsów współbieżności Swift, bez zewnętrznych zależności pakietów.
Utwórz nowy pakiet wykonywalny za pomocą tych poleceń. Jest to zgodne z udokumentowanym przez Swift.org procesem tworzenia i uruchamiania pakietu. Build a Command-line Tool
mkdir FirstNonNilDemo
cd FirstNonNilDemo
swift package init --name FirstNonNilDemo --type executable
Zastąp wygenerowany kod źródłowy programu wykonywalnego poniższym kompletnym programem. Zależnie od wygenerowanego układu plik może nazywać się Sources/FirstNonNilDemo/FirstNonNilDemo.swift albo Sources/main.swift. Użyj jednego pliku punktu wejścia o nazwie FirstNonNilDemo.swift; jeśli szablon utworzył main.swift, zmień nazwę tego pliku przed dodaniem programu z @main. Przewodnik Swifta dokumentuje, że program korzysta z main.swift albo z punktu wejścia @main. Build a Command-line Tool
import Foundation
func firstNonNil<Value: Sendable>(
_ calculations: [@Sendable () async -> Value?]
) async -> Value? {
guard !Task.isCancelled else { return nil }
return await withTaskGroup(
of: Value?.self,
returning: Value?.self
) { group in
defer { group.cancelAll() }
for calculation in calculations {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await calculation()
}
guard added else { break }
}
for await candidate in group {
guard !Task.isCancelled else { return nil }
if let value = candidate {
return value
}
}
return nil
}
}
func findFactor(total: Int, range: ClosedRange<Int>) -> Int? {
guard total > 1 else { return nil }
for rows in range {
guard !Task.isCancelled else { return nil }
guard rows > 1, rows < total else { continue }
if total % rows == 0 {
return rows
}
}
return nil
}
@main
struct FirstNonNilDemo {
static func main() async {
let checks: [@Sendable () async -> Int?] = [
{ nil },
{ 42 }
]
let checkResult = await firstNonNil(checks)
precondition(checkResult == 42)
let empty: [@Sendable () async -> Int?] = []
let emptyResult = await firstNonNil(empty)
precondition(emptyResult == nil)
for rawText in [" 221\n", " 223 ", "tiles"] {
let text = rawText.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard let total = Int(text), total > 1 else {
print("Input must be a whole number greater than 1.")
continue
}
let calculations: [@Sendable () async -> Int?] = [
{ findFactor(total: total, range: 2...5) },
{ findFactor(total: total, range: 6...16) },
{ findFactor(total: total, range: 17...30) }
]
if let rows = await firstNonNil(calculations) {
print("\(total) tiles: \(rows) rows x \(total / rows) columns")
} else {
print("\(total) tiles: no factor in the supplied ranges.")
}
}
}
}
Uruchom pakiet z jego katalogu:
swift run FirstNonNilDemo
Dla 221 poprawny jest wynik 221 tiles: 13 rows x 17 columns albo 221 tiles: 17 rows x 13 columns. Dla 223 program zgłasza brak dzielnika w podanych zakresach. Tekst tiles wyświetla komunikat o danych wejściowych. Są to oczekiwane wyniki, a nie zapis rzeczywistego uruchomienia. Obliczenia arytmetyczne sprawdzono osobno; w środowisku badawczym kompilator Swift był niedostępny, więc program przejrzano, ale go nie uruchomiono.
Foundation obsługuje wyłącznie usuwanie białych znaków z początku i końca tekstu. Metoda trimmingCharacters(in:) usuwa pasujące znaki z obu końców; nie usuwa znaków ze środka ciągu. W rezultacie 221 zmienia się w 221, a 2 21 pozostaje nieprawidłowe dla tego programu. trimmingCharacters(in:)
Dlaczego funkcja pomocnicza pomija nil, nie tracąc późniejszych wyników
Typ wyniku zadania potomnego w grupie to Value?. Każde zadanie potomne dostarcza więc albo użyteczną wartość, albo zwykły pusty wynik. Pętla for await odbiera ukończone wyniki zadań potomnych, a jej wewnętrzne wiązanie opcjonalne wybiera tylko kandydat niebędący nil.
Przy bezpośrednim użyciu group.next() występują dwa różne rodzaje pustego wyniku. Udokumentowany typ zwracany tej metody to ChildTaskResult?. Przy opcjonalnych wynikach zadań potomnych daje to Value??: opcjonalność zewnętrzna określa, czy istnieje kolejny wynik zadania potomnego, a wewnętrzna — czy to zadanie znalazło wartość. TaskGroup.next(isolation:)
To rozróżnienie wyjaśnia częsty błąd: pobranie jednego wyniku i natychmiastowe zwrócenie go. Jeśli pierwsze ukończone obliczenie zwróci nil, inne zadania potomne mogą nadal mieć użyteczne odpowiedzi. Pętla w kompletnym przykładzie nadal pobiera wyniki, aż znajdzie wartość albo grupa zostanie wyczerpana.
Funkcja pomocnicza używa addTaskUnlessCancelled i sprawdza zwracaną przez nią wartość logiczną. Apple dokumentuje, że ta metoda odmawia utworzenia zadania potomnego, jeśli grupa została już anulowana. Przerwanie pętli zgłaszającej zadania pozwala uniknąć dalszego dodawania pracy po wykryciu tego stanu. addTaskUnlessCancelled(priority:operation:)
Obliczenia przechwytują niezmienną liczbę kafelków i przekazują odpowiedzi przez grupę. Nie aktualizują współdzielonej zmiennej przechowującej zwycięski wynik. Generyczny typ wyniku musi spełniać wymaganie Sendable, zgodnie z udokumentowanym ograniczeniem grupy zadań. withTaskGroup(of:returning:isolation:body:)
Dlaczego wybór zwycięzcy nie oznacza natychmiastowego zwrotu
Zwrócenie wartości z domknięcia grupy wybiera odpowiedź, ale otaczające je withTaskGroup nadal czeka na zakończenie wszystkich zadań potomnych. Apple wyraźnie dokumentuje, że nawet anulowane zadania potomne muszą się zakończyć, zanim funkcja zwróci wynik. withTaskGroup(of:returning:isolation:body:)
Blok defer wywołuje group.cancelAll() przy każdym wyjściu z domknięcia. To żąda anulowania pozostałych obliczeń. Anulowanie nie przerywa dowolnych obliczeń: zadania potomne muszą współpracować i samodzielnie zakończyć pracę wcześniej. TaskGroup
W tym przykładzie findFactor sprawdza Task.isCancelled przed każdym kandydatem na dzielnik. Każda jednostka pracy jest niewielka, więc często pojawia się możliwość przerwania. Przy większym obliczeniu umieść kontrole między sensownymi, ograniczonymi fragmentami pracy. Kontrola wykonana tylko przed długą, niepodzielną operacją pozostawia ją w toku aż do zakończenia. Przewodnik języka Swift opisuje anulowanie jako kooperacyjne i wskazuje zwrócenie nil jako jedną z obsługiwanych reakcji. Concurrency
Jeśli konkurujące obliczenie nigdy się nie kończy i nie reaguje na anulowanie, ta ustrukturyzowana funkcja pomocnicza nie może zagwarantować szybkiego zwrotu. Wspieranym rozwiązaniem jest zapewnienie każdemu wykonawcy ograniczonego czasu pracy albo świadomości anulowania. Dodanie konkurującego wyniku limitu czasu nie znosi obowiązku grupy, by zaczekać na jej zadania potomne.
Anulowanie zadania nadrzędnego jest sprawdzane przed zgłoszeniem zadań, na początku zadania potomnego, wewnątrz pętli arytmetycznej i przed zaakceptowaniem każdego wyniku. Te kontrole wykrywają anulowanie w określonych momentach; nie tworzą atomowej reguły, zgodnie z którą anulowanie zawsze ma pierwszeństwo przed wartością. Anulowanie, które nastąpi po ostatniej kontroli, może zbiec się z pomyślnym zwrotem wyniku.
Wybierz właściwą regułę i sprawdź jej granice
Syntetyczny przykład ułatwia podjęcie decyzji: zarówno 13, jak i 17 spełniają określony kontrakt. Jeśli narzędzie wymaga najmniejszej liczby wierszy, zaakceptowanie dowolnego z tych wyników jest nieprawidłowe. Kolejność ukończenia jest użyteczna tylko wtedy, gdy każda zaakceptowana odpowiedź jest równoważna dla zadania odbiorcy.
| Czego potrzebuje wywołujący | Odpowiednie podejście |
|---|---|
| Dowolna akceptowalna wartość, gdy obliczenia się kończą | Użyj pokazanej wyżej pętli grupy zadań. |
| Najwcześniejsze udane obliczenie z listy o ustalonej kolejności preferencji | Wykonuj obliczenia w kolejności preferencji albo zachowuj indeksy i rozstrzygaj wcześniejszych kandydatów przed wyborem. |
| Najmniejszy wynik lub najwyższa ocena | Przed wyborem porównaj odpowiednie wyniki. |
| Kilka znanych wyników potrzebnych jednocześnie | Rozważ użycie async let. |
Przewodnik języka Swift przedstawia async let dla znanych zadań potomnych, których wyniki będą potrzebne później, a grupy zadań — do jawnego zarządzania dynamicznie dodawanymi zadaniami potomnymi. Oczekiwanie na nazwane wyniki w wybranej kolejności nie tworzy pętli wybierającej według kolejności ukończenia. Concurrency
Podczas dostosowywania programu użyj tych kontroli:
- Zachowaj asercję dla pustej listy: powinna zwracać
nil. - Zachowaj asercję z mieszanymi wynikami
nili42: powinna zwracać42niezależnie od kolejności ukończenia. - Przetestuj obliczenia, z których wszystkie zwracają
nil. - Przy kilku poprawnych odpowiedziach sprawdzaj przynależność wyniku do zbioru poprawnych wartości zamiast oczekiwać konkretnego zwycięzcy.
- Sprawdź każdego wykonawcę pod kątem ograniczonego postępu i kontroli anulowania.
- Informuj, że nieudane wyszukiwanie dotyczy podanych zakresów; nie wyciągaj z niego wniosku, że nigdzie indziej nie ma odpowiedzi.
Trzy niewielkie skany dzielników ilustrują działanie, a nie wzrost szybkości. Dla tych przykładowych liczb wystarczy skanowanie sekwencyjne. Wzorzec konkurujących obliczeń stosuj wtedy, gdy rzeczywiste zadanie zyskuje na akceptowaniu równoważnych wyników, a przed wyborem liczby równoległych zadań zmierz obciążenie.
Jasno określ regułę akceptacji, pomijaj puste wyniki zadań potomnych i umożliw pozostałej pracy zatrzymanie się. Dzięki temu narzędzie na Macu ma przewidywalny kontrakt zwracanej wartości: pierwszy użyteczny wynik dostarczony przez grupę, a następnie zakończenie współpracującego zatrzymywania zadań potomnych.
Źródła
- Install Swift — macOS — Swift.org
- Build a Command-line Tool — Swift.org
- TaskGroup — Apple
- withTaskGroup(of:returning:isolation:body:) — Apple
- next(isolation:) — Apple
- addTaskUnlessCancelled(priority:operation:) — Apple
- trimmingCharacters(in:) — Apple
- Concurrency — The Swift Programming Language, Swift.org