Behoud atomair vergelijken-en-vervangen wanneer je een Swift-model naar een actor overzet door de vergelijking en toewijzing in één synchrone, actor-geïsoleerde methode te houden. Aanroepers gebruiken await, maar de methode zelf mag niet worden onderbroken tussen het controleren en vervangen.

Deze uitleg is bedoeld voor een Mac-ontwikkelaar die een klein desktopmodel migreert dat eerder met een lock werd beschermd. De concrete taak is een synthetisch vormlabel alleen te vervangen als de huidige waarde nog overeenkomt met een verwachte waarde. Het voorbeeld werkt volledig in het geheugen en gebruikt geen netwerkverzoeken, externe gegevens of persistentie.

Definieer het waardesemantiek voordat je het model wijzigt

In dit voorbeeld heeft compareAndReplace(expected:replacement:) drie regels: vergelijk het huidige label met expected; wijs bij gelijkheid replacement toe en retourneer true; retourneer anders false zonder het label te wijzigen. De vergelijking gebruikt waarde-gelijkheid in Swift. De standaardbibliotheekdocumentatie voor Equatable definieert die vergelijking aan de hand van == en maakt onderscheid tussen waarde-gelijkheid en de identiteit van een klasse-instantie.

De vergelijking moet de waarde gebruiken die actueel is wanneer de bewerking wordt uitgevoerd. Een eerdere momentopname kan de verwachte waarde leveren, maar kan niet bepalen of de vervanging later moet slagen. Die beslissing hoort bij de bewerking die de mutatie beheert.

Huidige waarde Verwachte waarde Vervanging Resultaat Waarde achteraf
"Circle" "Circle" "Square" true "Square"
"Square" "Circle" "Triangle" false "Square"
"Square" "Square" "Square" true "Square"

De laatste rij is opzettelijk: succes betekent dat de vergelijking overeenkwam en de toewijzing is geaccepteerd. Het betekent niet dat de zichtbare waarde is veranderd. Die retourconventie wijzigen tijdens de migratie zou het contract veranderen.

In een implementatie met een lock moeten de volledige vergelijking en toewijzing in dezelfde beschermde kritieke sectie plaatsvinden. Apple beschrijft Foundation’s NSLock als een manier om een kritieke sectie te beschermen zodat die atomair wordt uitgevoerd. Behoud bij de migratie naar actors die bewerkingsgrens: verplaats de volledige beslissing naar de actor in plaats van de onderdelen ervan over aanroepers te verdelen.

Hier betekent ‘atomair’ dat een andere bewerking op deze actor niet kan tussenkomen tussen de vergelijking en de toewijzing. Dit voorbeeld doet geen uitspraken over hardwarematige atomaire instructies of lock-vrije uitvoering.

Waarom de await van de aanroeper de bewerking niet onderbreekt

Voor een synchrone actormethode kan await nodig zijn wanneer die vanuit buiten het isolatiedomein wordt aangeroepen. De aanroeper kan worden onderbroken terwijl die wacht tot hij de actor kan betreden. Zodra de methode wordt uitgevoerd, bevat de gewone synchrone body geen suspensiepunt waarop een andere taak kan binnenkomen en de toestand van dezelfde actor kan wijzigen. Swift documenteert dit onderscheid in SE-0306: Actors.

De taalgids raadt aan updates in synchrone actormethoden in te kapselen, omdat die methoden geen mogelijke suspensiepunten kunnen bevatten. Apples sessie over actors legt eveneens uit dat synchrone actorcode wordt voltooid zonder dat een andere taak de actor-geïsoleerde uitvoering ervan onderbreekt. Deze garanties vormen de basis voor deze implementatie. Zie Concurrency en Bescherm veranderlijke toestand met Swift-actors.

De nuttige grens is dus één actoraanroep die zowel de voorwaarde als de daaruit volgende mutatie bevat. Een aanroeper kan eerst een momentopname lezen, lokaal een berekening uitvoeren en daarna een voorwaardelijke vervanging indienen. De actor moet de voorwaarde opnieuw controleren wanneer die de aanvraag verwerkt.

Wat gaat er mis met afzonderlijk lezen en schrijven?

Stel dat een migratie afzonderlijke methoden voor momentopnamen en onvoorwaardelijke vervanging beschikbaar maakt. Eén aanroeper leest "Circle". Een andere aanroeper wijzigt het label in "Square". Vervolgens schrijft de eerste aanroeper "Triangle" op basis van de eerdere lezing. Elke afzonderlijke methode kan de actorisolatie respecteren, maar de volledige reeks zou het beoogde contract voor vergelijken-en-vervangen schenden.

Dit illustratieve tijdschema verklaart het defect zonder afhankelijk te zijn van een specifieke taakscheduler. Actorisolatie beschermt elke bewerking; de programmeur moet nog steeds een bewerking kiezen die groot genoeg is om de voorwaarde van de toepassing te behouden.

Bouw een compleet lokaal voorbeeld op je Mac

Gebruik een geïnstalleerde Swift 6-toolchain. Swift’s officiële installatiegids voor macOS beschrijft installatie via Swiftly en ontwikkeling met Xcode. Controleer in Terminal welke compiler is geselecteerd voordat je verdergaat:

swiftc --version

Sla het volgende volledige programma in een werkmap op als CompareReplace.swift. Het gebruikt alleen de Swift-standaardbibliotheek, die zonder expliciete import beschikbaar is. Het bestand definieert de actor, de beginstatus, alle voorbeeldinvoer en een asynchroon toegangspunt.

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.")
    }
}

Compileer en voer het uit vanuit die map. De optie -parse-as-library ondersteunt het expliciete toegangspunt @main, terwijl -swift-version 6 de Swift 6-taalmodus selecteert.

swiftc -swift-version 6 -parse-as-library CompareReplace.swift -o compare-demo
./compare-demo

De verwachte uitvoer, afgeleid van de controles in het programma, is:

Matching replacement: true
Stale replacement: false
Equal replacement: true
Exactly one competing replacement succeeded.
A restored value matches the old snapshot.

De code is gecontroleerd aan de hand van de documentatie, maar is hier niet uitgevoerd omdat deze omgeving geen Swift-compiler heeft. De opgenomen preconditions bieden lokale controles wanneer je het programma uitvoert; ze betekenen niet dat het al is uitgevoerd.

Wat de concurrerende aanroepen aantonen

De twee declaraties met async let maken onderliggende taken aan zonder op het eerste resultaat te wachten voordat de tweede start. Dit gebruik van gestructureerde concurrency volgt de uitleg van gelijktijdige asynchrone aanroepen in de taalgids. Beide aanroepers verwachten "Circle" en stellen elk een andere vervanging voor.

In deze specifieke opstelling slaagt precies één aanroep: degene die als eerste wordt uitgevoerd, verandert de waarde zodat die niet langer "Circle" is; de andere faalt vervolgens bij de vergelijking. De test accepteert beide mogelijke winnaars. Swift garandeert niet dat taken die op een actor wachten worden uitgevoerd in de volgorde waarin ze oorspronkelijk erop wachtten, zoals gedocumenteerd in SE-0306.

Het voorbeeld maakt ook onderscheid tussen afgewachte aanroepen en expressies met precondition. Het slaat elk resultaat eerst op in een lokale constante en controleert daarna die constante. Zo blijft asynchroon werk buiten synchrone assertie-expressies.

Houd asynchrone voorbereiding buiten de uiteindelijke vergelijking

Een toekomstige versie kan een voorgesteld label asynchroon berekenen met ander synthetisch lokaal werk. Rond die voorbereiding af voordat je de synchrone methode voor vergelijken-en-vervangen aanroept. Geef de oorspronkelijke verwachte waarde en de berekende vervanging samen door; behandel false als een afgewezen poging.

Een actormethode die de gelijkheid controleert, de voorbereiding afwacht en daarna onvoorwaardelijk toewijst, bevat een onderbreking. Tijdens die suspensie kan een andere taak de toestand van de actor wijzigen. De hervatte methode zou vertrouwen op een voorwaarde die mogelijk niet meer geldt. Het gedocumenteerde re-entrancymodel van actors van Swift staat deze tussenkomst uitdrukkelijk toe.

Als de voorbereiding in een asynchrone actormethode plaatsvindt, roep je de synchrone vergelijkingsmethode aan nadat de voorbereiding is afgerond. Die uiteindelijke methode moet opnieuw vergelijken. Een controle vóór de suspensie kan onnodig werk helpen voorkomen, maar kan de latere toewijzing niet autoriseren.

Bewaar bij een mismatch het voorgestelde resultaat als de desktopinterface het moet tonen, haal een nieuwe momentopname op en bepaal of je opnieuw wilt berekenen of het conflict wilt melden. De verwachte waarde automatisch vervangen en opnieuw proberen zou de aanvraag veranderen van ‘vervang de waarde die ik heb waargenomen’ in ‘blijf het proberen met nieuwere waarden’. Maak van dat gedrag een expliciete productbeslissing.

Houd de oude Foundation-lock niet vast over een await. Apple vereist dat een NSLock wordt ontgrendeld op dezelfde thread waarop die is vergrendeld. Het actorontwerp hier bereikt het vereiste resultaat via een korte, synchrone, geïsoleerde bewerking, zonder een lock vast te houden tijdens asynchroon werk.

Behoud de gelijkheidssemantiek en ken de grenzen ervan

De cycluscontrole toont een subtiele grens. Een aanroeper maakt een momentopname van "Circle"; het model verandert in "Square" en daarna terug in "Circle". Een vergelijking met de oude momentopname slaagt omdat het contract de huidige waarde vergelijkt, niet de reeks wijzigingen die daartoe heeft geleid.

Als je werkelijke vereiste is ‘vervang alleen als er sinds mijn momentopname geen geaccepteerde mutatie heeft plaatsgevonden’, volstaat waardevergelijking niet. Een ondersteund ontwerp geeft een momentopname terug met zowel een waarde als een revisietoken en controleert vervolgens het token en werkt de waarde bij binnen één synchrone actormethode. Dat is een sterker, ander contract. Bepaal hoe het token wordt opgehoogd en wat er gebeurt bij uitputting voordat je dit implementeert; introduceer dit niet stilzwijgend tijdens een migratie die waarden moet behouden.

Gebruik voor grotere modellen een momentopnamewaarde waarvan de gelijkheid alle velden omvat die relevant zijn voor vervanging. Volgens de Equatable-referentie kan Swift Equatable voor een structuur automatisch genereren als alle opgeslagen eigenschappen eraan voldoen. Een gelijkheidsimplementatie die een relevant veld negeert, zou vervangingen toestaan ondanks wijzigingen in dat veld.

Houd waarden die de actorgrens passeren geschikt voor gelijktijdig gebruik. De documentatie voor Sendable legt uit dat sendable waarden tussen concurrencydomeinen kunnen worden doorgegeven en dat sendable structuren sendable leden moeten bevatten. Een willekeurige veranderlijke referentie in een structuur wikkelen stelt die eigenschap niet vast.

Voor dit lokale model is de migratie voltooid wanneer de actor het veranderlijke label beheert, elke voorwaardelijke vervanging via de gecombineerde bewerking verloopt en aanroepers het Boolean-resultaat volgens het oorspronkelijke contract interpreteren. Houd de uiteindelijke vergelijking en toewijzing bij elkaar en laat de omliggende desktopworkflow bepalen wat een afgewezen poging betekent.

Bronnen