För att bevara atomisk jämför-och-ersätt när du flyttar en Swift-modell till en aktör ska du hålla jämförelsen och tilldelningen i en och samma synkrona, aktörsisolerade metod. Anropare använder await, men metoden får inte suspendera mellan kontrollen och ersättningen.
Den här genomgången riktar sig till en Mac-utvecklare som flyttar en liten skrivbordsmodell som tidigare skyddades av ett lås. Den konkreta uppgiften är att ersätta en syntetisk formetikett endast om dess aktuella värde fortfarande motsvarar ett förväntat värde. Exemplet körs helt i minnet och använder inga nätverksanrop, externa poster eller beständig lagring.
Definiera värdekontraktet innan du ändrar modellen
I det här exemplet har compareAndReplace(expected:replacement:) tre regler: jämför den aktuella etiketten med expected; vid likhet tilldelar du replacement och returnerar true; annars returnerar du false utan att ändra etiketten. Jämförelsen använder Swifts värdelikhet. Standardbibliotekets Equatable-dokumentation definierar den jämförelsen med == och skiljer värdelikhet från identitet mellan klassinstanser.
Jämförelsen måste använda det värde som är aktuellt när operationen körs. En tidigare ögonblicksbild kan tillhandahålla det förväntade värdet, men den kan inte fastställa att ersättningen ska lyckas senare. Det beslutet hör hemma i operationen som äger ändringen.
| Aktuellt värde | Förväntat värde | Ersättning | Resultat | Värde efteråt |
|---|---|---|---|---|
"Circle" |
"Circle" |
"Square" |
true |
"Square" |
"Square" |
"Circle" |
"Triangle" |
false |
"Square" |
"Square" |
"Square" |
"Square" |
true |
"Square" |
Den sista raden är avsiktlig: framgång innebär att jämförelsen stämde och att tilldelningen godkändes. Det innebär inte att det synliga värdet ändrades. Om du ändrar den returkonventionen under migreringen ändras kontraktet.
I en låsbaserad implementation måste hela jämförelsen och tilldelningen ske inom samma skyddade kritiska sektion. Apple beskriver Foundations NSLock som ett sätt att skydda en kritisk sektion så att den körs atomiskt. Vid migreringen till en aktör ska du bevara den operationsgränsen: flytta hela beslutet in i aktören i stället för att fördela dess delar mellan anroparna.
Här betyder ”atomisk” att en annan operation på den här aktören inte kan infogas mellan jämförelsen och tilldelningen. Exemplet gör inga anspråk på maskinvarans atomära instruktioner eller låsfri körning.
Varför anroparens await inte avbryter operationen
En synkron aktörsmetod kan kräva await när den anropas utanför sin isoleringsdomän. Anroparen kan behöva suspendera medan den väntar på att få komma in i aktören. När metoden väl körs har dess vanliga synkrona kropp ingen suspenderingspunkt där en annan uppgift kan gå in och ändra aktörens tillstånd. Swift beskriver den här skillnaden i SE-0306: Actors.
Språkhandboken rekommenderar att inkapsla uppdateringar i synkrona aktörsmetoder, eftersom dessa metoder inte kan innehålla möjliga suspenderingspunkter. I Apples aktörssession förklaras också att synkron aktörskod slutförs utan att en annan uppgift avbryter dess aktörsisolerade körning. Det är de garantier som den här implementationen bygger på. Se Concurrency och Protect mutable state with Swift actors.
Den användbara gränsen är alltså ett enda aktörsanrop som innehåller både villkoret och den resulterande ändringen. En anropare kan först läsa en ögonblicksbild, utföra en lokal beräkning och sedan skicka in en villkorad ersättning. Aktören måste kontrollera villkoret igen när den behandlar begäran.
Vad går fel med separata läsningar och skrivningar?
Anta att en migrering exponerar separata metoder för ögonblicksbilder och ovillkorlig ersättning. En anropare läser "Circle". En annan anropare ändrar etiketten till "Square". Den första anroparen skriver sedan "Triangle" baserat på sin tidigare läsning. Varje enskild metod kan följa aktörsisolationen, men den övergripande sekvensen skulle bryta mot det avsedda jämför-och-ersätt-kontraktet.
Det här illustrativa schemat förklarar felet utan att förutsätta någon särskild schemaläggare för uppgifter. Aktörsisolation skyddar varje operation; programmeraren behöver fortfarande välja en tillräckligt omfattande operation för att bevara programmets villkor.
Skapa ett komplett lokalt exempel på din Mac
Använd en installerad verktygskedja för Swift 6. Swifts officiella installationsguide för macOS beskriver installation via Swiftly och utveckling med Xcode. Kontrollera vilken kompilator som är vald i Terminal innan du fortsätter:
swiftc --version
Spara följande kompletta program som CompareReplace.swift i en arbetsmapp. Det använder endast Swifts standardbibliotek, som är tillgängligt utan en uttrycklig import. Filen definierar aktören, dess initiala tillstånd, alla exempelindata och en asynkron startpunkt.
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.")
}
}
Kompilera och kör programmet från den mappen. Alternativet -parse-as-library hanterar den uttryckliga startpunkten @main, medan -swift-version 6 väljer språkläget Swift 6.
swiftc -swift-version 6 -parse-as-library CompareReplace.swift -o compare-demo
./compare-demo
Förväntad utdata, härledd från programmets kontroller:
Matching replacement: true
Stale replacement: false
Equal replacement: true
Exactly one competing replacement succeeded.
A restored value matches the old snapshot.
Koden har granskats mot dokumentationen, men kördes inte här eftersom den här miljön saknar en Swift-kompilator. De inkluderade förvillkoren ger lokala kontroller när du kör programmet; de innebär inte att körningen redan har genomförts.
Vad de konkurrerande anropen visar
De två deklarationerna async let skapar underordnade uppgifter utan att invänta det första resultatet innan den andra startas. Den användningen av strukturerad samtidighet följer språkhandbokens förklaring av samtidiga asynkrona anrop. Båda anroparna förväntar sig "Circle", och var och en föreslår en annan ersättning.
I just den här situationen lyckas exakt ett anrop: det som körs först ändrar värdet så att det inte längre är "Circle"; det andra misslyckas då med sin jämförelse. Testet tillåter vilken som helst av dem att vinna. Swift garanterar inte att uppgifter som väntar på en aktör körs i den ordning de ursprungligen inväntade den, vilket dokumenteras i SE-0306.
Exemplet skiljer också mellan inväntade anrop och uttryck med precondition. Först lagras varje resultat i en lokal konstant, som sedan kontrolleras. På så sätt hålls det asynkrona arbetet utanför de synkrona assertionsuttrycken.
Håll asynkron förberedelse utanför den slutliga jämförelsen
En framtida version kanske beräknar en föreslagen etikett asynkront med hjälp av annat syntetiskt lokalt arbete. Slutför förberedelsen innan du anropar den synkrona jämför-och-ersätt-metoden. Skicka det ursprungliga förväntade värdet och den beräknade ersättningen tillsammans; hantera false som ett avvisat försök.
En aktörsmetod som kontrollerar likhet, inväntar förberedelsen och sedan tilldelar ovillkorligt har en lucka. Under suspenderingen kan en annan uppgift ändra aktörens tillstånd. När metoden återupptas kan den förlita sig på ett villkor som kanske inte längre gäller. Swifts dokumenterade modell för återinträde i aktörer tillåter uttryckligen den här typen av sammanflätning.
Om förberedelsen sker i en asynkron aktörsmetod ska du anropa den synkrona jämförelsemetoden när förberedelsen är klar. Den slutliga metoden måste göra en ny jämförelse. En kontroll före suspendering kan hjälpa till att undvika onödigt arbete, men den kan inte ge tillstånd till den slutliga tilldelningen.
Vid en avvikelse kan du behålla det föreslagna resultatet om skrivbordsgränssnittet behöver visa det, hämta en ny ögonblicksbild och välja om du vill räkna om eller rapportera konflikten. Att automatiskt ersätta det förväntade värdet och försöka igen skulle ändra begäran från ”ersätt värdet jag observerade” till ”fortsätt försöka med nyare värden”. Gör det till ett uttryckligt produktbeslut.
Behåll inte det gamla Foundation-låset över en await. Apple kräver att en NSLock låses upp på samma tråd som låste den. Aktörsdesignen här uppnår det önskade resultatet genom en kort synkron, isolerad operation utan att hålla kvar ett lås under asynkront arbete.
Bevara likhetssemantiken och förstå dess begränsningar
Kontrollen av en förändringscykel visar en subtil gräns. En anropare tar en ögonblicksbild av "Circle"; modellen ändras till "Square" och sedan tillbaka till "Circle". En jämförelse med den gamla ögonblicksbilden lyckas, eftersom kontraktet jämför det aktuella värdet och inte sekvensen av ändringar som ledde fram till det.
Om det faktiska kravet är ”ersätt bara om ingen godkänd ändring har skett sedan min ögonblicksbild” räcker det inte att jämföra värden. En möjlig lösning är att returnera en ögonblicksbild som innehåller både ett värde och en revisionstoken, och sedan kontrollera token och uppdatera värdet tillsammans i en synkron aktörsmetod. Det är ett starkare och annorlunda kontrakt. Definiera hur token uppdateras och vad som händer när den tar slut innan du implementerar det; inför det inte i tysthet under en migrering som ska bevara värden.
För större modeller kan du använda ett ögonblicksbildsvärde vars likhet omfattar alla fält som är relevanta för ersättningen. Swift kan syntetisera Equatable för en struktur när alla lagrade egenskaper uppfyller protokollet, enligt referensen för Equatable. En likhetsimplementation som ignorerar ett relevant fält skulle tillåta ersättningar trots ändringar i det fältet.
Se till att värden som passerar aktörsgränsen lämpar sig för samtidig användning. Dokumentationen för Sendable förklarar att värden som uppfyller protokollet kan skickas mellan samtidighetsdomäner och att sådana strukturer måste innehålla medlemmar som också uppfyller protokollet. Att kapsla in en godtycklig muterbar referens i en struktur gör inte att den egenskapen uppfylls.
För den här lokala modellen är migreringen klar när aktören äger den ändringsbara etiketten, varje villkorad ersättning går genom den kombinerade operationen och anroparna tolkar dess booleska resultat enligt det ursprungliga kontraktet. Håll den slutliga jämförelsen och tilldelningen tillsammans och låt sedan det omgivande arbetsflödet för skrivbordet avgöra vad ett avvisat försök innebär.
Källor
- NSLock — Apple Developer Documentation.
- Equatable — Apple Developer Documentation.
- Sendable — Apple Developer Documentation.
- Concurrency — The Swift Programming Language, Swift project.
- Actors — SE-0306 — Swift Evolution, Swift project.
- Protect mutable state with Swift actors — Apple Developer, WWDC21.
- Install Swift — macOS — Swift.org.