Swift 5.5 sürümünde yerel eşzamanlılık desteğini tanıttığında, mevcut Sequence protokolü IteratorProtocol aracılığıyla zaten senkron bir yineleme modelini oluşturmuştu. Sequence protokolü, askıya alma olmaksızın hemen öğeler üreten bir makeIterator() metodunu gerektirir. Bu tasarım, Swift'in async/await paradigmasından önce vardı ve senkron tüketim beklentileri ile eşzamanlı üretim yetenekleri arasında temel bir uyumsuzluk yarattı ve bu da paralel bir hiyerarşi gerektirdi.
Temel çatışma, Sequence'in next() metodunun imzasının async anahtar kelimesini içermemesi gerçeğindendir. Eğer AsyncSequence Sequence'ı iyileştirirse, senkron öğe erişimi gereksinimini miras alır ki bu da ağ I/O veya zamanlayıcılardan veri geldiğinde karşılanamaz. Ayrıca, senkron kodun eşzamanlı işlemleri tetiklemesine izin vermek, Swift'in yapılandırılmış eşzamanlılık garantilerini ihlal eder, bu da asenkron kodun bir Task bağlamında çalışmasına olanak tanır ve çalışma zamanı boyunca hiyerarşik iptal yayılımını bozabilir.
Swift mimarları AsyncSequence'in Sequence'dan miras almadığı bağımsız bir protokol hiyerarşisi oluşturdu. AsyncIteratorProtocol, tür imzasında askıya alma noktasını açıkça işaretleyen mutating func next() async throws -> Element? tanımlar. Bu izolasyon, yinelemenin yalnızca asenkron bir bağlamda gerçekleşmesini sağlar, böylece Swift çalışma zamanı devamı yönetebilir, görev iptallerini işleyebilir ve çağrı yığınını doğru bir şekilde korurken senkron kodun yanlışlıkla askıya alma bağımlı işlemleri çağırmasını önler.
// Senkron ve asenkronu karıştırmaya çalışma (örnek başarısızlık) protocol BrokenAsyncSequence: Sequence { // Hem senkron IteratorProtocol.next() hem de asenkron gereksinimleri karşılayamaz } // Doğru asenkron tasarım struct TimedEvents: AsyncSequence { typealias Element = Date struct Iterator: AsyncIteratorProtocol { var count = 0 mutating func next() async -> Date? { guard count < 5 else { return nil } count += 1 await Task.sleep(1_000_000_000) // Askıya alma noktası return Date() } } func makeAsyncIterator() -> Iterator { Iterator() } }
Senaryo: Bir sağlık izleme uygulamasında yüksek frekanslı sensör verilerini işlemek.
Sorun tanımı: Geliştirme ekibi, düşmeleri tespit etmek için CoreMotion kullanarak 60Hz frekansında ivmeölçer verilerini akıtmak zorunda kaldı. İlk olarak, donanımı ana iş parçacığında sıkı bir while döngüsünde sorgulayan bir Sequence olarak sensör akışını modellediler. Bu yaklaşım, veri toplama sırasında UI'nın engellenmesine neden oldu ve uygulamanın sonlanma riski taşıyordu. Async sensör geri çağırmalarını veri işleme boru hatlarıyla entegre etmek için üç mimari yaklaşımı değerlendirdiler.
Çözüm 1: İş parçacığını engelleyen köprü.
Asenkron sensör API'sini bir DispatchSemaphore içine sarmayı ve özel bir Sequence yineleyicisinde senkron bekleme zorunluluğuna zorlamayı düşündüler.
Artılar: Standart Array başlatıcılarını ve map/filter algoritmalarını kullanmayı sağlar.
Eksiler: Çağrılan iş parçacığını engeller, iOS'ta izleme süresi sonlandırma riskine yol açar, CPU döngülerini kaybeder ve uyku sırasında iptal edilmesini engeller.
Çözüm 2: Geri bildirim tabanlı delegasyon. Tamamen Sequence uyumunu terk etmeyi düşünerek, her sensör güncellemesi için tamamlanma işleyicileriyle delegasyon desenleri kullanmayı değerlendirdiler. Artılar: Engelleme yapmaz, ana iş parçacığını dondurmadan asenkron donanım erişimine izin verir. Eksiler: Sequence işlemlerinin uyumunu kaybeder, dönüşümler zincirlenirken derinlemesine yerleşik "geri bildirim cehennemi" yaratır ve arka basınç uygulamasını neredeyse imkansız hale getirir.
Çözüm 3: AsyncStream ile yerel AsyncSequence.
CoreMotion geri çağırmalarını devam ettirme ile bir AsyncStream içinde sarmayı, ardından for try await ve AsyncAlgorithms paketini kullanarak işlemeyi düşündüler.
Artılar: Swift eşzamanlılığıyla entegre olur, görev iptallerini destekler, throttle ve debounce operatörlerinin kullanımına izin verir ve duyarlı bir UI sağlar.
Eksiler: iOS 13+ dağıtım hedefi gerektirir ve ekibin yapılandırılmış eşzamanlılık desenlerini öğrenmesi gerekir.
Seçilen çözüm: Ekip, CMMotionManager güncellemelerini .bufferingNewest(1) politikası ile bir AsyncStream içinde sararak Çözüm 3'ü benimsedi. Bu, veri işleme 60Hz donanım örneklemesinin gerisinde kalırsa sadece en son okumanın korunmasını sağlayarak bellek şişmesini engelledi.
Sonuç: Düşme tespit algoritması tam örnekleme frekansını korudu, CPU kullanımı anketleme yaklaşımına göre %70 azaldı ve UI duyarlı kaldı. Sistem, kullanıcı uygulamayı arka plana aldığında otomatik Task iptali akış yineleyicisine yayıldığından donanım kaynaklarını uygun şekilde serbest bıraktı.
Soru 1: Asenkron bir döngüde etiketlerle break veya continue kullanılabilir mi ve yineleyiciye ne olur?
Cevap: Evet, etiketli kontrol akışı for try await döngülerinde çalışır. Ancak, adaylar genellikle yaşam döngüsü etkilerini yanlış anlarlar. Asenkron bir döngüden break yaptığınızda, AsyncIterator hemen kapsam dışı kalır. Eğer yineleyici bir değer türü ise, deinit çalıştırılır ve dosya tanımlayıcıları gibi kaynakları serbest bırakır. Eğer referans türüyse, referans düşer. Önemli olarak, AsyncSequence protokolünde cancel() metodu yoktur; iptal, Task hiyerarşisi aracılığıyla yönetilir. Yineleyicinin temizlenmesi, ayrı bir iptal yöneticisinde değil, deinit'te uygulanmalıdır, çünkü protokol tüm yineleyicilerin referans türleri olmasını garanti edemez.
Soru 2: Neden AsyncSequence düzenli diziler gibi Array(myAsyncSequence) başlatıcısını desteklemiyor?
Cevap: Array'in başlatıcısı, argümanının Sequence'a uyum sağlamasını gerektirir, AsyncSequence'a değil. AsyncSequence'ın Sequence'ı iyileştirmediğinden, onu doğrudan Array yapıcıya geçiremezsiniz. Adaylar, asenkron diziler için özel olarak tasarlanmış try await Array(myAsyncSequence) başlatıcısını kullanmanız gerektiğini genellikle gözden kaçırır. Bu, üye temelli bir başlatıcı değil, küresel bir asenkron işlevdir çünkü Swift bu bağlamda asenkron başlatıcıları desteklemiyor. İşlem, her next() çağrısını sıralı olarak bekleyerek tüm öğeleri toplar ve görev iptallerini dikkate alarak, eğer üst Task iptal edilirse bir CancellationError fırlatır.
Soru 3: AsyncStream'de arka basınç nasıl çalışır ve NotificationCenter'ın AsyncSequence'iyle nasıl karşılaştırılır?
Cevap: Bu, kritik bir uygulama ayrıntısını ortaya koyar. AsyncStream arka basıncı destekler: eğer tüketici yavaşsa, üreticinin yield çağrısı, tüketici next() çağrısını yaptığında askıya alınır. Bu, devam ettirme tabanlı bir semaphore aracılığıyla uygulanır. Ancak, NotificationCenter'ın dizisi arka basıncı uygulamaz; sınırsız bir tampon kullanarak tüketici ayak uydurmazsa bildirimlerin sonsuza kadar birikmesine izin verir. Adaylar genellikle tüm AsyncSequence uygulamalarının arka basıncı eşit şekilde ele aldığını varsayarlar. Gerçek şu ki, AsyncSequence bir çekme temelli protokoldür, ancak üreticinin davranışı uygulama tanımlıdır. Arka basıncı yüksek verim senaryolarında önlemek için itme tabanlı API'leri çekme tabanlı asenkron dizilere köprülemenin temel aracının AsyncStream olduğunu anlamak esastır.