İlk askıya alma gerçekleşmeden önce hesaplamanın görevini bir actor içinde saklayın, ardından eşzamanlı çağrıların aynı görevi beklemesini sağlayın. Bu anlatımda, Mac yardımcı uygulaması için iptal, hatalar ve temizleme konusunda açık kurallar içeren yerel bir kareler toplamı örneği oluşturuluyor.
Eşzamanlı çağrılar neyi paylaşmalı?
Mac masaüstü yardımcı uygulamasının iki bölümünün aynı hesaplanmış özete ihtiyaç duyduğunu varsayalım: bir önizleme ve bir ayrıntılar paneli. İkisi de sentetik [1, 2, 3, 4] dizisinin kareleri toplamını istiyor. İkisi de 30 sonucunu almalı ve aynı etkin çalışmaya katılan istekler tek bir yürütmeyi paylaşmalı.
Aşağıdaki uygulamada her hesaplayıcı örneğine değişmez bir girdi dizisi verilir. Görev, actor'ın çalışma yuvasını işgal ettiği sürece mevcut görev paylaşılır; ardından bir çağrı tamamlanmayı gözlemlediğinde yuva temizlenir. Sonraki bir istek yeni bir hesaplama başlatabilir. Burada devam eden iş paylaşılır; tamamlanan sonuçları süresiz saklamak ayrı bir önbellekleme politikası gerektirir.
Değer sözleşmesi bilerek küçük tutulmuştur:
- En fazla 100.000 tamsayı kabul edilir; her biri
0ile1,000arasında olmalıdır. - Karelerinin toplamı döndürülür; boş bir dizi
0döndürür. - Çağrıların paylaşılan yürütmeyi yalnızca eşit toplamları almaktan ayırt edebilmesi için bir çalışma tanımlayıcısı eklenir.
- Bu çalışmayı bekleyen çağrılara hesaplama hataları iletilir.
- Tek bir çağrının, paylaşılan çalışanı iptal etmeden bekleme talebini iptal etmesine izin verilir.
İzin verilen en büyük toplam 100,000 × 1,000² = 100,000,000,000 değeridir ve Mac'in 64 bitlik Int türüne sığar. Bu sınırlar, bu örneğin sözleşmesine aittir. Bunları değiştirmek, aritmetik taşmayı ve kaynak kullanımını yeniden değerlendirmeyi gerektirir.
Bir actor neden yine de saklanan bir göreve ihtiyaç duyar?
Bir actor değiştirilebilir durumunu korur, ancak eşzamansız bir actor metodu askıya alınabilir. Bu askıya alma sırasında başka bir çağrı actor'a girebilir. Swift'in dil rehberi actor izolasyonunu ve askıya alma noktalarını açıklar; Apple'ın actor oturumu ise await öncesinde yapılan varsayımların nasıl geçersiz hâle gelebileceğini gösterir. Concurrency — The Swift Programming Language, Protect mutable state with Swift actors.
Bu nedenle, mevcut bir yanıtı kontrol edip ardından yeni bir hesaplamayı beklemek yinelenen işi önlemez. Yanıt hazır olmadan başka bir çağrı gelebilir. Bunun yerine görev tanıtıcısını eşzamanlı olarak yayımlayın: çalışma yuvasını inceleyin, yalnızca yuva boşsa bir çalışan oluşturun, onu yuvaya kaydedin ve ardından sonucunu bekleyin.
Apple, bir görevin oluşturulduktan hemen sonra başlayabileceğini belgeliyor. Eşzamansız value özelliğine erişmek tamamlanmayı bekler ve fırlatılan hatayı iletir. Bu nedenle actor, tek bir tanıtıcının sahipliğini koordine eder; çağrılar sonuçlarını bu tanıtıcı aracılığıyla alır. Task, value.
Yerel örneğin tamamını oluşturun
macOS 13 veya sonraki sürümlerde Swift 6 derleyicisi kullanın. Buradaki macOS gereksinimi, gösterimdeki Task.sleep(for:) çağrısından kaynaklanır. Bu çağrı, temelindeki iş parçacığını engellemeden askıya alınır; kısa bekleme, çakışan isteklerin gözlemlenmesini kolaylaştırır. Aritmetiğin hiçbir bölümünü gerçekleştirmez ve daha sonra kaldırılabilir. sleep(for:tolerance:clock:).
Aşağıdaki programın tamamını SharedCalculation.swift adıyla kaydedin. Aşağıda gösterilen komut satırı derlemesi için sıradan, yalıtılmamış varsayılan ayarları kullanır. Foundation, belgelenmiş uyumlulukları arasında Sendable ve Equatable bulunan UUID öğesini sağlar; tanımlayıcı her çalışmayı etiketler ve koşullu temizlemeyi destekler. UUID.
import Foundation
struct CalculationResult: Sendable {
let total: Int
let runID: UUID
}
enum CalculationError: Error {
case invalidInput
}
actor SharedCalculation {
private struct Flight {
let id: UUID
let task: Task<CalculationResult, Error>
}
private let samples: [Int]
private var flight: Flight?
init(samples: [Int]) {
self.samples = samples
}
func value() async throws -> CalculationResult {
try Task.checkCancellation()
let active: Flight
if let existing = flight {
active = existing
} else {
let id = UUID()
let snapshot = samples
let task = Task.detached(priority: .userInitiated) {
[snapshot, id] () async throws -> CalculationResult in
guard snapshot.count <= 100_000,
snapshot.allSatisfy({ (0...1_000).contains($0) })
else {
throw CalculationError.invalidInput
}
// Demonstration only: allow overlap to be observed.
try await Task.sleep(for: .milliseconds(100))
let total = snapshot.reduce(0) { sum, sample in
sum + sample * sample
}
return CalculationResult(total: total, runID: id)
}
active = Flight(id: id, task: task)
flight = active
}
defer {
// An older waiter must not clear a newer flight.
if flight?.id == active.id {
flight = nil
}
}
let result = try await active.task.value
try Task.checkCancellation()
return result
}
}
@main
struct Demo {
static func main() async throws {
let calculation = SharedCalculation(samples: [1, 2, 3, 4])
async let preview = calculation.value()
async let details = calculation.value()
async let inspector = calculation.value()
let (a, b, c) = try await (preview, details, inspector)
precondition(a.total == 30 && b.total == 30 && c.total == 30)
let observedRuns = Set([a.runID, b.runID, c.runID]).count
print("Total: \(a.total)")
print("Runs observed across three requests: \(observedRuns)")
let later = try await calculation.value()
precondition(later.total == 30)
print("Later request started another run: \(later.runID != c.runID)")
}
}
Dosyanın bulunduğu dizinden derleyip çalıştırın:
swiftc -swift-version 6 -parse-as-library SharedCalculation.swift -o SharedCalculation
./SharedCalculation
Aritmetik doğrulamaları geçmelidir. İlk üç çağrının tümü çalışma yuvası temizlenmeden önce girerse gözlemlenen çalışma sayısı 1 olur. Sonraki istek, bu çağrılar döndükten sonra geldiği için başka bir hesaplama başlatır. Bekleme, çağrıların çakışmasını teşvik eder ancak zamanlanmayı garanti etmez: yeterince geciken ilk istek, sonraki çalışmayı geçerli olarak başlatabilir. Araştırma ortamında Swift mevcut olmadığından bu program incelendi, ancak burada derlenip çalıştırılmadı.
Çalışan, bu hesaplama tek bir istek gönderen çağrıya değil paylaşılan hesaplayıcıya ait olduğu ve aritmetiğin koordinatör actordan bağımsız çalışması gerektiği için ayrık bir görev kullanır. Yalnızca girdi anlık görüntüsünü ve tanımlayıcıyı yakalar. Apple, gerekli yaşam süresi yapılandırılmış alt görevlerle ifade edilebiliyorsa bunların kullanılmasını öneriyor; ayrık görevler, yaşam süresi ve iptal kararlarının açıkça verilmesini gerektirir. Örnekte öncelik de açıkça belirtilmiştir. detached(name:priority:operation:).
Temizleme sonraki hesaplamayı silmeyi nasıl önler?
Kontrol ve kaydetme sıralaması await içermediğinden, ikinci bir actor çağrısı boş yuvayı kontrol etmekle yeni çalışmayı yayımlamak arasına giremez. İlk çağrı active.task.value üzerinde askıya alındığında başka bir çağrı girebilir, bu çalışmayı bulabilir ve aynı tanıtıcıyı bekleyebilir.
Birden çok çağrı aynı tamamlanmış görevden farklı zamanlarda devam edebileceğinden temizleme için ayrı bir kimlik kontrolü gerekir. Şu sırayı ele alalım:
| Olay | Çalışma yuvası |
|---|---|
| A ve B çağrıları X çalışmasına katılır | X |
| X biter; A devam eder ve X'i temizler | Boş |
| C çağrısı girer ve Y çalışmasını başlatır | Y |
| B, X'ten devam eder ve temizleme yapar | Y kalmalı |
B'nin koşulsuz olarak nil ataması Y'nin tanıtıcısını siler. Dördüncü bir çağrı da Y hâlâ çalışırken yinelenen işi başlatabilir. flight?.id değerini active.id ile karşılaştırmak, temizlemeyi her çağrının gerçekten katıldığı çalışmaya özgü kılar. Bu sıra, uygulamanın ve belgelenmiş yeniden giriş davranışının sonucudur. Protect mutable state with Swift actors.
Paylaşımın kesin sınırı, saklanan çalışma yuvasıdır. Çalışan tamamlanmış olsa bile, bekleyen çağrıları yuvayı temizlemek üzere henüz devam etmemiş olabilir. Bu aralıkta giren bir istek, tamamlanmış görevin sonucunu yine de alabilir. Yuva temizlendikten sonra yeni bir istek yeni bir çalışma başlatır. Bu kısa tamamlanma aralığı sözleşmenin bir parçasıdır.
Bir çağrı iptal olursa veya hesaplama başarısız olursa ne olur?
Swift'te iptal işbirliğine dayalıdır: kod, nerede kontrol edeceğine ve ne yapacağına karar vermelidir. Bu örnek, katılmadan önce ve paylaşılan sonucu başarıyla aldıktan sonra istekte bulunan görevi kontrol eder. Task — Task Cancellation.
İlk kontrolde zaten iptal edilmiş bir çağrı, iş başlatmadan veya mevcut işe katılmadan hata fırlatır. Beklerken iptal edilen bir çağrı paylaşılan çalışanı beklemeye devam eder; çalışan başarılı bir sonuç verdikten sonra ikinci kontrolü CancellationError fırlatır. Bu uygulama tek bir çağrının iptalini çalışana hiçbir zaman iletmediğinden diğer çağrılar sonucu almaya devam edebilir.
Bu politika, beklemenin hemen iptal edilmesini sağlamaz. Tüm çağrılar iptal olsa bile çalışan işini tamamlar. Bu, kapsamı sınırlı olan yerel hesaplama için bilinçli bir seçimdir. Tek tek çağrıların hızla beklemeyi bırakması gerekiyorsa ek bekleyen çağrı takibi ve paylaşılan çalışanın ne zaman durması gerektiğini açıkça tanımlayan bir kural gerekir.
Geçersiz girdi, çalışanın CalculationError.invalidInput hatasını fırlatmasına neden olur. Bu görevi zaten paylaşan çağrılara hata value aracılığıyla iletilir ve ertelenmiş temizlemeleri eşleşen yuvayı boşaltır. Daha sonraki bir istek aynı değişmez girdiyi yeniden dener ve yine başarısız olur. Sonuç sessizce başka bir değerle değiştirilmez ve geçersiz öğeler atılmaz. value.
Ayrıca açıkça belirtilmesi gereken bir hata sıralaması ayrıntısı vardır: çalışan başarısız olurken bir çağrı iptal edilirse, ikinci iptal kontrolünden önce çalışanın hatası iletilir. Çalışan başarılı olursa, çağrının sonucu döndürüp döndürmeyeceğini çağrının iptal kontrolü belirler.
Bir Mac yardımcı uygulamasında bu kalıbı nasıl doğrulamalı ve kullanmalısınız?
Tek başına eşit toplamlar, yürütmenin paylaşıldığını göstermez: bu girdiye yönelik üç ayrı hesaplama da 30 sonucunu verir. Çakışmayı kontrol ederken çalışma tanımlayıcılarını karşılaştırın ve sayısal sonucu ayrıca doğrulayın.
Belirleyici bir eşzamanlılık testi için, gösterimdeki beklemeyi denetlenebilir bir eşzamansız kapıyla değiştirin ve çalışmaya katılımı araçlarla izleyin. İstenen tüm çağrılar katılana kadar çalışanı bekletin, sonra serbest bırakın. Çalışanın bir kez başladığını, tanımlayıcıların eşleştiğini, toplamların doğru olduğunu, hataların iletildiğini ve temizlemeden sonra yeni bir çalışmanın başlatıldığını kontrol edin. Tablodaki temizleme sırasını da sınayın. Bekleme süresini uzatmak bu koşulları güvenilir biçimde sağlayamaz.
Masaüstü uygulamasında, aynı girdiyi kullanan tüketiciler için tek bir hesaplayıcı örneği tutun. Her panel için yeni bir örnek oluşturmak, her panele bağımsız bir çalışma yuvası verir. Sentetik girdi değiştiğinde bu anlık görüntü için yeni bir hesaplayıcı oluşturun; bu örnek, değiştirilebilir girdileri veya anahtara göre paylaşımı tanımlamaz.
Döndürülen değerleri kullanıcı arayüzü durumuna ana actor üzerinde uygulayın. Swift'in dil rehberi, kullanıcı arayüzü işleri için @MainActor kullanımını belgeliyor; paylaşılan hesaplayıcı kendi durumunu ayrı olarak koordine etmeye devam edebilir. Concurrency — The Swift Programming Language.
Küçük bir toplam için görev oluşturmak gereksiz ek yüktür; örnek, sahiplik kurallarını görünür kılarak bunların daha kapsamlı bir yerel hesaplamaya uygulanmasını sağlar. Uyarlarken temel sıralamayı koruyun: askıya almadan önce tek bir görevi yayımlayın, bu tanıtıcıyı bekleyin ve yalnızca tamamlanan çalışmayı temizleyin.
Kaynaklar
- Concurrency — The Swift Programming Language — Swift.org.
- Task — Apple Developer Documentation.
- value — Apple Developer Documentation.
- detached(name:priority:operation:) — Apple Developer Documentation.
- sleep(for:tolerance:clock:) — Apple Developer Documentation.
- UUID — Apple Developer Documentation.
- Protect mutable state with Swift actors — Apple Developer.