Aby zachować atomowe porównanie i zastępowanie podczas przenoszenia modelu Swift do aktora, umieść porównanie i przypisanie w jednej synchronicznej metodzie izolowanej przez aktora. Wywołujący używają await, ale sama metoda nie może się zawiesić między sprawdzeniem a zastąpieniem.
Ten przewodnik jest przeznaczony dla programisty Maca, który przenosi niewielki model aplikacji desktopowej, wcześniej chroniony blokadą. Konkretnym zadaniem jest zastąpienie syntetycznej etykiety kształtu tylko wtedy, gdy jej bieżąca wartość nadal odpowiada oczekiwanej wartości. Przykład działa w całości w pamięci i nie używa żądań sieciowych, zewnętrznych rekordów ani trwałego zapisu.
Zdefiniuj kontrakt wartości przed zmianą modelu
W tym przykładzie compareAndReplace(expected:replacement:) ma trzy reguły: porównaj bieżącą etykietę z expected; jeśli są równe, przypisz replacement i zwróć true; w przeciwnym razie zwróć false bez zmiany etykiety. Porównanie wykorzystuje równość wartości w Swift. Dokumentacja standardowej biblioteki dotycząca protokołu Equatable definiuje to porównanie za pomocą == i odróżnia równość wartości od tożsamości instancji klasy.
Porównanie musi uwzględniać wartość bieżącą w chwili wykonania operacji. Wcześniejsza migawka może dostarczyć oczekiwanej wartości, ale nie może przesądzić, że późniejsza próba zastąpienia ma się powieść. Ta decyzja należy do operacji, która odpowiada za mutację.
| Bieżąca wartość | Oczekiwana wartość | Zastąpienie | Wynik | Wartość po operacji |
|---|---|---|---|---|
"Circle" |
"Circle" |
"Square" |
true |
"Square" |
"Square" |
"Circle" |
"Triangle" |
false |
"Square" |
"Square" |
"Square" |
"Square" |
true |
"Square" |
Ostatni wiersz jest zamierzony: sukces oznacza, że porównanie się powiodło, a przypisanie zostało zaakceptowane. Nie oznacza, że widoczna wartość się zmieniła. Zmiana tej konwencji zwracania wyniku podczas migracji zmieniłaby kontrakt.
W implementacji opartej na blokadzie całe porównanie i przypisanie muszą znajdować się w tej samej sekcji krytycznej chronionej blokadą. Apple opisuje NSLock z Foundation jako sposób ochrony sekcji krytycznej, tak aby była wykonywana atomowo. Podczas migracji do aktora zachowaj tę granicę operacji: przenieś całą decyzję do aktora zamiast rozdzielać jej części między wywołujących.
Tutaj „atomowy” oznacza, że żadna inna operacja na tym aktorze nie może wkroczyć między porównanie a przypisanie. Ten przykład nie dotyczy sprzętowych instrukcji atomowych ani wykonywania bez blokad.
Dlaczego await wywołującego nie przerywa operacji
Synchroniczna metoda aktora może wymagać await, gdy jest wywoływana spoza domeny jego izolacji. Wywołujący może się zawiesić, czekając na wejście do aktora. Gdy metoda zacznie się wykonywać, jej zwykłe synchroniczne ciało nie ma punktu zawieszenia, w którym inne zadanie mogłoby wejść do aktora i zmienić jego stan. Swift opisuje to rozróżnienie w SE-0306: Actors.
Przewodnik języka zaleca hermetyzowanie aktualizacji w synchronicznych metodach aktora, ponieważ takie metody nie mogą zawierać potencjalnych punktów zawieszenia. Sesja Apple poświęcona aktorom wyjaśnia również, że synchroniczny kod aktora kończy działanie bez przerwania jego wykonania izolowanego przez aktora przez inne zadanie. Te gwarancje stanowią podstawę tej implementacji. Zobacz Concurrency oraz Ochrona zmiennego stanu za pomocą aktorów Swift.
Przydatna granica to jedno wywołanie aktora zawierające zarówno warunek, jak i wynikającą z niego mutację. Wywołujący może najpierw odczytać migawkę, wykonać lokalne obliczenia, a następnie zlecić warunkowe zastąpienie. Aktor musi ponownie sprawdzić warunek podczas obsługi tego żądania.
Co się psuje przy osobnych odczytach i zapisach?
Załóżmy, że migracja udostępnia osobne metody pobierania migawki i bezwarunkowego zastępowania. Jeden wywołujący odczytuje "Circle". Drugi zmienia etykietę na "Square". Następnie pierwszy, opierając się na wcześniejszym odczycie, zapisuje "Triangle". Każda z metod z osobna może przestrzegać izolacji aktora, a mimo to cała sekwencja naruszy zamierzony kontrakt porównania i zastąpienia.
Ten przykładowy harmonogram objaśnia usterkę bez uzależniania jej od konkretnego planisty zadań. Izolacja aktora chroni każdą operację; programista nadal musi wybrać operację o zakresie wystarczającym do zachowania warunku aplikacji.
Zbuduj kompletny lokalny przykład na Macu
Użyj zainstalowanego łańcucha narzędzi Swift 6. Oficjalny przewodnik instalacji Swift dla macOS opisuje instalację przez Swiftly i programowanie w Xcode. W Terminalu sprawdź wybrany kompilator przed kontynuowaniem:
swiftc --version
Zapisz poniższy kompletny program w folderze roboczym jako CompareReplace.swift. Korzysta on wyłącznie ze standardowej biblioteki Swift, która jest dostępna bez jawnego importu. Plik definiuje aktora, jego stan początkowy, wszystkie przykładowe dane wejściowe i asynchroniczny punkt wejścia.
actor ShapeLabelModel {
private var label: String
init(initial: String) {
label = initial
}
func snapshot() -> String {
label
}
func compareAndReplace(
expected: String,
replacement: String
) -> Bool {
guard label == expected else {
return false
}
label = replacement
return true
}
}
@main
struct CompareReplaceDemo {
static func main() async {
let model = ShapeLabelModel(initial: "Circle")
let matched = await model.compareAndReplace(
expected: "Circle", replacement: "Square"
)
let afterMatch = await model.snapshot()
precondition(matched && afterMatch == "Square")
let stale = await model.compareAndReplace(
expected: "Circle", replacement: "Triangle"
)
let afterStale = await model.snapshot()
precondition(!stale && afterStale == "Square")
let unchanged = await model.compareAndReplace(
expected: "Square", replacement: "Square"
)
precondition(unchanged)
let contested = ShapeLabelModel(initial: "Circle")
async let first = contested.compareAndReplace(
expected: "Circle", replacement: "Square"
)
async let second = contested.compareAndReplace(
expected: "Circle", replacement: "Triangle"
)
let (firstWon, secondWon) = await (first, second)
let finalLabel = await contested.snapshot()
precondition(firstWon != secondWon)
precondition(
finalLabel == (firstWon ? "Square" : "Triangle")
)
let cycle = ShapeLabelModel(initial: "Circle")
let oldSnapshot = await cycle.snapshot()
let movedAway = await cycle.compareAndReplace(
expected: "Circle", replacement: "Square"
)
let movedBack = await cycle.compareAndReplace(
expected: "Square", replacement: "Circle"
)
let matchedAgain = await cycle.compareAndReplace(
expected: oldSnapshot, replacement: "Triangle"
)
let afterCycle = await cycle.snapshot()
precondition(movedAway && movedBack && matchedAgain)
precondition(afterCycle == "Triangle")
print("Matching replacement: \(matched)")
print("Stale replacement: \(stale)")
print("Equal replacement: \(unchanged)")
print("Exactly one competing replacement succeeded.")
print("A restored value matches the old snapshot.")
}
}
Skompiluj i uruchom program z tego folderu. Opcja -parse-as-library obsługuje jawny punkt wejścia @main, a -swift-version 6 wybiera tryb języka Swift 6.
swiftc -swift-version 6 -parse-as-library CompareReplace.swift -o compare-demo
./compare-demo
Oczekiwany wynik, wyprowadzony ze sprawdzeń w programie, to:
Matching replacement: true
Stale replacement: false
Equal replacement: true
Exactly one competing replacement succeeded.
A restored value matches the old snapshot.
Kod został przeanalizowany w odniesieniu do dokumentacji, ale nie został tutaj uruchomiony, ponieważ w tym środowisku nie ma kompilatora Swift. Dołączone asercje zapewniają lokalne sprawdzenia po uruchomieniu programu; nie oznaczają, że program został już wykonany.
Co potwierdzają konkurujące wywołania
Dwie deklaracje async let tworzą zadania podrzędne, nie czekając na wynik pierwszego przed uruchomieniem drugiego. To użycie współbieżności strukturalnej jest zgodne z omówieniem równoległych wywołań asynchronicznych w przewodniku języka. Oba wywołania oczekują "Circle", a każde proponuje inne zastąpienie.
W tej konkretnej konfiguracji powiedzie się dokładnie jedno wywołanie: to, które wykona się pierwsze, zmieni wartość z "Circle"; drugie nie przejdzie porównania. Test dopuszcza dowolnego zwycięzcę. Swift nie gwarantuje, że zadania oczekujące na aktora wykonają się w kolejności, w jakiej pierwotnie na niego czekały, co opisano w SE-0306.
Przykład oddziela również wywołania z await od wyrażeń precondition. Najpierw zapisuje każdy wynik w lokalnej stałej, a potem sprawdza tę stałą. Dzięki temu praca asynchroniczna odbywa się poza synchronicznymi wyrażeniami asercji.
Pozostaw asynchroniczne przygotowanie poza końcowym porównaniem
Przyszła wersja może asynchronicznie obliczać proponowaną etykietę na podstawie innej lokalnej, syntetycznej pracy. Zakończ to przygotowanie przed wywołaniem synchronicznej metody porównania i zastąpienia. Przekaż razem pierwotną oczekiwaną wartość i obliczone zastąpienie; potraktuj false jako odrzuconą próbę.
Metoda aktora, która sprawdza równość, wykonuje await podczas przygotowania, a następnie bezwarunkowo przypisuje wartość, tworzy lukę. Podczas zawieszenia innego zadanie może zmienić stan aktora. Po wznowieniu metoda opierałaby się na warunku, który może już nie być spełniony. Udokumentowany model ponownego wejścia aktora w Swift wyraźnie dopuszcza takie przeplatanie.
Jeśli przygotowanie odbywa się w asynchronicznej metodzie aktora, po jego zakończeniu wywołaj synchroniczną metodę porównania. Ta końcowa metoda musi wykonać nowe porównanie. Sprawdzenie przed zawieszeniem może pomóc uniknąć zbędnej pracy, ale nie może uprawniać do późniejszego przypisania.
W razie niezgodności zachowaj proponowany wynik, jeśli interfejs aplikacji desktopowej ma go pokazać, pobierz nową migawkę i zdecyduj, czy ponownie obliczyć wartość, czy zgłosić konflikt. Automatyczne zastępowanie oczekiwanej wartości i ponawianie próby zmieniłoby żądanie z „zastąp wartość, którą zaobserwowałem” na „ponawiaj próbę względem nowszych wartości”. Takie zachowanie powinno być jawną decyzją produktową.
Nie utrzymuj starej blokady Foundation przez await. Apple wymaga, aby NSLock została odblokowana przez ten sam wątek, który ją zablokował. Przedstawiony tutaj projekt z aktorem osiąga wymagany rezultat za pomocą krótkiej, synchronicznej operacji izolowanej, bez utrzymywania blokady podczas pracy asynchronicznej.
Zachowaj semantykę równości i uwzględnij jej ograniczenia
Test cyklu pokazuje subtelną granicę. Wywołujący zapisuje migawkę "Circle"; model zmienia wartość na "Square", a następnie z powrotem na "Circle". Porównanie ze starą migawką powiedzie się, ponieważ kontrakt porównuje bieżącą wartość, a nie sekwencję zmian, które do niej doprowadziły.
Jeśli rzeczywisty wymóg brzmi „zastąp tylko wtedy, gdy od czasu mojej migawki nie zaszła żadna zaakceptowana mutacja”, samo porównanie wartości nie wystarczy. Obsługiwany projekt może zwracać migawkę zawierającą zarówno wartość, jak i token rewizji, a następnie sprawdzać token i aktualizować wartość razem w jednej synchronicznej metodzie aktora. To silniejszy i odmienny kontrakt. Przed implementacją określ zasady zwiększania tokenu i postępowania w razie jego wyczerpania; nie wprowadzaj ich po cichu podczas migracji zachowującej wartość.
W przypadku większych modeli użyj wartości migawki, której równość obejmuje każde pole istotne dla zastąpienia. Swift może automatycznie wygenerować implementację Equatable dla struktury, jeśli wszystkie jej przechowywane właściwości są zgodne z tym protokołem, zgodnie z dokumentacją referencyjną Equatable. Implementacja równości pomijająca istotne pole pozwoliłaby na zastąpienie mimo zmian tego pola.
Upewnij się, że wartości przekraczające granicę aktora nadają się do współbieżnego użycia. Dokumentacja Sendable wyjaśnia, że wartości typu sendable mogą być przekazywane między domenami współbieżności, a struktury tego typu muszą zawierać składowe typu sendable. Umieszczenie dowolnego zmiennego odwołania w strukturze nie zapewnia tej właściwości.
W przypadku tego lokalnego modelu migracja jest zakończona, gdy aktor zarządza zmienną etykietą, każde warunkowe zastąpienie odbywa się przez połączoną operację, a wywołujący interpretują jej wynik Boolean zgodnie z pierwotnym kontraktem. Zachowaj końcowe porównanie i przypisanie razem, a następnie pozwól otaczającemu je procesowi aplikacji desktopowej zdecydować, co oznacza odrzucona próba.
Źródła
- NSLock — dokumentacja Apple dla deweloperów.
- Equatable — dokumentacja Apple dla deweloperów.
- Sendable — dokumentacja Apple dla deweloperów.
- Concurrency — The Swift Programming Language, projekt Swift.
- Actors — SE-0306 — Swift Evolution, projekt Swift.
- Ochrona zmiennego stanu za pomocą aktorów Swift — Apple Developer, WWDC21.
- Instalacja Swift — macOS — Swift.org.