Om du vill avbryta en timerväntare oberoende av andra, ger du varje konsument en egen AsyncStream och en egen task-hanterare. Dela Foundation-timern och skicka sedan dess värden till separata strömmar. Då kan du avsluta konsument A:s väntan medan konsument B fortsätter.
Den här handledningen bygger en liten Mac-app för skrivbordet med två fiktiva talvisningar och knappen Cancel A. Båda visningarna får värden från en timer. Den konkreta uppgiften är att stoppa A:s asynkrona konsumtion utan att stoppa B eller göra den delade timern ogiltig.
Varför kan avbrytning av en väntare påverka andra konsumenter?
AsyncSequence beskriver asynkron iteration; avbrytningsbeteendet kommer från den konkreta sekvensimplementationen. För AsyncStream anger den aktuella standardbibliotekskällkoden att en avbruten uppgift som är pausad i next() avslutar strömmen. Anropet kan returnera nil omedelbart eller vid efterföljande anrop. En ny iterator över samma ström skapar inte en oberoende prenumeration. Swift-standardbibliotekets källkod: AsyncStream.swift.
Därför är ägarskapet viktigt. Om två uppgifter konsumerar samma ström är det fel kontrakt för den här uppgiften att behandla avbrytning som en privat åtgärd på en iterator. Skapa i stället två strömmar med separata continuations och skicka uttryckligen varje genererat värde till båda. Apple beskriver AsyncStream som en brygga från återanrop till asynkron iteration, där yield(_:) används för att tillhandahålla element och finish() för att avsluta en ström. AsyncStream.
Avbrytning av uppgifter är också kooperativ: ett anrop till cancel() markerar uppgiften och kör dess avbrytningshanterare, men avbryter inte godtycklig kod. Konsumenten bör kontrollera avbrytning innan den tillämpar ett mottaget värde. Avbrytning fortplantas också till underordnade uppgifter, så att avbryta en gemensam överordnad uppgift kan få bredare effekt än att avbryta den avsedda konsumentens hanterare. Task.cancel().
Använd följande ägarskapsregler för den här appen:
| Åtgärd | Avsedd effekt |
|---|---|
| Avbryt A:s uppgift | Avsluta A:s konsumtion och ta bort A:s prenumeration |
| Avsluta A:s continuation | Avsluta A:s ström normalt |
| Avbryt B:s uppgift | Avsluta B:s konsumtion oberoende av A |
| Stoppa timerhubben | Ogiltigförklara timern och avsluta alla återstående strömmar |
Vilka värden ska timerkonsumenterna få?
Exemplet använder ett avsiktligt enkelt värdekontrakt. Varje behandlat timeråteranrop ökar ett heltal från 1 till 1 000 och börjar sedan om på 1. Varje visning visar heltalet och dess kvadrat. Till exempel ger det illustrativa värdet 7 n=7, square=49. Gränsen håller beräkningen liten genom upprepade cykler.
Varje prenumeration buffrar bara det senaste väntande värdet. Det passar en visning vars uppgift är att visa det senaste syntetiska tillståndet: en långsam visning kan hoppa över mellanliggande värden. Det passar inte när varje genererad händelse måste behandlas. Apple dokumenterar både begränsad buffring och den obegränsade standardbufferten för AsyncStream. Buffring i AsyncStream.
Räknaren mäter behandlade återanrop, inte förflutna sekunder. Foundation-timers är beroende av sin run loop, kan utlösas sent och spelar inte upp varje missad utlösning. Intervallen på två sekunder nedan gör avbrytningsförloppet lätt att observera; det ger ingen exakt klocka. Timer.
Hur bygger du oberoende prenumerationer?
Använd ett macOS SwiftUI-appmål i Xcode med en Swift 6-verktygskedja och ett distributionsmål för macOS 14 eller senare. Lägg till följande tre filer och ersätt appens mallstartpunkt och vy. Tillsammans utgör de ett komplett exempel; de kräver inga externa paket, nätverksåtkomst eller lagrade data. Koden har granskats men har inte kompilerats i den här forskningsmiljön.
Skapa den delade timerhubben
Hubben äger timern och en ordbok med prenumerationernas continuations. Varje anrop till subscribe() konstruerar en ny ström. Dess termineringsåteranrop tar bara bort den matchande identifieraren. Apple dokumenterar att avbrytning av aktiv strömiteration anropar onTermination och avslutar iterationen med nil. AsyncStream.Continuation.onTermination.
Lägg den här koden i TickHub.swift:
import Foundation
@MainActor
final class TickHub {
private var timer: Timer?
private var number = 0
private var continuations:
[UUID: AsyncStream<Int>.Continuation] = [:]
func subscribe() -> (id: UUID, stream: AsyncStream<Int>) {
let id = UUID()
let pair = AsyncStream<Int>.makeStream(
bufferingPolicy: .bufferingNewest(1)
)
pair.continuation.onTermination = { [weak self] _ in
Task { @MainActor [weak self] in
self?.unsubscribe(id)
}
}
continuations[id] = pair.continuation
return (id, pair.stream)
}
func start() {
guard timer == nil else { return }
let newTimer = Timer(
timeInterval: 2,
repeats: true
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.emit()
}
}
newTimer.tolerance = 0.2
timer = newTimer
RunLoop.main.add(newTimer, forMode: .common)
}
private func emit() {
guard timer != nil else { return }
number = number == 1_000 ? 1 : number + 1
for continuation in continuations.values {
_ = continuation.yield(number)
}
}
func unsubscribe(_ id: UUID) {
let continuation = continuations.removeValue(forKey: id)
continuation?.finish()
}
func stop() {
timer?.invalidate()
timer = nil
let remaining = Array(continuations.values)
continuations.removeAll()
for continuation in remaining {
continuation.finish()
}
}
}
Timerinitieraren skapar en schemalagd timer, så hubben lägger uttryckligen till den i huvudtrådens run loop. Återanropet går in i en main-actor-uppgift innan det hanterar hubbens tillstånd. Kontrollen timer != nil förhindrar att ett återanrop som redan har köats skickar ut värden efter avstängning. Timer.init(timeInterval:repeats:block:) och RunLoop.add(_:forMode:).
Allt tillstånd i hubben isoleras till MainActor, vars executor motsvarar huvudtrådens dispatch-kö. Termineringsåteranropet schemalägger sin rensning där i stället för att komma åt ordboken direkt från återanropets exekveringskontext. MainActor.
Ge varje konsument en separat task-hanterare
Lägg följande kod i TimerDemoModel.swift. De två anropen till subscribe() är viktiga: de skapar oberoende strömlivscykler. Blocken defer avslutar också prenumerationen när en konsument avslutas, både vid ett uttryckligt avslut ur loopen och vid avbrytning.
import Combine
@MainActor
final class TimerDemoModel: ObservableObject {
@Published private(set) var aText = "Waiting"
@Published private(set) var bText = "Waiting"
@Published private(set) var aCancelled = false
@Published private(set) var stopped = false
private let hub = TickHub()
private var aTask: Task<Void, Never>?
private var bTask: Task<Void, Never>?
private var started = false
func start() {
guard !started else { return }
started = true
let a = hub.subscribe()
let b = hub.subscribe()
let source = hub
aTask = Task { @MainActor [weak self] in
defer { source.unsubscribe(a.id) }
for await number in a.stream {
guard !Task.isCancelled else { break }
self?.aText = "n=\(number), square=\(number * number)"
}
}
bTask = Task { @MainActor [weak self] in
defer { source.unsubscribe(b.id) }
for await number in b.stream {
guard !Task.isCancelled else { break }
self?.bText = "n=\(number), square=\(number * number)"
}
}
hub.start()
}
func cancelA() {
guard !aCancelled, !stopped else { return }
aCancelled = true
aTask?.cancel()
}
func stopAll() {
guard !stopped else { return }
stopped = true
aTask?.cancel()
bTask?.cancel()
hub.stop()
aTask = nil
bTask = nil
}
}
Lägg märke till att cancelA() utför exakt en avbrytningsåtgärd. Den anropar inte hub.stop() och rör inte B:s uppgift. Loopen kontrollerar Task.isCancelled efter att ha tagit emot ett tal och innan visningen ändras, i enlighet med Swifts dokumenterade modell för kooperativ avbrytning. The Swift Programming Language — Concurrency.
Båda rensningsvägarna kan försöka ta bort A: termineringsåteranropet och uppgiftens defer. Det är avsiktligt här. unsubscribe(_:) tar först bort posten och avslutar sedan den returnerade continuationen. Ett senare borttagningsförsök hittar inget och lämnar övriga prenumerationer orörda.
Lägg till skrivbordskontrollerna
Lägg den här koden i TimerWaiterDemoApp.swift. Behåll bara en @main-appdeklaration i målet.
import SwiftUI
@main
@MainActor
struct TimerWaiterDemoApp: App {
var body: some Scene {
WindowGroup {
TimerDemoView()
}
}
}
@MainActor
struct TimerDemoView: View {
@StateObject private var model = TimerDemoModel()
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Independent timer consumers").font(.headline)
Text("A: \(model.aText)")
Text(model.aCancelled ? "A cancelled" : "A active")
Text("B: \(model.bText)")
HStack {
Button("Cancel A") { model.cancelA() }
.disabled(model.aCancelled || model.stopped)
Button("Stop all") { model.stopAll() }
.disabled(model.stopped)
}
}
.padding(24)
.frame(minWidth: 420)
.onAppear { model.start() }
.onDisappear { model.stopAll() }
}
}
@StateObject ger modellen stabil lagring under vybehållarens livstid, och publicerade ändringar uppdaterar beroende textvyer. Åtgärden vid försvinnande rensar upp på vynivå; dess exakta tidpunkt beror på vyn. Den uttryckliga knappen Stop all ger också en direkt avstängningsåtgärd. StateObject och onDisappear(perform:).
Hur verifierar du att avbrytningen stannar lokal?
Kör appen och följ den här godkännandesekvensen. Talen nedan är illustrativa observationer att hålla utkik efter, inte registrerade testresultat.
- Vänta tills båda visningarna uppdateras. Normalt ska de visa samma senaste tal och dess kvadrat.
- Klicka på
Cancel Amellan timerutlösningarna. A:s status ändras tillA cancelled, och värdet som visas för A förblir oförändrat. - Vänta på minst två ytterligare timerutlösningar. B ska fortsätta uppdateras medan A förblir oförändrad.
- Klicka på
Stop all. Då ska även B sluta uppdateras. - Starta om appen och avbryt A omedelbart, innan det första värdet kommer. A ska stå kvar på
Waiting, medan B börjar ta emot tal.
Den här sekvensen kontrollerar avbrytning både efter att konsumtionen har börjat och innan det första användbara värdet kommer. Avbrytningsåtgärden på main actor och visningsuppdateringarna serialiseras i den här utformningen. En uppdatering som slutfördes före knappåtgärden förblir synlig; avbrytningskontrollen hindrar en senare återupptagen iteration för A från att tillämpa ytterligare ett värde.
Om beteendet skiljer sig åt bör du undersöka ägarskapsgränsen innan du ändrar timerintervallet:
| Symtom | Första saken att kontrollera |
|---|---|
| B stannar när A avbryts | Leta efter en delad ström eller rensning som anropar hub.stop() |
| Visningarna får olika delmängder av värdena | Kontrollera att varje konsument får ett separat resultat från subscribe() |
| Inga värden visas | Kontrollera att timern startades och lades till i appens körande huvudtråds-run loop |
| Värden hoppas ibland över | Kontrollera om buffringspolicyn för senaste värdet passar visningskravet |
| Arbetet fortsätter efter att vyn försvunnit | Kontrollera att avstängningen når båda task-hanterarna och hubben |
Vad ändras för en väntare som bara körs en gång eller vid omstart?
För en väntare som bara körs en gång håller du dess prenumeration privat, behandlar det första godtagbara värdet och avslutar loopen. Behåll avprenumerationsåtgärden defer: den gör den avsedda prenumerationslivstiden tydlig även när loopen avslutas via break. En normal engångsavslutning bör ta bort den prenumerationen och lämna den delade producenten tillgänglig för B.
Om du vill starta om A skapar du en ny prenumeration och en ny uppgift när den gamla uppgiften har slutförts. Försök inte återställa den avbrutna uppgiften eller återanvända dess avslutade ström. I ett gränssnitt som kan startas om väntar du på att den gamla uppgiften ska bli klar innan du installerar en ersättare, eller skiljer på annat sätt mellan successiva prenumerationslivscykler så att gammal rensning inte kan ta bort nytt tillstånd.
Exemplet erbjuder avsiktligt en körning per vyinstans. Global avstängning ogiltigförklarar dess upprepande timer på main actor, där den installerades. Apple dokumenterar att ogiltigförklarade timers inte kan återanvändas; en omstart måste skapa en ny timer. Timerlivscykel.
Oberoende avbrytning följer direkt av oberoende prenumerationsägarskap. Behåll timern i hubben, ge varje konsument en separat ström och task-hanterare, och ta bara bort den konsumentens prenumeration när den avslutas. Då kan en kontroll på skrivbordet stoppa en väntande visning medan den andra fortsätter att ta emot samma syntetiska beräkning.
Källor
- AsyncStream.swift — Swift Project.
- AsyncStream — Apple.
- onTermination — Apple.
- cancel() — Apple.
- Concurrency — Swift Project.
- Timer — Apple.
- init(timeInterval:repeats:block:) — Apple.
- add(_:forMode:) — Apple.
- MainActor — Apple.
- StateObject — Apple.
- onDisappear(perform:) — Apple.