Ders 11 / 19
Kuyruk Tabanlı Eşzamanlılık
Seri bir kuyruğa eklenen işlerin sırayı koruduğunun, ama sona ekleme ile değer atamanın yineleme ve sıra karşısında farklı sınıflara düştüğünün ölçümü.
Yayın Güncelleme
Bu derste
- Seri kuyruk ile eşzamanlı kuyruk arasındaki farkı ayırır
- Kuyruğa sona ekleme işleminin yinelemeye ve sıraya birlikte duyarlı olduğunu ölçer
- Kuyruğa değer atama işleminin yalnız sıraya duyarlı olduğunu ölçer
- Durumu sıralayarak ölçmenin sıra farkını gizlediğini gösterir
İçindekiler
Önceki derste sürüm kontrolü, geç gelen eski bir yanıtın yeni sonucu ezmesini önledi; numarayı yanlış yerde vermek ise kontrolü etkisiz bıraktı. Eşzamanlı Swift yazılmadan önce işler dağıtım kuyruklarıyla yürütülüyordu ve çok sayıda kod hâlâ öyle yazılıdır. Bu derste kuyruğa eklenen iki tür işi ö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 kuyruğa eklenir ve kuyruk boşaltılıp sonuç okunur. Dört koşum her seferinde yeni bir kuyrukta kurulur ve Trial’a doğrudan verilir. Model, işlemin duruma ne yaptığını kurar; ağın ya da çalışma zamanının kendisi değildir.
Seri ve Eşzamanlı Kuyruk
Bir dağıtım kuyruğu, kendisine eklenen işleri bir iş parçacığı havuzunda çalıştırır. Seri kuyruk işleri eklendikleri sırayla ve birer birer çalıştırır: bir iş bitmeden sıradaki başlamaz. Eşzamanlı kuyruk ise işleri aynı anda başlatabilir; yalnız başlama sırası eklenme sırasıdır, bitiş sırası belli değildir.
Seri kuyruğun ürettiği sonuç, ekleme sırası belliyse deterministiktir ve bu yüzden ölçülebilir. Eşzamanlı kuyrukta bitiş sırası zamanlamaya bağlı olduğu için ölçüme girmez. Bu derste yalnız seri kuyruk ölçülür.
// Queued.swift — seri kuyrukta sirayla calisan ekleme ve atama islemleri
import Dispatch
final class Cell: @unchecked Sendable {
var lines: [String] = []
var current = "-"
}
func makeSerialQueue() -> DispatchQueue {
.init(label: "serial")
}
func runAppends(_ labels: [String]) -> [String] {
let cell = Cell()
let queue = makeSerialQueue()
for label in labels {
queue.async { cell.lines.append(label) }
}
queue.sync {}
return cell.lines
}
func runAssigns(_ labels: [String]) -> String {
let cell = Cell()
let queue = makeSerialQueue()
for label in labels {
queue.async { cell.current = label }
}
queue.sync {}
return cell.current
}
Cell, kuyruğun işlediği paylaşılan durumdur; derleyiciye “bunu ben koruyorum” demek için @unchecked Sendable taşır, yani güvence kuyruktan gelir, derleyiciden gelmez. runAppends her etiketi bir iş olarak kuyruğa ekler. runAssigns ise her etiketi tek bir alana yazar. Sonda boş bir sync {} bloğu kuyruğu boşaltır: seri kuyrukta bu blok eklenen tüm işlerden sonra çalışır, dolayısıyla dönüşte hepsi bitmiştir.
Sona Ekleme ve Değer Atama
a etiketi a, b etiketi b olan iki işi iki ayrı işlemle ölçüyoruz: sona ekleme ve değer atama.
// main.swift — kuyruga sona ekleme ve deger atama let appendOnce = runAppends(["a"]) let appendTwice = runAppends(["a", "a"]) let appendForward = runAppends(["a", "b"]) let appendBackward = runAppends(["b", "a"]) let appending = Trial(label: "sona-ekleme", once: appendOnce, twice: appendTwice, forward: appendForward, backward: appendBackward).report() let assignOnce = runAssigns(["a"]) let assignTwice = runAssigns(["a", "a"]) let assignForward = runAssigns(["a", "b"]) let assignBackward = runAssigns(["b", "a"]) let assigning = Trial(label: "deger-atama", once: assignOnce, twice: assignTwice, forward: assignForward, backward: assignBackward).report() countSensitivities([appending, assigning])
sona-ekleme | bir=["a"] iki=["a", "a"] ab=["a", "b"] ba=["b", "a"] -> cift-duyarli deger-atama | bir=a iki=a ab=b ba=a -> siraya-duyarli saglam: 0 yinelemeye-duyarli: 0 siraya-duyarli: 1 cift-duyarli: 1
Sona ekleme satırında bir ile iki farklıdır: ikinci ekleme listeye bir satır daha yazar. ab ile ba da farklıdır: liste ekleme sırasını taşır. İşlem çift duyarlıdır. Değer atama satırında bir ile iki aynıdır, çünkü aynı değeri yeniden atamak bir şey değiştirmez. Ama ab b, ba a: son atanan kazanır. İşlem sıraya duyarlıdır. Seri kuyruk sırayı korudu; ama sıra korunduğu için sonuç sıraya bağlı oldu. Sayım bir sıraya duyarlı, bir çift duyarlı.
ES15. Bu ölçüm eklenen işlerin tek bir kuyrukta, yazıldıkları sırayla çalıştığını varsayar. Birden çok kuyruğun aynı Cell’e eriştiği durum ölçülmedi.
ES16. İki ekleme işi aynı etiketle, aynı iş sayılır. İki işin kendi aralarında ayırt edilebildiği ve farklı etiket taşıdığı durum yalnız ab ile ba satırında vardır.
Tekrarı Elemek
Çift duyarlılığın bir kısmı tekrardan gelir: aynı etiket iki kez eklenince liste uzar. Pratikte bu çoğu zaman istenmez; bir bildirimin gönderildiğini kaydeden liste, aynı bildirimi iki kez içermemelidir. Çare, eklemeden önce etiketin listede olup olmadığına bakmaktır. Önemli olan, bakma ile ekleme işinin kuyruğun aynı işinde yapılmasıdır; ikisi ayrı işler olursa aralarına başka bir iş girebilir.
// Unique.swift — yalniz yeni etiketi ekleyen kuyruk islemi
import Dispatch
func runUniqueAppends(_ labels: [String]) -> [String] {
let cell = Cell()
let queue = makeSerialQueue()
for label in labels {
queue.async {
if !cell.lines.contains(label) { cell.lines.append(label) }
}
}
queue.sync {}
return cell.lines
}
// main.swift — tekrari eleyen ekleme let uniqueOnce = runUniqueAppends(["a"]) let uniqueTwice = runUniqueAppends(["a", "a"]) let uniqueForward = runUniqueAppends(["a", "b"]) let uniqueBackward = runUniqueAppends(["b", "a"]) let unique = Trial(label: "benzersiz-ekleme", once: uniqueOnce, twice: uniqueTwice, forward: uniqueForward, backward: uniqueBackward).report() countSensitivities([unique])
benzersiz-ekleme | bir=["a"] iki=["a"] ab=["a", "b"] ba=["b", "a"] -> siraya-duyarli saglam: 0 yinelemeye-duyarli: 0 siraya-duyarli: 1 cift-duyarli: 0
Aynı etiketin ikinci eklenişi artık listeyi değiştirmez; bir ile iki eşittir. Ama ab ile ba hâlâ ayrıdır, çünkü liste eklenme sırasını taşır. İşlem sıraya duyarlıdır. Önceki satırın çift duyarlılığından yinelemeye duyarlılık çıkmış, sıra duyarlılığı kalmıştır. Bu, tekrarı elemenin neyi çözdüğünü ve neyi çözmediğini gösterir: tekrar bir kopyaya yol açmaz, ama hangi etiketin önce geleceği sorusunu da yanıtlamaz.
ES17. Tekrar eleme, etiketlerin tam eşitliğine dayanır. Aynı anlama gelen ama farklı yazılmış etiketler (örneğin büyük küçük harf farkı) ölçülmedi.
Eşzamanlı Kuyruk ve Bariyer
Eşzamanlı kuyrukta okumalar birlikte koşabilir, ama yazma tek başına koşmalıdır. Bunun için yazma işi bariyer işareti taşır: bariyerli iş, kuyrukta önüne ve arkasına geçen hiçbir işle aynı anda çalışmaz.
// Es zamanli kuyruk ve bariyer: okumalar birlikte, yazma tek basina
let shared = DispatchQueue(label: "shared", attributes: .concurrent)
nonisolated(unsafe) var values: [String] = []
func read() -> Int {
shared.sync { values.count }
}
func write(_ value: String) {
shared.async(flags: .barrier) { values.append(value) }
}
Bariyer, yazmanın okumalarla çakışmasını önler. Hangi okumanın hangi yazmadan önce ya da sonra çalışacağı yine ekleme sırasına ve zamanlamaya bağlıdır; bu yüzden parça ölçülmez.
Gerçek Uygulamada
Eski kodda kuyruk çoğunlukla bir kilit yerine kullanılır. Bir sınıfın paylaşılan bir sözlüğü vardır; her okuma ve yazma aynı seri kuyruğa gönderilir ve sözlüğe yalnız o kuyruktan dokunulur. Bu kurulum, önbellek ve oturum bilgisi gibi yapılarda onlarca yıl çalışmıştır. Kuyruk sırayı korur ve aynı anda yalnız bir iş çalıştırır; yani tam olarak önceki iki dersin ölçtüğü şeyi, aktörden önceki biçimiyle yapar.
Kuyruğun asıl tuzağı, korumanın disipline bağlı olmasıdır. Sözlüğe kuyruk dışından tek bir satırla dokunan kod, bütün korumayı geçersiz kılar ve bunu derleyici söylemez. Bir diğer sık hata, kuyruğa yazıp sonucu aynı anda okumaya çalışmaktır.
// Yanlis: yazma kuyrukta, okuma kuyrugun disinda
func record(_ label: String, in cell: Cell, on queue: DispatchQueue) -> Int {
queue.async { cell.lines.append(label) }
return cell.lines.count
}
async çağrısı işi kuyruğa bırakır ve hemen döner; iş henüz çalışmamış olabilir. Dönen sayı ekleme öncesinin sayısıdır, bazen sonrasınındır, ve hangisi olduğu zamanlamaya bağlıdır. Üstelik okuma kuyruk dışında yapıldığı için cell.lines’a yazan iş ile aynı anda çalışabilir; bu bir veri yarışıdır. Düzeltme, okumayı da aynı kuyruğa vermek ve sonucu sync ile beklemektir. Ama ana iş parçacığından bir kuyruğa sync ile girmek ekranı o kuyruk kadar bekletir; bu yüzden yeni kodda aynı iş aktörle yazılır ve okuma await ile yapılır.
Başka bir tuzak, kuyruğun içinden aynı kuyruğa sync ile girmektir: kuyruk, kendi çalışan işinin bitmesini bekler ve iş de kuyruğun boşalmasını bekler. Bu yüzden çağıranın hangi kuyrukta olduğunu bilmeyen yardımcı işlevler, sync yerine async ile geri çağrı almalıdır.
Ölçümlerin yeniden okunuşu şudur: seri kuyruk sıra güvencesi verir, ama bu güvence sonucu sıradan bağımsız kılmaz. Sıranın önemli olduğu yerde kuyruk yeterli olabilir; önemli olmadığı yerde sonucu sıradan bağımsız kılacak olan işlemin kendisidir, yani değer atama yerine birleştirme, sona ekleme yerine küme gibi. Kuyruk korur, işlemin biçimi sağlamlaştırır.
Sık Yapılan Hatalar
- Kuyruğun sırayı korumasını sonucun sağlam olması sanmak. Sıra korunur, ama sonuç yine sıraya bağlı olabilir.
- Aynı kuyruğa kendi içinden
syncile iş eklemek. Kuyruk kendini bekler ve kilitlenir. - Eşzamanlı kuyruğa bariyersiz yazmak. Okumalar yazmayla çakışır.
- Kuyruğu boşaltmadan sonucu okumak. Ekleme işleri henüz bitmemiş olabilir.
Sınırlayıcı Ölçüm
Aynı ekleme işlemini bu kez eklenen satırların sıralanmış hâli ve satır sayısıyla karşılaştırıyoruz. İşlem değişmedi; yalnız durumun görünümü değişti.
// main.swift — ayni ekleme, uc durum secimi let rawTrial = Trial(label: "ham-liste", once: runAppends(["a"]), twice: runAppends(["a", "a"]), forward: runAppends(["a", "b"]), backward: runAppends(["b", "a"])) let raw = rawTrial.report() let sortedTrial = Trial(label: "sirali-liste", once: rawTrial.once.sorted(), twice: rawTrial.twice.sorted(), forward: rawTrial.forward.sorted(), backward: rawTrial.backward.sorted()) let sortedView = sortedTrial.report() let sizeTrial = Trial(label: "satir-sayisi", once: rawTrial.once.count, twice: rawTrial.twice.count, forward: rawTrial.forward.count, backward: rawTrial.backward.count) let sizeView = sizeTrial.report() countSensitivities([raw, sortedView, sizeView])
ham-liste | bir=["a"] iki=["a", "a"] ab=["a", "b"] ba=["b", "a"] -> cift-duyarli sirali-liste | bir=["a"] iki=["a", "a"] ab=["a", "b"] ba=["a", "b"] -> yinelemeye-duyarli satir-sayisi | bir=1 iki=2 ab=2 ba=2 -> yinelemeye-duyarli saglam: 0 yinelemeye-duyarli: 2 siraya-duyarli: 0 cift-duyarli: 1
Ham listede işlem çift duyarlıdır. Liste sıralanınca ab ile ba eşitlenir ve geriye yalnız yineleme farkı kalır; işlem yinelemeye duyarlı çıkar. Satır sayısında da aynı sonuç. Kural nedeni görmez, eşitsizliği görür; durumu sıralamak sıra farkını gizledi, ama yineleme farkını gizleyemedi çünkü yineleme satır sayısını gerçekten artırır. Sayım iki yinelemeye duyarlı, bir çift duyarlı.
ES18. Sıralanmış görünümde elemanlar karşılaştırılabilir kabul edilir. Sırasının anlamı olan elemanlarda, örneğin zaman damgalı satırlarda, sıralamanın hangi bilgiyi sildiği ölçülmedi.
Kuyruktan Aktöre Geçerken
Eski bir kuyruk tabanlı sınıfı aktöre çevirmek çoğu zaman mekanik görünür: sınıfı actor yapmak, kuyruğu kaldırmak, çağrılara await eklemek. Asıl iş çağıranlardadır. Eski kodda sync ile beklenen yerlerde artık await gerekir ve bu, çağıranın kendisinin eşzamansız bir bağlamda olmasını şart koşar. Geri çağrıyla çalışan çağıranlar, ya bir Task içine alınır ya da aktör çağrısı geri çağrıya sarılır; her iki durumda da çağrının bitiş zamanı artık çağrının dönüş zamanı değildir ve kod buna göre yeniden düşünülmelidir.
İkinci fark sıradır. Seri kuyruk, işleri async ile eklendikleri sırayla çalıştırırdı; aktör ise çağrıları geliş sırasıyla işlemeyi garanti etmez, çünkü iki çağıranın aktöre ulaşma sırası aralarındaki zamanlamaya bağlıdır. Eski kodun sessizce dayandığı sıra güvencesi, taşıma sırasında kaybolabilir. Bu yüzden taşımadan önce sıranın gerçekten gerektiği yerler belirlenir. Sıra gerekiyorsa çağrılar tek bir görevden art arda yapılır; gerekmiyorsa taşıma sorunsuzdur. Bu dersin ölçümleri, sıranın gerektiği yerlerin hangileri olduğunu bulmak için bir araçtır: sıraya duyarlı çıkan her işlem, taşımada dikkat isteyen bir işlemdir.
Son olarak, sınama. Kuyruk tabanlı kodu sınamak, kuyruğu boşaltan bir bekleme çağrısı gerektirir; aktör tabanlı kodda bu ihtiyaç ortadan kalkar çünkü her çağrı kendi sonucunu await ile döndürür. Sınamalar sadeleşir, ama sıra duyarlılığı olan işlemlerde sınamanın hâlâ iki sıralı bir senaryo içermesi gerekir.
Kuyruk Etiketleri ve Hata Ayıklama
Kuyruklara ad vermek küçük bir alışkanlıktır ve hata ayıklamada büyük fark yaratır. Bir çökme izinde ya da bir takılma raporunda kuyruğun adı görünür; adı serial olan bir kuyruktan hangi alt sistemin sorumlu olduğunu çıkarmak mümkün değildir, ama reservations.state gibi bir ad doğrudan yol gösterir. Bu yüzden ölçümlerdeki serial adı yalnız örnek amaçlıdır; gerçek kodda kuyruğun adı, koruduğu durumu anlatmalıdır.
Aynı sebeple bir kuyruğun yalnız bir durumu korumasına dikkat edilir. Birkaç ilgisiz durumu tek kuyruğa toplamak, birinin yavaş işinin diğerlerini beklettiği bir kurulum doğurur. Her durum için ayrı bir kuyruk, bağımsız sorunların bağımsız kalmasını sağlar; ama birden çok kuyruğa dokunan bir işlem artık sıra konusunda tek bir güvence vermez ve ölçümdeki ilk uyarı burada geçerli olur: birden çok kuyruğun aynı duruma eriştiği durum ölçülmedi. Kuyruk adlarını, kodu okuyan bir sonraki kişinin ilk bakışta anlayacağı biçimde seçmek, en ucuz hata ayıklama yatırımıdır; çünkü takılma raporlarında yalnız bu ad görünür ve aranacak yeri o daraltır. Aynı şey sınama çıktıları için de geçerlidir: başarısız bir sınama, hangi kuyruğun hangi durumu bozduğunu adından söyleyebiliyorsa, hatayı bulmak dakikalar yerine saniyeler sürer. Ad vermeyi kodun sonunda eklenecek bir süs saymamak, ilk yazılışta yapılması gereken küçük bir karardır ve sonradan eklenmesi çoğu zaman unutulur.
Özet
- Seri kuyruk işleri eklendikleri sırayla ve birer birer çalıştırır; eşzamanlı kuyruk işleri aynı anda başlatabilir.
- Kuyruğa sona ekleme hem yinelemeye hem sıraya duyarlıdır.
- Kuyruğa değer atama yalnız sıraya duyarlıdır; aynı değeri yeniden atamak etkisizdir.
- Bariyer, eşzamanlı kuyrukta yazmanın okumalarla çakışmasını önler.
- Durumu sıralayarak ölçmek sıra farkını gizler.
- Bu dersin duyarlılığı: çift duyarlı (ekleme) ve sıraya duyarlı (atama).
Sonraki Adım
Kuyruklar işleri sıraya soktu; ama bir değerin zaman içinde akıp birden çok aboneye ulaşması başka bir modeldir. Sıradaki derste tepkisel akışlarda aynı yayımcıya iki kez abone olmanın ve ardışık değerleri düşüren işleçlerin ne yaptığını ölçeceğiz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.