İçeriğe geç
academia.sh

Ders 07 / 19

Eşzamansız Fonksiyonlar

Duraklayabilen iki işlemin sırayla beklendiğinde stoğun hangi sırada ayrıldığına bağlı kaldığının, salt okuma işleminin ise sırayı görmediğinin ölçümü.

Yayın Güncelleme

Bu derste

  • Eşzamansız fonksiyonun duraklama noktasında bekleyen ve sonucu sonra dönen bir işlev olduğunu ayırır
  • İki ayırma işleminin iki sırada koşturulduğunda aynı stoğu bırakmadığını gerçekten ölçer
  • Durumu değiştirmeyen okuma işleminin her iki sırada da aynı sonucu verdiğini doğrular
  • Ölçümün sırayı çalışma zamanının değil dersin kurduğunu, araya girmeyi ise görmediğini söyler
İçindekiler

Önceki derste, Arka Plan Aktarımı, uygulama yeniden açılınca indirme listesinin denetlenmeden yeniden kuyruğa alınmasının aynı dosyayı iki kez indirttiğini gördük. Ağ katmanının açılış ölçümü altı işlemi dört sınıfa dağıtmıştı: bir sağlam, iki yinelemeye duyarlı, iki sıraya duyarlı, bir çift duyarlı. Bu konu aynı soruyu eşzamanlı koda taşır: iki işlem yinelenirse ya da sırası değişirse sonuç değişir mi? Sıra artık yalnız programcının yazdığı sıra değildir; bir işlem beklerken başka bir iş koşabilir.

Ölçüm çekirdek dosyayla yapılır. Bu 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)")
    }
}

Çekirdek bir işlemi dört kez koşturur: bir kez, iki kez, önce a sonra b, önce b sonra a. Bir ve iki arasındaki fark yinelemeye duyarlılığı, iki sıra arasındaki fark sıraya duyarlılığı gösterir. Eşzamansız işlemde replay kullanılmaz, çünkü ona verilen işlevler await edemez. Dört koşum await ile elle kurulur ve sonuçlar doğrudan Trial’a verilir. Model, işlemin duruma ne yaptığını kurar; ağın ya da çalışma zamanının kendisi değildir.

Duraklayabilen İşlev

Bir eşzamansız fonksiyon (async fonksiyon), çalışırken bir noktada durup sonucu beklemeyi göze alan işlevdir. Çağıran onu await ile çağırır. await yazılan her yer bir duraklama noktasıdır (suspension point): işlev orada durabilir, kontrolü bırakabilir ve sonuç hazır olunca kaldığı yerden sürer. Duraklarken iş parçacığı bloke olmaz; sistem o sürede başka işi koşturabilir.

KAYNAK tarafta duraklama noktası tipik olarak ağdadır. Aşağıdaki parça ölçülmez, çünkü gerçek bir istek ağa çıkar.

import Foundation

func fetchStock(for sku: String) async throws -> Int {
    let url = URL(string: "https://example.com/stock/\(sku)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return Int(String(decoding: data, as: UTF8.self)) ?? 0
}

Parçadaki tek await isteğin sonucunu bekler; try ise hatanın çağırana yayılabileceğini söyler. Ölçülen katman bu çağrıyı kurmaz. Onun yerine duruma ne olduğuna bakar: bir raf, iki ayırma isteği ve ayırmanın yetersiz stokta yapılmaması.

// Stock.swift — raf stoku: yetersiz stokta ayirma yapilmaz
actor StockShelf {
    private(set) var quantity: Int
    private(set) var accepted: [Int] = []

    init(_ quantity: Int) { self.quantity = quantity }

    func reserve(_ amount: Int) -> Int {
        if quantity >= amount {
            quantity -= amount
            accepted.append(amount)
        }
        return quantity
    }

    func remaining() -> Int { quantity }
}

func runReserve(_ amounts: [Int]) async -> Int {
    let shelf = StockShelf(10)
    var left = 10
    for amount in amounts { left = await shelf.reserve(amount) }
    return left
}

func runReads(_ times: Int) async -> Int {
    let shelf = StockShelf(10)
    var seen = 0
    for _ in 0..<times { seen = await shelf.remaining() }
    return seen
}

StockShelf bir aktördür; aktörler sonraki derslerin konusudur, burada yalnız duraklayabilen bir durum sahibi olarak kullanılır. Rafta on birim vardır. runReserve, her çağrıda taze bir raftan başlar ve verilen miktarları sırayla, her birini await ederek ayırır. Dönen sayı rafta kalan stoktur.

Çağrı Zinciri

await yalnız async bir bağlamda yazılabilir. Bir async işlevi çağıran işlevin kendisi de async olmalıdır; bu zincir yukarı doğru uzar ve en tepede sıradan, eşzamanlı bir kodla karşılaşır. Oradaki köprü bir Task’tır: eşzamanlı bağlamdan eşzamansız işi başlatır ve kendisi beklemez. Bir düğmenin dokunma işleyicisi bunun tipik örneğidir.

// Eszamanli baglamdan eszamansiz ise gecis: dokunma islemcisi
func refreshTapped() {
    Task {
        do {
            let left = try await fetchStock(for: "sku-1")
            print("kalan: \(left)")
        } catch {
            print("hata: \(error)")
        }
    }
}

Task gövdesi hemen döner; refreshTapped sonucu beklemez. Bu yüzden iki dokunma iki bağımsız iş başlatır ve ikisi de aynı rafa ulaşabilir. Ölçümün iki sırasının gerçek olmasının nedeni budur: hangi isteğin sunucuya önce varacağını ya da hangi yanıtın önce döneceğini işlevi yazan bilmez. Hatanın yayılması da burada kesilir. Task içinde yakalanmayan bir hatayı kimse görmez, çünkü görevin sonucunu bekleyen yoktur. Hata yönetimini görevin içinde kurmak bu yüzden gerekir.

Ayırma Sırası

İki istek aynı rafa gelir: altı birim ve beş birim. Birlikte on birimden fazladır, yani ikisi sığmaz. Hangisinin önce ayrıldığı sonucu belirler. Dört koşum ayrı ayrı kurulur: yalnız altı, altı iki kez, önce altı sonra beş, önce beş sonra altı.

// main.swift — iki ayirma iki sirada, bir okuma
let single = await runReserve([6])
let repeated = await runReserve([6, 6])
let sixThenFive = await runReserve([6, 5])
let fiveThenSix = await runReserve([5, 6])
let reservation = Trial(label: "stok-ayirma", once: single, twice: repeated, forward: sixThenFive, backward: fiveThenSix).report()

let readOnce = await runReads(1)
let readTwice = await runReads(2)
let reading = Trial(label: "stok-okuma", once: readOnce, twice: readTwice, forward: readTwice, backward: readTwice).report()

countSensitivities([reservation, reading])
stok-ayirma | bir=4 iki=4 ab=4 ba=5 -> siraya-duyarli
stok-okuma | bir=10 iki=10 ab=10 ba=10 -> saglam
saglam: 1
yinelemeye-duyarli: 0
siraya-duyarli: 1
cift-duyarli: 0

Ayırma satırında bir=4: altı birim ayrıldı, dört kaldı. iki=4: ikinci altı sığmadı ve reddedildi, yani yineleme stoğu değiştirmedi. ab=4 ve ba=5: önce altı ayrılınca beş sığmadı ve dört kaldı; önce beş ayrılınca altı sığmadı ve beş kaldı. İki sıra iki farklı stok bıraktı, bu yüzden işlem sıraya duyarlıdır. Okuma satırında dört değer de on: okuma duruma dokunmadığı için ne yineleme ne sıra bir şey değiştirir ve işlem sağlamdır. Sayım bir sağlam, bir sıraya duyarlı.

ES1. Bu ölçüm iki await arasına başka bir işin girmediğini varsayar. Her koşum kendi rafından başlar ve istekler yazıldığı sırayla işlenir; çalışma zamanının sırayı değiştirip değiştirmeyeceği ölçülmedi.

Ayrı Raflar

Sıraya duyarlılık yalnız işlemin değil, işlemlerin paylaştığı durumun özelliğidir. Bunu görmek için aynı iki isteği, altı ve beş birimi, bu kez iki ayrı rafa gönderiyoruz. Her isteğin hangi rafa gittiği çiftle verilir.

// main.swift — ayni iki istek, iki ayri raf
func runShelves(_ requests: [(shelf: Int, amount: Int)]) async -> [Int] {
    let shelves = [StockShelf(10), StockShelf(10)]
    for request in requests { _ = await shelves[request.shelf].reserve(request.amount) }
    let firstLeft = await shelves[0].remaining()
    let secondLeft = await shelves[1].remaining()
    return [firstLeft, secondLeft]
}

let apartOnce = await runShelves([(0, 6)])
let apartTwice = await runShelves([(0, 6), (0, 6)])
let apartForward = await runShelves([(0, 6), (1, 5)])
let apartBackward = await runShelves([(1, 5), (0, 6)])
let apart = Trial(label: "ayri-raflar", once: apartOnce, twice: apartTwice, forward: apartForward, backward: apartBackward).report()
countSensitivities([apart])
ayri-raflar | bir=[4, 10] iki=[4, 10] ab=[4, 5] ba=[4, 5] -> saglam
saglam: 1
yinelemeye-duyarli: 0
siraya-duyarli: 0
cift-duyarli: 0

İkinci altı yine ilk rafa sığmadığı için reddedilir ve bir ile iki aynı kalır. ab ile ba ise aynı iki rafı aynı durumda bırakır, çünkü iki istek birbirinin göreceği bir şeyi değiştirmez. İşlem sağlamdır. Aynı altı ve beş birimlik çift tek rafa gittiğinde sıraya duyarlıydı; fark isteklerde değil, onların ortak bir duruma dokunup dokunmamasındadır. Pratikte bu, sırayı sabitlemenin maliyetini nereye ödeyeceğinizi söyler: yalnız gerçekten paylaşılan durum için. Sayım bir sağlam.

ES2. Ayrı raflar birbirinden tamamen bağımsız sayıldı. İki rafın arkasında ortak bir depo ya da toplam bir stok sınırı varsa ayrılık yanıltıcı olur; böyle bir ortak sınır ölçülmedi.

Duraklama Noktasında Ne Olur

Ölçümün görmediği şey await içinde olur. İşlev durduğunda aynı aktöre ya da aynı veriye başka bir işin ulaşması mümkündür. Önce koşan iş, sonra koşan işin gördüğü durumu değiştirmiş olabilir; durumu await’ten önce okuyup await’ten sonra kullanan kod bu yüzden dikkat ister. Bu araya girmenin hangi sırayla olacağı sistemin zamanlamasına bağlıdır ve her koşumda aynı olmak zorunda değildir. Bu derste bunu koşturmuyoruz: sıralı iki çağrı, araya girme olmadan, deterministik bir deneydir.

Pratik kural şudur: durumu okuyan ve ona göre karar veren kod, karar ile yazma arasında await bırakmamalıdır. Aksi hâlde karar, yazıldığı anda artık doğru olmayabilir. Raftaki örnekte reserve işlevi kontrol ile yazmayı aynı adımda yapar ve aradaki hiçbir await yoktur; bu yüzden tek başına tutarlıdır. Tutarlı olması, iki isteğin sırasının sonucu etkilemediği anlamına gelmez; ölçüm tam bunu gösterdi.

Karar ile Yazma Arasındaki Boşluk

Ölçülmeyen ama sık görülen bir kurulum, kontrolü ve yazmayı iki ayrı await ile yapmaktır. Aşağıdaki işlev önce rafa kaç birim kaldığını sorar, sonra ayırmayı ister.

// Yanlis kurulum: karar ile yazma arasinda await var
func reserveCarelessly(_ amount: Int, on shelf: StockShelf) async -> Bool {
    let available = await shelf.remaining()
    guard available >= amount else { return false }
    _ = await shelf.reserve(amount)
    return true
}

İki await arasında raf başka bir istekle değişmiş olabilir. O zaman available artık doğru değildir, reserve isteği sessizce reddeder ve işlev yine de true döner. Çağıran ayırmanın yapıldığını sanır, oysa stok hiç düşmemiştir. Düzeltme, kararı ve yazmayı aktörün tek bir yordamında toplamak ve sonucu o yordamdan döndürmektir.

// StockShelf'e eklenen yordam: karar ve yazma tek adimda
func tryReserve(_ amount: Int) -> Bool {
    guard quantity >= amount else { return false }
    quantity -= amount
    accepted.append(amount)
    return true
}

Yordam async değildir ve içinde await yoktur; aktör onu başka bir işe bölmeden bitirir. Dönen değer, ayırmanın gerçekten yapıldığını söyler. Ardışık koşumlarda iki kurulum aynı sonucu verir, bu yüzden fark ölçümde görünmez; yalnız araya girme olduğunda görünür. Ölçülmeyen bir farkı kodun şeklinden okumak bu yüzden önemlidir.

Gerçek Uygulamada

Bu kalıp sepet ve rezervasyon ekranlarında sürekli karşılaşılır. Kullanıcı “ayır” düğmesine iki kez dokunur, iki ekran aynı ürüne bakar ya da sunucudan gelen bir yenileme, kullanıcının az önce yaptığı ayırmayla yarışır. Her birinde işlemlerin kendisi doğrudur; sorun, hangisinin önce sonuçlandığıdır. Ağ yanıtlarının dönüş sırası istek sırasıyla aynı olmak zorunda değildir ve eşzamansız kod bu sıranın üstüne bir şey koymaz.

Bu yüzden iki soru her zaman birlikte sorulur. İşlem tekrarlanırsa ne olur? İki işlem yer değiştirirse ne olur? Birinci sorunun yanıtı yinelemeye duyarlılığı, ikincisininki sıraya duyarlılığı verir. Okuma gibi durumu değiştirmeyen işlemler ikisinden de bağışıktır; bu yüzden ekranların çoğu okumayı serbestçe paylaşır, yazmayı ise tek bir yere toplar.

Sık Yapılan Hatalar

  • await yazmanın işlemin sırasını güvenceye aldığını sanmak. await yalnız çağırıp beklemeyi söyler; başka çağrıların sırasını belirlemez.
  • Okuma ile yazmayı tek bir adım saymak. Aralarında bir duraklama noktası varsa durum o sürede değişmiş olabilir.
  • Hata yayılmasını unutmak. throws işaretli bir async işlev, await’in yanında try da ister; try atlanırsa kod derlenmez.
  • Ölçümün sırasını gerçek sıra sanmak. Dört koşum elle kurulmuş iki sıradır; çalışma zamanının seçtiği sıra değildir.

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

Kural, durumda ne varsa onu karşılaştırır. Yalnız kalan stoğa bakan bir kural, hangi isteklerin kabul edildiğini görmez. Aşağıda altı ve dört birim ayrılır; ikisi de sığar ve rafı tamamen boşaltır.

// main.swift — ayni iki istek, iki farkli durum secimi
func runLedger(_ amounts: [Int]) async -> [Int] {
    let shelf = StockShelf(10)
    for amount in amounts { _ = await shelf.reserve(amount) }
    return await shelf.accepted
}

let leftSingle = await runReserve([6])
let leftRepeated = await runReserve([6, 6])
let leftForward = await runReserve([6, 4])
let leftBackward = await runReserve([4, 6])
let leftover = Trial(label: "kalan-stok", once: leftSingle, twice: leftRepeated, forward: leftForward, backward: leftBackward).report()

let ledgerSingle = await runLedger([6])
let ledgerRepeated = await runLedger([6, 6])
let ledgerForward = await runLedger([6, 4])
let ledgerBackward = await runLedger([4, 6])
let ledger = Trial(label: "kabul-defteri", once: ledgerSingle, twice: ledgerRepeated, forward: ledgerForward, backward: ledgerBackward).report()

countSensitivities([leftover, ledger])
kalan-stok | bir=4 iki=4 ab=0 ba=0 -> saglam
kabul-defteri | bir=[6] iki=[6] ab=[6, 4] ba=[4, 6] -> siraya-duyarli
saglam: 1
yinelemeye-duyarli: 0
siraya-duyarli: 1
cift-duyarli: 0

Kalan stok iki sırada da sıfırdır; kural fark görmez ve işlem sağlam çıkar. Kabul defteri ise iki sırada [6, 4] ve [4, 6] tutar; aynı iki istek bu kez sıraya duyarlıdır. İki istek de aynıydı, değişen karşılaştırılan durumdur. Sayım bir sağlam, bir sıraya duyarlı.

ES3. Bu ölçüm kabul defterinin sırasını durumun parçası sayar. Sıranın ayrıca anlam taşıyıp taşımadığı, örneğin bir faturanın satır sırası olup olmadığı ölçülmedi.

Okuma Sırası ve Beklenti

Bu dersteki ölçümleri okurken tek bir soruyu akılda tutmak yeter: aynı istek iki kez, ya da iki istek iki sırada çalışırsa son durum değişir mi? Raf örneğinde farklı raflara yapılan istekler birbirini etkilemediği için sıra ve yineleme önemsizdir. Aynı rafa gelen iki istek ise ortak bir durumu değiştirir ve sonuç çağrıların sırasına bağlıdır. Eşzamansız işlev bu ayrımı ortadan kaldırmaz; yalnız iki çağrının birbirine karışabileceği aralığı uzatır. Her await, işlevin durdurulup başka işin araya girebileceği bir noktadır ve ortak duruma dokunan kodda bu noktaların nerede olduğunu bilmek, ölçümün gösterdiği duyarlılığı önceden tahmin etmenin en kısa yoludur.

Özet

  • Eşzamansız fonksiyon duraklama noktasında bekleyebilen işlevdir; çağıran onu await ile çağırır.
  • Yetersiz stokta ayırma yapılmayan iki istek iki sırada farklı stok bıraktı; işlem sıraya duyarlıdır.
  • Durumu değiştirmeyen okuma işlemi iki sırada da aynı sonucu verdi ve sağlam çıktı.
  • Kural yalnız durumda olanı görür: kalan stok sağlam, kabul defteri sıraya duyarlı çıktı.
  • Araya girme ölçülmedi; sıra, dersin kurduğu sıradır.
  • Bu dersin duyarlılığı: sıraya duyarlı.

Sonraki Adım

Bir işlev beklerken başka işler de koşabiliyorsa, birden çok işi birlikte başlatmanın bir yolu gerekir. Sıradaki derste görev grubunun sonuçları toplanırken toplamın sağlam kaldığı, sonuçları geliş sırasıyla dizmenin ise sıraya duyarlı olduğu ölçülecek.

İ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