Ders 10 / 19
Ana İş Parçacığı Yalıtımı
Arayüzün gösterdiği listeyi güncelleyen işlemin, sürüm numarası olmadan geliş sırasına bağlı kaldığının, sürümle ise sağlamlaştığının ölçümü.
Yayın Güncelleme
Bu derste
- Arayüz durumunun ana iş parçacığına ve ana aktöre bağlı olduğunu açıklar
- Sürümsüz bir liste güncellemesinin geliş sırasına duyarlı olduğunu ölçer
- Sürüm kontrolüyle aynı güncellemenin sağlamlaştığını ölçer
- Sürüm numarasını kimin verdiğinin sınıfı değiştirdiğini gösterir
İçindekiler
Önceki derste aktör yalıtımı veri yarışını önledi ama iki isteğin sırasını sabitlemedi. Bir ekranın gösterdiği liste de bu tür bir paylaşılan durumdur: arama kutusuna yazdıkça yeni istekler gider ve yanıtlar hangi sırayla gelirse gelsin ekrana yazılır. Bu derste bu durumu ölçüyoruz.
Ç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 işlemler duruma yalnız bir liste yazar ve eşzamansız değildir; bu yüzden replay ile ölçülür. Model, işlemin duruma ne yaptığını kurar; ağın ya da ekranın kendisi değildir. Hangi iş parçacığının hangi işi koşturduğu ölçülmez, çünkü ölçüm bunu göremez.
Ana İş Parçacığı ve Ana Aktör
Arayüz yalnız ana iş parçacığında güncellenir. Eşzamanlı kodda bu kural ana aktörle (@MainActor) ifade edilir: ana aktöre bağlı bir tipin durumuna yalnız ana aktörden erişilir ve derleyici bunu denetler. Arka plandaki bir iş sonucunu ekrana yazmak istediğinde await ile ana aktöre geçer.
Bu, arayüzün tutarlılığını korur ama yarışı çözmez. Arka planda iki arama isteği koşuyorsa ikisinin de sonucu sonunda ana aktöre gelir ve sonuç hangisi geç gelirse ekranda o kalır. Yanıt sırası istek sırasından farklı olabilir.
// Shown.swift — ekranda gosterilen liste ve iki guncelleme bicimi
struct ShownList: Equatable, CustomStringConvertible {
var version: Int
var items: [String]
var description: String { "v\(version) \(items)" }
func overwriting(items newItems: [String]) -> ShownList {
ShownList(version: version, items: newItems)
}
func applying(version newVersion: Int, items newItems: [String]) -> ShownList {
newVersion > version ? ShownList(version: newVersion, items: newItems) : self
}
}
overwriting gelen listeyi sürüme bakmadan ekrana yazar. applying ise yalnız gelen sürüm gösterilen sürümden büyükse yazar; eski sürümlü bir yanıt gelirse yok sayar.
Sürümsüz ve Sürümlü Güncelleme
İki yanıt var: eski sorgunun sonucu (sürüm 1, eski) ve yeni sorgunun sonucu (sürüm 2, yeni). a eski yanıtı, b yeni yanıtı yazar. İki sırada, sürümsüz ve sürümlü yazım ayrı ayrı ölçülür.
// main.swift — ayni iki yanit, surumsuz ve surumlu yazim
let start = ShownList(version: 0, items: [])
let blind = replay("surumsuz-yazim", from: start,
{ $0.overwriting(items: ["eski"]) },
then: { $0.overwriting(items: ["yeni"]) }).report()
let checked = replay("surumlu-yazim", from: start,
{ $0.applying(version: 1, items: ["eski"]) },
then: { $0.applying(version: 2, items: ["yeni"]) }).report()
countSensitivities([blind, checked])
surumsuz-yazim | bir=v0 ["eski"] iki=v0 ["eski"] ab=v0 ["yeni"] ba=v0 ["eski"] -> siraya-duyarli surumlu-yazim | bir=v1 ["eski"] iki=v1 ["eski"] ab=v2 ["yeni"] ba=v2 ["yeni"] -> saglam saglam: 1 yinelemeye-duyarli: 0 siraya-duyarli: 1 cift-duyarli: 0
Sürümsüz yazımda yinelemek bir şey değiştirmez: bir ile iki aynı. Ama ab yeni, ba eski: ekranda son gelen kalır. Eski sorgunun yanıtı geç gelirse yeni sonucu ezer. İşlem sıraya duyarlıdır. Sürümlü yazımda eski yanıt, yeni yanıttan sonra gelse de yok sayılır; iki sıra da v2 ["yeni"] verir ve yinelemek de bir şey değiştirmez. İşlem sağlamdır. Sayım bir sağlam, bir sıraya duyarlı.
ES11. Ölçüm yanıtların kaybolmadığını, yalnız sıralarının değişebildiğini varsayar. Bir yanıtın hiç gelmediği ya da hata olduğu durum ölçülmedi.
ES12. Sürüm numaralarının 1 ve 2 olarak istek sırasıyla verildiği varsayılır. Numaranın kimin elinde üretildiği aşağıdaki sınırlayıcı ölçümde ele alınır.
Ekran Neden Tek Yerde Güncellenir
Ekranı çizen sistem tek bir akış üzerinde çalışır: bir karede neyin nerede görüneceğine karar verirken, gösterilen verinin o sırada değişmemesi gerekir. Liste çizilirken başka bir iş listeye eleman eklese, çizim ya eski ya yeni ya da yarım bir görüntü üretirdi. Bu yüzden arayüze dokunan her işlem tek bir yerde, ana iş parçacığında toplanır ve sırayla yürür. Ana aktör bu kuralın derleyici tarafından denetlenen biçimidir: @MainActor işaretli bir tipe başka yerden await olmadan dokunmak derleme hatasıdır.
Ana aktörün kazandırdığı şey kısaca şudur: ekranın durumunu yazan kod her zaman aynı yerde, tek tek çalışır. Kaybettirdiği şey de aynı kuraldan çıkar: ana aktörde yapılan her uzun iş ekranı bekletir. Bu yüzden iş bölünür. Ağ isteği, ayrıştırma ve sıralama arka planda yapılır; yalnız son adım, yani hazır listenin ekran durumuna yazılması ana aktörde olur. Bu dersin ölçümleri o son adımı modeller.
Ana Aktörde Güncelleme
Aynı kuralın gerçek koddaki karşılığı şudur. Parça ölçülmez, çünkü ana aktöre bağlı ekran durumu sandbox’ta anlamlı bir çıktı vermez.
import Foundation
@MainActor
final class SearchResults {
private(set) var items: [String] = []
private var shownVersion = 0
func apply(version: Int, items newItems: [String]) {
guard version > shownVersion else { return }
shownVersion = version
items = newItems
}
}
apply yalnız daha yeni bir sürümü kabul eder. Sürüm numarası, isteği başlatan kodda üretilir: arama her başladığında sayaç bir artar ve numara isteğe bağlanır. Yanıt geldiğinde numarasını taşır.
Yükleniyor Göstergesi
Arama ekranlarında bir de dönen gösterge bulunur: istek giderken görünür, yanıt gelince kaybolur. Göstergeyi tek bir Bool ile tutmak akla en yakın kurulumdur ve tek istek varken doğru çalışır. İki istek üst üste bindiğinde ise ilk yanıt göstergeyi kapatır, ikinci istek hâlâ sürerken ekran bitmiş görünür. Alternatif, süren isteklerin sayısını tutmaktır: başlangıçta bir artar, yanıtta bir azalır, gösterge sayı sıfırdan büyük olduğu sürece görünür.
İki kurulum aynı çifte, yani başlatma ve bitirme işlemlerine ayrı ayrı uygulanır. Ölçüm tek bir başlatma ile tek bir bitirmeyi sınar; üst üste binen iki isteği doğrudan kurmaz.
// main.swift — yukleniyor gostergesi: bayrak ve sayac
let flag = replay("bayrak", from: false,
{ _ in true },
then: { _ in false }).report()
let counter = replay("sayac", from: 0,
{ $0 + 1 },
then: { max($0 - 1, 0) }).report()
countSensitivities([flag, counter])
bayrak | bir=true iki=true ab=false ba=true -> siraya-duyarli sayac | bir=1 iki=2 ab=0 ba=1 -> cift-duyarli saglam: 0 yinelemeye-duyarli: 0 siraya-duyarli: 1 cift-duyarli: 1
Bayrak satırında yinelemek bir şey değiştirmez, çünkü ikinci başlatma göstergeyi zaten açık bulur. Ama ab ile ba ayrılır: bitirme başlatmadan önce gelirse gösterge açık kalır. Bayrak sıraya duyarlıdır. Sayaç satırı ise çift duyarlı çıktı, ve bu bir kusur değil, sayacın ne olduğunun doğru sonucudur: her başlatma gerçekten bir istek daha demektir, bu yüzden iki başlatma bir başlatmadan farklıdır. Duyarlılığın doğru okunuşu şudur: bir sınıf “iyi” ya da “kötü” değildir, işlemin ne iddia ettiğini gösterir. Sayaç iki başlatmayı iki istek olarak sayar ve bunu yapması gerekir; hata, aynı başlatma bildirimi yinelenerek iki kez uygulandığında ortaya çıkar. Yinelenen bildirim ağ katmanlarında ve olay yayımcılarında görülür; sayaçla çalışan bir gösterge bu durumda sonsuza dek açık kalır.
ES13. Bayrak ve sayaç ölçümü, tek başlatma ile tek bitirmeyi kurar. Üst üste binen iki isteğin dört işlemlik sıralamaları (başlat, başlat, bitir, bitir) ölçülmedi.
Gerçek Uygulamada
Bir arama ekranında kullanıcı her harfe bastığında istek gider. En sık görülen kurulum, her değişiklikte bağımsız bir görev başlatıp sonucu doğrudan ekran durumuna yazmaktır.
// Yanlis: her harf yeni gorev acar, eski gorev iptal edilmez
@MainActor
final class SearchModel {
private(set) var results: [String] = []
func queryChanged(_ text: String) {
Task {
let found = await search(text)
results = found
}
}
}
Bu kod sürümsüz yazımın ta kendisidir. Hızlı yazan kullanıcı için a, ab ve abc istekleri art arda gider; yavaş ağda ab yanıtı abc yanıtından sonra gelirse ekran abc yazarken ab sonuçlarını gösterir. Geliştirici makinesinde ağ hızlı ve düzenlidir, hata görünmez; üretimde bir mobil bağlantıda görünür.
Düzeltmenin iki yolu vardır ve çoğu zaman ikisi birlikte kullanılır. Birincisi önceki görevi iptal etmektir: görevin tutamacı saklanır, yeni harf gelince eskisi iptal edilir ve eski görev sonucu yazmadan önce iptal edilip edilmediğine bakar. İkincisi, dersin ölçtüğü gibi sürüm numarasıdır: numara istek başlarken, yani ana aktörde ve sırayla verilir, yanıtla birlikte döner ve apply daha eski numarayı elemeye devam eder. İptal gereksiz ağ işini durdurur; sürüm ise iptale rağmen ekrana ulaşmış bir yanıtı eler. İptal yoklaması yapılmayan bir görev işaretini taşıyıp işini bitirebilir, bu yüzden iptali tek başına yeterli saymak da bir kurulum hatasıdır.
Bu parça ölçülmedi: gerçek Task zamanlaması, ağ gecikmesi ve iptalin görevin hangi noktasında göründüğü ardışık bir koşumda kurulmaz. Ölçülen, yazım kuralının kendisidir.
Sık Yapılan Hatalar
- Ekran durumunu arka plan işinden doğrudan değiştirmeye çalışmak. Ana aktöre
awaitile geçmek gerekir. - Sonucun ekrana yazılma anını isteğin başlama anıyla karıştırmak. Yazım, yanıt geldiğinde olur.
- Eski bir yanıtı yeni olanın üstüne yazmak. Sürüm ya da iptal olmadan bunu engelleyen bir şey yoktur.
- Ana aktörü uzun bir hesabın işyeri yapmak. Ekran o sürede tepkisiz kalır.
Sınırlayıcı Ölçüm
Sürümün işe yaraması, numarayı kimin verdiğine bağlıdır. Numarayı isteği başlatan kod verirse eski yanıt eski numarayı taşır ve elenir. Numarayı yanıt geldiğinde ekran verirse her yanıt öncekinden yeni görünür.
// main.swift — surum numarasi gelisinde verilirse
let stampedStart = ShownList(version: 0, items: [])
let stamped = replay("gelisinde-damga", from: stampedStart,
{ $0.applying(version: $0.version + 1, items: ["eski"]) },
then: { $0.applying(version: $0.version + 1, items: ["yeni"]) }).report()
countSensitivities([stamped])
gelisinde-damga | bir=v1 ["eski"] iki=v2 ["eski"] ab=v2 ["yeni"] ba=v2 ["eski"] -> cift-duyarli saglam: 0 yinelemeye-duyarli: 0 siraya-duyarli: 0 cift-duyarli: 1
Aynı iki yanıt, aynı applying kuralı; yalnız numara değişti. Her yanıt gelişinde bir üst numara aldığı için hiçbiri elenmedi. bir=v1 ile iki=v2 farklıdır: ikinci uygulama sürümü yine artırdı ve işlem yinelemeye duyarlı oldu. ab ile ba da farklıdır. İşlem çift duyarlıdır. Kural nedeni görmez, eşitsizliği görür; sürüm kontrolü vardı ama numarayı yanlış yerde veren kod onu etkisiz kıldı. Sayım bir çift duyarlı.
ES14. Bu ölçüm sürümü yanıt gelişinde tek bir adımla artıran bir modeldir. Numaranın bir sayaçtan değil de bir zaman damgasından üretildiği durum ölçülmedi.
Yanıt Gelmediğinde
Ölçümler her yanıtın sonunda geldiğini varsayar. Gerçek ağda bir yanıt hiç gelmeyebilir ya da hata ile dönebilir ve bu durum ekran için ayrı bir karardır. En yaygın kurulum hatası, hatayı yutup gösterge açık bırakmaktır: kullanıcı sonsuza dek dönen bir göstergeye bakar. Hata durumu da bir sürümü taşımalıdır; çünkü eski bir isteğin geç gelen hatası, yeni isteğin başarılı sonucunun üstüne hata mesajı yazmamalıdır. Kural aynıdır: ekran durumuna yazan her yol, yazmadan önce kendi isteğinin hâlâ güncel olup olmadığını sorar.
Bunun pratik sonucu, ekran durumunu tek bir yazma kapısından geçirmektir. Başarı, hata ve iptal aynı işleve gelir ve işlev sürümü bir kez denetler. Üç ayrı yerde üç ayrı denetim yazan kod, bir gün bu denetimlerden birini unutur; unutulan yer genellikle en nadir görülen yoldur, yani hata yoludur ve kimse onu geliştirme sırasında denemez. Ölçümlerimiz de bu yüzden iki yanıtlı basit bir modelle başlar: önce temel kuralı görünür kılar, sonra kuralın hangi yollara uygulanacağı sorusunu okura bırakır.
Ekranın dayanıklılığını sınamanın yolu, yanıtları kasıtlı olarak ters sırada ve geciktirerek göndermektir. Ağı yavaşlatan bir geliştirici ayarı ya da yanıt sırasını değiştiren bir sahte sunucu, sürümsüz yazımı dakikalar içinde ortaya çıkarır. Bu sınama ölçümün yerini almaz; ölçüm kuralın neden çalıştığını, sınama ise uygulamanın kuralı gerçekten uyguladığını gösterir.
Ana Aktörü Hafif Tutmak
Ana aktör tek bir yoldur ve üzerinde ne kadar iş yapılırsa ekran o kadar geç yanıt verir. Pratik bir ölçü, bir işin ana aktörde geçirdiği sürenin bir kare süresinden kısa kalmasıdır; bu süre ekranın yenileme hızına bağlıdır. Bunun üstüne çıkan her iş, kaydırma sırasında takılma olarak görünür. Büyük bir listeyi sıralamak, bir görseli çözmek ya da bir yanıtı ayrıştırmak bu eşiği hızla aşar; bu işler arka planda yapılır ve ana aktöre yalnız hazır sonuç taşınır.
Burada ince bir nokta var: sonuç ana aktöre taşınırken sürüm de taşınmalıdır. Arka plandaki iş başladığında sürüm numarasını alır, bitince ana aktöre bu numarayla döner ve ana aktör numarayı güncel numarayla karşılaştırır. Numarayı arka planın bitiş anında üretmek, dersin son ölçümündeki hatanın aynısıdır. Bu yüzden numaranın kaynağı her zaman işin başladığı yerdir. Numara başlangıçta bir kez alınır, iş boyunca değişmez ve sonuçla birlikte geri döner; böylece ekrana yazan kod, hangi isteğin yanıtını taşıdığını her zaman bilir. Bu bilgi olmadan hiçbir denetim güvenilir olmaz.
Özet
- Arayüz durumu ana iş parçacığına bağlıdır; eşzamanlı kodda bu ana aktörle ifade edilir.
- Sürümsüz yazım geliş sırasına duyarlıdır; eski yanıt yeniyi ezebilir.
- Sürüm kontrolü eski yanıtı eler ve yazımı sağlam yapar.
- Numarayı yanıt gelişinde vermek kontrolü etkisiz bırakır ve işlemi çift duyarlı yapar.
- Hangi iş parçacığının hangi işi koşturduğu ölçülmedi.
- Bu dersin duyarlılığı: sıraya duyarlı (sürümsüz) ve sağlam (sürümlü).
Sonraki Adım
Ana aktör ve aktörler yeni kodun yapı taşlarıdır; ama eski kod hâlâ dağıtım kuyruklarıyla yazılmıştır. Sıradaki derste seri bir kuyruğa iş ekleme ve değer atama bloklarını ölçeceğiz ve kuyruğun neyi sıraya soktuğuna bakacağız.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.