İçeriğe geç
academia.sh

Ders 04 / 10

Eşzamansız Programlama

Örtüşmenin kaynağı işletim sistemi değil kodun kendisidir: her adımda denetimi bırakan eş yordam 80 teslimle 31 tik ve 49 örtüşen adım verir, yalnız girdi/çıktı adımında bırakan 54 teslimle 36 tik ve 44 örtüşen adım, hiç bırakmayan 0 teslimle 80 tik ve 0 örtüşen adım.

İçindekiler

Önceki iki ders örtüşmeyi hep dışarıdan aldı. İş parçacığı işletim sisteminden işçi istedi, süreç ayrı bir yorumlayıcıdan yuva istedi. İkisinde de hangi görevin ne zaman ilerleyeceğine karar veren şey kodun dışındaydı; program yalnız kaç akış istediğini söyledi.

Üçüncü bir yol var ve bu ders onu ölçer. Eşzamansız programlamada örtüşmenin kaynağı ne işletim sistemidir ne de ikinci bir yorumlayıcı: kodun kendisidir. Bir eş yordam denetimi bırakacağı yeri kendi yazar. Bu dersin sorusu bu yüzden dar ve sayılabilir: bir eş yordam denetimi ne sıklıkla bırakırsa ne kadar örtüşme çıkar, ve hiç bırakmazsa ne olur?

Eş Yordam Sürdürülebilir Bir Gövdedir

Eş yordam (coroutine), çağrıldığında gövdesi koşmayan, çağrıya karşılık bir nesne döndüren ve sürdürüldükçe ilerleyen bir işlevdir. async def ile tanımlanır. Gövdesindeki await, bir sonucu bekleyen bir işaret değil, denetimi bırakma noktasıdır: eş yordam o noktada durur, kendini çağıran tarafa döner ve başka bir şeyin ilerlemesine izin verir.

Duraklayan gövde bu kursta ilk kez geçmiyor. Veri Yapıları ve Fonksiyonel Araçlar kursu üreteci ölçtü ve gövdenin yield’de durup yerel adlarını koruduğunu gösterdi; o ölçüm burada tekrarlanmaz. Aradaki fark tek cümlede yazılabilir: üreteç bir değer vermek için durur, eş yordam denetimi bırakmak için durur — birinin duruşu akışa bir öğe ekler, öbürünün duruşu başkasına sıra verir.

Olay döngüsü (event loop) bu duruşları yöneten taraftır. Elinde sürdürülmeyi bekleyen eş yordamlar vardır; birini sürdürür, o bir teslim noktasına gelip döndüğünde sıradakini sürdürür. Bir eş yordamı döngüye kaydedip bağımsız ilerlemesini istemek, onu bir görev yapmaktır.

Olay Döngüsü Yeni Bir Yuva Getirmez

Modelin diliyle söylenirse: eşzamansız rejim ne işçi sayısını ne yuva sayısını işletim sisteminden ister. Olay döngüsü tek bir akışta koşar; aynı anda tek bir gövde ilerler ve yuva birdir.

O zaman örtüşme nereden gelir? Tek bir yerden: bir gövde denetimi bıraktığında öbürünün ilerleyebilmesinden. Bu kursun ölçüsüyle, teslim noktası olmadan örtüşen adım da yoktur. Buradan doğrudan bir üst sınır çıkıyor ve ölçüm onu sınayacak: örtüşen adım, teslim sayısını geçemez.

Bunun tersi de var ve dersin sonunda ölçülüyor: teslim etmek örtüşme için gerekli ama yeterli değil. Bırakılan denetimi alacak, o anda hazır bekleyen ikinci bir görev de olmak zorunda.

Ölçüm Kendi Döngüsünü Yazıyor

Ölçümün çizelgeleyicisi burada elle yazılmış bir olay döngüsüdür; eş yordamlar ise gerçektir. async def ile tanımlanmış, gerçek await içeren gövdeler, gerçek eş yordam nesneleri üretiyor ve send ile sürdürülüyor. Değiştirilen tek şey kimin sürdürdüğü: standart olay döngüsü yerine sırası yazılı bir döngü. Sebebi ölçümün koşulu — hangi görevin hangi tikte sürdürüldüğü sabit olmazsa örtüşen adım sayılamaz.

Dersin sonundaki kısa blok gerçek olay döngüsünü çalıştırıyor ve sırayı await’in kendisi sabitliyor.

Ölçümün varsayımları:

  • EM36 — 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.
  • EM37 — Eş yordamlar gerçektir; async def ile tanımlanır ve await ile durur. Onları sürdüren döngü ders içinde yazılmıştır.
  • EM38 — Teslim noktası, eş yordamın olay döngüsüne dönüş sayısıdır; her await bir teslimdir.
  • EM39 — Üç rejim yalnız teslim noktalarının yerinde ayrılır: her adımda, yalnız girdi/çıktı adımında, hiç. Adım dizileri üçünde de aynıdır.
  • EM40 — Olay döngüsü tek akıştır: işlemci yuvası birdir ve değişmez.
  • EM41 — Bir eş yordam denetimi bırakmadan k adım atarsa döngü o k adım boyunca meşguldür; ilk adım sürdürüldüğü tike yazılır, kalan k-1 adım kimsenin ilerlemediği ayrı tiklere.
  • EM42 — Sürdürme sırası görev indisine göredir ve koşum boyunca değişmez.
  • EM43 — Karşılaştırma tabanı önceki derslerin çizelgeleyicisidir: aynı görevler, sekiz işçi, tek yuva.
  • EM44 — Gerçek olay döngüsü yalnız sıralı await ile çalıştırılır; çıktı sırası await’in kendisiyle sabitlenir, çizelgeleyiciye bırakılmaz.
  • EM45sleep(0) bu ölçümde bir bekleme değil, bir teslim işaretidir; sayılan şey süre değil, denetimin kaç kez bırakıldığıdır.
  • EM46 — Süre ölçülmez; sayılan birim adım, tik, örtüşen adım ve teslimdir.
  • EM47 — Ölçüm tek koşumdur ve tohum sabittir.

Ölçüm

"""Es yordam: denetimi kim, ne siklikla birakiyor."""

import asyncio

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


def kosum(gorevler, isci, islemci_yuvasi):
    """Onceki derslerin cizelgeleyicisi; karsilastirma tabani."""
    kalan = [list(g) for g in gorevler]
    tik = ortusen = 0
    while any(kalan):
        etkin = [i for i, g in enumerate(kalan) if g][:isci]
        if not etkin:
            break
        yuva, ilerleyen = islemci_yuvasi, 0
        for i in etkin:
            if kalan[i][0] == ISLEMCI:
                if yuva <= 0:
                    continue
                yuva -= 1
            kalan[i].pop(0)
            ilerleyen += 1
        tik += 1
        ortusen += max(0, ilerleyen - 1)
    return tik, ortusen


class Bekleyis:
    """Es yordamin denetimi olay dongusune biraktigi nokta."""

    def __init__(self, tur):
        self.tur = tur

    def __await__(self):
        return (yield self.tur)


async def her_adimda(adimlar, gunluk):
    """Her adimdan once denetimi birakir: teslim noktasi = adim sayisi."""
    for a in adimlar:
        await Bekleyis(a)
        gunluk.append(a)


async def gc_adiminda(adimlar, gunluk):
    """Yalniz girdi/cikti adiminda birakir; islemci adimlarini pes pese atar."""
    for a in adimlar:
        if a == GC:
            await Bekleyis(a)
        gunluk.append(a)


async def hic(adimlar, gunluk):
    """Denetimi hic birakmaz."""
    for a in adimlar:
        gunluk.append(a)


def dongu(yapici, gorevler, islemci_yuvasi=1):
    """El yazimi belirlenimci olay dongusu. Donen: tik, ortusen, teslim.

    Bir es yordam denetimi birakmadan k adim atarsa dongu o k adim boyunca
    mesguldur: borc olarak k-1 tik daha yazilir ve o tiklerde kimse ilerlemez.
    """
    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)                       # ilk teslim noktasina kadar
    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("girdi/çıktı ağırlıklı yük, seksen adım, tek yuva")
print(f"{'teslim noktası':<24s} {'teslim':>7s} {'tik':>5s} {'örtüşen':>8s}")
for ad, yapici in (("her adımda", her_adimda),
                   ("yalnız g/ç adımında", gc_adiminda),
                   ("hiç", hic)):
    tik, ortusen, teslim = dongu(yapici, g)
    print(f"{ad:<24s} {teslim:7d} {tik:5d} {ortusen:8d}")

tik, ortusen = kosum(g, 8, 1)
print(f"{'önceki dersin çizelgesi':<24s} {'':>7s} {tik:5d} {ortusen:8d}")


async def basamak(ad, kere, gunluk):
    """Her yinelemede denetimi olay dongusune birakir."""
    for i in range(kere):
        await asyncio.sleep(0)
        gunluk.append(f"{ad}{i}")


async def sirali():
    """Sirali await: ikinci es yordam birincisi bitmeden baslamaz."""
    gunluk = []
    for ad in ("a", "b"):
        await basamak(ad, 3, gunluk)
    return gunluk


print()
gunluk = asyncio.run(sirali())
print("gerçek olay döngüsü, sıralı await:", " ".join(gunluk))
print(f"teslim noktası {len(gunluk)}, örtüşen adım 0 — "
      f"bırakan var, alan yok")
girdi/çıktı ağırlıklı yük, seksen adım, tek yuva
teslim noktası            teslim   tik  örtüşen
her adımda                    80    31       49
yalnız g/ç adımında           54    36       44
hiç                            0    80        0
önceki dersin çizelgesi             31       49

gerçek olay döngüsü, sıralı await: a0 a1 a2 b0 b1 b2
teslim noktası 6, örtüşen adım 0 — bırakan var, alan yok

Teslim Sıklığı Örtüşmeyi Belirliyor

Tablo üç satırda tek bir değişkeni değiştiriyor: await’in nereye yazıldığı. Adım dizileri üçünde de aynı, görev sayısı aynı, yuva aynı.

Her adımda teslim eden eş yordam 80 kez denetimi bırakıyor ve seksen adımı 31 tikte bitiriyor; örtüşen adım 49.

Yalnız girdi/çıktı adımında teslim eden eş yordam 54 kez bırakıyor — girdi/çıktı adımı sayısı kadar. Tik 36’ya çıkıyor, örtüşen adım 44’e iniyor. Beş tiklik kayıp, işlemci adımlarının pes peşe atılmasından geliyor: eş yordam iki girdi/çıktı adımı arasında üç işlemci adımı taşıyorsa, o üç adımı tek bir sürdürmede yapar ve döngü o sırada kimseye sıra veremez.

Hiç teslim etmeyen eş yordam 0 kez bırakıyor ve sonuç 80 tik, 0 örtüşen adım — tek akış rejiminin tam olarak kendisi. async def ile yazılmış olması hiçbir şey değiştirmiyor.

Üç satırın verdiği sıralama şu: teslim 80 → 54 → 0, örtüşen adım 49 → 44 → 0. İlişki tek yönlü ve üst sınır her satırda korunuyor: örtüşen adım teslim sayısını hiç geçmiyor.

Buradan doğrudan bir tasarım sonucu çıkıyor: eşzamansız bir programda örtüşmeyi belirleyen şey kaç görev başlatıldığı değil, gövdelerin nerede durduğudur. async sözcüğü bir teslim noktası üretmez; await üretir.

İkinci satırın beş tiklik kaybı ayrıca okunmayı hak ediyor, çünkü gerçek gövdelerin çoğu o satıra benzer. Kayıp, işlemci adımlarının uzunluğuyla büyür: iki teslim noktası arasında ne kadar çok adım varsa döngü o kadar uzun meşgul kalır. Bu, eşzamansız kodda uzun hesapların neden ayrı bir yere taşındığının sayılabilir gerekçesidir — taşınmadıklarında bütün görevleri birden bekletirler.

Aynı Çizelge, Başka Kaynak

Tablonun son satırı bir doğrulama taşıyor ve dersin en dikkate değer sayısı orada.

Önceki derslerin çizelgeleyicisi, aynı görevleri sekiz işçi ve tek yuvayla koşturunca 31 tik / 49 örtüşen adım veriyordu. Her adımda teslim eden eş yordam rejimi de 31 tik / 49 örtüşen adım veriyor. İki sayı birebir aynı.

Aynılık bir tesadüf değil, tanımın sonucu. İş parçacığı rejiminde bir görev ilerledikten sonra sıra ötekine geçiyordu; kararı çizelgeleyici veriyordu. Eş yordam rejiminde de bir görev bir adım atıp duruyor ve sıra ötekine geçiyor; kararı kod veriyor. Görülen çizelge aynı, çizelgeyi yazan taraf başka.

Bu, eşzamansız programlamanın ne kazandırdığını da yerine koyuyor. Kazandırdığı şey daha çok örtüşme değil — üst satır bunu gösteriyor, sayı aynı. Kazandırdığı şey örtüşmenin kaynağı: işletim sisteminden iş parçacığı istemeden, tek akışta, bırakma noktalarını kodda yazarak aynı çizelgeyi elde etmek. Bedeli de aynı yerde: bırakma noktalarını yazmayı unutan gövde, üçüncü satıra düşer.

Bırakan Var, Alan Yok

Son iki satır teslim noktasının yetmediğini gösteriyor.

Gerçek olay döngüsünde koşan iki eş yordam var; her biri üç kez denetimi bırakıyor, toplam 6 teslim noktası. Çıktı a0 a1 a2 b0 b1 b2. İkinci eş yordam birincisi bitmeden hiç ilerlemiyor ve örtüşen adım 0.

Sebebi await’in ne yaptığı: sirali gövdesi basamak("a", ...) üzerinde bekliyor ve o bitmeden basamak("b", ...) başlatılmamış oluyor. Denetim bırakılıyor, ama alacak hazır bir görev yok; döngü aynı eş yordamı geri sürdürüyor.

Buradan iki koşullu bir kural çıkıyor. Örtüşme için hem gövdenin denetimi bırakması hem de o anda ilerlemeye hazır ikinci bir görevin bulunması gerekir. Birincisi await ile, ikincisi eş yordamları döngüye görev olarak kaydetmekle sağlanır. İkisinden biri eksikse tablonun üçüncü satırındaki sonuç çıkar: teslim sayısı kaç olursa olsun, örtüşen adım sıfır.

Bu ayrım, eşzamansız kodda en sık yapılan hatanın da açıklamasıdır: gövde async yazılmış, await konmuş, hiçbir şey örtüşmemiştir. Ölçülmesi gereken sayı await sayısı değil, aynı anda ilerlemeye hazır görev sayısıdır.

Teslimin Modelde Sayılmayan Bedeli

Tablonun ilk satırı en çok teslimi ve en çok örtüşmeyi birlikte veriyor; buradan “ne kadar çok await o kadar iyi” sonucu çıkarılabilir. Model bunu doğrular ama model bir şeyi saymıyor ve bunu yazmak gerekir.

Her teslim bir defter işidir: eş yordamın durumu saklanır, döngünün sırasına geri konur, sonra yeniden sürdürülür. Modelde bu işin bedeli sıfır sayılıyor; sayılan tek şey adımın hangi tikte atıldığı. Gerçek bir programda teslim bedelsiz değildir ve her adımda teslim eden bir gövde, kazandığı örtüşmenin bir kısmını deftere geri öder.

Bunun ölçüme etkisi yönlüdür ve okunabilir: 31 tik, eşzamansız rejimden beklenebilecek kazancın üst sınırıdır. Bedel eklendiğinde bu sayı yalnız büyür. İkinci satırdaki 36 ise iki bedelin arasında duruyor: daha az teslim yazıyor ama işlemci adımlarını pes peşe attığı için beş tik kaybediyor.

Pratikte yazılan gövdeler ikinci satıra benzer, çünkü await bekleyen çağrıların önüne yazılır, hesap yapan satırların arasına değil. Ölçümün söylediği şey şu: ikinci satır ile üçüncü satır arasındaki fark — 44 örtüşen adıma karşı 0 — hesap yapan satırların uzunluğuna değil, aradaki bekleyen çağrının denetimi bırakıp bırakmadığına bağlı. Uzun bir işlemci bölümü örtüşmeyi biraz azaltır; bırakmayan tek bir çağrı onu sıfırlar.

Özet

  • Eş yordam, çağrıldığında koşmayan ve sürdürüldükçe ilerleyen bir gövdedir; await bir bekleme değil, denetimin bırakıldığı teslim noktasıdır.
  • Olay döngüsü ne işçi ne yuva ekler; tek akışta koşar. Örtüşmenin tek kaynağı gövdelerin denetimi bırakmasıdır ve örtüşen adım teslim sayısını geçemez.
  • Aynı seksen adım, teslim her adımda yazıldığında 80 teslim / 31 tik / 49 örtüşen adım, yalnız girdi/çıktı adımında yazıldığında 54 / 36 / 44, hiç yazılmadığında 0 / 80 / 0 verir.
  • Her adımda teslim eden eş yordam rejimi, sekiz işçili tek yuvalı çizelgeyle birebir aynı sayıyı verir; değişen şey çizelge değil, çizelgeyi yazan taraftır.
  • Teslim noktası örtüşme için gerekli ama yeterli değildir: sıralı await ile koşan iki eş yordam 6 kez denetimi bırakır ve 0 örtüşen adım verir.

Sonraki Adım

Tablonun üçüncü satırı bu dersin sonucu ama bir sonraki dersin sorusu. Gövdenin denetimi hiç bırakmaması her zaman yazarın tercihi değildir: çağrılan bir işlev bırakmayı bilmiyorsa gövde onu bekler ve döngü durur. Sonraki ders bunu ölçer — 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?

İ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