Tek bir zamanlayıcı bekleyicisini bağımsız olarak iptal etmek için her tüketiciye kendi AsyncStream’ini ve görev tanıtıcısını verin. Foundation zamanlayıcısını paylaşın, ardından değerlerini ayrı akışlara dağıtın. Böylece A tüketicisini iptal etmek, B tüketicisi devam ederken A’nın bekleyişini sonlandırabilir.

Bu eğitimde iki sentetik sayı göstergesi ve bir Cancel A düğmesi olan küçük bir Mac masaüstü uygulaması oluşturuluyor. Her iki gösterge de değerleri tek bir zamanlayıcıdan alıyor. Somut görev, paylaşılan zamanlayıcıyı durdurmadan veya geçersiz kılmadan A’nın eşzamansız tüketimini durdurmaktır.

Bir bekleyiciyi iptal etmek neden diğer tüketicileri etkileyebilir?

AsyncSequence eşzamansız yinelemeyi tanımlar; iptal davranışı ise somut dizi uygulamasından kaynaklanır. AsyncStream için mevcut standart kitaplık kaynağı, next() içinde askıya alınmış bir görevin iptal edilmesinin akışı sonlandırdığını belgeliyor. Çağrı hemen veya sonraki çağrılarda nil döndürebilir. Aynı akış üzerinde yeni bir yineleyici oluşturmak bağımsız bir abonelik oluşturmaz. Swift standart kitaplık kaynağı: AsyncStream.swift.

Bu nedenle sahiplik önemlidir. İki görev aynı akışı tüketiyorsa iptali tek bir yineleyiciye özel bir işlem gibi ele almak bu görev için yanlış sözleşmedir. Bunun yerine, ayrı devam nesnelerine sahip iki akış oluşturun ve oluşturulan her değeri açıkça ikisine de gönderin. Apple, AsyncStream’i geri çağrılardan eşzamansız yinelemeye köprü olarak belgeliyor; öğeleri sağlamak için yield(_:), akışı bitirmek içinse finish() kullanılıyor. AsyncStream.

Görev iptali de işbirlikçidir: cancel() çağrısı görevi iptal edildi olarak işaretler ve iptal işleyicilerini çalıştırır, ancak rastgele bir kodu kesintiye uğratmaz. Tüketici, aldığı bir değeri uygulamadan önce iptal durumunu denetlemelidir. İptal alt görevlere de yayılır; bu nedenle ortak bir üst görevi iptal etmek, hedeflenen tüketici tanıtıcısını iptal etmekten daha geniş etki yaratabilir. Task.cancel().

Bu uygulama için aşağıdaki sahiplik kurallarını kullanın:

İşlem Amaçlanan etki
A’nın görevini iptal et A’nın tüketimini sonlandır ve A’nın aboneliğini kaldır
A’nın devam nesnesini bitir A’nın akışını normal biçimde sonlandır
B’nin görevini iptal et B’nin tüketimini bağımsız olarak sonlandır
Zamanlayıcı merkezini durdur Zamanlayıcıyı geçersiz kıl ve kalan tüm akışları bitir

Zamanlayıcı tüketicileri hangi değerleri almalı?

Örnekte bilerek küçük bir değer sözleşmesi kullanılıyor. İşlenen her zamanlayıcı geri çağrısında bir tam sayı 1’den 1.000’e ilerler ve ardından tekrar 1’e döner. Her gösterge bu tam sayıyı ve karesini gösterir. Örneğin, 7’lik örnek bir değer n=7, square=49 üretir. Sınır, yinelenen döngüler boyunca hesabın küçük kalmasını sağlar.

Her abonelik yalnızca bekleyen en yeni değeri arabelleğe alır. Bu, görevi en son sentetik durumu göstermek olan bir ekran için uygundur: yavaş kalan bir ekran ara değerleri atlayabilir. Üretilen her olayın işlenmesi gerekiyorsa uygun değildir. Apple, AsyncStream için hem sınırlı arabelleğe almayı hem de varsayılan sınırsız arabelleği belgeliyor. AsyncStream arabelleğe alma.

Sayaç geçen saniyeleri değil, işlenen geri çağrıları sayar. Foundation zamanlayıcıları çalışma döngülerine bağlıdır, geç çalışabilir ve kaçırılan her tetiklemeyi yeniden oynatmaz. Aşağıdaki iki saniyelik aralık, iptal etkileşimini gözlemlemeyi kolaylaştırır; kesin bir saat sağlamaz. Timer.

Bağımsız abonelikleri nasıl oluşturursunuz?

Xcode’da Swift 6 araç zinciri ve macOS 14 veya daha yeni bir dağıtım hedefi olan bir macOS SwiftUI uygulama hedefi kullanın. Şablonun uygulama giriş noktasını ve görünümünü değiştirerek aşağıdaki üç dosyayı ekleyin. Birlikte eksiksiz bir örnek oluştururlar; harici paket, ağ erişimi veya kayıtlı veri gerektirmezler. Kod incelendi ancak bu araştırma ortamında derlenmedi.

Paylaşılan zamanlayıcı merkezini oluşturun

Merkez, zamanlayıcıya ve abonelik devam nesnelerinden oluşan bir sözlüğe sahiptir. subscribe() öğesine yapılan her çağrı yeni bir akış oluşturur. Akışın sonlandırma geri çağrısı yalnızca eşleşen tanımlayıcıyı kaldırır. Apple, etkin akış yinelemesinin iptal edilmesinin onTermination öğesini çağırdığını ve yinelemeyi nil ile bitirdiğini belgeliyor. AsyncStream.Continuation.onTermination.

Bu kodu TickHub.swift dosyasına koyun:

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()
        }
    }
}

Zamanlayıcı başlatıcısı planlanmamış bir zamanlayıcı oluşturur; bu nedenle merkez onu ana çalışma döngüsüne açıkça ekler. Geri çağrı, merkez durumuna erişmeden önce ana aktörde çalışan bir göreve girer. timer != nil denetimi, sıraya alınmış bir geri çağrının kapatmadan sonra değer göndermesini önler. Timer.init(timeInterval:repeats:block:) ve RunLoop.add(_:forMode:).

Tüm merkez durumu MainActor üzerinde yalıtılmıştır; bu aktörün yürütücüsü ana gönderim kuyruğuna denktir. Sonlandırma geri çağrısı, sözlüğe doğrudan kendi yürütme bağlamından erişmek yerine temizleme işlemini orada planlar. MainActor.

Her tüketiciye ayrı bir görev tanıtıcısı verin

Aşağıdaki kodu TimerDemoModel.swift dosyasına koyun. subscribe() öğesine yapılan iki çağrı çok önemlidir: bağımsız akış yaşam süreleri oluştururlar. defer blokları, hem açık döngü çıkışında hem de iptalde tüketici çıktığında abonelikten çıkılmasını da sağlar.

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
    }
}

cancelA() öğesinin yalnızca tek bir iptal işlemi gerçekleştirdiğine dikkat edin. hub.stop() çağrısı yapmaz ve B’nin görevine dokunmaz. Döngü, Swift’in belgelenmiş işbirlikçi iptal modelini izleyerek bir sayı aldıktan sonra ve göstergeyi değiştirmeden önce Task.isCancelled denetimi yapar. Swift Programlama Dili — Eşzamanlılık.

İki temizleme yolu da A’yı kaldırmayı deneyebilir: sonlandırma geri çağrısı ve görevin defer işlemi. Bu burada kasıtlıdır. unsubscribe(_:) önce girdiyi kaldırır, ardından döndürülen devam nesnesini bitirir. Daha sonraki bir kaldırma işlemi hiçbir şey bulmaz ve diğer aboneliklere dokunulmaz.

Masaüstü denetimlerini ekleyin

Bu kodu TimerWaiterDemoApp.swift dosyasına koyun. Hedefte yalnızca bir @main uygulama bildirimi bulunsun.

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, modelin görünüm kapsayıcısının yaşam süresi boyunca kararlı depolamaya sahip olmasını sağlar; yayımlanan değişiklikler de bağımlı metin görünümlerini günceller. Görünümün kaybolma eylemi, görünüm düzeyinde temizlik sağlar; kesin zamanlaması görünüme bağlıdır. Açık Stop all düğmesi de doğrudan bir kapatma işlemi sunar. StateObject ve onDisappear(perform:).

İptalin yalnızca ilgili tüketiciyle sınırlı kaldığını nasıl doğrularsınız?

Uygulamayı çalıştırın ve aşağıdaki kabul sırasını izleyin. Aşağıdaki sayılar, aranması gereken örnek gözlemlerdir; kaydedilmiş test sonuçları değildir.

  1. Her iki göstergenin de ilerlemesini bekleyin. Normalde aynı en son sayıyı ve kareyi göstermeleri gerekir.
  2. Zamanlayıcı tetiklemeleri arasında Cancel A düğmesine tıklayın. A’nın durumu A cancelled olarak değişir ve görüntülenen değeri sabit kalır.
  3. En az iki tetikleme daha bekleyin. A sabit kalırken B ilerlemeyi sürdürmelidir.
  4. Stop all düğmesine tıklayın. B de ilerlemeyi durdurmalıdır.
  5. Uygulamayı yeniden başlatın ve ilk değeri gelmeden A’yı hemen iptal edin. B sayıları almaya başlarken A Waiting durumunda kalmalıdır.

Bu sıra, hem tüketim başladıktan sonra hem de ilk yararlı değer ulaşmadan önce iptali denetler. Bu tasarımda ana aktördeki iptal eylemi ve gösterge güncellemeleri sıraya konur. Düğme eyleminden önce tamamlanan bir güncelleme görünür kalır; iptal denetimi ise A’nın daha sonra devam eden bir yinelemesinin başka bir değer uygulamasını önler.

Davranış farklıysa zamanlayıcı aralığını değiştirmeden önce sahiplik sınırını inceleyin:

Belirti İlk incelenecek şey
A iptal edildiğinde B duruyor Paylaşılan bir akış veya hub.stop() çağıran bir temizleme işlemi olup olmadığına bakın
Göstergelere değerlerin farklı alt kümeleri geliyor Her tüketicinin ayrı bir subscribe() sonucu aldığını doğrulayın
Hiç değer görünmüyor Zamanlayıcının başlatıldığını ve uygulamanın çalışan ana çalışma döngüsüne eklendiğini doğrulayın
Değerler ara sıra atlanıyor En yeni değer arabelleğe alma politikasının gösterge gereksinimine uyup uymadığını kontrol edin
Görünüm kaybolduktan sonra işler sürüyor Kapatma işleminin her iki görev tanıtıcısına ve merkeze ulaştığını doğrulayın

Tek seferlik bir bekleyici veya yeniden başlatma için ne değişir?

Tek seferlik bir bekleyici için aboneliğini gizli tutun, kabul edilebilir ilk değeri işleyin ve döngüden çıkın. defer abonelikten çıkma işlemini koruyun: döngü break yoluyla sonlansa bile amaçlanan abonelik yaşam süresini açıkça belirtir. Normal bir tek seferlik tamamlama, paylaşılan üreticiyi B için kullanılabilir durumda bırakırken bu aboneliği kaldırmalıdır.

A’yı yeniden başlatmak için eski görev tamamlandıktan sonra yeni bir abonelik ve yeni bir görev oluşturun. İptal edilmiş görevi sıfırlamaya veya sonlandırılmış akışını yeniden kullanmaya çalışmayın. Yeniden başlatılabilir bir arayüzde, yeni bir görev yüklemeden önce eski görevin tamamlanmasını bekleyin ya da eski temizleme işleminin yeni durumu kaldırmaması için abonelik yaşam sürelerini başka şekilde ayırt edin.

Örnek, görünüm örneği başına tek bir çalıştırma sunar. Genel kapatma, yinelenen zamanlayıcısını kurulduğu ana aktörde geçersiz kılar. Apple, geçersiz kılınmış zamanlayıcıların yeniden kullanılamayacağını belgeliyor; yeniden başlatma için başka bir zamanlayıcı oluşturulmalıdır. Timer yaşam döngüsü.

Bağımsız iptal, bağımsız abonelik sahipliğinden doğrudan kaynaklanır. Zamanlayıcıyı merkezde tutun, her tüketiciye ayrı bir akış ve görev tanıtıcısı verin, işi bittiğinde yalnızca o tüketicinin aboneliğini kaldırın. Böylece bir masaüstü denetimi bekleyen göstergelerden birini durdururken diğer gösterge aynı sentetik hesaplamayı almaya devam eder.

Kaynaklar