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/awaitsö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:
- Timers: Süresi dolmuş
setTimeoutvesetIntervalcallback'leri çalışır. - Pending callbacks: Önceki turdan ertelenen bazı sistem callback'leri işlenir.
- Idle, prepare: Yalnızca Node.js'in iç kullanımına ayrılmıştır.
- 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.
- Check:
setImmediatecallback'leri çalışır. - 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,awaitsonrası devam) vequeueMicrotaskile eklenen işler. - Makro görevler:
setTimeout,setInterval,setImmediateve 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
setImmediateile turlar arasına yaymak - Hesaplamayı
worker_threadsile 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:
awaitedilmeyen 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_threadsveya ayrı servislere taşıyın. awaitedilmeyen 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.