Ders 02 / 17
Olay Döngüsü
Çağrı yığıtı, görev kuyruğu ve mikro görev kuyruğunun ayrımı; olay döngüsünün tur kuralı; karışık bir örnekte çıktı sırasının satır satır gerekçelendirilmesi.
İçindekiler
Önceki ders geri çağrıların “sıraya girdiğini” söyledi. Sıra tek değildir ve içindeki işler eşit öncelikli değildir. Bir söz geri çağrısı, kendisinden önce kaydedilmiş bir zamanlayıcı geri çağrısından önce çalışır; bu, öncelik değil, kuyruk farkıdır.
Bu ders, o kuyrukları ve aralarındaki kuralı kurar. Kural öğrenildiğinde, karışık bir programın çıktı sırası çalıştırmadan önce söylenebilir hale gelir — bu kursun geri kalanı boyunca kullanılacak temel yetkinlik budur.
Çağrı Yığıtı
Çağrı yığıtı (call stack), o anda yürüyen fonksiyon çağrılarını tutan yapıdır.
Programlama Temelleri kursunda tanıtılan çağrı çerçevesi kavramı burada aynen geçerlidir:
her çağrı bir çerçeve iter, her return bir çerçeve çıkarır.
Yığıt boşaldığında, o anda yürüyen bir iş kalmamış demektir. Önceki dersteki sonuna kadar çalışma kuralının kesin ifadesi budur: bir sonraki iş, ancak yığıt boşaldığında başlar.
Bunun gözlenebilir bir sonucu vardır. Bir try bloğu, yalnız kendi yığıt çerçevesi
yaşarken oluşan hataları yakalar. Geri çağrı ise boş yığıtta, bambaşka bir turda çalışır:
process.on("uncaughtException", (hata) => { console.log("2 — çalışma zamanı düzeyinde yakalandı:", hata.message); }); try { setTimeout(() => { throw new Error("geri çağrının içinden"); }, 0); console.log("1 — try bloğu sorunsuz bitti"); } catch (hata) { console.log("bu satır hiç çalışmaz:", hata.message); }
1 — try bloğu sorunsuz bitti 2 — çalışma zamanı düzeyinde yakalandı: geri çağrının içinden
try bloğu yalnız setTimeout çağrısının kendisini kapsar; geri çağrının gövdesini
kapsamaz. Geri çağrı çalıştığında try bloğunun çerçevesi çoktan yığıttan çıkmıştır. Bu
gözlem, kursun ilerleyen bölümlerinde eşzamansız hata yönetiminin neden ayrı bir düzenek
gerektirdiğini açıklayacak.
Görev Kuyruğu ve Olay Döngüsü
Ana ortam, tamamlanan işleri görev kuyruğuna (task queue) koyar: süresi dolan bir zamanlayıcı, gelen bir ağ yanıtı, tıklanan bir düğme. Kuyruktaki her giriş bir görev (task) — bazı kaynaklarda makro görev — yani baştan sona çalıştırılacak bir geri çağrıdır.
Olay döngüsü (event loop), iki adımı sonsuza kadar tekrarlayan denetim akışıdır:
- Yığıt boşsa, görev kuyruğundan bir görev al ve sonuna kadar çalıştır.
- Görev bittikten sonra, mikro görev kuyruğunu tamamen boşalt.
Bu iki adıma tur denir. Tarayıcı ortamında turların arasına çizim (render) adımı da girer; bu nedenle uzun bir görev, çizimi de erteler.
Mikro Görev Kuyruğu
Mikro görev (microtask), aynı turun sonunda çalışması güvence altına alınmış küçük
iştir. Söz geri çağrıları ve queueMicrotask ile kaydedilen işler bu kuyruğa girer.
İki kuyruk arasındaki fark tek cümleyle ifade edilir: görev kuyruğundan tur başına bir iş alınır; mikro görev kuyruğu ise tamamen boşaltılır.
Boşaltma sırasında eklenen yeni mikro görevler de aynı boşaltmaya dahildir. Yani bir mikro görev, başka bir mikro görev üretirse, o da sıradaki göreve geçilmeden önce çalışır.
setTimeout(() => { console.log("A — birinci görev"); queueMicrotask(() => console.log("B — birinci görevin mikro görevi")); setTimeout(() => console.log("D — birinci görevin ürettiği görev"), 0); }, 0); setTimeout(() => console.log("C — ikinci görev"), 0);
A — birinci görev B — birinci görevin mikro görevi C — ikinci görev D — birinci görevin ürettiği görev
Birinci görev, hem bir mikro görev hem bir görev üretti. Mikro görev, ikinci göreve geçilmeden önce çalıştı; üretilen görev ise kuyruğun sonuna eklendiği için en sona kaldı.
Karışık Bir Örnekte Sıra
Kural, eşzamanlı kod ile iki kuyruğun birlikte göründüğü bir örnekte sınanır. Aşağıdaki programın çıktı sırasını, çalıştırmadan önce söyleyebilmelisiniz.
console.log("1 — eşzamanlı"); setTimeout(() => console.log("6 — görev kuyruğu (setTimeout)"), 0); Promise.resolve().then(() => console.log("4 — mikro görev (söz)")); queueMicrotask(() => console.log("5 — mikro görev (queueMicrotask)")); console.log("2 — eşzamanlı"); (function () { console.log("3 — eşzamanlı (iç fonksiyon)"); })();
1 — eşzamanlı 2 — eşzamanlı 3 — eşzamanlı (iç fonksiyon) 4 — mikro görev (söz) 5 — mikro görev (queueMicrotask) 6 — görev kuyruğu (setTimeout)
Gerekçe satır satır şudur:
- 1, 2, 3 — Bunlar yürüyen işin kendisidir. Yığıt boşalana kadar hiçbir kuyruğa
bakılmaz. Ara satırlardaki
setTimeout,thenvequeueMicrotaskçağrıları yalnız kayıt yapar; kaydettikleri fonksiyonu çalıştırmaz. - 4, 5 — Yığıt boşaldı. Mikro görev kuyruğu tamamen boşaltılır. İçindeki iki iş,
kaydedilme sırasına göre çalışır: önce söz geri çağrısı, sonra
queueMicrotask. - 6 — Mikro görev kuyruğu boş. Ancak şimdi görev kuyruğundan bir görev alınır.
Sıranın kaynağı önceliğin kendisi değil, kuyruk yapısıdır: setTimeout en başta
kaydedildiği halde en sona kaldı; çünkü farklı bir kuyruğa girdi.
Mikro Görev Kuyruğu Aç Bırakabilir
“Tamamen boşaltılır” kuralının bir bedeli vardır: kendini yeniden kaydeden bir mikro görev zinciri, görev kuyruğunu süresiz bekletir.
let sayac = 0; setTimeout(() => console.log("zamanlayıcı çalıştı; biriken mikro görev:", sayac), 0); function zincir() { sayac += 1; if (sayac < 100000) queueMicrotask(zincir); } queueMicrotask(zincir);
zamanlayıcı çalıştı; biriken mikro görev: 100000
Yüz bin mikro görevin tamamı, sıfır gecikmeli zamanlayıcıdan önce çalıştı. Sayaç sınırı kaldırılsaydı zamanlayıcı hiç çalışmayacaktı. Buna açlık (starvation) denir: bir kuyruk, diğerinin çalışma fırsatını tüketir.
Pratik sonucu şudur: uzun ve bölünebilir bir işi mikro görevlere bölmek çözüm değildir. Ana ortama gerçekten sıra vermek için işi görev kuyruğuna aktarmak gerekir. Bu kalıp, başarım konusunda ayrıntılandırılacak.
Kuralı Ölçüm Akışına Uygulamak
Kural, kursun örneğinde de aynen işler. Aşağıdaki program iki istasyondan ölçüm ister; istasyonların yanıt gecikmeleri farklıdır ve yanıt geldiğinde bir kayıt işi mikro göreve bırakılır.
const KAYNAK = { A1: { gecikme: 30, deger: 21.4 }, B2: { gecikme: 10, deger: 19.8 }, }; function olcumIste(istasyon, gericagri) { console.log("istek kaydedildi:", istasyon); const kayit = KAYNAK[istasyon]; setTimeout(() => gericagri({ istasyon, deger: kayit.deger }), kayit.gecikme); } olcumIste("A1", (olcum) => { console.log("görev: yanıt işleniyor —", olcum.istasyon); queueMicrotask(() => console.log("mikro görev: kayıt tamamlandı —", olcum.istasyon)); }); olcumIste("B2", (olcum) => { console.log("görev: yanıt işleniyor —", olcum.istasyon); queueMicrotask(() => console.log("mikro görev: kayıt tamamlandı —", olcum.istasyon)); }); console.log("betiğin eşzamanlı kısmı bitti");
istek kaydedildi: A1 istek kaydedildi: B2 betiğin eşzamanlı kısmı bitti görev: yanıt işleniyor — B2 mikro görev: kayıt tamamlandı — B2 görev: yanıt işleniyor — A1 mikro görev: kayıt tamamlandı — A1
İki gözlem önemlidir. Birincisi, istek sırası ile yanıt sırası aynı değildir: A1 önce istendi, B2 önce yanıtladı. Eşzamansız bir akışta sonuçların geliş sırası, isteklerin başlatılma sırasına bağlı değildir. İkincisi, her yanıtın kayıt mikro görevi kendi turunun sonunda, bir sonraki yanıt işlenmeden çalışır; iki istasyonun işleme adımları birbirine karışmaz.
Kuyruk Ayrıntıları Ortama Bağlıdır
İki kuyruklu model standarttır ve her ortamda geçerlidir. Buna karşılık çalışma zamanları görev kuyruğunu evrelere bölebilir ve kendi ek kuyruklarını ekleyebilir. Bu ek kuyrukların sözlerin mikro görev kuyruğuna göre yeri, aynı çalışma zamanında bile bağlama göre değişebilir.
Aşağıdaki iki dosya aynı üç satırı içerir; yalnız modül biçimleri farklıdır. Modül biçimlerinin kendisi Modüller, Araçlar ve Ekosistem kursunun konusudur.
// sira.cjs Promise.resolve().then(() => console.log("söz mikro görevi")); process.nextTick(() => console.log("çalışma zamanına özgü kuyruk")); console.log("eşzamanlı");
$ node sira.cjs eşzamanlı çalışma zamanına özgü kuyruk söz mikro görevi
// sira.mjs Promise.resolve().then(() => console.log("söz mikro görevi")); process.nextTick(() => console.log("çalışma zamanına özgü kuyruk")); console.log("eşzamanlı");
$ node sira.mjs eşzamanlı söz mikro görevi çalışma zamanına özgü kuyruk
İki kuyruğun göreli sırası ters döndü. Buradan çıkarılacak kural, hangi biçimin doğru olduğu değildir: taşınabilir kod, standart dışı kuyrukların göreli sırasına dayanmamalıdır. Eşzamanlı kod, mikro görevler ve görevler arasındaki üç aşamalı sıra her ortamda güvenilirdir; ötesi ortamın ayrıntısıdır.
Özet
- Çağrı yığıtı yürüyen çağrıları tutar; bir sonraki iş ancak yığıt boşaldığında başlar.
- Olay döngüsü her turda görev kuyruğundan bir görev alır, ardından mikro görev kuyruğunu tamamen boşaltır.
- Mikro görevler aynı turda, görevler sonraki turlarda çalışır; bu nedenle sonradan kaydedilen bir söz geri çağrısı, önceden kaydedilen bir zamanlayıcıdan önce çalışır.
- Boşaltma sırasında üretilen mikro görevler de aynı boşaltmaya dahildir; bu, görev kuyruğunu aç bırakabilir.
- Üç aşamalı sıra (eşzamanlı kod, mikro görevler, görevler) standarttır; çalışma zamanına özgü ek kuyrukların yeri taşınabilir değildir.
Sonraki Adım
Kuyruk kuralı kurulduğuna göre, kuyruğa iş koyan en yaygın araca dönülebilir:
zamanlayıcılar. Sonraki ders, setTimeout içindeki sayının neden bir söz değil bir alt
sınır olduğunu, eşit gecikmeli zamanlayıcıların hangi sıraya girdiğini ve yinelemeli
çalıştırmanın kendi geri çağrısı uzun sürdüğünde nasıl kaydığını gösterir. Ölçüm akışının
ilk somut parçası — zamanlayıcıya dayalı sahte bir istasyon — orada kurulacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.