Bir Node.js Sözünü Kıyaslama
Yayınlanan: 2022-03-11Cesur yeni bir dünyada yaşıyoruz. JavaScript ile dolu bir dünya. Son yıllarda JavaScript, tüm endüstriyi fırtına gibi alarak web'e hakim oldu. Node.js'nin piyasaya sürülmesinden sonra, JavaScript topluluğu, her şeyi yapmak için tek dil olmak için dilin basitliğini ve dinamikliğini kullanabildi, sunucu tarafını, istemci tarafını ele aldı ve hatta cesurca gitti ve makine öğrenimi için bir pozisyon talep etti. Ancak JavaScript, son birkaç yılda bir dil olarak büyük ölçüde değişti. Ok işlevleri ve vaatler gibi daha önce hiç olmayan yeni kavramlar tanıtıldı.
Ah, vaatler. Node.js'yi öğrenmeye ilk başladığımda, söz verme ve geri arama kavramı bana pek mantıklı gelmiyordu. Kod yürütmenin prosedürel yöntemine alışmıştım, ancak zamanla bunun neden önemli olduğunu anladım.
Bu bizi şu soruya getiriyor, neden geri aramalar ve vaatler yine de tanıtıldı? Neden JavaScript'te sıralı olarak yürütülen kodu yazamıyoruz?
Teknik olarak yapabilirsin. Ama yapmalısın?
Bu makalede, JavaScript ve çalışma zamanı hakkında kısa bir giriş yapacağım ve daha da önemlisi, JavaScript topluluğunda, senkronize kodun performans açısından ortalamanın altında olduğu ve bir anlamda sadece kötü olduğu ve asla olmaması gerektiğine dair yaygın inancı test edeceğim. kullanılacak. Bu efsane gerçekten doğru mu?
Başlamadan önce, bu makale JavaScript'teki vaatlere zaten aşina olduğunuzu varsayar, ancak değilseniz veya bir tazelemeye ihtiyacınız varsa, lütfen JavaScript Sözleri: Örneklerle Bir Eğitim bölümüne bakın.
Not Bu makale, salt bir JavaScript ortamında değil, bir Node.js ortamında test edilmiştir. Node.js sürüm 10.14.2 çalıştırılıyor. Tüm kıyaslamalar ve sözdizimi, büyük ölçüde Node.js'ye dayanacaktır. Testler, temel saat hızı 2,3 GHz olan Intel i5 8. Nesil Dört Çekirdekli İşlemciye sahip bir MacBook Pro 2018'de gerçekleştirildi.
Olay Döngüsü
JavaScript yazmanın sorunu, dilin kendisinin tek iş parçacıklı olmasıdır. Bu, Go veya Ruby gibi, çekirdek dizilerinde veya süreç dizilerinde aynı anda birden çok prosedürü yürütme ve thread oluşturma yeteneğine sahip diğer dillerin aksine, aynı anda birden fazla tek prosedür yürütemeyeceğiniz anlamına gelir. .
JavaScript, kodu yürütmek için birden çok aşamadan oluşan olay döngüsü adı verilen bir prosedüre dayanır. JavaScript süreci her aşamadan geçer ve sonunda yeniden başlar. Ayrıntılar hakkında daha fazla bilgiyi node.js'nin resmi kılavuzunda buradan okuyabilirsiniz.
Ancak JavaScript'in engelleme sorunuyla mücadele etmek için kollarında bir şeyler var. G/Ç geri aramaları.
Bir iş parçacığı oluşturmamızı gerektiren gerçek hayattaki kullanım durumlarının çoğu, dilin sorumlu olmadığı bazı eylemleri talep etmemizdir - örneğin, veritabanından bazı verilerin alınmasını talep etmek. Çok iş parçacıklı dillerde, isteği oluşturan iş parçacığı yalnızca askıda kalır veya veritabanından yanıt bekler. Bu sadece kaynak israfıdır. Ayrıca, bir iş parçacığı havuzunda doğru sayıda iş parçacığını seçme konusunda geliştiriciye bir yük getirir. Bu, uygulama yüksek talep gördüğünde bellek sızıntılarını ve çok sayıda kaynağın tahsis edilmesini önlemek içindir.
JavaScript'ler, G/Ç işlemlerini ele almada diğer faktörlerden daha üstündür. JavaScript, bir veritabanından veri isteme, bir dosyayı belleğe okuma, diske dosya yazma, bir kabuk komutu yürütme vb. gibi bir G/Ç işlemini çağırmanıza izin verir. İşlem tamamlandığında, bir geri arama yürütürsünüz. Veya söz verilmesi durumunda, vaadi sonucu ile çözersiniz veya bir hata ile reddedersiniz.
JavaScript topluluğu, G/Ç işlemlerini yaparken asla eşzamanlı kod kullanmamamızı her zaman tavsiye eder. Bunun iyi bilinen nedeni, kodumuzu diğer görevleri çalıştırmaktan ENGELLEMEK İSTEMİYORUZ. Tek iş parçacıklı olduğu için, bir dosyayı eşzamanlı olarak okuyan bir kod parçamız varsa, kod okuma tamamlanana kadar tüm süreci engeller. Bunun yerine, eşzamansız koda güveniyorsak, birden fazla G/Ç işlemi yapabilir ve tamamlandığında her işlemin yanıtını ayrı ayrı ele alabiliriz. Herhangi bir engelleme yok.
Ama elbette çok fazla işlem yapmayı hiç umursamadığımız bir ortamda, senkron ve asenkron kod kullanmak hiç fark yaratmıyor, değil mi?
Kalite Testi
Çalıştıracağımız test, senkronizasyon ve zaman uyumsuz kodun ne kadar hızlı çalıştığı ve performansta bir fark olup olmadığı konusunda bize karşılaştırmalar sağlamayı amaçlayacaktır.
Test edilecek G/Ç işlemi olarak bir dosyayı okumayı seçmeye karar verdim.
Öncelikle Node.js Crypto modülü ile oluşturulan rastgele baytlarla dolu rastgele bir dosya yazacak bir fonksiyon yazdım.
const fs = require('fs'); const crypto = require('crypto'); fs.writeFileSync( "./test.txt", crypto.randomBytes(2048).toString('base64') )
Bu dosya, bir sonraki adımımız olan dosyayı okumak için bir sabit görevi görecektir. işte kod
const fs = require('fs'); process.on('unhandledRejection', (err)=>{ console.error(err); }) function synchronous() { console.time("sync"); fs.readFileSync("./test.txt") console.timeEnd("sync") } async function asynchronous() { console.time("async"); let p0 = fs.promises.readFile("./test.txt"); await Promise.all([p0]) console.timeEnd("async") } synchronous() asynchronous()
Önceki kodu çalıştırmak aşağıdaki sonuçlarla sonuçlandı:
Koşmak # | senkronizasyon | zaman uyumsuz | Zaman uyumsuz/Senkronizasyon Oranı |
---|---|---|---|
1 | 0,278 ms | 3.829ms | 13.773 |
2 | 0,335 ms | 3.801 ms | 11.346 |
3 | 0.403ms | 4.498 ms | 11.161 |
Bu beklenmedik bir şeydi. İlk beklentilerim, aynı zamanı almaları gerektiğiydi. Peki bir dosya daha ekleyip 1 yerine 2 dosya okusak nasıl olur?

Oluşturulan test.txt dosyasını kopyaladım ve buna test2.txt adını verdim. İşte güncellenmiş kod:
function synchronous() { console.time("sync"); fs.readFileSync("./test.txt") fs.readFileSync("./test2.txt") console.timeEnd("sync") } async function asynchronous() { console.time("async"); let p0 = fs.promises.readFile("./test.txt"); let p1 = fs.promises.readFile("./test2.txt"); await Promise.all([p0,p1]) console.timeEnd("async") }
Her biri için başka bir okuma ekledim ve vaatlerde paralel olarak çalışması gereken okuma vaatlerini bekliyordum. Sonuçlar şunlardı:
Koşmak # | senkronizasyon | zaman uyumsuz | Zaman uyumsuz/Senkronizasyon Oranı |
---|---|---|---|
1 | 1.659ms | 6.895 ms | 4.156 |
2 | 0.323ms | 4.048ms | 12.533 |
3 | 0.324ms | 4.017ms | 12.398 |
4 | 0.333ms | 4.271 ms | 12.826 |
İlki, takip eden 3 çalıştırmadan tamamen farklı değerlere sahiptir. Tahminimce, her çalıştırmada kodu optimize eden JavaScript JIT derleyicisi ile ilgili.
Yani, zaman uyumsuz işlevler için işler pek iyi görünmüyor. Belki işleri daha dinamik hale getirirsek ve belki uygulamayı biraz daha vurgularsak, farklı bir sonuç elde edebiliriz.
Bu yüzden bir sonraki testim 100 farklı dosya yazmayı ve ardından hepsini okumayı içeriyor.
İlk olarak, testin yürütülmesinden önce kodu 100 dosya yazacak şekilde değiştirdim. Dosyalar her çalıştırmada farklıdır, ancak hemen hemen aynı boyutu korurlar, bu nedenle her çalıştırmadan önce eski dosyaları temizleriz.
İşte güncellenmiş kod:
let filePaths = []; function writeFile() { let filePath = `./files/${crypto.randomBytes(6).toString('hex')}.txt` fs.writeFileSync( filePath, crypto.randomBytes(2048).toString('base64') ) filePaths.push(filePath); } function synchronous() { console.time("sync"); /* fs.readFileSync("./test.txt") fs.readFileSync("./test2.txt") */ filePaths.forEach((filePath)=>{ fs.readFileSync(filePath) }) console.timeEnd("sync") } async function asynchronous() { console.time("async"); /* let p0 = fs.promises.readFile("./test.txt"); let p1 = fs.promises.readFile("./test2.txt"); */ // await Promise.all([p0,p1]) let promiseArray = []; filePaths.forEach((filePath)=>{ promiseArray.push(fs.promises.readFile(filePath)) }) await Promise.all(promiseArray) console.timeEnd("async") }
Ve temizleme ve yürütme için:
let oldFiles = fs.readdirSync("./files") oldFiles.forEach((file)=>{ fs.unlinkSync("./files/"+file) }) if (!fs.existsSync("./files")){ fs.mkdirSync("./files") } for (let index = 0; index < 100; index++) { writeFile() } synchronous() asynchronous()
Ve koşalım.
İşte sonuç tablosu:
Koşmak # | senkronizasyon | zaman uyumsuz | Zaman uyumsuz/Senkronizasyon Oranı |
---|---|---|---|
1 | 4.999ms | 12.890ms | 2.579 |
2 | 5.077ms | 16.267ms | 3.204 |
3 | 5.241 ms | 14.571 ms | 2.780 |
4 | 5.086 ms | 16.334ms | 3.213 |
Bu sonuçlar burada bir sonuç çıkarmaya başlar. Talep veya eşzamanlılıktaki artışla birlikte, genel gider vaatlerinin anlam kazanmaya başladığını gösterir. Detaylandırma için, sunucu başına saniyede yüzlerce veya belki de binlerce istek çalıştırması gereken bir web sunucusu çalıştırıyorsak, senkronizasyon kullanarak G/Ç işlemlerini çalıştırmak avantajını oldukça hızlı bir şekilde kaybetmeye başlayacaktır.
Sadece deneme uğruna, bakalım gerçekten vaatlerle ilgili bir sorun mu yoksa başka bir şey mi? Bunun için, kesinlikle hiçbir şey yapmayan bir sözü ve 100 boş sözü çözen bir sözü çözme zamanını hesaplayacak bir fonksiyon yazdım.
İşte kod:
function promiseRun() { console.time("promise run"); return new Promise((resolve)=>resolve()) .then(()=>console.timeEnd("promise run")) } function hunderedPromiseRuns() { let promiseArray = []; console.time("100 promises") for(let i = 0; i < 100; i++) { promiseArray.push(new Promise((resolve)=>resolve())) } return Promise.all(promiseArray).then(()=>console.timeEnd("100 promises")) } promiseRun() hunderedPromiseRuns()
Koşmak # | Tek Söz | 100 Söz |
---|---|---|
1 | 1.651 ms | 3.293 ms |
2 | 0.758 ms | 2.575 ms |
3 | 0.814ms | 3.127ms |
4 | 0.788ms | 2.623ms |
İlginç. Görünüşe göre, gecikmenin ana nedeni vaatler değil, bu da gecikme kaynağının gerçek okumayı yapan çekirdek iş parçacıkları olduğunu tahmin etmemi sağlıyor. Bu, gecikmenin arkasındaki ana neden hakkında kesin bir sonuca varmak için biraz daha deneme gerektirebilir.
Son Söz
Öyleyse vaatleri kullanmalı mısın, kullanmamalı mısın? Benim fikrim şu olacaktır:
Bir işlem hattı veya tek bir kullanıcı tarafından tetiklenen belirli bir akışla tek bir makinede çalışacak bir komut dosyası yazıyorsanız, eşitleme koduyla gidin. Çok fazla trafik ve istekten sorumlu olacak bir web sunucusu yazıyorsanız, zaman uyumsuz yürütmeden kaynaklanan ek yük, eşitleme kodunun performansının üstesinden gelecektir.
Bu makaledeki tüm işlevlerin kodunu depoda bulabilirsiniz.
JavaScript Geliştirici yolculuğunuzda vaatlerden sonraki mantıklı adım, zaman uyumsuz/bekleme sözdizimidir. Bununla ilgili daha fazla bilgi edinmek ve buraya nasıl geldiğimizi öğrenmek isterseniz, Asynchronous JavaScript: From Callback Hell to Async ve Await bölümüne bakın.