İçeriğe geç
academia.sh

Ders 08 / 19

Görevler ve Görev Grupları

Birlikte başlatılan işlerin sonuçlarının küme olarak toplandığında geliş sırasından bağımsız kaldığının, dizi olarak toplandığında ise sıraya bağlı olduğunun ölçümü.

Yayın Güncelleme

Bu derste

  • Görev ile görev grubunun yapılandırılmış eşzamanlılıktaki yerini ayırır
  • Çocuk görevlerin sonucunu kümede toplayan işlemin geliş sırasından bağımsız olduğunu ölçer
  • Aynı sonuçları geliş sırasıyla dizen işlemin sıraya duyarlı olduğunu ölçer
  • Karşılaştırılan durumun seçiminin sınıfı değiştirdiğini aynı veriyle gösterir
İçindekiler

Önceki derste iki ayırma isteği iki sırada farklı stok bıraktı; kalan stok ise iki sırada aynı çıktı ve ancak durumu değiştiren işlem sıraya duyarlıydı. Şimdi aynı soruyu birden çok işi birlikte başlatan koda soruyoruz: işler hangi sırayla biterse bitsin, topladığımız sonuç aynı mı kalır?

Çekirdek dosya konunun her dersinde aynıdır.

// Replay.swift — bir islemin yinelemeye ve siraya duyarliligini siniflandirir
struct Trial<State: Equatable & CustomStringConvertible> {
    let label: String
    let once: State
    let twice: State
    let forward: State
    let backward: State

    var sensitivity: String {
        switch (once != twice, forward != backward) {
        case (false, false): return "saglam"
        case (true, false): return "yinelemeye-duyarli"
        case (false, true): return "siraya-duyarli"
        case (true, true): return "cift-duyarli"
        }
    }

    func report() -> String {
        print("\(label) | bir=\(once) iki=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
        return sensitivity
    }
}

func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
    Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}

func countSensitivities(_ results: [String]) {
    for name in ["saglam", "yinelemeye-duyarli", "siraya-duyarli", "cift-duyarli"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}

Bu derste iki işlem ölçülür. Birincisi eşzamansız olduğu için dört koşumu elle kurulur ve Trial’a doğrudan verilir. İkincisi duruma yalnız bir dizi ekler, bu yüzden replay ile ölçülür. Model, işlemin duruma ne yaptığını kurar; ağın ya da çalışma zamanının kendisi değildir.

Üç Görev Biçimi

Eşzamansız işi başlatmanın üç yolu vardır ve fark, işin kimin kapsamına bağlı olduğundadır. async let, sayısı kod yazılırken belli olan çocuk görevleri başlatır: her async let bir çocuk doğurur ve sonuç await edildiğinde alınır. Görev grubu, sayısı çalışırken belli olan çocuklar içindir; bir listenin her elemanı için bir çocuk eklenir. Yapılandırılmamış görev (Task { }) ise çağıranın kapsamından bağımsızdır; çağıran bitse de görev sürer ve iptali elle yönetilir.

// async let: sayisi bilinen iki cocuk gorev birlikte baslar
func loadDashboard() async throws -> (stock: Int, orders: Int) {
    async let stock = fetchStockCount()
    async let orders = fetchOrderCount()
    return try await (stock, orders)
}
func fetchStockCount() async throws -> Int { 12 }
func fetchOrderCount() async throws -> Int { 7 }

İki çocuk aynı anda başlar; return satırı ikisini de bekler. İşlev bitmeden çocuklardan hiçbiri geride kalmaz: kapsam bitince hepsi ya bitmiştir ya da iptal edilmiştir. Yapılandırılmış eşzamanlılığın güvencesi budur ve Task { } ile kurulan görevde yoktur. Başıboş bırakılmış bir görev, ekran kapandıktan sonra bile ağ isteği göndermeyi sürdürebilir.

Görev ve Görev Grubu

Bir görev (Task), eşzamansız işi başlatan birimdir. Yapılandırılmış eşzamanlılıkta (structured concurrency) bir görev kendi kapsamından uzun yaşamaz: kapsam bitmeden içindeki tüm çocuk görevler bitmiş olur. Bunun en yaygın aracı görev grubudur: kapsamın içinde istediğiniz kadar çocuk görev eklenir ve sonuçlar bir döngüde toplanır. Çocuklar aynı anda koşabilir, bu yüzden hangisinin önce bittiği belli değildir.

Bu belirsizliği ölçüme sokmuyoruz. Hangi çocuğun önce bittiği her koşumda değişebilir ve çıktıyı onun üzerine kurmak ölçümü kararsız yapar. Ölçülen şey, toplanan sonucun geliş sırasına bağlı olup olmadığıdır.

// Groups.swift — gorev grubu sonuclarini kumede toplar, geliste dizer
func collectNames(_ names: [String]) async -> [String] {
    await withTaskGroup(of: String.self) { group in
        for name in names {
            group.addTask { name }
        }
        var collected = Set<String>()
        for await name in group {
            collected.insert(name)
        }
        return collected.sorted()
    }
}

func record(_ name: String) -> ([String]) -> [String] {
    { list in list.contains(name) ? list : list + [name] }
}

collectNames her adı bir çocuk görevde döndürür ve for await ile geliş sırasıyla alır; ama sonucu bir Set’e koyar ve sıralanmış olarak verir. Küme sırayı tutmaz, tekrarı da tutmaz. record ise duruma bir ad ekleyen işlemdir: ad listede yoksa sona ekler, varsa dokunmaz.

Kümede Toplamak ve Dizide Toplamak

Önce kümeye toplayan işlem dört koşumda ölçülür: tek ad, aynı ad iki kez, alfa sonra beta, beta sonra alfa. Ardından aynı iki ad record ile dizide toplanır.

// main.swift — ayni iki ad, iki toplama bicimi
let setOnce = await collectNames(["alfa"])
let setTwice = await collectNames(["alfa", "alfa"])
let setForward = await collectNames(["alfa", "beta"])
let setBackward = await collectNames(["beta", "alfa"])
let grouped = Trial(label: "grup-kumesi", once: setOnce, twice: setTwice, forward: setForward, backward: setBackward).report()

let arrival = replay("gelis-sirasi", from: [], record("alfa"), then: record("beta")).report()

countSensitivities([grouped, arrival])
grup-kumesi | bir=["alfa"] iki=["alfa"] ab=["alfa", "beta"] ba=["alfa", "beta"] -> saglam
gelis-sirasi | bir=["alfa"] iki=["alfa"] ab=["alfa", "beta"] ba=["beta", "alfa"] -> siraya-duyarli
saglam: 1
yinelemeye-duyarli: 0
siraya-duyarli: 1
cift-duyarli: 0

Küme satırında dört değerin her biri ["alfa"] ya da ["alfa", "beta"]: tekrarlanan ad kümede erir, iki sıra aynı sıralı listeyi verir. İşlem sağlamdır. Geliş sırası satırında bir ve iki aynıdır, çünkü record var olan adı yeniden eklemez; ama ab ile ba farklıdır. Önce alfa gelirse liste ["alfa", "beta"], önce beta gelirse ["beta", "alfa"] olur. İşlem sıraya duyarlıdır. Sayım bir sağlam, bir sıraya duyarlı.

ES4. Bu ölçüm çocukların hangi sırayla bitebileceğini iki sıradan ibaret sayar. Gerçek bir grupta çocuk sayısı büyüdükçe olası sıra sayısı da büyür; hepsi ölçülmedi.

ES5. Ölçüm çocuk görevlerin hepsinin başarıyla döndüğünü varsayar. Bir çocuğun hata fırlattığı ya da iptal edildiği durum ölçülen modelde yoktur.

Toplamak Yerine Saymak

Küme tekrarı siler; bu, toplamı sağlam yapan şeyin bir parçasıdır. Çocukların döndürdüğü sayıları bir kümeye değil, bir toplama yazarsak tekrar artık silinmez. Aynı ölçüm bu kez toplamla yapılır: bir değer, aynı değer iki kez, iki farklı değer iki sırada.

// main.swift — grup sonuclarini toplamak
func sumValues(_ values: [Int]) async -> Int {
    await withTaskGroup(of: Int.self) { group in
        for value in values { group.addTask { value } }
        var total = 0
        for await value in group { total += value }
        return total
    }
}

let sumOnce = await sumValues([4])
let sumTwice = await sumValues([4, 4])
let sumForward = await sumValues([4, 5])
let sumBackward = await sumValues([5, 4])
let summed = Trial(label: "grup-toplami", once: sumOnce, twice: sumTwice, forward: sumForward, backward: sumBackward).report()
countSensitivities([summed])
grup-toplami | bir=4 iki=8 ab=9 ba=9 -> yinelemeye-duyarli
saglam: 0
yinelemeye-duyarli: 1
siraya-duyarli: 0
cift-duyarli: 0

Toplamada sıra önemsizdir: ab ve ba ikisi de dokuz. Ama bir dört, iki sekizdir; aynı değeri iki kez eklemek toplamı değiştirir. İşlem yinelemeye duyarlıdır. Bir önceki kümede aynı yineleme erimişti. Yani iki toplama biçimi aynı çocuk görevlerden çıkar ve biri sağlam, öteki yinelemeye duyarlıdır. Hangisinin doğru olduğunu görev grubu söylemez; toplamın anlamı söyler. Eşsiz kullanıcıları sayarken küme, sipariş tutarlarını toplarken toplam gerekir ve yanlış seçim sessizce yanlış sonuç üretir.

ES6. Toplam ölçümünde tamsayı toplamı kullanıldı; tamsayı toplamı değişmeli ve birleşmelidir. Kayan noktalı sayıların toplamı sıraya göre son basamakta farklanabilir, bu ölçülmedi.

İptal

Görev gruplarında iptal yapılandırmanın parçasıdır. Bir çocuk hata fırlatır ve grup kapsamı bunu yayarsa kalan çocuklar iptal edilir; group.cancelAll() ile bu elle de yapılır. İptal bir görevi öldürmez, ona istenmediğini bildirir. Görevin kendisi iptali yoklar ve temiz biçimde çıkar.

// Iptal: grup iptal edilince cocuklar yarida kesilebilir
func fetchAll(_ ids: [Int]) async throws -> [Int] {
    try await withThrowingTaskGroup(of: Int.self) { group in
        for id in ids {
            group.addTask {
                try Task.checkCancellation()
                return id * 2
            }
        }
        var results: [Int] = []
        for try await value in group {
            results.append(value)
            if results.count == 2 { group.cancelAll() }
        }
        return results
    }
}

Parçadaki Task.checkCancellation() iptal edilmiş bir görevde hata fırlatır. İptal, sonuçların kaçının toplandığını belirleyebilir; hangi çocukların kaçıncı sırada yetiştiği ise zamanlamaya bağlıdır. Bu yüzden parça yalnız okunur, koşturulmaz.

İptal Nasıl Çalışır

İptal işbirlikçidir: görev iptal edildi işaretini taşır, ama kendiliğinden durmaz. Görevin kodu bu işareti iki yoldan yoklar. Task.checkCancellation() iptal edilmişse hata fırlatır ve işlevi sonlandırır. Task.isCancelled ise yalnız bir Bool döndürür; böylece görev kısmi sonucu bırakıp temiz biçimde çıkabilir. Bir ağ isteği gibi sistemin kendi eşzamansız işlevleri iptali zaten yoklar ve iptal edilince hata fırlatır.

İptal aşağı doğru yayılır. Üst görev iptal edilince çocukları da iptal edilir; bir grup kapsamı hata fırlatarak çıkarsa, henüz bitmemiş çocuklar iptal edilir. Yukarı doğru yayılmaz: bir çocuğun iptali kardeşlerini ya da ebeveynini durdurmaz. Bu asimetri bilinmezse iki tür hata çıkar. Birincisi, uzun bir çocuğa iptal yoklaması koymamaktır; işaret taşınır, kimse bakmaz. İkincisi, iptali hata sanmaktır; iptal edilmiş bir istek kullanıcıya hata olarak gösterilmemelidir, çünkü kullanıcı zaten başka bir şeye geçmiştir.

Sık Yapılan Hatalar

  • Çocuk görevlerin bitiş sırasını gelen sonucun sırası sanmak. Sıra, görevlerin eklendiği sıra değildir.
  • Sonucu dizi olarak toplayıp sıralamayı unutmak. Sıra anlam taşımıyorsa sıralanmış küme güvenli bir toplamadır; taşıyorsa sıranın kaynağı ayrıca belirlenmelidir.
  • İptali yoklamayan uzun bir çocuk yazmak. İptal istenmiş olsa da çocuk bitene kadar sürer.
  • Görev grubunu kapsamın dışına taşımaya çalışmak. Grup yalnız kapsamı boyunca yaşar.

Sıra Gerekiyorsa

Geliş sırası anlam taşıyorsa, örneğin sonuçlar bir listede girdi sırasıyla gösterilecekse, çözüm sonuca sıra numarasını taşımaktır. Çocuklar yine istediği sırada biter, ama her biri nereye ait olduğunu bilir.

// Sirayi korumak gerekiyorsa sira numarasi sonuca tasinir
func loadInOrder(_ ids: [Int]) async -> [Int] {
    await withTaskGroup(of: (Int, Int).self) { group in
        for (index, id) in ids.enumerated() {
            group.addTask { (index, id * 2) }
        }
        var slots = [Int?](repeating: nil, count: ids.count)
        for await (index, value) in group { slots[index] = value }
        return slots.compactMap { $0 }
    }
}

Burada sonuç artık varış sırasına değil girdi sırasına bağlıdır. Girdi listesi sabitse çıktı da her koşumda aynıdır. Sıraya duyarlılık ortadan kalkmaz, kaynağı değişir: sonucun sırası zamanlamadan çıkar ve girdinin kendisine taşınır. Ölçtüğümüz gelis-sirasi satırı varış sırasını sınıflıyordu; sıra numarasıyla yazılan sürümde sınıflanan şey artık girdi sırasıdır. Bu ölçülmedi, yalnız kodun yapısından çıkarılan bir sonuçtur.

Yaygın yanlış kurulum, sıra numarası taşımadan sonuçları gelişine göre bir diziye eklemek ve ekranda bu diziyi girdi sırasıymış gibi göstermektir. Geliştirici makinesinde ağ hızlı ve dengelidir, çocuklar çoğunlukla başlatıldıkları sırayla biter ve hata fark edilmez. Yavaş bir bağlantıda üçüncü istek birinciden önce döner ve liste kullanıcının gözü önünde yer değiştirir. Düzeltme, yukarıdaki gibi sonuca konumu taşımaktır; konum bir kez yazıldığı için dizi her koşumda aynı çıkar.

Gerçek Uygulamada

Bir ürün sayfası açılırken fiyat, stok ve yorumlar aynı anda istenir; ekran üçü de gelince çizilir ya da gelen her parçayı hemen gösterir. İlk durumda async let yeterlidir. İkincisinde her parça için ayrı görev gerekir ve ekranın hangi sırayla güncellendiği artık tasarım kararıdır. Kullanıcı sayfadan ayrılırsa bu görevlerin hepsi iptal edilmelidir; görünüme bağlı bir görev (SwiftUI’daki task değiştiricisi bunun bir örneğidir) görünüm kaybolunca iptal edilir, başıboş bir Task ise edilmez.

Toplama yönteminin seçimi de buradan çıkar. Sayaçlar toplamla, eşsiz kimlikler kümeyle, görüntülenecek satırlar girdi sırasına bağlı dizi ile toplanır. Üçü de aynı görev grubunun üstüne kurulur; fark yalnız sonucu toplayan satırdadır ve ölçüm bu farkı görünür kılar.

Sınırlayıcı Ölçüm

Aynı işlem iki farklı durumla karşılaştırılırsa iki farklı sınıfa düşebilir. Geliş sırası işlemini yeniden ele alalım: aynı alfa ve beta eklemeleri, ama bu kez durum olarak listeyi değil, listenin yalnız eleman sayısını karşılaştırıyoruz.

// main.swift — ayni islem, iki durum secimi
let listTrial = replay("liste-gorunumu", from: [], record("alfa"), then: record("beta"))
let listView = listTrial.report()

let countTrial = Trial(label: "sayi-gorunumu", once: listTrial.once.count, twice: listTrial.twice.count, forward: listTrial.forward.count, backward: listTrial.backward.count)
let countView = countTrial.report()

countSensitivities([listView, countView])
liste-gorunumu | bir=["alfa"] iki=["alfa"] ab=["alfa", "beta"] ba=["beta", "alfa"] -> siraya-duyarli
sayi-gorunumu | bir=1 iki=1 ab=2 ba=2 -> saglam
saglam: 1
yinelemeye-duyarli: 0
siraya-duyarli: 1
cift-duyarli: 0

Liste görünümünde ab ile ba farklıdır ve işlem sıraya duyarlıdır. Sayı görünümünde ikisi de iki; kural farkı göremez ve işlem sağlam çıkar. Kural nedeni görmez, eşitsizliği görür; karşılaştırılan durumun seçimi sınıfı değiştirir. Bir toplamanın sağlam olduğunu söylemeden önce neyin karşılaştırıldığı yazılmalıdır. Sayım bir sağlam, bir sıraya duyarlı.

Hangi Biçimi Seçmeli

Seçim üç soruya iner. Çocuk sayısı kod yazılırken biliniyorsa async let en az gürültüyle aynı işi görür. Sayı çalışırken belirleniyorsa görev grubu gerekir. İş, çağıranın ömrüne bağlı olmamalıysa, örneğin bir kullanıcı eylemi sonrasında arka planda sürecek bir yükleme için, yapılandırılmamış görev kullanılır; ama bu durumda iptal ve hata yönetimi artık çağıranın değil sizin sorumluluğunuzdadır. Yanlış seçimin tipik belirtisi, her şey için Task { } açıp sonuçları paylaşılan bir değişkene yazmaktır. Kapsam güvencesi yitirilir; hangi görevin ne zaman biteceği, hata verirse kimin duyacağı ve ekran kapanınca kimin iptal edeceği belirsiz kalır; bu dersteki üç duyarlılık sorusu, her görev için ayrı ayrı yeniden açılır.

Özetle, görev grubunun verdiği güvence yapısaldır: hiçbir çocuk grubun dışına sızmaz. Sonucun anlamlı olup olmadığı ise toplama satırında verilen karardır.

Özet

  • Görev grubu, çocuk görevleri kapsam içinde başlatır ve kapsam bitmeden hepsinin bitmesini bekler.
  • Sonuçlar kümede toplanıp sıralanınca işlem geliş sırasından bağımsızdır ve sağlam çıkar.
  • Aynı sonuçlar geliş sırasıyla dizilince işlem sıraya duyarlıdır.
  • Aynı işlem, yalnız eleman sayısı karşılaştırılırsa sağlam görünür; durum seçimi sınıfı değiştirir.
  • İptal yapılandırmanın parçasıdır; çocuk iptali yoklamazsa sürer.
  • Bu dersin duyarlılığı: sağlam (kümede) ve sıraya duyarlı (dizide).

Sonraki Adım

Bir toplamanın sırayı gizlemesi, durumu paylaşan işlerin güvenli olduğu anlamına gelmez. Sıradaki derste aynı veriye birden çok yerden ulaşan işleri aktör yalıtımının nasıl sıraya soktuğunu ve iki istek arasındaki sıranın yine ölçülebilir kaldığını göreceğiz.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat