Node.js ile geliştirme yaparken “tek iş parçacığıyla aynı anda birçok isteği nasıl yönetiyor?” sorusuyla mutlaka karşılaşırsınız. Bu sorunun merkezinde Node.js Event Loop bulunur.
Event Loop ilk bakışta karmaşık görünebilir. Timer aşamaları, callback kuyrukları, mikro görevler ve asenkron API’ler devreye girdiğinde hangi kodun önce çalışacağını tahmin etmek zorlaşır. Ancak temel çalışma mantığını görmek için uzun teorik açıklamalara ihtiyacımız yok. Birkaç satırlık kodla davranışı doğrudan gözlemleyebiliriz.
Bu yazıda Node.js Event Loop’un çalışma mantığını basit deneylerle inceleyeceğiz. Senkron kodların, Promise callback’lerinin, process.nextTick, setTimeout ve setImmediate görevlerinin hangi sırayla çalıştığını adım adım açıklayacağız.
Node.js Event Loop Nedir?
Node.js Event Loop, JavaScript kodunun ve tamamlanan asenkron işlemlere ait callback’lerin hangi sırayla çalıştırılacağını yöneten mekanizmadır.
JavaScript kodu temel olarak tek bir ana iş parçacığında yürütülür. Buna rağmen Node.js; dosya okuma, ağ isteği, veri tabanı sorgusu ve zamanlayıcı gibi işlemleri beklerken ana iş parçacığını gereksiz yere durdurmaz.
Genel akış şu şekilde düşünülebilir:
- Senkron JavaScript kodu çalıştırılır.
- Asenkron işlem ilgili sisteme devredilir.
- Ana iş parçacığı diğer kodları çalıştırmaya devam eder.
- Asenkron işlem tamamlandığında callback uygun kuyruğa eklenir.
- Çağrı yığını boşaldığında Event Loop sıradaki görevi çalıştırır.
Buradaki önemli nokta şudur: Asenkron işlem tamamlandığı anda callback doğrudan çalışmaz. Önce uygun kuyruğa alınır ve çalıştırılabileceği zamanı bekler.
İlk Deney: Senkron Kod ve setTimeout
Yeni bir event-loop.js dosyası oluşturalım:
console.log("1: Program başladı");
setTimeout(() => {
console.log("2: Timer çalıştı");
}, 0);
console.log("3: Program bitti");Dosyayı çalıştıralım:
node event-loop.jsBeklenen çıktı şöyledir:
1: Program başladı
3: Program bitti
2: Timer çalıştıBurada kafa karıştıran bölüm, sıfır milisaniyelik timer’ın neden hemen çalışmadığıdır.
setTimeout(fn, 0) Neden Hemen Çalışmaz?
setTimeout(callback, 0), callback’in anında çalışacağını garanti etmez. Yalnızca belirtilen minimum bekleme süresinden sonra çalıştırılmaya uygun hâle gelmesini sağlar.
Node.js şu sırayı takip eder:
- İlk
console.logsenkron olarak çalışır. setTimeoutzamanlayıcı olarak kaydedilir.- Son
console.logsenkron olarak çalışır. - Ana çağrı yığını boşalır.
- Event Loop timer callback’ini çalıştırır.
Bu nedenle sıfır milisaniyelik süre, senkron kodun önüne geçemez.
Basit bir kuralla ifade edersek:
Senkron kodlar tamamlanmadan kuyruktaki callback’ler çalıştırılmaz.
İkinci Deney: Promise Kuyruğa Eklendiğinde Ne Olur?
Şimdi örneğe bir Promise ekleyelim:
console.log("1: Senkron başlangıç");
setTimeout(() => {
console.log("2: setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("3: Promise");
});
console.log("4: Senkron bitiş");Beklenen çıktı:
1: Senkron başlangıç
4: Senkron bitiş
3: Promise
2: setTimeoutSenkron kodların önce çalışması artık şaşırtıcı değildir. Peki Promise neden setTimeout callback’inden önce çalıştı?
Çünkü Promise tarafından oluşturulan callback bir microtask, yani mikro görev olarak kuyruğa alınır. setTimeout callback’i ise timer aşamasında çalıştırılır.
Mevcut JavaScript işlemi tamamlandığında Node.js, bir sonraki Event Loop aşamasına geçmeden önce mikro görevleri çalıştırır. Bu nedenle Promise callback’i timer’dan önce yürütülür.
Microtask ve Timer Arasındaki Fark
Basitleştirilmiş şekilde iki kuyruk türünden söz edebiliriz:
- Microtask kuyruğu: Promise callback’leri gibi yüksek öncelikli görevleri içerir.
- Event Loop aşama kuyrukları: Timer, I/O ve
setImmediategibi callback’leri içerir.
Çalışma mantığını başlangıç seviyesinde şöyle düşünebiliriz:
Senkron kod
↓
Microtask'lar
↓
Event Loop'un uygun aşamasındaki callback'lerGerçek Node.js mimarisi daha ayrıntılıdır. Ancak bu model, günlük geliştirmede karşılaşılan birçok sıralama problemini anlamak için yeterlidir.
Üçüncü Deney: process.nextTick Nerede Çalışır?
Node.js, Promise mikro görevlerinden ayrı olarak process.nextTick kuyruğuna sahiptir.
Aşağıdaki örneği çalıştıralım:
console.log("1: Senkron başlangıç");
setTimeout(() => {
console.log("2: setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("3: Promise");
});
process.nextTick(() => {
console.log("4: process.nextTick");
});
console.log("5: Senkron bitiş");Beklenen çıktı:
1: Senkron başlangıç
5: Senkron bitiş
4: process.nextTick
3: Promise
2: setTimeoutİlk olarak senkron kod çalışır. Ardından Node.js, process.nextTick kuyruğunu işler. Sonra Promise mikro görevleri ve uygun Event Loop aşamasındaki timer callback’i çalıştırılır.
Basitleştirilmiş sıralama şöyledir:
Senkron kod
↓
process.nextTick kuyruğu
↓
Promise microtask kuyruğu
↓
Timer ve diğer Event Loop aşamalarıprocess.nextTick Kullanırken Dikkat Edilmesi Gerekenler
process.nextTick, callback’in mevcut işlem tamamlandıktan hemen sonra çalıştırılması gereken durumlarda yararlı olabilir. Ancak kontrolsüz kullanılması Event Loop’un diğer aşamalara geçmesini geciktirebilir.
Örneğin:
function tekrarCalistir() {
process.nextTick(tekrarCalistir);
}
tekrarCalistir();
setTimeout(() => {
console.log("Bu timer çalışmakta zorlanacak");
}, 0);Bu örnekte tekrarCalistir her seferinde yeni bir process.nextTick görevi oluşturur. Node.js sürekli olarak bu kuyruğu boşaltmaya çalıştığı için timer’a sıra gelmez.
Bu duruma Event Loop starvation denir. Yani belirli bir görev türü, diğer görevlerin çalışmasını engeller.
Üretim kodunda sınırsız process.nextTick döngülerinden kaçınmak gerekir.
Dördüncü Deney: Uzun Süren Senkron Kod Ne Yapar?
Node.js asenkron işlemleri verimli biçimde yönetebilir. Ancak uzun süren bir senkron işlem ana iş parçacığını meşgul ederse Event Loop ilerleyemez.
Şu örneği deneyelim:
console.log("1: Başlangıç");
setTimeout(() => {
console.log("2: Timer tamamlandı");
}, 0);
const baslangic = Date.now();
while (Date.now() - baslangic < 3000) {
// Ana iş parçacığını yaklaşık 3 saniye meşgul ediyoruz.
}
console.log("3: Senkron işlem tamamlandı");Beklenen çıktı:
1: Başlangıç
3: Senkron işlem tamamlandı
2: Timer tamamlandıTimer sıfır milisaniye sonra çalışmaya uygun hâle gelse bile yaklaşık üç saniye beklemek zorunda kalır. Çünkü while döngüsü çağrı yığınını ve ana iş parçacığını meşgul eder.
Bu deney Node.js Event Loop için en önemli kurallardan birini gösterir:
Asenkron API kullanmak, bütün kodun otomatik olarak engellemesiz çalışacağı anlamına gelmez.
CPU yoğun senkron işlemler Event Loop’u bloke edebilir.
Event Loop Bloke Olursa Ne Olur?
Event Loop’un bloke olması, uygulamanın yeni callback’leri zamanında çalıştıramaması anlamına gelir.
Bir web sunucusunda bu durum:
- HTTP isteklerinin gecikmesine
- Zaman aşımı hatalarına
- Sağlık kontrollerinin başarısız olmasına
- Diğer kullanıcıların aynı anda yavaşlık yaşamasına
- Timer ve I/O callback’lerinin geç çalışmasına
- Uygulamanın yanıt vermiyor gibi görünmesine
neden olabilir.
Ağır hesaplamalar için iş küçük parçalara bölünebilir, Worker Threads kullanılabilir veya görev ayrı bir servis ya da iş kuyruğuna taşınabilir.
Beşinci Deney: setImmediate ile setTimeout Farkı
setTimeout(callback, 0) ve setImmediate(callback) benzer görünse de Event Loop’un farklı aşamalarında çalışır.
Şu örneğin çıktısı ilk bakışta kesinmiş gibi düşünülmemelidir:
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});Ana modül seviyesinde çalıştırıldığında iki callback’in sırası çalışma koşullarına göre değişebilir. Bu nedenle “setImmediate her zaman önce çalışır” veya “sıfır süreli timer her zaman önce çalışır” demek doğru değildir.
Farkı daha net görmek için ikisini bir I/O callback’i içinde çalıştıralım:
const fs = require("node:fs");
fs.readFile(__filename, () => {
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
});Bu senaryoda beklenen çıktı genellikle şöyledir:
setImmediate
setTimeoutDosya okuma callback’i tamamlandıktan sonra Event Loop, setImmediate callback’lerinin işlendiği check aşamasına geçer. Timer ise sonraki uygun timers aşamasında çalışır.
Burada önemli olan ezber yapmak değil, callback’lerin farklı Event Loop aşamalarında işlendiğini anlamaktır.
Event Loop Aşamaları Nelerdir?
Node.js Event Loop basitleştirilmiş olarak şu aşamalardan oluşur:
Timers
Süresi dolan setTimeout ve setInterval callback’leri bu aşamada değerlendirilir.
Pending callbacks
Bazı sistem işlemlerine ait ertelenmiş callback’ler çalıştırılır.
Idle ve prepare
Node.js tarafından iç işlemler için kullanılan aşamalardır. Uygulama geliştirirken genellikle doğrudan etkileşim kurulmaz.
Poll
Yeni I/O olayları alınır. Dosya sistemi ve ağ işlemleriyle ilişkili birçok callback bu süreçte değerlendirilir.
Check
setImmediate callback’leri bu aşamada çalıştırılır.
Close callbacks
Kapanan bağlantı ve kaynaklara ait bazı callback’ler burada yürütülür.
Mikro görevler bu aşamalardan bağımsız tek bir Event Loop aşaması gibi düşünülmemelidir. Node.js, JavaScript callback’leri ve aşamalar arasında process.nextTick ile Promise mikro görevlerini uygun noktalarda işler.
Deneyleri Tek Dosyada Birleştirelim
Temel sıralamayı görmek için aşağıdaki örneği kullanabiliriz:
console.log("1: Senkron başlangıç");
setTimeout(() => {
console.log("5: setTimeout");
}, 0);
setImmediate(() => {
console.log("6: setImmediate");
});
Promise.resolve().then(() => {
console.log("4: Promise");
});
process.nextTick(() => {
console.log("3: process.nextTick");
});
console.log("2: Senkron bitiş");Çıktının ilk bölümü şu şekilde olacaktır:
1: Senkron başlangıç
2: Senkron bitiş
3: process.nextTick
4: PromiseAncak ana modül seviyesindeki setTimeout ve setImmediate sırasını mutlak kabul etmemek gerekir. Çalışma ortamına bağlı olarak ikisinin sırası değişebilir.
Bu örneğin asıl gösterdiği kesin noktalar şunlardır:
- Senkron kod önce çalışır.
process.nextTick, Promise mikro görevlerinden önce işlenir.- Promise callback’i timer ve
setImmediategibi sonraki aşama görevlerinden önce çalışır. setTimeoutvesetImmediatefarklı Event Loop aşamalarına aittir.
async/await Event Loop İçinde Nasıl Davranır?
async/await, Promise tabanlı bir yapıdır. await sonrasındaki kod, Promise mikro görev kuyruğu üzerinden devam eder.
Örneğin:
async function deneme() {
console.log("2: Fonksiyon başladı");
await Promise.resolve();
console.log("4: await sonrası");
}
console.log("1: Program başladı");
deneme();
console.log("3: Program bitti");Beklenen çıktı:
1: Program başladı
2: Fonksiyon başladı
3: Program bitti
4: await sonrasıFonksiyon await satırına kadar senkron olarak çalışır. await sonrasındaki bölüm ise mikro görev olarak planlanır. Bu sırada ana programdaki senkron kod tamamlanır.
Bu davranış, async/await kullanan fonksiyonların neden her zaman baştan sona kesintisiz çalışmadığını açıklar.
Node.js Asenkron İşlemleri Nerede Gerçekleştirir?
Event Loop, tamamlanan görevlerin callback’lerini düzenler. Ancak dosya sistemi veya ağ işlemlerinin tamamını doğrudan kendisi gerçekleştirmez.
Node.js bu süreçte işletim sistemi özelliklerinden ve libuv kütüphanesinden yararlanır. Bazı görevler işletim sistemi tarafından asenkron biçimde yönetilirken bazıları libuv iş parçacığı havuzunda gerçekleştirilebilir.
İşlem tamamlandığında sonuç ilgili kuyruğa alınır. Event Loop uygun zamanda callback’i JavaScript ana iş parçacığında çalıştırır.
Bu ayrım önemlidir:
- Asenkron işlemin kendisi arka planda yürütülebilir.
- Callback içindeki JavaScript kodu ana iş parçacığında çalışır.
- Callback içindeki ağır senkron işlem Event Loop’u yine bloke edebilir.
Event Loop Bilgisi Neden Önemlidir?
Event Loop’u anlamak yalnızca mülakat sorularına cevap vermek için gerekli değildir. Gerçek uygulamalarda performans ve hata ayıklama açısından doğrudan fayda sağlar.
Bu bilgi sayesinde:
- Callback sırasını daha doğru tahmin edebilirsiniz.
async/awaitdavranışını daha iyi anlayabilirsiniz.- Geciken timer’ların nedenini bulabilirsiniz.
- CPU yoğun kodların etkisini fark edebilirsiniz.
- API yanıt sürelerindeki dalgalanmaları analiz edebilirsiniz.
process.nextTickgibi API’leri daha güvenli kullanabilirsiniz.- Uygun paralellik ve iş kuyruğu stratejileri geliştirebilirsiniz.
Event Loop’u Bloke Etmemek İçin Neler Yapılabilir?
Senkron API’leri istek akışında kullanmayın
Dosya okuma ve şifreleme gibi işlemlerin senkron sürümleri, özellikle HTTP isteği işlenirken ana iş parçacığını durdurabilir.
Büyük döngüleri kontrol edin
Milyonlarca kayıt üzerinde senkron işlem yapmak diğer isteklerin beklemesine neden olabilir. İş yükü parçalara bölünebilir veya farklı bir çalışana aktarılabilir.
CPU yoğun görevleri ayırın
Görüntü işleme, büyük veri dönüşümü ve karmaşık hesaplamalar için Worker Threads, ayrı servisler veya görev kuyrukları değerlendirilebilir.
Büyük JSON işlemlerine dikkat edin
Çok büyük verilerin JSON.parse veya JSON.stringify ile işlenmesi ana iş parçacığını uzun süre meşgul edebilir.
Event Loop gecikmesini ölçün
Üretim ortamında yalnızca CPU ve bellek değil, Event Loop gecikmesi de izlenmelidir. Böylece uygulama tamamen durmadan önce performans sorunları fark edilebilir.
Üçüncü taraf kodlarını inceleyin
Kullanılan bir paket asenkron görünen API içinde yoğun senkron işlem gerçekleştiriyor olabilir. Kritik bağımlılıkların performans davranışı test edilmelidir.
Sık Yapılan Yanlış Varsayımlar
“setTimeout(fn, 0) hemen çalışır”
Hayır. Callback, en erken uygun timers aşamasında çalışabilir. Önce mevcut senkron kod ve ilgili mikro görevler tamamlanır.
“Node.js’te her şey paralel çalışır”
Hayır. JavaScript callback’leri ana iş parçacığında sırayla çalışır. Bazı I/O ve sistem işlemleri arka planda yürütülebilir.
“async fonksiyon ana iş parçacığını bloke etmez”
Bir fonksiyonun async olarak tanımlanması içindeki bütün kodu otomatik olarak engellemesiz yapmaz. await öncesindeki veya sonrasındaki ağır senkron işlemler Event Loop’u bloke edebilir.
“Promise her zaman setTimeout’tan sonra çalışır”
Genellikle tam tersi geçerlidir. Mevcut senkron işlem tamamlandıktan sonra Promise mikro görevleri, timer aşamasından önce işlenir.
“setImmediate her zaman setTimeout’tan önce çalışır”
Hayır. Sıra, çağrıların hangi bağlamda oluşturulduğuna bağlıdır. Ana modül seviyesinde kesin sıra varsayılmamalıdır.
Kısa Bir Zihin Modeli
Node.js Event Loop’u ilk aşamada şu modelle düşünebilirsiniz:
1. Senkron kodu çalıştır
2. process.nextTick kuyruğunu boşalt
3. Promise microtask kuyruğunu boşalt
4. Event Loop'un uygun aşamasındaki callback'leri çalıştır
5. Her callback sonrasında yeni microtask'ları kontrol et
6. Bekleyen işler olduğu sürece döngüyü sürdürBu model bütün düşük seviyeli ayrıntıları kapsamaz ancak günlük Node.js geliştirmesi için güçlü bir başlangıç noktasıdır.
Sonuç
Node.js Event Loop, senkron JavaScript kodu ile asenkron işlemlerden gelen callback’lerin koordinasyonunu sağlar. Kod tek bir ana iş parçacığında çalışsa da I/O işlemleri beklenirken diğer görevlerin ilerlemesine imkân tanır.
Yaptığımız deneylerden çıkarabileceğimiz temel sonuçlar şunlardır:
- Senkron kodlar önce tamamlanır.
setTimeout(fn, 0)callback’i hemen çalıştırmaz.process.nextTick, Promise mikro görevlerinden önce işlenir.- Promise callback’leri timer gibi sonraki aşama görevlerinden önce çalışır.
setImmediatevesetTimeoutfarklı aşamalarda değerlendirilir.- Uzun süren senkron kodlar bütün Event Loop’u bloke edebilir.
async/await, Promise mikro görev mekanizması üzerinde çalışır.
Event Loop’u öğrenmenin en etkili yolu, küçük kod örneklerinin çıktılarını çalıştırmadan önce tahmin etmektir. Önce sırayı bir kâğıda yazın, ardından kodu çalıştırıp sonucu karşılaştırın. Yanıldığınız nokta, çoğu zaman öğrenmeniz gereken kavramı doğrudan gösterecektir.
Sıkça Sorulan Sorular
Node.js Event Loop nedir?
Node.js Event Loop, senkron kodları ve tamamlanan asenkron işlemlere ait callback’leri belirli bir sırayla çalıştıran mekanizmadır.
Node.js gerçekten tek iş parçacıklı mı?
JavaScript kodu temel olarak tek ana iş parçacığında çalışır. Ancak Node.js bazı I/O ve sistem işlemlerini işletim sistemi veya libuv iş parçacığı havuzu üzerinden yürütebilir.
setTimeout sıfır olduğunda neden hemen çalışmaz?
Çünkü sıfır değeri kesin çalışma zamanı değil, minimum bekleme süresidir. Callback’in çalışması için mevcut senkron kodun ve öncelikli mikro görevlerin tamamlanması gerekir.
Promise mi, setTimeout mı önce çalışır?
Aynı senkron işlem içinde planlandıklarında Promise callback’i genellikle önce çalışır. Promise mikro görev kuyruğunda, setTimeout ise timer aşamasında değerlendirilir.
process.nextTick ile Promise arasındaki fark nedir?
Her ikisi de mevcut işlemden sonra çalışabilir ancak Node.js process.nextTick kuyruğunu Promise mikro görev kuyruğundan önce işler. Aşırı process.nextTick kullanımı diğer görevlerin gecikmesine neden olabilir.
Event Loop neden bloke olur?
Uzun döngüler, senkron dosya işlemleri, büyük JSON dönüşümleri ve CPU yoğun hesaplamalar ana iş parçacığını meşgul ederek Event Loop’un diğer callback’lere geçmesini engelleyebilir.