İçeriğe geç
academia.sh

Ders 09 / 17

Eşzamansız Yineleme

Eşzamansız yineleyici protokolü, for await döngüsü, eşzamansız üreteçler, erken çıkışta temizlik ve çekmeli akışın geri basınç karşısındaki üstünlüğü.

İçindekiler

Buraya kadar her ölçüm tek seferlik bir sonuçtu: iste, bekle, al. Bir istasyon ise sürekli veri üretir. Sonucu tek bir söz değil, ucu açık bir dizi temsil eder — ve bu dizinin elemanları zaman içinde, birer birer gelir.

Bu ders o diziyi ele alır. Sıradan yineleme protokolü burada yetersiz kalır: bir sonraki elemanın hazır olması beklenmelidir, oysa next çağrısı değeri hemen döndürmek zorundadır. Çözüm, protokolü söz döndürecek biçimde genişletmektir.

Eşzamansız Yineleyici Protokolü

JavaScript’te Nesneler ve Fonksiyonlar kursunda tanıtılan yineleme protokolü, next çağrısında value ve done alanları taşıyan bir nesne bekler. Eşzamansız protokolde tek fark, next çağrısının böyle bir nesneyle yerine gelen bir söz döndürmesidir.

Protokolü uygulayan nesne, Symbol.iterator yerine Symbol.asyncIterator adlı yöntemi tanımlar.

const KAYNAK = {
  A1: { gecikme: 30, deger: 21.4 },
  B2: { gecikme: 10, deger: 19.8 },
  C3: { gecikme: 20, deger: 23.1 },
  D4: { gecikme: 15, hata: "sensör arızası" },
  E5: { gecikme: 5, hata: "bağlantı yok" },
};

function olcumGetir(istasyon) {
  return new Promise((karar, red) => {
    const kayit = KAYNAK[istasyon];
    setTimeout(() => {
      if (kayit.hata) red(new Error(`${istasyon}: ${kayit.hata}`));
      else karar({ istasyon, deger: kayit.deger });
    }, kayit.gecikme);
  });
}

const olcumKaynagi = {
  istasyonlar: ["A1", "B2"],
  [Symbol.asyncIterator]() {
    const istasyonlar = this.istasyonlar;
    let dizin = 0;
    return {
      async next() {
        if (dizin >= istasyonlar.length) return { value: undefined, done: true };
        const olcum = await olcumGetir(istasyonlar[dizin]);
        dizin += 1;
        return { value: olcum, done: false };
      },
    };
  },
};

for await (const olcum of olcumKaynagi) {
  console.log("protokolden gelen:", olcum.istasyon, olcum.deger);
}
protokolden gelen: A1 21.4
protokolden gelen: B2 19.8

for await döngüsü her turda next çağırır, dönen sözü bekler ve done alanı doğru olana kadar sürdürür. Döngünün gövdesi de eşzamansız olabilir; her tur bir öncekinin tamamlanmasını bekler.

Eşzamansız Üreteçler

Protokolü elle yazmak, durum değişkenlerini elle taşımak demektir. Eşzamansız üreteç bu işi devralır: async function* biçiminde tanımlanır, gövdesinde hem await hem yield kullanılabilir.

const KAYNAK = {
  A1: { gecikme: 30, deger: 21.4 },
  B2: { gecikme: 10, deger: 19.8 },
  C3: { gecikme: 20, deger: 23.1 },
  D4: { gecikme: 15, hata: "sensör arızası" },
  E5: { gecikme: 5, hata: "bağlantı yok" },
};

function olcumGetir(istasyon) {
  return new Promise((karar, red) => {
    const kayit = KAYNAK[istasyon];
    setTimeout(() => {
      if (kayit.hata) red(new Error(`${istasyon}: ${kayit.hata}`));
      else karar({ istasyon, deger: kayit.deger });
    }, kayit.gecikme);
  });
}

async function* olcumAkisi(istasyonlar) {
  for (const istasyon of istasyonlar) {
    console.log("üretici: istek başlatıldı —", istasyon);
    yield await olcumGetir(istasyon);
  }
}

for await (const olcum of olcumAkisi(["A1", "B2", "C3"])) {
  console.log("tüketici: işlendi —", olcum.istasyon, olcum.deger);
}
console.log("akış bitti");
üretici: istek başlatıldı — A1
tüketici: işlendi — A1 21.4
üretici: istek başlatıldı — B2
tüketici: işlendi — B2 19.8
üretici: istek başlatıldı — C3
tüketici: işlendi — C3 23.1
akış bitti

Çıktıdaki sıra, bu yapının en önemli özelliğini gösterir: üretici, tüketici bir öğeyi işlemeden bir sonrakini istemiyor. Üreteç yield noktasında askıya alınır ve yalnız yeni bir next çağrısı geldiğinde devam eder. Buna çekmeli (pull-based) akış denir; hızı tüketici belirler.

Hata ve Erken Çıkış

Akış içindeki bir reddetme, for await döngüsünün bulunduğu yerde atılır ve sıradan bir try/catch ile yakalanır. Yakalanan hata döngüyü sonlandırır.

const KAYNAK = {
  A1: { gecikme: 30, deger: 21.4 },
  C3: { gecikme: 20, deger: 23.1 },
  D4: { gecikme: 15, hata: "sensör arızası" },
};

function olcumGetir(istasyon) {
  return new Promise((karar, red) => {
    const kayit = KAYNAK[istasyon];
    setTimeout(() => {
      if (kayit.hata) red(new Error(`${istasyon}: ${kayit.hata}`));
      else karar({ istasyon, deger: kayit.deger });
    }, kayit.gecikme);
  });
}

async function* olcumAkisi(istasyonlar) {
  for (const istasyon of istasyonlar) {
    yield await olcumGetir(istasyon);
  }
}

try {
  for await (const olcum of olcumAkisi(["A1", "D4", "C3"])) {
    console.log("tüketici:", olcum.istasyon, olcum.deger);
  }
} catch (hata) {
  console.log("akış hatayla durdu:", hata.message);
}
tüketici: A1 21.4
akış hatayla durdu: D4: sensör arızası

Üçüncü istasyona hiç sıra gelmedi: üreteç, hata atılan noktada sonlandı. Akışın hataya rağmen sürmesi isteniyorsa, hata üretecin içinde yakalanıp bir sonuç nesnesine dönüştürülmelidir — önceki dersteki allSettled yaklaşımının akış karşılığı budur.

Döngüden break ile çıkmak da üreteci sonlandırır. Bu, açılan kaynakların kapatılması için bir fırsat sağlar: üretecin finally bloğu her iki durumda da çalışır.

const KAYNAK = {
  A1: { gecikme: 30, deger: 21.4 },
  B2: { gecikme: 10, deger: 19.8 },
  C3: { gecikme: 20, deger: 23.1 },
};

function olcumGetir(istasyon) {
  return new Promise((karar) => {
    const kayit = KAYNAK[istasyon];
    setTimeout(() => karar({ istasyon, deger: kayit.deger }), kayit.gecikme);
  });
}

async function* olcumAkisi(istasyonlar) {
  try {
    for (const istasyon of istasyonlar) {
      yield await olcumGetir(istasyon);
    }
  } finally {
    console.log("üretici: temizlik çalıştı");
  }
}

for await (const olcum of olcumAkisi(["A1", "B2", "C3"])) {
  console.log("tüketici:", olcum.istasyon);
  if (olcum.istasyon === "B2") break;
}
console.log("döngüden çıkıldı");
tüketici: A1
tüketici: B2
üretici: temizlik çalıştı
döngüden çıkıldı

Temizlik, döngüden çıkılmadan önce çalıştı. Sürekli bir kaynağı — açık bir bağlantıyı, kurulmuş bir zamanlayıcıyı — okuyan üreteçlerde bu blok zorunludur; aksi halde tüketici vazgeçse bile kaynak açık kalır.

Söz Dizisi Üzerinde for await

for await, eşzamansız yineleyici olmayan sıradan bir dizi üzerinde de çalışır: her elemanı await ile bekler. Bu, bir söz dizisini sırayla tüketmenin kısa yoludur.

const KAYNAK = {
  A1: { gecikme: 30, deger: 21.4 },
  B2: { gecikme: 10, deger: 19.8 },
  C3: { gecikme: 20, deger: 23.1 },
};

function olcumGetir(istasyon) {
  return new Promise((karar) => {
    const kayit = KAYNAK[istasyon];
    setTimeout(() => karar({ istasyon, deger: kayit.deger }), kayit.gecikme);
  });
}

const sozler = ["A1", "B2", "C3"].map((istasyon) => olcumGetir(istasyon));
for await (const olcum of sozler) {
  console.log("sırayla tüketildi:", olcum.istasyon, olcum.deger);
}
sırayla tüketildi: A1 21.4
sırayla tüketildi: B2 19.8
sırayla tüketildi: C3 23.1

Buradaki ayrım önemlidir: istekler birlikte başladı, sonuçlar sırayla tüketildi. Sonuçlar dizideki sırayla işlenir; A1 en yavaş olduğu halde ilk sırada beklenir. Bu, işleme sırasının önemli olduğu durumlarda istenen davranıştır.

Geri Basınç

Veri Yapıları kursunda kuyruklar anlatılırken tanıtılan geri basınç (backpressure) kavramı, eşzamansız akışların merkezindedir: üretici tüketiciden hızlıysa, aradaki fark bir yerde birikmek zorundadır.

İtmeli (push-based) bir kaynakta üretici tüketiciyi beklemez; öğeler bir tampona yığılır.

const tampon = [];
let uretilen = 0;

const kimlik = setInterval(() => {
  uretilen += 1;
  tampon.push({ sira: uretilen });
  if (uretilen === 6) clearInterval(kimlik);
}, 5);

await new Promise((karar) => setTimeout(karar, 45));
console.log("üretilen:", uretilen, "— tampondaki bekleyen:", tampon.length);
üretilen: 6 — tampondaki bekleyen: 6

Tüketici hiç okumadığı için altı öğenin altısı da bellekte bekliyor. Üretim ucu açık olsaydı tampon sınırsız büyür ve bellek tükenirdi. Bu, bellek konusunda ele alınacak sızıntı biçimlerinden biridir.

Çekmeli akışta bu sorun tanım gereği yoktur: üreteç bir sonraki değeri ancak istendiğinde üretir, bu yüzden birikecek bir şey olmaz. İtmeli bir kaynağı çekmeli bir arayüzün arkasına almak — yani tamponu sınırlamak ve dolduğunda üretimi durdurmak — akış tasarımının temel kalıbıdır.

Özet

  • Eşzamansız yineleyici protokolü, next çağrısının sonucu bir sözle döndürmesi dışında sıradan yineleme protokolüyle aynıdır; nesne Symbol.asyncIterator tanımlar.
  • async function* biçimindeki üreteçler, protokolün durum yönetimini devralır ve gövdede hem await hem yield kullanılmasına izin verir.
  • for await her turda bir sonraki değeri bekler; akıştaki reddetme döngünün bulunduğu yerde atılır ve döngüyü sonlandırır.
  • Erken çıkışta üretecin finally bloğu çalışır; sürekli kaynakların temizliği buraya yazılır.
  • Çekmeli akışta hızı tüketici belirler ve geri basınç kendiliğinden oluşur; itmeli akışta tampon sınırlanmazsa bellek tükenir.

Sonraki Adım

Bu konuda kullanılan ölçüm kaynağı hep aynı süreçte, zamanlayıcıyla taklit edildi. Sonraki konu kaynağı süreç dışına taşır: ağ üzerinden istek ve yanıt. Orada istek ile yanıt nesnelerinin yapısı, gövdenin akış halinde okunması ve bu dersteki eşzamansız yinelemenin ağ gövdelerine doğrudan uygulanışı ele alınacak.

İ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