JavaScript’te Sync-Async-Thread Kavramları — 1
Herkese merhaba, bu yazıda sizlere senkron - asenkron kavramlarından bahsetmeye çalışacağım. Bu konulardan bahsederken call stack, thread ve callback fonksiyon kavramlarına da değineceğiz.
Konuları daha detaylı incelemeden önce şu iki bilgiyle giriş yapmak istiyorum. JavaScript’in çalışma mantığı single thread şeklindedir ve senkron olarak çalışır. Bu iki bilgi üzerinden ilerleyelim o zaman. Öncelikle thread, bir process sırasında yani genel bir işlem sırasında yapılan iş parçacığıdır. Başka bir deyişle o an ki çalışan kod parçacığı şeklinde düşünebiliriz. JavaScript yalnızca tek bir kod parçacığı çalıştırır ve senkron olarak çalışır yani bu kodları sıralı bir şekilde çalıştırır. Özetle JavaScript tek bir işlem yapar ve bunları da sıralı bir şekilde yapar. Şimdi bunları bir de kod ortamında örnek üzerinden görmeye çalışalım.
Yukarıdaki kodları kaydettiğimizde tarayıcıda şöyle bir durum ile karşılaşırız;
İlk olarak func1'deki console log 1 ve console log 2 işlemleri yapılmış. Daha sonra da alert mesajı geldi. Burada dikkat ederseniz hâlâ tarayıcımız çalışıyor yani hâlâ JavaScript kodunu işliyor. Biz func2 fonksiyonunu çağırmamıza rağmen onu işlemiyor. Neden? Çünkü alert mesajı bizden bir konfirmasyon bekliyor ve o konfirmasyonu alana kadar akışı blokluyor. Confirm ettikten sonra;
Dikkat ettiyseniz ok dedikten sonra func2 fonksiyonu içindeki console log 1 ve console log 2 sırayla yazdırıldı. İşte bizim yukarıda senkron derken bahsetmeye çalıştığımız durum bu aslında, sıralı bir şekilde işlemleri yapıyor ve o an tek bir işlem yapıyor. Biz senkron dedik ancak buna karşı çıkanlar olabilir sonuçta asenkron JavaScript diye bir şey var, callback, promise, async/await yapıları var her şeyi geçtik ajax var. Nasıl oluyor da JavaScript senkron çalışıyor şeklinde itiraz edenler olabilir. Bu konuya tekrar geri döneceğiz ancak senkron bir şekilde gayet güzel çalışıyor burada problem nedir? Neden JavaScript ve diğer diller asenkron olarak çalışmak ister? Bu soruların cevabını yukarıdaki basit örneğimizden ilerleyerek vermeye çalışalım.
Yukarıda func1 içindeki console log 1 ve console log 2 çalışıyor. Şimdi bizim her zaman uğraştığımız uygulamalar bu şekilde basit olmaz. Kullanıcının karşısına boş bir alert çıkarıp hadi ok’e tıklayın gibi tuhaf bir uygulama yapmayız, bizim yaptığımız işlemler daha çok bir veriyi bir veritabanından veya rest api’dan almaktır. Hatta birden fazla farklı farklı rest api’lardan veriler alıp bunları karşlılaştırıyor da olabiliriz vs. Diyelim ki bizim veri almak istediğimiz rest api’da server çalışmıyor. Ne yapacağız? Yukarıdaki örnekten gidecek olursak ben ok butonuna tıklamıyorum sonuçta kullanıcı olarak tıklamak zorunda değilim. Böyle bir durumda biz ok’e tıklamadığımız için uygulama çalışmaya devam etmez. Yani senkron çalışmada işlem ilerlerken iyi de işlem bloklandığı zaman yazdığımız kodun yarıda kalma ihtimali var. Bu yüzden de performans ciddi bir şekilde tehlike altında ve kontrol bizde değil. Şimdi başka bir örnek yaparak konuyu daha iyi anlamaya çalışalım.
Yukarıdaki örnekte basit bir simülasyon yaptığımızı düşünelim.
İlk başta 10 adet verimiz vardı sonra üzerine 5 adet veri geldi şeklinde düşünebiliriz. Şimdi ilk olarak 1. satırda x değerine 10 atadık, 2. satırda bunu ekranda gösterdik. 4. satırda ise setTimeout ile veri gelirken 1 saniyelik gecikme olduğunu simüle ediyoruz. 5. satırda da x değerine 5 eklemesini istiyoruz. Yani setTimeout yardımıyla içindeki kodun çalışmasını 1 saniyelik geciktiriyoruz. Daha sonra 8. satırda tekrar x’i ekrana yazdırıp, 10. satırda tekrar 5 veri geldiğini düşünerek x değerine 5 ekliyoruz son olarak x’i tekrar ekrana yazdırıyoruz. Şimdi console’a baktığımızda 1. gelen veri 10 olarak yazıldı bunda herhangi bir problem yok, 2. gelen veri de yazıldı ancak x artmamış, 3. gelen veri de yazıldı x artmış. Dikkat ettiyseniz setTimeout’un içindeki artış gerçekleşmemiş. İşte bütün püf nokta burada yatıyor. Bizim sonuçta ana bir işleyişimiz var 1. satırdan başlayıp aşağıya doğru bir işleyiş 4. satırda setTimeout’a geliyor ve parametre olarak bir callback almış. Şimdilik callback’e takılmadan 1 saniye geç çalışacak şeklinde düşünelim. Tabi JavaScript engine bu 1. saniyeyi beklemeden 8. satırdan çalıştırmaya devam edip tüm işlemleri yapıyor. Yani x’in değerini 20 beklerken 15 olarak aldık. İşte bunun sebebi, senkron çalışmadan dolayı veri alımında herhangi bir gecikme olduğu zaman eksik veri almış oluyoruz. Bu da senkron çalışmanın bizim açımızdan ikinci temel sorunu, aldığımız verilerde değişik sonuçlara veya eksik verilere sebep oluyor. Özetle senkron çalışmanın getirmiş olduğu iki temel problem var dedik. Bunlardan birincisi program bloklanma, yavaş çalışma tehlikesi, ikincisi aldığımız verilerde gecikmeden dolayı kaynaklanan eksik veri alma durumu. İşte bu gibi sorunlarla karşılaşmamak için JavaScript’te asenkron işlemlere ihtiyaç duyarız. Yani aynı anda birden fazla işlem yapıyormuş gibi görünmek isteriz ve sonradan ihtiyacımız olacak olan verilere, onları kullanmaya başlamadan önce sahip olduğumuzdan emin olmak isteriz.
Ee iyi de bir JavaScript senkron çalışır diyoruz bir asenkron işlemler diyoruz nedir bu olay? Buradaki kritik nokta şu, JavaScript her zaman senkron olarak çalışır ancak biz yazacağımız JavaScript kodlarıyla runtime’da sanki aynı anda farklı işlemleri de yapıyormuş gibi JavaScript kodunu manipüle deriz. JavaScript bu asenkron çalışmalarda callback, promise ve async/await yapılarını kullanır bu da şimdilik aklımızda bulunsun. Bu konulara girmeden önce call stack nedir biraz ona bakalım.
Call Stack Nedir?
Call Stack, bizim açımızdan fonksiyonların çalışma sırasını gösterir diyebiliriz kısaca. Fonksiyonların çalışma sırasından da kastımız şudur;
Yukarıdaki örneği incelediğimizde 17. satırda func1 fonksiyonunu çağırdık ve 1. satırdaki func1'e gitti. İlk önce 2. satırda “Ben birinciyim” çıktısını aldık. Daha sonra 3. satırda func2'yi gördü ve func2 fonksiyonuna gitti yani 8. satıra geldik ekrana “Ben ikinciyim” yazdırdı dikkat ederseniz, aşağıdan devam edip “Ben tekrar birinciyim”i yazdırmadı. Neden? Çünkü single thread, bu kısım önemli. Daha sonra func2'de 9. satıra geldiğinde ise func3'ü gördü ve func3 fonksiyonun içine giderek 14. satırda “Ben üçüncüyüm” yazdırdı. Artık func3 fonksiyonunun işi bitti, nerede kalmıştık en son? Kaldığı yere tekrar gitti bu sefer 10. satırdaki “Ben tekrar ikinciyim” yazısını ekrana yazdırdı. Böylece func2 ile de işimiz bitti daha sonra 4. satıra gelerek “Ben tekrar birinciyim” yazısını ekrana yazdırdı ve işlem tamamlanmış oldu. Fonksiyonların çalışma sırası call stack’te bu şekildedir, başka kaynaklarda last in first out yani (LIFO) son giren ilk çıkar olarak da görebilirsiniz. Bir görselle gösterecek olursak eğer;
Yukarıdaki görselle paralel şekilde bunu bir de kod ortamında görmenin daha faydalı olacağını düşünüyorum. (main’i projenin bir giriş noktası gibi düşünebiliriz)
Yukarıdaki örneklere bakacak olursak biz ilk olarak 9. satırda average fonksiyonunu çağırıyoruz ve call stack’te main’in üzerine average ekleniyor. Dolayısıyla 5. satırda average fonksiyonunun içindeyiz artık daha sonra 6. satıra bakıyor ve add fonksiyonunu görünce call stack’te average’in üzerine add ekleniyor. Böylece add fonksiyonu kendi içinde işlemi bitirdikten sonra call stack’ten ayrılıyor, daha sonra average fonksiyonuna geliyor o da kendine ait işlemleri yaptıktan sonra call stack’ten ayrılıyor. Yani özetle call stack bize uygulamadaki fonksiyonların çalışma sırasını verir. Peki bu bizim için neden önemli? Çünkü ilerleyen zamanlarda göreceğimiz callback, promise ve async-await’in aslında manipüle etmek istediği temel nokta bu. Şimdi başka bir örnek yaparak konuyu daha iyi anlamaya çalışalım.
Yukarıdaki örneği incelediğimizde ekranda ilk olarak sırasıyla 9. satırdaki console log 1, 15. satıdaki console log 3 ve 16. satırdaki console log 4 hemen yazdırılır aşağıdaki gibi.
Daha sonra 11. satırda setTimeout 1 saniye sonra çalışacak. Ancak 18. satırdaki task işlemi 1 saniyeden uzun sürüyor ve bir saniyeden uzun sürmesine rağmen ekranda 12. satırdaki console log 2 yazdırılmıyor. Önce task işlemi tamamlanıyor daha sonrasında hemen console log 2 yazdırılıyor. Neden? İşte single thread olmasından dolayı. En son çıktı aşağıdaki gibidir olacaktır.
İyi güzel de bu nasıl bir çalışma sırası ve bunların call stack ile alakası nedir diyebilirsiniz. Fonksiyonların çalışma sırasında öncelik, call stack’teki normal fonksiyonlarındır. Call stack’teki işlemler tamamlanıp boşaldıktan sonra gelip callback fonksiyonları çalıştırır. İşte bu durumu event loop mekanizması sağlıyor. Bununla ilgili bir görsel faydalı olacaktır.
Yukarıdaki görseli incelediğimizde JavaScript Runtime’da call stack’e dikkat ederseniz her zaman bir tane fonksiyon var, işte single thread olmasından dolayı. Yukarıda da bahsettiğimiz gibi JavaScript önce call stack’teki işlemleri yapar daha sonra callback sırasına giren işlemleri yapar. Şimdi örneği bir ileri daha götürelim.
Şimdi yukarıdaki örneği incelersek önce 9. satırdaki 1 değerini yazdıracak. 11. satıra baktığımızda setTimeout’u çalıştıracak ancak callback’ten geleceği için (biz hâlâ callback’in ne olduğunu bilmiyoruz) callback’i callback queue’ya atıyor. Daha sonra 15. satırdaki 3 ve 16. satırdaki 4 değerlerini de ekrana yazdıracak. 18. satırda task var, o da normal bir call stack fonksiyonu olduğu için “İşlem Tamamlandı” yazdıracak. 20. satırdaki setTimeout’a geldiğinde o da bir callback fonksiyon olduğu için onu da callback queue’ya ekledi. Son olarak 12. satırdaki 2 değerini ve 21. satırdaki 5 değerini yazdıracak.
Yukarıda da farklı renklerle göstererek anlatmaya çalıştım. Kırmızı ile ilgili çizdiğim kısımda herhangi bir problem yok zaten. Burada değinmek istediğim nokta şu, 11. satırda setTimeout içindeki fonksiyon callback queue’ya ekleniyor. Daha sonra 18. satırdaki task işleminin bitmesini beklemek zorunda. Çünkü öncelik call stack’teki fonksiyonlarındır. Dolayısıyla task işlemi yapılana kadar 1 saniye geçtiği için “İşlem Tamamlandı” yazısından hemen sonra 2 çıktısını görüyoruz. Ancak 20. satırdaki setTimeout içindeki callback fonksiyon 2 değerini yazdıktan 2 saniye bekleyip sonra çalışıyor. Anlatmak istediğim şu, 11. satırdaki callback fonksiyonu 1 saniye beklemiyor çünkü task işlemi yapılırken o 1 saniye zaten geçtiği için direkt yazdırılıyor ancak 20. satırdaki callback en sonda başlayacağı için 2 saniye bekliyor. Şimdi örneği bir adım daha ileri götürelim.
Yukarıdaki örneğe ek olarak 24. satırdaki task fonksiyonunu ekledik sadece. Bir önceki örneğe benzer senaryo, ilk olarak 9. satır ekrana yazdırılacak, 11. satırdaki fonksiyonu callback queue’ya ekleyecek daha sonra 15. satırdaki 3 ve 16. satırdaki 4 değerini ekrana yazıracak. Sonrasında “İşlem Tamamlandı” yazdırılacak. 20. satırdaki fonksiyonu da callback queue’ya ekleyecek daha sonra “İşlem Tamamlandı 2” yazdıktan hemen sonra 2 ve 5 değeri yazdırılacak. Çünkü 24. satırdaki işlem yapılırken zaten 20. satırdaki callback fonksiyonu için 2 saniye geçmiş olacak. Aynı şekilde 18. satırdaki işlem yapılırken zaten 11. satırdaki callback fonksiyon için 1 saniye geçmiş olacak. Burada kritik nokta, bir önceki örnekteki gibi işlem bittikten sonra 2 saniye daha bekleyip ekrana 2 yazdırmıyor. O 2 saniye zaten task işlemi gerçekleştirilirken geçtiği için direkt 2 ve 5 değerlerini yazdırıyor.
Özetle fonksiyonların çalışması bu şekilde. Önce klasik call stack’teki fonksiyonları çalıştırır, çalıştıracak fonksiyon kalmadığında ayrı bir callback kuyruğuna eklenen callback fonksiyonları çalıştırır. Evet o kadar callback dedik biraz da callback’lerden bahsedelim o zaman.
Callback Nedir?
Callback, JavaScript’in asenkron çalışma için bulduğu çözümlerden birisidir. En basit tanımıyla callback, bize bir işin tamamlandığını veya bir iş tamamlandığında kendine ait olan başka bir iş yapılabileceğini gösterir. Bir de bu tanımı kod üzerinde görelim.
Yukarıda first class function olarak myName adında bir fonksiyon yazıyoruz. Fonksiyonun içinde yapmasını istediğimiz şey de ekrana “My name is michael” yazdırmak. Şimdi 5. satıra baktığımızda setTimeout myName fonksiyonunu parametre olarak alıyor ve ikinci parametre olarak da milisaniye cinsinden 3000 veriyoruz. Yani 3 saniye sonra ekrana gerekli yazıyı yazdıracak. İşte bu durumda myName bizim için callback fonksiyon olur. Neden? Çünkü 3 saniye ara verdikten sonra ekrana “My name is michael” yazdırmasını istiyoruz. Şimdi tekrar callback tanımına bakacak olursak “bize bir işin tamamlandığını veya bir iş tamamlandığında kendine ait olan başka bir iş yapılabileceğini gösterir.” demiştik. Bu tanımda başka bir işten kastımız nedir? 3 saniyenin geçmesi. Özetle 3 saniye geçtikten sonra bana şu callback fonksiyonunu (myName) geri çağır diyoruz. Yani bir fonksiyon başka bir fonksiyona parametre olarak geliyorsa bu parametre olarak gelen fonksiyona callback fonksiyon diyoruz. Bir de callback fonksiyonların genel syntax’ına bakacak olursak;
Bir önceki örnekten farklı olarak myName fonksiyonunu direkt parametre olarak vermektense anonymous bir fonksiyon olarak da yazdırabiliriz. Genel kullanım bu şekildedir, bu örnek de bir önceki örnekte olduğu gibi 3 saniye sonra ekrana “My name is michael” yazdıracaktır. Özetle yukarıdaki arrow function bizim açımızdan bir callback fonksiyondur ve genel kullanım bu şekildedir. Tabi JavaScript’te callback’lerin kullanım alanı yalnızca bununla sınırlı değil, JavaScript’te callback’lerin en önemli kullanım şekli event listener’lardır. O zaman JavaScript MDN’den bir örnek ile bu konuyu biraz daha detaylı inceleyelim.
Yukarıdaki örneğe baktığımızda 1. satırda index.html’deki butonu yakaladık. 3. satırda butona tıkladığımız zaman bir callback çalışacak. (dikkat ederseniz click diyene kadar bloke durumda) Öncelikle bir alert işlemi çıkacak karşımıza biz alert işlemini onayladıktan sonra yeni bir paragraf eklenecek. Bunun bir callback olduğunu ve callback kuyruğuna girdiğini ispatlamak için 11, 12 ve 13. satırları ekledim. Burada anlatmaya çalıştığım şey şu, öncelikli olarak JavaScript engine senkron bir şekilde 1. satırı, 11. satırı, 12. satırı ve 13. satırı çalıştırıyor. Zaten ekranda “Header” çıktısını alıyoruz doğal olarak. Ancak bu arada 3. satırı çalıştırmadı. Neden? Çünkü 3. satırda event listener içindeki arrow fonksiyon bir callback fonksiyondur. Biz fonksiyonların çalışma sırasında ne demiştik? Önce call stack’teki normal fonksiyonlar daha sonra callback kuyruğundaki fonksiyonları çalıştırır. Tabi bu callback fonksiyon click olduğunda tetiklenir. Yani son olarak çıktı şu şekilde olacaktır;
İşte yazının en başındaki kafa karışıklığıyla ilgili yorumumuzu tekrar yapabiliriz. JavaScript her zaman senkron olarak single thread çalışır. JavaScript asenkron çalışmadan bahsettiğimiz şey şu, JavaScript runtime’ın içinde call stack’teki fonksiyonları sıralı bir şekilde çalıştırıyor zaten ancak tarayıcı ortamında sadece JavaScript runtime yok Web API var vs. işte bunlarla iletişimi asenkron. O nedenle biz, bir işlem yapıldığında aynı zamanda birden fazla işlem yapıldığını düşünüyoruz. Ancak JavaScript’in kendisi single thread ve senkrondur.
Son olarak callback’lerle ilgili çok verilen örneklerden birini daha yapalım.
Yukarıdaki örneği incelediğimizde 1. satırda books array’i iki objeden oluşuyor. Daha sonra 6. satırda listBooks isminde listeleme fonksiyonumuzu tanımladık, map metodu bize tekil book ve indexini dönsün. Son olarak 8. satırda da bunları ekrana yazdırmasını istiyoruz. 12. satırda da listBooks metodunu çağırdık ve ekranda objelerimizi gördük. Daha sonra 14. satırda addNewBook isminde fonksiyon oluşturuyoruz parametre olarak newBook alıyoruz daha sonra bu aldığımız parametreyi push yardımıyla books array’ine ekliyoruz. 18. satırda addNewBook fonksiyonunu çalıştırıp parametre olarak da bir Zygmunt Bauman’dan Yaşam Sanatı newBook objesini gönderiyoruz. Kodu çalıştırdığımızda son kitabı eklememize rağmen console’da göremiyoruz? Neden? Çünkü biz kitap ekleme işlemini listeledikten sonra yapıyoruz. Eğer 12. satırdaki listBooks fonksiyonunu addNewBook fonksiyonunun altına yazarsak problem çözülür. Ancak bu çözüm ne kadar işimizi görür? İşte burada programsal anlamda kalıcı çözümü bize callback’ler sağlıyor. Biz en başta callback tanımı için “Callback, bize bir işin tamamlandığını veya bir iş tamamlandığında kendine ait olan başka bir iş yapılabileceğini gösterir.” demiştik zaten. Şimdi bu örneği bir de callback üzerinden yapalım.
Yukarıdaki örneği incelediğimizde 12. satırdaki addNewBook fonksiyonu bir önceki örnekten farklı parametre olarak newBook’un yanında callback diye sonradan çalıştıracak bir fonksiyon da alıyor. 13. satırda books array’ine ekleme yaptıktan sonra 14. satırda callbak fonksiyonunu çağırıyoruz ki eklemeyi yaptıktan sonra listelesin. Yani 17. satırda addNewBook fonksiyonunu çağırırken newBook objemizin yanında ek olarak bir de callback şeklinde listBooks parametresini gönderiyoruz artık. Böylece console ekranında üç tane kitabımızı başarılı bir şekilde ekranda görüyoruz. Özetle biz artık listBooks fonksiyonunu bağımsız bir şekilde değil de addNewBook’a callback olarak geçtiğimiz için ekleme işleminden hemen sonra listeliyoruz. İstediğimiz de zaten buydu. Tabii callback’ler her işimizi çözseydi promise ve async/await yapılarına ihtiyacımız olmazdı. Yukarıda basit bir örnekle callback’in bize sağladığı faydayı gördük ancak uzun vadede sürekli iç içe callback kullanımı daha sonrasında ister istemez karışık bir yapı ortaya çıkarıyor. Bu konuyu daha detaylı araştırmak isteyenler için “callback hell” yazarak inceleyebilirler.
Evet arkadaşlar bu yazıda sizlere elimden geldiği kadar sade bir şekilde JavaScript’te senkron, asenkron, thread, call stack ve callback konularından bahsetmeye çalıştım. Umarım faydalı olmuştur, iyi çalışmalar.
Referanslar;
1)Arin Yazılım
2)freeCodeCamp — Callback Hell
3)JavaScript MDN
4)Tarık Güney — Event Loop, Async, Single Thread
5)JSConf | Philiph Roberts — Event Loop