İçeriğe geç
academia.sh

Ders 21 / 21

Eşzamanlılık Güvenliği

Aktörün tembel özelliğinin eşzamanlı okumalarda bir kez kurulduğunu ve sonucu hiç beklenmeyen async let'in yine de değerlendirildiğini ölçer; Sendable ve @MainActor sınırlarını anlatır, kurs kapanışını yapar.

Yayın Güncelleme

Bu derste

  • Aktör yalıtımının ne sağladığını ve await ile aktör sınırının nasıl geçildiğini açıklar
  • Aktördeki lazy özelliğin birden çok eşzamanlı görevden okunsa da bir kez değerlendirildiğini ölçer
  • Sendable ve @MainActor ile veri yarışının derlemede nasıl engellendiğini ayırt eder
  • Sonucu hiç beklenmeyen async let bağlamasının yine değerlendirildiğini gösterir
İçindekiler

Önceki dersler tek akışta çalışan kodun değerlendirme anlarını ölçtü. Birden çok iş aynı anda aynı belleğe dokunduğunda başka bir soru doğar: bir değere aynı anda iki yerden yazılırsa ne olur. Buna veri yarışı (data race) denir ve sonucu tahmin edilemez. Swift 6 bu soruyu çalışma zamanına bırakmaz; katı eşzamanlılık denetimi (strict concurrency checking) yarışa yol açabilecek kodu derlemede reddeder.

Bu ders üç araç anlatır ve iki şey ölçer. Araçlar aktör yalıtımı, Sendable ve @MainActor’dır. Ölçümler bir aktörün tembel özelliğinin ne zaman kurulduğu ve async let ile başlatılan işin ne zaman koştuğudur. Aktörün içindeki sayaçlar düz Int’tir: Probe bir sınıftır, Sendable değildir ve aktör sınırını geçemez. Görevlerin koşma sırası belirsiz olduğundan sonuçlar toplanıp sıralanarak basılır; yalnız sayılar basılır.

// Probe.swift — bir ifadenin ne zaman degerlendirildigini kaydeder
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "hic" }
        if evaluations > 1 { return "her-kullanimda" }
        return evaluatedBeforeUse ? "hemen" : "ilk-kullanimda"
    }

    func report() {
        print("\(label): \(evaluations) degerlendirme, \(uses) kullanim -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["hemen", "ilk-kullanimda", "her-kullanimda", "hic"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}

Aktör Yalıtımı

Aktör (actor) durumunu kendi içinde saklayan ve bu duruma aynı anda tek bir görevin dokunmasına izin veren bir tiptir. Sınıfa benzer, ama değişebilir durumuna ve yalıtımlı yöntemlerine dışarıdan await yazmadan erişilemez; değişmez let özellikler ve nonisolated üyeler bunun dışındadır. Dışarıdaki bir görev aktörün yöntemini çağırırken await yazar: çağrı aktörün sırasına girer, aktör o an meşgulse görev bekler. Bu bekleme noktasına askıya alma noktası (suspension point) denir ve aktörün değişebilir durumuna await yazılmadan ulaşılamaz.

Aşağıdaki aktör iki sayaç tutar ve total adlı tembel bir özellik taşır. Özelliğin ifadesi bir yöntemi çağırır; yöntem kaç kez koştuğunu builds içinde sayar.

// Ledger.swift — tembel ozelligi olan aktor
actor Ledger {
    private(set) var builds = 0
    private(set) var reads = 0
    lazy var total: Int = makeTotal()

    private func makeTotal() -> Int {
        builds += 1
        return 42
    }

    func read() -> Int {
        reads += 1
        return total
    }
}

Ölçüm önce özelliğe hiç dokunmadan sayaçlara bakar, sonra dört görevi aynı anda read çağırmak üzere başlatır.

// main.swift — aktordeki lazy ozellik eszamanli okumalarda bir kez kurulur
let ledger = Ledger()
print("onceki kurulum \(await ledger.builds)")

let values = await withTaskGroup(of: Int.self) { group in
    for _ in 1...4 {
        group.addTask { await ledger.read() }
    }
    var all: [Int] = []
    for await value in group {
        all.append(value)
    }
    return all
}

print(values.sorted())
print("okuma \(await ledger.reads), kurulum \(await ledger.builds)")
onceki kurulum 0
[42, 42, 42, 42]
okuma 4, kurulum 1

Dört görev aynı özelliği okudu ama kurulum bir kez yapıldı. Özelliğe dokunulmadan önce kurulum sıfırdı: tembel ifade tanım satırında değil, ilk okumada çalıştı. Aktör yalıtımı burada iki işi birden yapar. Okumaları sıraya dizer ve ilk okumanın kurulumu bitmeden ikincisinin başlamasına izin vermez. Bu yüzden iki görevin aynı anda tembel ifadeyi çalıştırması ve sayacın bozulması olanaksızdır. Bu dersin anı ilk kullanımdadır.

Bu ölçümün ne söylemediğini de belirtmek gerekir. Aktör, işleri aynı anda yürütmeyi engellemez; yalnız aktörün durumuna dokunan kısmı sıraya dizer. Dört görev aynı anda başlar, ama read yöntemine tek tek girer. Tembel özelliğin tek kez kurulması bu sıralamanın doğrudan sonucudur: ikinci görev girdiğinde özellik zaten kuruludur.

Askıya alma noktasının bir bedeli vardır. Bir aktör yöntemi await ile başka bir işi beklerken aktör boşalır ve sıradaki görev içeri girebilir. Bu yüzden await öncesinde okunan bir değerin sonrasında aynı kaldığı varsayılmaz. Yalıtım iki görevin aynı anda içeride olmasını engeller; beklerken durumun değişmeyeceğini garanti etmez. Bu dersin ölçümlerinde yöntemlerin içinde await bulunmaz, bu yüzden bu durum ölçülmedi.

Aktarılabilir Tip ve Ana Aktör

Bir değer görevler arasında gidip geliyorsa bir yalıtım sınırını geçiyordur. Sınırı geçebilen tiplere Sendable denir; iş adıyla aktarılabilir tip. Sayılar, dizgiler ve yalnız aktarılabilir tipleri saklayan, genel (public) olmayan değer tipleri bu özelliği kendiliğinden taşır; genel tiplerde uyum açıkça yazılır. Aktörler de taşır, çünkü kendi içlerini zaten korurlar. Değiştirilebilir durum tutan sıradan bir sınıf ise taşımaz: iki görev aynı örneğe aynı anda yazabilir.

Probe bu yüzden aktör sınırını geçemez. Aşağıdaki parça yalnız kaynaktır ve çalıştırılmaz: bir Probe, başka bir görevin kapanışına yakalanır. Swift 6 bunu derlemede reddeder; hata metni burada basılmaz.

// Kaynak parca: calistirilmaz
let moved = Probe("tasinan")
let work = Task.detached { moved.eval(1) }
print(await work.value)

Neden reddedildiği bellidir: Task.detached ile başlayan iş ana aktörden bağımsız koşar, moved ise ana aktörün durumudur. Aynı örneğe iki yerden aynı anda dokunulabilirdi ve bu tam olarak veri yarışının tanımıdır. Derleyici örneğin hangi satırda iki yalıtım bölgesine sızdığını gösterir. Çözümlerden biri değeri taşımamak ya da onu yalıtım altına almaktır: sayacı bir aktörün içine koymak, bu dersteki ölçümlerin yaptığı gibi. Derleyici sorunu çalıştırmadan önce yakaladığı için hata, programın bozuk çalıştığı bir gün yerine yazıldığı gün görülür.

Ana aktör (@MainActor) önceden tanımlı, tek bir yalıtım bölgesidir. @MainActor ile işaretlenen bir tipin durumuna yalnız ana aktörde koşan kod dokunabilir; başka bir yerden gelen çağrı await ister. Bir betiğin üst düzey kodu da ana aktörde koşar. Aşağıdaki sınıf bu işareti taşır ve bir sayaç tutar.

// Screen.swift — ana aktore baglanan sinif
@MainActor
final class Screen {
    private(set) var taps = 0

    func tap() {
        taps += 1
    }
}

Ölçüm beş görevi arka planda başlatır; her biri tap yöntemini await ile çağırır.

// main.swift — ana aktore bagli sinifa baska gorevlerden await ile erisilir
let screen = Screen()

await withTaskGroup(of: Void.self) { group in
    for _ in 1...5 {
        group.addTask { await screen.tap() }
    }
}

print("dokunus \(screen.taps)")
dokunus 5

Beş çağrı beş artış verdi, kayıp yok. Çağrılar ana aktörün sırasına girdi ve tek tek işlendi. screen bir sınıf olduğu halde görevlere yakalanabildi: ana aktöre bağlı sınıflar aktarılabilir sayılır, çünkü durumlarına yalnız tek bir yalıtım bölgesinden dokunulur.

Görev Grupları

Yukarıdaki ölçümler görev grubu (task group) kullandı. Grup, sayısı çalışma zamanında belirlenebilen alt görevleri başlatır ve hepsinin bitmesini bekler; for await ile sonuçlar bittikçe toplanır. Sonuçların geliş sırası belirsizdir, bu yüzden dersin ölçümleri onları sıralayarak basar. Sayısı derleme anında bilinen birkaç iş için ise async let vardır; her bağlama tek bir alt görevdir; sonucu bağlamanın adıyla await edilerek okunur. Bir async let bağlaması sağ tarafındaki ifadeyi bir alt görev olarak başlatır; sonuç await ile okunur.

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

Soru şudur: async let ne zaman başlar. Akla gelen yanıt, sonucun istendiği satırdır, yani await yazılan yer. Bunu sınamak için bir bağlama sonucu okunur, ikincisi hiç okunmaz. Aktör iki bağlamanın da bump yöntemini çağırıp çağırmadığını sayar.

// Counter.swift — cagri sayisini tutan aktor
actor Counter {
    private(set) var calls = 0

    func bump() -> Int {
        calls += 1
        return calls
    }
}
// main.swift — async let kuruldugu satirda baslar, beklenmese de
let counter = Counter()

do {
    async let waited = counter.bump()
    async let ignored = counter.bump()
    _ = await waited
}

print("cagri \(await counter.calls)")
cagri 2

İki çağrı sayıldı. ignored hiç await edilmedi, ama kapsam bitmeden iş yine koştu: sonucun beklenmemesi işin koşmasına engel olmadı. Kapsam çıkışında Swift beklenmeyen alt görevi iptal eder ve bitmesini bekler; bump iptale bakmadığı için çalışmasını tamamlar. Bu ölçüm bir sınırı gösterir. Sıradan bir çağrıda ifade, çağrının yazıldığı yerde ve çağıran beklenerek değerlendirilir; async let ise beklemeden bir alt görev başlatır. İki kural farklı alanlara aittir: async let bir ifade değil bir görevdir, ve yan etkisi sonucun okunup okunmamasından bağımsızdır. Bu yüzden async let için değerlendirme kullanımı beklemez.

BL6. Askıya alma noktası içeren aktör yöntemleri ölçülmedi. Ölçümler tek bir aktör ve dört ya da beş eşzamanlı görevle yapıldı; çok daha kalabalık gruplarda aynı kuralın geçtiği gösterilmedi. Görevlerin hangi sırayla koştuğu ölçülmedi, yalnız sayılar okundu. İptal edilen bir görevin iptale bakan bir yöntemde yarıda kaldığı durum ölçülmedi. Ölçüm yalnız işin beklenmese de koştuğunu gösterir; işin bağlama satırında mı yoksa daha sonra mı başladığı bu ölçümle ayrılamaz.

Özet

  • Aktör, durumunu kendi içinde korur ve dışarıdan erişimi await ile sıraya dizer; aktörün değişebilir durumuna await yazmadan erişmek mümkün değildir.
  • Aktördeki lazy özellik birden çok eşzamanlı görevden okunsa da bir kez değerlendirilir ve ilk okumada kurulur.
  • Sendable olmayan bir değeri görevler arasında taşımak derlemede reddedilir; çözümlerden biri, bu derste ölçülen, değeri yalıtım altına almaktır.
  • async let bağlaması bir alt görev başlatır; sonucu hiç beklenmese de iş kapsam bitmeden koşar.
  • Bu dersin anı: ilk kullanımda. Aktörün tembel özelliği eşzamanlı okumalarda yalnız ilk okumada, bir kez kurulur.

Kurs Kapanışı

Bu kursun yirmi dört dersi tek bir soruyu yanıtladı: bir ifade ne zaman değerlendirilir. Her ders bir ifadeyi ölçtü ve anını dört addan biriyle söyledi. Tabloda ders, ölçülen ifade, an ve o dersin sınırı yan yana durur.

Ders Ölçülen ifade An Sınır
Swift’in Tasarım Hedefleri let sabitinin sağ tarafı hemen Aynı ifade tek Probe’a iki kez bağlanırsa kural her-kullanimda der
Sabitler ve Değişkenler Yerel sabitin ilk değeri hemen Başka dosyadaki küresel sabit ilk erişimde kurulur
Temel Tipler let satırındaki tip dönüşümü hemen Okunmayan demet elemanı da değerlendirilir
Dizgi İşlemleri İfade yerleştirmedeki description çağrısı her-kullanimda Bir let içinde saklanan dizgi bir kez kurulur
İşleçler Sol taraf doğruyken || işlecinin sağı hic Sağ tarafı @autoclosure ile almayan fonksiyon onu çağrıdan önce değerlendirir
Denetim Akışı Eşleşmeden sonraki where koşulu hic Döngüde koşul her turda, while çıkışta bir kez daha sorulur
Fonksiyonlar Varsayılan argüman ifadesi her-kullanimda Verilen argüman gövde onu okumasa da çağrıdan önce değerlendirilir
Kapanışlar Kapanış gövdesi her-kullanimda Yakalama listesindeki ifade kurulurken bir kez değerlendirilir
İsteğe Bağlı Değerler Boş kutuda map dönüştürücüsü hic Dış katmanı dolu .some(nil) kutusunda dönüştürücü koşar
İsteğe Bağlı Açma if let sağ tarafı hemen Zincirin boş halkasından sonrası atlanır, aynı satırdaki bağımsız ifade atlanmaz
Yapılar ve Sınıflar Yazarken kopyalanan belgenin kopyası ilk-kullanimda Tek sahipli ya da sınıf alanındaki belgede kopya hiç oluşmaz
Numaralandırmalar İlişkili değerin ifadesi hemen Numaralandırmanın hesaplanan özelliği her okunuşta koşar
Özellikler lazy özelliğin ilk değeri ilk-kullanimda Yapıda her kopya kendi lazy değerini ayrıca kurar
Protokoller Protokol uzantısındaki varsayılan gerçekleştirim her-kullanimda Anı protokol değil, gereksinimi karşılayan tip belirler
Uzantılar Uzantıyla eklenen hesaplanan özellik ve yöntem her-kullanimda Uzantıdaki static let ilk erişimde bir kez kurulur
Genellikler lazy.map dönüştürücüsü her-kullanimda Tembel dizinin okunmayan elemanı hiç dönüştürülmez
Hata Yönetimi throw sonrasındaki ifade hic defer bloğu her çıkışta koşar
Erişim Denetimi private lazy önbellek ilk-kullanimda Anı erişim düzeyi değil, özelliğin çeşidi belirler
Otomatik Referans Sayımı deinit gövdesi her-kullanimda İkinci referans varken bırakma deinit’i koşturmaz
Güçlü, Zayıf ve Sahipsiz Referanslar Tutma döngüsündeki deinit gövdesi hic Gitmiş örneğe unowned okuma ölçülemez, programı durdurur
Kapanışlarda Yakalama Yakalama listesindeki ifade hemen Saklanmayan kapanış döngü kurmaz
Özellik Sarmalayıcıları wrappedValue okuyucusu her-kullanimda @autoclosure alan sarmalayıcı anı ilk-kullanimda’ya çevirir
Sonuç Kurucular Seçilmeyen dalın ifadesi hic Hangi dalın seçileceği koşumda belirlenir
Eşzamanlılık Güvenliği Aktördeki lazy özellik ilk-kullanimda Sonucu beklenmeyen async let yine koşar

Yirmi dört dersin dağılımı şöyledir: altısı hemen, dördü ilk-kullanimda, sekizi her-kullanimda, altısı hic. Bu sayım tablonun an sütunundan çıkar. Kursun çekirdeğindeki sekiz ifadenin dağılımı ise 2, 1, 3 ve 2’dir. İkisi aynı şey değildir: çekirdek sekiz ifadeyi, tablo yirmi dört dersi sayar. Bir dersin anı o dersin ölçtüğü ifadeye aittir.

Kursun bitirdiği iş, ifadeyi kuralıyla birlikte okumayı öğretmektir. Aynı ifade yazılışına göre dört andan her birini verebilir: yerel sabit hemen, hesaplanan özellik her-kullanimda, lazy özellik ilk-kullanimda, seçilmeyen dal hic. Kursun bıraktığı alışkanlık, bir ifadeye bakınca onun yazılışından anını tahmin etmeye çalışmak yerine hangi kuralın altında durduğunu sormaktır. Bu kurs dilin kendisini ölçtü.

İ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