Ders 05 / 10
Eşzamansız Kütüphane Uyumu
Her görevde tek bir engelleyen çağrı örtüşen adımı 44'ten 37'ye düşürür, sekiz görevin yedisi engellediğinde örtüşme 6 teslime rağmen 0'a iner; çağrıyı yürütücü havuzuna devretmek örtüşmeyi 44'e döndürür ama bedelin görev başına mı çağrı başına mı düştüğü tik sayısını 44 ile 90 arasında ayırır.
İçindekiler
Önceki dersin tablosunda üç satır vardı ve üçüncüsü şuydu: denetimi hiç bırakmayan eş yordam
0 teslim, 80 tik, 0 örtüşen adım. async def ile yazılmış, gerçek bir eş yordamdı ve
sonucu tek akıştan ayırt edilemiyordu.
O satır bir yazım hatası gibi görünür ama pratikte hiç öyle çıkmaz. Gövdeyi yazan kişi
genellikle await yazmayı unutmaz; unutan çağırdığı işlevdir. Denetimi bırakmayı bilmeyen
bir çağrı, onu çağıran eş yordamın da bırakmasını engeller. Bu dersin ölçtüğü şey o çağrının
faturasıdır: akıştaki bir çağrı denetimi bırakmadığında örtüşen adım kaç düşer, bütün çağrılar
bırakmadığında ne kalır, ve bırakmayı bilmeyen bir çağrıyı akışa geri sokmanın bedeli kaç
adımdır?
Engelleyen Kod Teslim Noktası Üretmeyen Koddur
Engelleyen kod (blocking code), denetimi olay döngüsüne geri vermeden işini bitiren koddur. Adı yanıltıcı olabilir: engellenen şey çağıran gövde değil, döngünün kendisidir. Gövde zaten sonucu bekleyecekti; asıl kayıp, o beklerken başka hiçbir görevin ilerleyememesi.
Bir çağrının engelleyen olup olmadığı ne yaptığından değil, nasıl yazıldığından gelir. Aynı iş — bir dosyayı okumak, bir yanıtı beklemek — teslim noktası üreten bir biçimde de yazılabilir, üretmeyen bir biçimde de. Eşzamansız bir akışta ikisi arasındaki fark, önceki dersin tablosunda ölçülen farkın kendisidir.
Buradan bu dersin dar sorusu doğuyor. Bir programda çağrıların hepsi birden değişmez; kimi teslim eder, kimi etmez. Karışım hâlinde ne çıkar?
Bir Çağrı Yeri, Sekiz Görev
Ölçüm iki ekseni ayırıyor, çünkü ikisi karıştırıldığında sonuç okunamaz hâle geliyor.
Birinci eksen çağrı sayısı: her görevin akışındaki kaç girdi/çıktı çağrısının engellediği. Bir görevin akışında on adım var ve bunların bir kısmı girdi/çıktı adımı; bu adımların kaçının teslim etmeyen bir çağrıdan geçtiği ölçülüyor. Burada dikkat edilecek nokta şu: kodda tek bir çağrı yeri engelleyen olabilir ama o çağrı yerinden sekiz görev de geçer. Bir satırlık kusur, sekiz akışa birden yazılır.
İkinci eksen görev sayısı: kaç görevin baştan sona engelleyen çağrılar kullandığı. Bir programın bir bölümü eşzamansız yazılmışken bir bölümünün eski biçimde kalması bu eksende ölçülür.
Ölçümün varsayımları:
- EM48 — Görev kurgusu önceki derslerin aynısıdır: sekiz görev, görev başına on adım, toplam seksen adım, girdi/çıktı ağırlıklı yük, tek yuva.
- EM49 — Eş yordamlar ve onları sürdüren döngü önceki dersin aynısıdır; değişen tek şey gövdenin hangi adımda teslim ettiğidir.
- EM50 — Uyumlu çağrı, girdi/çıktı adımında denetimi bırakan çağrıdır. Engelleyen çağrı, aynı adımı denetimi bırakmadan atan çağrıdır. İkisi de aynı işi yapar ve aynı adımı harcar.
- EM51 — İşlemci adımları hiçbir rejimde teslim etmez; ölçülen değişken yalnız girdi/çıktı çağrılarının biçimidir.
- EM52 — Birinci eksende her görevin ilk
kgirdi/çıktı çağrısı engeller; hangi çağrıların seçildiği kurguya yazılıdır ve koşumlar arasında değişmez. - EM53 — İkinci eksende ilk
kgörev baştan sona engeller, kalanı uyumludur. - EM54 — Yürütücü havuzuna devir modelde eklenen bir işlemci adımıdır: iş havuzda koşar, döngü teslim noktasını geri kazanır, karşılığında bir adım daha sayılır.
- EM55 — Devir bedelinin iki yerleşimi ölçülür: görev başına bir kez ve çağrı başına bir kez. İkisi de aynı işi devreder, yalnız bedelin düştüğü yer değişir.
- EM56 — Havuzun genişliği bu ölçümde sınırsızdır; havuz genişliğinin etkisi sonraki dersin konusudur.
- EM57 — Süre ölçülmez; sayılan birim adım, tik, örtüşen adım ve teslimdir.
- EM58 — Ölçüm tek koşumdur ve tohum sabittir.
Ölçüm
"""Engelleyen kod: teslim etmeyen bir cagri ortusmeyi ne kadar goturur.""" TOHUM = 20260817 ISLEMCI, GC = "islemci", "g_c" def uretec(tohum): d = tohum % 2147483646 + 1 def r(n): nonlocal d d = (d * 48271) % 2147483647 return d % n return r def gorevler(sayi=8, adim=10, gc_payi=7, tohum=TOHUM): """Her gorev bir adim dizisi; gc_payi/10 orani girdi/cikti adimi.""" r, liste = uretec(tohum), [] for i in range(sayi): liste.append([GC if r(10) < gc_payi else ISLEMCI for _ in range(adim)]) return liste class Bekleyis: """Es yordamin denetimi olay dongusune biraktigi nokta.""" def __init__(self, tur): self.tur = tur def __await__(self): return (yield self.tur) def uyumlu_yapici(engelleyen_cagri=0): """Her gorevin ilk 'engelleyen_cagri' g/c cagrisi teslim etmez.""" async def gorev(adimlar, gunluk): kalan = engelleyen_cagri for a in adimlar: if a == GC and kalan > 0: kalan -= 1 # engelleyen cagri: denetim birakilmaz elif a == GC: await Bekleyis(a) # uyumlu cagri: denetim birakilir gunluk.append(a) return gorev def karma_yapici(engelleyen_gorev=0): """Ilk 'engelleyen_gorev' gorev hic teslim etmez, kalani uyumludur.""" uyumlu = uyumlu_yapici(0) def yapici(adimlar, gunluk): yapici.sira += 1 if yapici.sira <= engelleyen_gorev: return uyumlu_yapici(len(adimlar))(adimlar, gunluk) return uyumlu(adimlar, gunluk) yapici.sira = 0 return yapici def dongu(yapici, gorevler, islemci_yuvasi=1): """El yazimi belirlenimci olay dongusu. Donen: tik, ortusen, teslim.""" gunluk = [[] for _ in gorevler] coro = [yapici(g, gunluk[i]) for i, g in enumerate(gorevler)] istek = [None] * len(coro) canli = [True] * len(coro) borc = [0] * len(coro) teslim = 0 def surdur(i, pesin=0): nonlocal teslim onceki = len(gunluk[i]) try: istek[i] = coro[i].send(None) teslim += 1 except StopIteration: istek[i] = None canli[i] = False borc[i] += max(0, len(gunluk[i]) - onceki - pesin) for i in range(len(coro)): surdur(i) tik = ortusen = 0 while any(canli) or any(borc): mesgul = [i for i in range(len(coro)) if borc[i] > 0] if mesgul: borc[mesgul[0]] -= 1 tik += 1 continue yuva, ilerleyen = islemci_yuvasi, 0 for i in range(len(coro)): if not canli[i] or istek[i] is None: continue if istek[i] == ISLEMCI: if yuva <= 0: continue yuva -= 1 surdur(i, 1) ilerleyen += 1 if ilerleyen == 0: break tik += 1 ortusen += max(0, ilerleyen - 1) return tik, ortusen, teslim g = gorevler(gc_payi=7) print("sekiz görev, seksen adım, girdi/çıktı ağırlıklı yük, tek yuva") print() print("A — her görevde kaç girdi/çıktı çağrısı engelliyor") print(f"{'engelleyen çağrı':>17s} {'teslim':>7s} {'tik':>5s} {'örtüşen':>8s}") for k in range(9): tik, ortusen, teslim = dongu(uyumlu_yapici(k), g) print(f"{k:17d} {teslim:7d} {tik:5d} {ortusen:8d}") print() print("B — kaç görev baştan sona engelliyor") print(f"{'engelleyen görev':>17s} {'teslim':>7s} {'tik':>5s} {'örtüşen':>8s}") for k in range(9): tik, ortusen, teslim = dongu(karma_yapici(k), g) print(f"{k:17d} {teslim:7d} {tik:5d} {ortusen:8d}") print() print("C — engelleyen çağrıyı yürütücü havuzuna devretmek") devir_gorev = [[ISLEMCI] + list(t) for t in g] devir_cagri = [[s for a in t for s in ([ISLEMCI, GC] if a == GC else [a])] for t in g] print(f"{'rejim':<28s} {'adım':>5s} {'teslim':>7s} {'tik':>5s} {'örtüşen':>8s}") for ad, dizi, yapici in ( ("hepsi engelliyor", g, uyumlu_yapici(10)), ("hepsi uyumlu", g, uyumlu_yapici(0)), ("devir görev başına bir kez", devir_gorev, uyumlu_yapici(0)), ("devir çağrı başına", devir_cagri, uyumlu_yapici(0))): tik, ortusen, teslim = dongu(yapici, dizi) print(f"{ad:<28s} {sum(len(t) for t in dizi):5d} {teslim:7d} " f"{tik:5d} {ortusen:8d}")
sekiz görev, seksen adım, girdi/çıktı ağırlıklı yük, tek yuva
A — her görevde kaç girdi/çıktı çağrısı engelliyor
engelleyen çağrı teslim tik örtüşen
0 54 36 44
1 46 43 37
2 38 50 30
3 30 57 23
4 22 64 16
5 14 71 9
6 8 76 4
7 5 78 2
8 2 80 0
B — kaç görev baştan sona engelliyor
engelleyen görev teslim tik örtüşen
0 54 36 44
1 46 44 36
2 40 50 30
3 30 58 22
4 22 64 16
5 17 69 11
6 11 75 5
7 6 80 0
8 0 80 0
C — engelleyen çağrıyı yürütücü havuzuna devretmek
rejim adım teslim tik örtüşen
hepsi engelliyor 80 0 80 0
hepsi uyumlu 80 54 36 44
devir görev başına bir kez 88 54 44 44
devir çağrı başına 134 54 90 44
Tek Bir Çağrının Bedeli
A tablosunun ilk iki satırı bu dersin en kısa sonucunu veriyor. Bütün çağrılar uyumluyken 36 tik / 44 örtüşen adım. Her görevde tek bir girdi/çıktı çağrısı engellediğinde 43 tik / 37 örtüşen adım.
Tek çağrı, yedi tik ve yedi örtüşen adım götürüyor. Kodda değişen şey bir satır; ölçümde değişen şey örtüşmenin altıda biri.
Kayıp oranı, engelleyen çağrının payından büyük. Elli dört girdi/çıktı çağrısının sekizi engelliyor, yani çağrıların yaklaşık yedide biri; örtüşen adımdaki kayıp ise 44’ün 7’si. Sebebi engelleyen çağrının kendi görevini değil, o tikte ilerleyebilecek herkesi durdurması. Bir görev engellerken öbür yedisi hazır bekliyor olabilir; ilerlemiyorlar.
Tablonun kalanı bu eğrinin sonuna kadar gidiyor: iki çağrıda 30, dört çağrıda 16, altı çağrıda 4 örtüşen adım. Sekiz çağrıda — yani her görevin bütün girdi/çıktı çağrıları engellediğinde — örtüşen adım 0 ve tik 80. Rejim tek akışa dönüyor.
Bu son satır, dersin başlığındaki uyumun ne demek olduğunu tanımlıyor: eşzamansız bir akışın sayısı, akıştaki en engelleyen parçaya göre belirleniyor.
Yedi Görev Yeter
B tablosu aynı çöküşü öbür eksende gösteriyor ve bir satırı ötekilerden ayrılıyor.
Yedi görev engellediğinde tik 80, örtüşen adım 0. Sekizinci görev hâlâ uyumlu; altı kez denetim bırakıyor. Teslim sütunu bunu yazıyor: 6 teslim, 0 örtüşen adım.
Önceki dersin kuralı burada tam olarak sınanıyor. Teslim örtüşme için gerekliydi ama yeterli değildi; ikinci bir hazır görev de gerekiyordu. Yedi görev kendi engelleyen çağrılarında takılıyken sekizinci görev denetimi bırakıyor ve bırakacağı denetimi alacak kimse bulunmuyor. Döngü aynı görevi geri sürdürüyor.
Bunun pratik okuması sert: eşzamansız bir akışı bir tek uyumlu parça kurtarmaz. Bir kod tabanının bir bölümünü eşzamansız yazmak, kalan bölüm engellediği sürece ölçülebilir bir şey kazandırmayabilir. B tablosunun ilk yarısı da aynı yönü gösteriyor: bir görev engellediğinde kayıp 8 örtüşen adım, ikisinde 14, üçünde 22. Kayıp görev sayısıyla birlikte hızlanarak büyüyor, çünkü her engelleyen görev hem kendi örtüşmesini hem başkalarının örtüşme fırsatını götürüyor.
Engelleyenin Görünmezliği
İki tablonun ortak bir özelliği var ve kusurun neden bu kadar sık kaçtığını açıklıyor: hiçbir satırda hata yok. Sekiz görevin sekizi de bütün adımlarını atıyor, hiçbiri yarım kalmıyor, hiçbir istisna çıkmıyor. Değişen tek şey adımların hangi tike düştüğü.
Çağrı yerine bakarak da anlaşılmıyor. Uyumlu çağrı ile engelleyen çağrı ölçümde aynı işi
yapıyor ve aynı adımı harcıyor; ayrıldıkları yer gövdenin içinde. Bir işlevin engelleyen olup
olmadığını söyleyen tek şey, çağrıldığında sürdürülebilir bir nesne verip vermediğidir —
async def ile tanımlanmış bir işlev bir eş yordam nesnesi döndürür, sıradan bir işlev
doğrudan sonucu.
Bunun doğrudan sonucu şu: eşzamansız bir akışta await yazılmayan her çağrı adaydır.
Sonucu doğrudan dönen bir çağrı ya hiç beklemiyor ya da bekleyip döngüyü tutuyordur; ikisi
çağrı yerinden ayırt edilemez.
Ölçümün buradaki katkısı, kusurun belirtisini adlandırmak. Engelleyen bir çağrı hata üretmez, çıktıyı bozmaz, günlüğe bir şey yazmaz. Ürettiği tek belirti örtüşen adımın düşmesidir — ve örtüşen adım sayılmıyorsa hiçbir belirti kalmaz. B tablosunun yedinci satırı bunun en uç hâli: program eşzamansız yazılmış, altı teslim noktası gerçekten çalışıyor ve sonuç tek akışla aynı.
Havuza Devir ve Bedelin Düştüğü Yer
C tablosu düzeltmeyi ölçüyor. Engelleyen çağrının uyumlu bir karşılığı yoksa, çağrı olduğu gibi bırakılıp başka bir yerde koşturulabilir: bir yürütücü havuzuna devredilir, olay döngüsü o sırada teslim noktasını geri kazanır.
İki uçtaki satırlar bilinen sayılar: hepsi engelliyorsa 80 adım, 0 teslim, 80 tik, 0 örtüşen adım; hepsi uyumluysa 80 adım, 54 teslim, 36 tik, 44 örtüşen adım.
Devir iki satır ekliyor ve ikisinin de örtüşen adımı 44 — devir örtüşmeyi tam olarak geri kazandırıyor. Ayrıldıkları yer tik sayısı.
Devir bedeli görev başına bir kez düştüğünde toplam adım 80’den 88’e çıkıyor ve tik 44 oluyor. Engelleyen rejimin 80 tikine göre yarı yarıya kazanç, uyumlu rejimin 36 tikine göre sekiz tik geride. Bedel sekiz adım, kayıp sekiz tik; hesap birebir.
Devir bedeli çağrı başına düştüğünde toplam adım 134‘e çıkıyor ve tik 90 oluyor. Bu satır dersin en dikkat çekici sayısı: 90, hiçbir şey yapmayıp engellemenin 80 tikinden büyük. Örtüşen adım 0’dan 44’e çıkmış olmasına rağmen toplam kötüleşmiş.
Çelişki değil, aritmetik. Devredilen her çağrı bir adım ekliyor ve elli dört çağrı elli dört adım ediyor; kazanılan kırk dört örtüşen adım bu ekten küçük. Örtüşen adımı tek başına okumak bu yüzden yanıltır — üçüncü dersin aktarım tablosunda da aynı tuzak vardı. Bir ölçüm, hangi adımların sayıya girdiğini söylemeden karşılaştırılamaz.
Buradan çıkan yönerge somut: devri çağrı başına değil, mümkün olan en büyük parça başına yapmak. Sekiz görevi tek tek devretmek sekiz adım, her çağrıyı ayrı devretmek elli dört adım ediyor. Devredilecek işin büyüklüğü, devrin kazanıp kazanmayacağını doğrudan belirliyor.
Üç satırın birlikte verdiği sıra da bir öncelik kuralı öneriyor. En iyi sonuç uyumlu çağrıya geçmek (36 tik), ikinci en iyi büyük parçalar hâlinde devretmek (44), en kötüsü ise her çağrıyı ayrı devretmek (90). Havuz, uyumlu çağrının yerini tutan bir çözüm değil, uyumlu karşılığı bulunmadığında başvurulan bir çözümdür ve bedeli her zaman sayılabilir.
Özet
- Engelleyen kod, denetimi olay döngüsüne geri vermeyen koddur; engellediği şey çağıran gövde değil döngünün kendisidir.
- Her görevde tek bir engelleyen girdi/çıktı çağrısı, örtüşen adımı 44’ten 37‘ye düşürür ve tik’i 36’dan 43’e çıkarır; bütün çağrılar engellediğinde sonuç 80 tik / 0 örtüşen adım, yani tek akıştır.
- Sekiz görevin yedisi engellediğinde örtüşen adım 0’dır; sekizinci görev 6 kez denetim bıraksa da bırakılan denetimi alacak hazır görev yoktur.
- Çağrıyı yürütücü havuzuna devretmek örtüşen adımı 44’e geri getirir; devir bedeli görev başına düştüğünde tik 44, çağrı başına düştüğünde 90 olur.
- Çağrı başına devir, hiç düzeltmemekten daha çok tik üretir (90’a karşı 80): kazanılan örtüşme, eklenen adım sayısından küçük kaldığında ölçüm ters işaret verir.
Sonraki Adım
Bu dersin son bölümü havuzu bir düzeltme aracı olarak kullandı ve genişliğini hiç sormadı; havuz sınırsız sayıldı. Gerçek bir havuzun genişliği vardır ve arkasında ne olduğu — iş parçacığı mı, süreç mi — ölçümü değiştirir. Sonraki ders bunu sorar: aynı arayüzle iş dağıtmak, arkasında iş parçacığı varken ve süreç varken aynı görevde hangi sayıları verir, ve arayüzün aynı olması seçimin de aynı olduğu anlamına gelir mi?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.