Swift'in eşzamanlılık modeli, Swift 5.5 ile bir paradigma değişikliği geçirdi ve geleneksel Grand Central Dispatch desenlerini terk ederek yapılandırılmış eşzamanlılığı tanıttı. Bu, genellikle yetim görevler ve kaynak sızıntılarına neden oluyordu. Bunun öncesinde, geliştiriciler DispatchGroup örneklerini manuel olarak yöneterek eşzamanlı çalışmayı takip ediyordu, iptali önlemek için açık senkronizasyon gerektiriyordu. TaskGroup soyutlaması, ana-çocuk ilişki ağacını yerel olarak kapsüllemek üzere tasarlandı, böylece runtime yaşam döngüsü meta verilerini geliştirici yerine koruyabiliyor.
Temel sorun, ana görevlerin, tüm soyundan gelenlere iptal sinyali verebileceği kesin bir hiyerarşiyi korumakta yatıyor. Bu, küresel kayıt defterlerini veya manuel zayıf referans dizilerini geçmeden yapılmalıdır. Geleneksel yaklaşımlar OperationQueue kullanarak her zaman tamamlanan işlemleri kaydetmek ve kaydettirmek zorunda kalıyor, bu da eksik bir tamamlanma yöneticisi nedeniyle kırılgan durum yönetimi yaratıyor. Ayrıca, iptalin yayılması karmaşık atomik bayrak anketlemeyi gerektiriyor ki bu da gecikmiş yanıt vermeye veya aşırı CPU aşamasına yol açıyor.
Swift bunu, her görev bağlamında bir görev kaydını gömerek, bunun ana görevine işaret etmesini sağlayarak çözüyor. Bu, TaskGroup içinde köklenen müdahale eden bir bağlı liste oluşturuyor. addTask çağrıldığında, runtime bu listeye bir çocuk görev kaydı ekliyor, bunu anağın iptal yöneticisi ile atomik olarak kaydediyor. İptal mekanizması bir durum makinesi kullanıyor: cancelAll() çağrıldığında, runtime bu listeyi dolaşarak, her çocuk görev meta verisinde isCancelled bayrağını ayarlıyor ve askıya alınmış yürütücüleri uyandırıyor. Bu, O(n) yayılımını sağlıyor, burada n ağaç derinliği, küresel kilitleri önleyerek.
import Foundation func downloadImages(urls: [URL]) async throws -> [Data] { try await withThrowingTaskGroup(of: Data.self) { group in for url in urls { group.addTask { // Çocuk görev, otomatik olarak ana iptali kontrol eder let (data, _) = try await URLSession.shared.data(from: url) return data } } // Kullanıcı iptali simülasyonu group.cancelAll() var results: [Data] = [] for try await data in group { results.append(data) } return results } }
Bir medya işleme uygulaması, 10.000 görüntü için küçük resimler üretmek zorunda kalırken, kullanıcıların uçuş sırasında iptal etmesine izin verdi. Mühendislik ekibi başlangıçta, iptali sağlamak için iş parçacığı güvenli bir NSHashTable içinde aktif URLSessionDataTask nesnelerini takip ederek DispatchGroup yaklaşımını kullandı.
İlk çözüm, eşzamanlılığı sınırlamak için bir DispatchSemaphore ile birlikte DispatchGroup kullanıldı. İşlevsel olmasına rağmen, tamamlanan görevleri iptal setinden kaldırmak için karmaşık bir mantık gerektiriyordu. İptal sinyali ile setin sıralanması arasında görevlerin tamamlandığı yarış durumları ortaya çıkıyordu ve bu, uygulamanın serbest bırakılan nesnelere başvurmasına neden oluyordu. Bu yaklaşım, görünüm denetleyicisi kapatıldığında DispatchGroup bildirimlerinin delegate'ı güçlü bir şekilde tuttuğu için bellek sızıntısı da yaşadı.
İkinci yaklaşım Combine'nun FlatMap'ini, iptal için bir PassthroughSubject ile benimsedi. Bu, daha iyi bir bileşim sağladı ancak yayıcı zinciri tahsisi nedeniyle önemli bir bellek aşınması getirdi. İptal yayılımı, manuel temizleme gerektiren bir koleksiyonda AnyCancellable belirteçlerini saklama gerektiriyordu. Açıklayıcı soyutlama, gerçek görev hiyerarşisini gizleyerek, iptal sinyallerinin işlem operatör zinciri üzerinden yayılmadığı durumlarda hata ayıklamayı zorlaştırıyordu.
Ekip, Swift'in TaskGroup'una geçti. Bu, runtime'ın her küçük resim oluşturma görevini grubun iptal alanına otomatik olarak bağladığı için manuel NSHashTable yönetimini ortadan kaldırdı. Kullanıcı iptal'e dokunduğunda, görünüm denetleyicisi group.cancelAll() çağrısını yaptı ve bu, tüm çalışan görevlere bir sonraki await askıya alma noktasında durmalarını atomik olarak bildirdi. Bu çözüm, görünüm serbest bırakıldıktan sonra yetim görevlerin işlemeye devam etmesini garanti etti ve withThrowingTaskGroup'un deterministik kapsamı, fonksiyon bir hata fırlatsa bile otomatik temizliği sağladı.
İptal gecikmesi, manuel set sıralamasını beklemekten ortalama 500 ms'den (ortalama) 10 ms'den (doğrudan bağlı liste geçişi) azalmıştır. Bellek profil çıkışı, iptalden sonra sıfır sızmış Task nesnesi gösterdi ve kod tabanı 40 satır senkronizasyon şablon kodu ile azaldı.
TaskGroup, bir çocuk görev iptali göz ardı edip sonsuza dek çalışmaya devam ederse bu durumu nasıl yönetir?
Adaylar genellikle TaskGroup'un görevleri zorla sonlandırdığını veya istisnalar enjekte ettiğini düşünür. Gerçekte, Swift'in iptali iş birliğine dayalıdır: runtime görev bağlamında isCancelled bayrağını ayarlar, ancak görev, bir askıya alma noktasına ulaşana kadar devam eder veya açıkça Task.isCancelled kontrolü yapar. Çocuk görev, belirli aralıklarla Task.checkCancellation()'ı yerine getirmeli veya iptal farkındalığı olan API'lere güvenmelidir. Eğer bir görev, iptalleri kontrol eden askıya alma noktaları olmadan sıkı bir CPU bağlı döngü gerçekleştirirse, grup tamamlanmasını sonsuza dek engeller. Bunu önlemek için, uzun süreli hesaplamalar Task.yield() kullanmalı veya çalışmayı, iptal bayraklarını kontrol eden parçalara böldürmelidir.
cancelAll() çağrıldıktan sonra bir TaskGroup'a bir görev eklemek neden hala o yeni görevin hemen iptal edilmesine neden olur?
Birçok kişi cancelAll()'ın mevcut çocuklara gönderilen bir tek seferlik sinyal olduğunu varsayıyor. Ancak, Swift'in uygulanması, TaskGroup'u kendi durum kaydında iptal olarak işaretler. addTask daha sonra çağrıldığında, runtime görev yaratma sırasında grubun iptal durumunu atomik olarak kontrol eder; eğer iptal edilmişse, yeni çocuk görev, isCancelled bayrağı önceden ayarlanmış olarak oluşturulur. Bu, geç eklenen görevlerin iptal alanından kaçamayacağını garanti eder ve iptal edilmiş bir alanın yeni geçerli sonuçlar üretemeyeceği yapısal garantiyi korur. İptal sürdürüldüğünde, yeni görevlerin sınırlı durumuna geçişi engeller.
TaskGroup'un yapılandırılmış eşzamanlılığı ile Task.init ile oluşturulmuş bir Görev arasındaki temel fark, yakalanan değişkenlerin bellek yönetimidir?
Adaylar genellikle TaskGroup çocuk görevlerinin ana bağlamın aktör izolasyonu ve önceliğini miras aldığını, ancak daha kritik olarak, yakalanan değişkenlerin ömrünü yalnızca grup kapsamı çıkana kadar uzattığını gözden kaçırıyor. Aksine, Task { ... } ile oluşturulan yapısal olmayan Task nesneleri, oluşturma kapsamının ömründen daha uzun süre kalır ve potansiyel olarak self'i sonsuza dek yakalayabilir. Bu demektir ki, TaskGroup içinde addTask'da self'i yakalarsanız, görev withThrowingTaskGroup bloğunun dışına çıkamayacağı için [weak self] gerekmez. Ancak, geliştiriciler genellikle yapısal olmayan görevlerden [weak self] kalıplarını yanlış bir şekilde uygular ve gereksiz yere kodu karmaşıklaştırırken, tamamlanma için self'in var olduğuna güveniyorlarsa nil-referans hatalarını içerebilirler.