CPU yoğun Swift hesaplamalarını paralel çalıştırmak için çalışana açık bir @concurrent sınırı verin ve bağımsız işleri alt görevlere bölün. Bu Mac örneği, arayüz güncellemelerini MainActor üzerinde tutar ve sentetik sonuçları paylaşılan değişken durum kullanmadan birleştirir.

Görev eklemek hesaplamayı neden hâlâ seri hâle getirebilir?

Bir Mac uygulamasında kapsamlı bir yerel hesaplamayı başlatan bir Calculate düğmesi olduğunu düşünün. Hesaplamayı Task { ... } içine almak eşzamansız bir giriş noktası sağlar; ancak ana aktör bağlamında oluşturulan bir görev bu yalıtımı devralabilir. İçindeki uzun bir eşzamanlı döngü yine de ana aktörü meşgul eder. Apple bu ayrımı Embracing Swift concurrency belgesinde gösteriyor.

Bir görev grubu eklemek de önemli bir soruyu yanıtsız bırakır: pahalı döngüyü içeren işlev nerede çalışıyor? Her alt görev aynı @MainActor hesaplama yöntemini çağırıyorsa, bu aktör hesaplamayı seri hâle getirir. Swift dil kılavuzunun Concurrency bölümü açıklandığı üzere, aktörler yalıtılmış durumlarına aynı anda yalnızca bir görevin erişmesine izin verir.

Swift 6.2 ve sonraki sürümlerde eşzamansız hesaplama giriş noktasını @concurrent olarak işaretleyin. Bu, yürütmeyi çağıranın aktöründen açıkça çıkarır. NonisolatedNonsendingByDefault özelliği etkin olduğunda, sıradan yalıtılmamış eşzamansız bir işlev bunun yerine çağıranın aktöründe çalışır; önceki davranış farklıydı. Uygulamaya alınan SE-0461 önerisi, hem özellik sınırını hem de açık özniteliği belgeliyor.

İstenen davranışı iki ayrı tercih sağlar: çalışanın yalıtımı MainActor dışında çalışmasına izin verir ve birden fazla alt görev bağımsız işlerin zamanlanmasını sağlar. Kullanılabilir iş parçacıklarından hangilerinin bu görevleri yürüteceğine çalışma zamanı karar verir. Ne görev sayısı ne de @concurrent, aynı anda çalışan belirli sayıda çekirdek vaat eder. Apple bu zamanlama modelini Embracing Swift concurrency belgesinde açıklıyor.

Doğru biçimde bölünebilen bir hesaplama tanımlayın

Aşağıdaki örnekte Mac kullanıcısı sentetik bir hesaplamayı başlatabilir veya iptal edebilir ve son sağlama toplamını görüntüleyebilir. Tüm girdiler sabit değerlerden veya üretilen tamsayılardan oluşur. Hesaplamanın kesin değer sözleşmesi şöyledir:

  1. 0..<count aralığındaki her tamsayı indeksini tam olarak bir kez ziyaret edin.
  2. Her indeksi UInt64 türüne dönüştürün ve istenen tur sayısı boyunca x = (x &* 1_664_525) &+ 1_013_904_223 işlemini yinelemeli olarak uygulayın.
  3. Elde edilen değerleri taşmalı toplamayla toplayın.
  4. Boş girdi için sıfır döndürün; negatif sayıları veya tur sayılarını reddedin ve pozitif olmayan bir çalışan sınırını reddedin.
  5. Gözlemlenen iptal durumunda, tamamlanmış bir sonuç gibi sunmak yerine hata fırlatın.

Taşmalı aritmetik burada bilinçli olarak kullanılıyor. Swift’in olağan tamsayı aritmetiği taşmada hata verir; &+ ve &* işleçleri taşma davranışını açıkça destekler. Bkz. Advanced Operators.

Bu sözleşmede her indeks bağımsızdır ve taşmalı toplama kısmi toplamları 2 üzeri 64 modunda birleştirir. Bu nedenle gruplandırmayı veya tamamlanma sırasını değiştirmek kesin son değeri korur. Bu sonuç seçilen aritmetikten kaynaklanır. Kayan noktalı bir hesaplamada kendi doğruluk kuralınız gerekir; çünkü toplama gruplarını değiştirmek yuvarlamayı değiştirebilir.

Bölümleme, bölme ve kalan işlemlerini kullanır: her çalışan eşit sayıda temel indeks alır, ilk birkaç çalışana birer ek indeks verilir. Örnek olarak 17 indeks ve üç çalışan için aralıklar 0..<6, 6..<12 ve 12..<17 olur. Kalan indeksler de dâhil olmak üzere girdinin tamamını bir kez kapsarlar.

Mac için eksiksiz hesaplama örneğini oluşturun

Swift 6.2 veya üzeri bir araç zinciri ve dağıtım hedefi macOS 13 veya üzeri olan bir macOS SwiftUI uygulama hedefi kullanın. Oluşturulan uygulama giriş noktasını ve içerik görünümünü aşağıdaki iki blokla değiştirin; blokları aynı Swift dosyasına yerleştirin. Birlikte eksiksiz bir program oluştururlar; hedefte yalnızca bir @main uygulama bildirimi bulundurun.

Hesaplama ad alanı açıkça nonisolated olarak belirtilmiştir; bu nedenle üyeleri örtük küresel aktör yalıtımı edinmez. Swift bu tür düzeyindeki bildirimi SE-0449 içinde belgeliyor. Her iki eşzamansız hesaplama yöntemi de yürütme sınırını açıkça belirtmek için @concurrent kullanır.

import Foundation
import SwiftUI

nonisolated enum CalculationError: Error {
    case invalidInput
}

nonisolated enum SyntheticCalculator {
    @concurrent
    static func run(
        count: Int,
        rounds: Int,
        workerLimit: Int
    ) async throws -> UInt64 {
        guard count >= 0, rounds >= 0, workerLimit > 0 else {
            throw CalculationError.invalidInput
        }
        try Task.checkCancellation()
        guard count > 0 else { return 0 }

        let workers = min(count, workerLimit)
        let baseSize = count / workers
        let remainder = count % workers

        return try await withThrowingTaskGroup(
            of: UInt64.self,
            returning: UInt64.self
        ) { group in
            var lower = 0

            for worker in 0..<workers {
                try Task.checkCancellation()
                let size = baseSize + (worker < remainder ? 1 : 0)
                let start = lower
                let end = start + size
                lower = end

                group.addTask {
                    try await calculateChunk(
                        lower: start,
                        upper: end,
                        rounds: rounds
                    )
                }
            }

            var total: UInt64 = 0
            for try await partial in group {
                total = total &+ partial
            }
            try Task.checkCancellation()
            return total
        }
    }

    @concurrent
    private static func calculateChunk(
        lower: Int,
        upper: Int,
        rounds: Int
    ) async throws -> UInt64 {
        try Task.checkCancellation()
        var total: UInt64 = 0

        for index in lower..<upper {
            if index % 256 == 0 {
                try Task.checkCancellation()
            }
            var value = UInt64(index)
            for round in 0..<rounds {
                if round % 4096 == 0 {
                    try Task.checkCancellation()
                }
                value = (value &* 1_664_525) &+ 1_013_904_223
            }
            total = total &+ value
        }

        try Task.checkCancellation()
        return total
    }
}

Her alt görev değişmez sınırları yakalar ve bir tamsayı döndürür. Yalnızca üst görev total değerini günceller; alt görevler paylaşılan bir koleksiyona ekleme yapmaz veya arayüz durumunu değiştirmez. Görev grupları alt görevlerin eşzamanlı yürütülmesini destekler, gönderilebilir sonuçlar gerektirir ve alt görevlerin ömrünü grup kapsamı içinde tutar. Apple’ın TaskGroup belgelerine bakın.

Bu arayüz kodunu aynı dosyaya ekleyin:

@main
@MainActor
struct ParallelCalculationApp: App {
    var body: some Scene {
        WindowGroup {
            CalculationView()
        }
    }
}

@MainActor
struct CalculationView: View {
    @State private var status = "Ready"
    @State private var work: Task<Void, Never>?

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Synthetic Calculation")
                .font(.title2)
            Text(status)
                .textSelection(.enabled)

            HStack {
                Button("Calculate") {
                    startCalculation()
                }
                .disabled(work != nil)

                Button("Cancel") {
                    work?.cancel()
                }
                .disabled(work == nil)
            }
        }
        .padding(24)
        .frame(minWidth: 440, minHeight: 180)
        .onDisappear {
            work?.cancel()
        }
    }

    private func startCalculation() {
        guard work == nil else { return }
        status = "Calculating…"
        let workerLimit = max(
            1,
            ProcessInfo.processInfo.activeProcessorCount
        )

        work = Task { @MainActor in
            defer { work = nil }
            do {
                let checksum = try await SyntheticCalculator.run(
                    count: 1_000_000,
                    rounds: 128,
                    workerLimit: workerLimit
                )
                try Task.checkCancellation()
                status = "Checksum: \(checksum)"
            } catch is CancellationError {
                status = "Cancelled"
            } catch {
                status = "Calculation failed: \(error)"
            }
        }
    }
}

Arayüz, dış görev tutamacına sahip olur; böylece Cancel düğmesi görevi iptal edebilir. Görevi ana aktörde yalıtılmış kalır, eşzamanlı hesaplamayı çağırır ve sonucu bekledikten sonra görüntülenen durumu günceller. Tutamacı saklamak, görünüm kaybolduğunda da iptale olanak tanır. Apple’ın Task belgeleri, tutamacı atmanın görevi iptal etmediğini açıklar.

Çalışan sınırı, bilgisayarda kullanılabilir etkin işlemci çekirdeklerini bildiren Foundation’ın activeProcessorCount değeriyle başlar. Bunu, en uygun görev sayısının garantisi olarak değil, ölçülecek başlangıç boyutu seçimi olarak değerlendirin. Belgelenen anlamı activeProcessorCount sayfasında açıklanmıştır.

Performansı karşılaştırmadan önce doğruluğu denetleyin

Küçük örnek girdisiyle başlayın: çağrıyı count: 17, rounds: 4 ve workerLimit: 3 olacak şekilde değiştirin. Beklenen sağlama toplamı 18149427060254280572 değeridir. workerLimit: 1 ile yineleyin; değer eşleşmelidir. Ayrıca sıfır döndürmesi gereken count: 0 değerini ve çalışan sınırından küçük pozitif bir sayıyı denetleyin; tüm indeksler yine kapsanmalıdır.

Örnek sağlama toplamı ve aralık kapsamı, taşmalı aritmetiği uygulayan küçük bir Python programıyla denetlendi. Araştırma ortamında Swift derleyicisi bulunmadığından Swift programı burada derlenip çalıştırılmadı.

Ardından büyük girdiyi geri yükleyin ve optimize edilmiş derlemeleri bir, iki ve etkin işlemci sayısına eşit çalışan sınırlarıyla karşılaştırın. Sayıyı ve tur sayısını değiştirmeyin, elde edilen sağlama toplamını kaydedin ve birkaç çalıştırmayı ölçün. Bu, önerilen bir ölçüm yöntemidir; herhangi bir süre veya hızlanma iddiası ileri sürülmüyor.

Tek çalışanlı çalıştırma aynı eşzamanlı sınırı ve görev grubu yapısını kullanır; bu da onu bu uygulama için yararlı bir karşılaştırma hâline getirir. Küçük iş yüklerinde bölümleme ve zamanlama ek yüküne değmeyebilir. Gerçekçi bir hesaplamada farklı indekslerin maliyeti önemli ölçüde değişiyorsa eşit boyutlu aralıklar bir çalışanın daha geç bitmesine de yol açabilir; sınırlı sayıda eşzamanlı görevle daha küçük parçalar kullanmak, denenebilecek makul bir sonraki adımdır.

Dizi üreten bir hesaplamada her sonuca parça indeksi ekleyin ve parçaları girdi sırasına göre birleştirin. Görev grubu alt görevleri herhangi bir sırada yürütebilir ve tamamlanan sonuçları kullanılabilir olur olmaz sunar. Sağlama toplamı, yalnızca seçilen indirgeme işlemi sıradan bağımsız olduğu için sıralama sorunlarını ortadan kaldırır. Bkz. TaskGroup.

İptal ve yalıtım sorunlarını görünür kılın

İptal işbirliğine dayalıdır. cancel() çağrısı görevi işaretler; hesaplama kodunun bu durumu gözlemlemesi gerekir. Her iki döngüdeki denetimler, tek bir indekste çok sayıda tur olduğunda bile çalışana hata fırlama fırsatı verir. Bu aralıklar uygulama tercihidir; belirli bir iptal gecikmesi vaat etmez. Apple bu sorumluluğu Task içinde belgeliyor.

Hata fırlatan sonuç döngüsü de önemlidir. Bir alt görev hata fırlatır ve hatası grup gövdesinin dışına yayılırsa kalan alt görevler iptal edilir. Grup yine de onların bitmesini bekler; bu nedenle iptal denetimleri gereklidir. Swift standart kitaplığı uygulaması bu davranışı TaskGroup.swift içinde belgeliyor.

Arayüz hâlâ takılıyorsa pahalı çağrı yolunun tamamını izleyin. Çalışanın bir ana aktör yöntemini çağırıp çağırmadığına, aktör yalıtımlı özelliklere erişip erişmediğine veya eşzamanlı sınıra ulaşmadan önce kapsamlı hazırlık yapıp yapmadığına bakın. Gerekli tamsayı değerlerini çalışana aktarın ve uzun döngüleri bu sınırın içinde tutun.

Derleyici @concurrent özniteliğini reddediyorsa araç zinciri sürümünü denetleyin. Bu öznitelik Swift 6.2’de uygulandı ve eşzamansız işlevlere uygulanır. Daha eski bir araç zinciri için önceki yalıtılmamış eşzamansız yürütme modeli SE-0461 içinde belgelenmiştir; tüm sürümlerin nonisolated async ifadesine aynı davranışı verdiğini varsaymak yerine derleyicinizle eşleşen belgeleri kullanın.

Bu Mac görevi için kullanışlı mimari basittir: arayüz durumunun sahibi ana aktördür, eşzamanlı çalışanlar bağımsız değerler alır ve üst görev tamamlanmış sonuçları birleştirir. Parça boyutlarını ayarlarken değer sözleşmesini koruyun; performans değişikliğini benimsemeden önce hem sağlama toplamı eşitliğini hem de arayüzün yanıt verebilirliğini doğrulayın.

Kaynaklar