JavaScript işleri nasıl paralel yürütür?

Bu flood’da JavaScript’in Event Loop ve Run-to-Completion kavramlarından bahsedeceğim.

Python, Ruby, Java gibi dillerde, Runtime’lar birden çok Thread’i aynı anda çalıştırabilmek için işlemci zamanını Thread’ler arasında paylaştırırlar. Çok kabaca, işlemci zamanı belirli dilimlere (time slice) ayrılır (örneğin 5 ms). Runtime ilk Thread’in 5 ms boyunca çalışmasına izin verir. Thread 5 ms’den önce işini bitirip uyutulmak isteyebilir ancak işini 5 ms’de bitirmezse Runtime Scheduler tarafından uyutulur ve sonraki Thread işlemciyi kullanmaya başlar.

Bu şekilde işlemci Thread’ler arasında paylaştırılır. Klasik Thread Scheduling mekanizması aşağıda görselleştirilmiştir.

JavaScript, yukarıda sayılan dillerden farklı olarak sadece tek bir Thread ile (single-threaded) çalışır. Uygulamanın işlevsellik gösterebilmesi için gerekli olan bütün kodları yegane Thread koşturur. Yapılması gereken işler JS Runtime’ına (Chrome V8, Chakra, vb) Event’ler ve Callback’ler ile iletilir. Örneğin, tarayıcı üzerinde bir butona bastığımızda çalışması gereken kod bloğu (Event Handler) ilgili butonun Click Event’i ile Thread’e gönderilir ve sırası geldiğinde Thread tarafından işlenir. Bahsedilen Event’lerin ve Callback’lerin sırada tutulduğu yapı, basit bir kuyruk (Queue) mekanizmasıdır. Thread’in her defasında kuyruktaki ilk Event’i işleyip yeni bir Event alması da Event Loop olarak adlandırılır.

JavaScript, single-threaded çalıştığı için diğer Runtime'lardan farklı olarak işlemci zamanını Thread'lere paylaştırmaz, Run-to-Completion adı verilen, elindeki işi tamamlamadan başka bir işe geçmeyen bir mekanizmaya sahiptir. Yani Event Loop'ta sıradaki Event işlenmeye başlandıktan sonra o Event'in Handler kodu tamamen koşturulmadan yeni bir Event'e başlanmaz. Başka dillerden JavaScript'e gelen geliştiricilerin kafasını en fazla karıştıran konu belki de budur.

Event Loop ve Run-to-completion'ın nasıl çalıştığı aşağıda görselleştirilmiştir. Şimdi örneklerle bu Run-to-completion meselesini derinlemesine inceleyelim.

Run-to-completion uygulamanın bütün parçalarının birbirinin hakkına riayet ederek (Cooperative Concurrency) çalışmasını zorunlu kılar. Thread tarafından koşturulmaya başlanan Event Handler Thread'i saniyelerce çalıştırırsa, o sırada koşturulmayı bekleyen Event'ler zamanında koşturulamayacak ve kullanıcı deneyimi etkilenecektir. Örneğin kullanıcı butona basacak ancak butona bastığında gerçekleşmesi gereken aksiyon yerine donan, klavye, mouse etkilerine tepki vermeyen bir tarayıcı ile karşılaşacaktır. Anlaşılacağı üzere JavaScript'te Thread, diğer dillerde ara ara başvurulduğu gibi hiçbir şartta durdurulmamalıdır, zaten JavaScript'te Sleep benzeri bir sistem çağrısı da yoktur.

Meseleyi daha iyi anlamak için iife (immediately-invoked function expression) ile aşağıda verilen kodun çıktısının ne olacağını yorumlamaya çalışalım. iife ile fonksiyon tanımlanır tanımlanmaz çağrılmıştır.

JS Runtime dosyayı yorumlamaya başlar başlamaz bu fonksiyonun 2. satırdan itibaren koşturulması için Event kuyruğuna bir Event bırakır. Thread, bu Event’in sırası geldiğinde kodu 2. satırdan itibaren koşturur ve konsola this is the start yazar. Sonra 4. satırdaki setTimeout çağrısını görür, setTimeout için Event Queue'ya bir Event bırakır ve 8. satıra geçerek yine konsola this is the end yazar ve ilk Event'in Handling'i bitmiş olur. Thread ilk döngüyü bitirdikten sonra, Event Loop içinde kuyruğa bakar ve kuyruktaki setTimeout Event'ini alır ve işlemeye başlar. Konsola this is a msg from callback yazarak işlemi bitirir.

JS’in Single-Threaded çalışmasının avantaj ve dezavantajlarını başka bir flood'da ele alacağız.