Node.js Event Loop Rehberi: Fazlar ve Görev Sırası

Node.js'in tek iş parçacıklı (single-threaded) olduğu sık söylenir, ama aynı süreç binlerce eşzamanlı bağlantıyı sorunsuz taşır. Bu görünürdeki çelişk...

Node.js Event Loop Rehberi: Fazlar ve Görev Sırası

Node.js'in tek iş parçacıklı (single-threaded) olduğu sık söylenir, ama aynı süreç binlerce eşzamanlı bağlantıyı sorunsuz taşır. Bu görünürdeki çelişki, event loop'un nasıl çalıştığını anlamadan çözülemez. Bu rehber, event loop'un fazlarını, mikro ve makro görev ayrımını ve asenkron kodun bu yapı üzerinde nasıl davrandığını uygulamalı örneklerle ele alıyor.

Önkoşullar ve Kapsam

Bu rehber orta seviye geliştiriciler için yazıldı. İçeriği takip etmek için şunlara aşina olmanız beklenir:

  • JavaScript'te fonksiyonlar, kapsam (scope) ve callback kavramları
  • Promise, async/await sözdizimi
  • Node.js'te modül kullanımı ve temel CLI komutları

Kapsam Node.js'in çalışma zamanı modeliyle sınırlı. Tarayıcı event loop'u ile Node.js event loop'u benzer olsa da aynı değildir; burada yalnızca Node.js ele alınıyor.

Sürüm notu: Node.js'in event loop davranışı yıllar içinde olgunlaştı, ancak bu rehberdeki faz yapısı ve mikro/makro görev ayrımı uzun süredir kararlı. Belirli bir sürüm numarası vermek yanıltıcı olur; davranışı kendi Node.js sürümünüzde küçük deneylerle doğrulamanız en sağlıklısı.

Event Loop Neden Var?

Node.js, G/Ç (I/O) işlemlerini işletim sistemine devreder. Dosya okuma, ağ isteği veya zamanlayıcı gibi işler arka planda yürürken ana iş parçacığı boş durmaz. İş tamamlandığında sonucu işleyecek callback kuyruğa alınır ve event loop bu kuyrukları sırayla işler.

Yani "tek iş parçacıklı" ifadesi JavaScript kodunuzun yürütülmesi için geçerlidir. Asıl paralellik, işletim sistemi ve libuv'un yönettiği arka plan işlerinde gerçekleşir.

Event Loop'un Fazları

Event loop tek bir döngü değil, sırayla çalışan fazlardan oluşur. Her turda bu fazlar belirli bir sırayla ziyaret edilir:

  1. Timers: Süresi dolmuş setTimeout ve setInterval callback'leri çalışır.
  2. Pending callbacks: Önceki turdan ertelenen bazı sistem callback'leri işlenir.
  3. Idle, prepare: Yalnızca Node.js'in iç kullanımına ayrılmıştır.
  4. Poll: Yeni G/Ç olayları beklenir ve ilgili callback'ler çalıştırılır. Döngünün en uzun kalabileceği faz burasıdır.
  5. Check: setImmediate callback'leri çalışır.
  6. Close callbacks: socket.on('close', ...) gibi kapanış callback'leri işlenir.

Bu fazlar arasında, önemli bir kural devreye girer: her fazın sonunda mikro görev kuyruğu boşaltılır.

Mikro Görevler ve Makro Görevler

Bu ayrım, asenkron kodun gerçek çalışma sırasını anlamanın anahtarıdır.

  • Mikro görevler: Promise çözümlemeleri (.then, await sonrası devam) ve queueMicrotask ile eklenen işler.
  • Makro görevler: setTimeout, setInterval, setImmediate ve G/Ç callback'leri.

Kritik kural: Bir fazın callback'i bittiğinde, bir sonraki faza geçmeden önce tüm mikro görev kuyruğu boşaltılır. Mikro görevler kendi aralarında da yeni mikro görev ekleyebilir; bu durumda kuyruk tamamen boşalana kadar döngü devam eder.

Sıralamayı Gösteren Bir Örnek

console.log('1: senkron');

setTimeout(() => {
  console.log('2: setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('3: promise');
});

console.log('4: senkron');

Beklenen çıktı:

1: senkron
4: senkron
3: promise
2: setTimeout

Neden? Önce tüm senkron kod çalışır (1 ve 4). Senkron kod bittiğinde mikro görev kuyruğu boşaltılır ve 3 yazılır. setTimeout bir makro görev olduğu için timers fazında, yani en sonra çalışır.

Bu örneği kendi ortamınızda çalıştırıp çıktıyı doğrulamanız, kavramı pekiştirmenin en hızlı yoludur.

await Aslında Ne Yapar?

async/await, Promise üzerine kurulu bir sözdizimi şekeridir. await gördüğünüzde, fonksiyonun kalanı bir mikro görev olarak kuyruğa alınır.

async function ornek() {
  console.log('A');
  await null;
  console.log('B');
}

ornek();
console.log('C');

Beklenen çıktı:

A
C
B

await ifadesi, fonksiyonun geri kalanını askıya alır ve kontrolü çağırana bırakır. null await etmek bile bir mikro görev oluşturur; bu yüzden B, senkron C'den sonra yazılır. await'in arkasındaki değerin bir Promise olması şart değildir.

setImmediate ve setTimeout(0) Farkı

Bu iki API sık karıştırılır. Temel fark, hangi fazda çalıştıklarıdır: setImmediate check fazında, setTimeout ise timers fazında çalışır.

Ana modülde (doğrudan çalıştırılan bir dosyada) hangisinin önce çalışacağı ortama ve zamanlamaya bağlıdır, bu yüzden sabit bir sıra beklemeyin. Ancak bir G/Ç callback'i içinde çağrıldıklarında sıra belirginleşir: poll fazında çalışan bir callback içinde setImmediate, bir sonraki turun check fazında; setTimeout(0) ise bir sonraki turun timers fazında çalışır. Pratikte bu senaryoda setImmediate genellikle önce çalışır.

Bu davranışa güvenen kod yazmak yerine, sıralamanın gerçekten önemli olduğu yerde mantığı açıkça kurmak daha güvenlidir.

Event Loop'u Bloke Etmek: En Yaygın Hata

Event loop tek iş parçacıklı olduğu için, uzun süren senkron bir işlem tüm döngüyü durdurur. Bu sırada gelen istekler, zamanlayıcılar ve G/Ç callback'leri bekler.

// Kötü örnek: event loop'u bloke eder
function agirHesap(veri) {
  let toplam = 0;
  for (let i = 0; i < veri.length; i++) {
    // Uzun süren senkron iş
    toplam += Math.sqrt(veri[i]);
  }
  return toplam;
}

Bu fonksiyon çalışırken süreç başka hiçbir isteği işleyemez. Çözüm seçenekleri:

  • İşi parçalara bölüp setImmediate ile turlar arasına yaymak
  • Hesaplamayı worker_threads ile ayrı bir iş parçacığına taşımak
  • Ağır işi ayrı bir servise veya iş kuyruğuna devretmek

Senkron fs.readFileSync gibi çağrılar da aynı etkiyi yapar. Sunucu kodunda, istek işleyen bir yol içinde senkron G/Ç kullanmaktan kaçının.

Güvenlik Açısından Konuya Bakış

Event loop modeli doğrudan bir güvenlik açığı üretmez, ancak dolaylı etkileri vardır:

  • Hizmet dışı bırakma (DoS): Kullanıcı girdisine bağlı ağır senkron işlemler, tek bir istekle tüm sunucuyu kilitleyebilir. Girdi boyutunu sınırlamak ve ağır işi izole etmek bu riski azaltır.
  • Zamanlama sızıntıları: Güvenlik açısından hassas karşılaştırmalarda (örneğin token doğrulama) sabit zamanlı karşılaştırma kullanmak gerekir; bu, event loop'tan bağımsız bir konudur ama asenkron kod yazarken gözden kaçabilir.
  • Yakalanmayan Promise redleri: await edilmeyen bir Promise reddi, sessizce yutulabilir ve beklenmeyen durumlara yol açabilir. process.on('unhandledRejection', ...) ile bu durumları izlemek yararlıdır.

Bunlar event loop'un kendisinden değil, onun üzerine yazılan kodun dikkatsizliğinden kaynaklanır.

Uygulamalı Kontrol Listesi

Asenkron mimari kurarken şu noktaları gözden geçirin:

  • İstek işleyen yollarda senkron G/Ç ve uzun senkron döngülerden kaçının.
  • Sıralamanın kritik olduğu yerde mikro/makro görev ayrımını bilinçli kullanın; ortama bağlı sıralamalara güvenmeyin.
  • Ağır hesaplamaları worker_threads veya ayrı servislere taşıyın.
  • await edilmeyen Promise'leri ve yakalanmayan redleri izleyin.
  • Kullanıcı girdisine bağlı iş yüklerini boyut ve süre açısından sınırlayın.

Sonuç

Node.js'in eşzamanlılık modeli, event loop'un fazları ve mikro/makro görev ayrımı üzerine kuruludur. Bu yapıyı anlamak, "neden bu kod bu sırayla çalışıyor" sorusunu tahmine bırakmadan yanıtlamanızı sağlar. En sık yapılan hata, event loop'u senkron işle bloke etmektir; en sık yapılan yanlış varsayım ise setTimeout ve setImmediate sıralamasının her yerde aynı olduğunu düşünmektir. Bu iki noktaya dikkat eden bir mimari, hem daha öngörülebilir hem de daha dayanıklı olur.

İLGİLİ İÇERİKLER

Bunları da Okumak İsteyebilirsiniz