Go Goroutine'in Çalışma Prensibi: 2KB Değişken Yığın (Stack) ve M:N GMP Zamanlayıcısı (Scheduler)

Goroutine
Gopher'lara Golang'ın avantajlarını anlatmaları istendiğinde sıkça karşımıza çıkan bir konu eşzamanlılık (Concurrency) ile ilgilidir. Bu konunun temelinde hafif ve basit bir şekilde işlenebilen goroutine (goroutine) yer almaktadır. Bu nedenle goroutine hakkında kısaca bir yazı hazırladık.
Eşzamanlılık (Concurrency) ve Paralellik (Parallelism)
Goroutine'i anlamadan önce, sıkça karıştırılan iki kavramı gözden geçirmek istiyoruz.
- Eşzamanlılık: Eşzamanlılık, çok sayıda işin aynı anda ele alınmasıyla ilgilidir. Bunun gerçekten aynı anda yürütüldüğü anlamına gelmediğini, birden fazla görevin küçük birimlere ayrılıp dönüşümlü olarak çalıştırılmasıyla kullanıcıya birden fazla görevin aynı anda işleniyormuş gibi görünmesini sağlayan yapısal ve mantıksal bir kavram olduğunu belirtmek gerekir. Tek çekirdekli işlemcilerde de eşzamanlılık mümkündür.
- Paralellik: Paralellik, "birden fazla çekirdekte birden fazla işin aynı anda işlenmesi"dir. Kelimenin tam anlamıyla işlerin paralel olarak ilerletilmesidir ve diğer görevler aynı anda yürütülür.
Goroutine, Go çalışma zamanı zamanlayıcısı (scheduler) aracılığıyla eşzamanlılığı kolayca gerçekleştirmeyi sağlar ve GOMAXPROCS ayarı ile paralelliği de doğal olarak kullanır.
Yaygın olarak kullanılan Java'nın çoklu iş parçacığı (Multi thread) yapısı ise paralelliğin temsili bir kavramıdır.
Goroutine neden iyidir?
Hafiftir (lightweight)
Oluşturma maliyeti diğer dillere kıyasla oldukça düşüktür. Burada akla şu soru gelebilir: Golang neden daha az kullanır? Bunun nedeni, goroutine'lerin oluşturulmasının ve yönetiminin Go çalışma zamanı içinde gerçekleşmesidir. Çünkü goroutine, yukarıda bahsedilen hafif mantıksal bir iş parçacığıdır; OS iş parçacığı birimine göre daha küçüktür, başlangıç yığını (stack) yaklaşık 2KB boyutundadır ve kullanıcının uygulamasına bağlı olarak yığın eklenerek dinamik biçimde büyüyebilir.
Yığın bazında yönetildiği için oluşturulması ve sonlandırılması çok hızlı ve düşük maliyetlidir; bu sayede milyonlarca goroutine çalıştırılsa bile sistem üzerinde ağır bir yük oluşturmaz. Bunun sonucunda Goroutine, çalışma zamanı zamanlayıcısı sayesinde OS çekirdeğinin müdahalesini en aza indirebilir.
Performansı yüksektir (performance)
Öncelikle, yukarıda açıklandığı gibi Goroutine'in OS çekirdeği müdahalesi azdır; bu nedenle kullanıcı seviyesinde (User-Level) bağlam değiştirme (context switching) yapılırken OS iş parçacığı birimine göre maliyeti düşüktür ve görevler hızlı bir şekilde değiştirilebilir.
Bunun yanında M:N modeli kullanılarak goroutine'ler OS iş parçacıklarına atanarak yönetilir. OS iş parçacığı havuzu oluşturularak çok sayıda iş parçacığına ihtiyaç duyulmadan, az sayıda iş parçacığıyla işlem yapılabilir. Örneğin sistem çağrısı gibi bir bekleme durumuna girildiğinde Go çalışma zamanı, aynı OS iş parçacığında başka bir goroutine'i çalıştırır; böylece OS iş parçacığı durmadan ve verimli bir şekilde CPU'yu kullanarak hızlı işlem yapabilir.
Bu sayede Golang, özellikle I/O işlemlerinde diğer dillere kıyasla yüksek performans gösterebilir.
Sadedir (concise)
Eşzamanlılık gerektiğinde tek bir go anahtar kelimesiyle bir fonksiyonu kolayca çalıştırabilmek de büyük bir avantajdır.
Mutex, Semaphore gibi karmaşık kilitleri kullanmak gerekir; kilit kullanıldığında mutlaka göz önünde bulundurulması gereken kilitlenme (DeadLock) durumu da dikkate alınmak zorunda kalınır ve bu da geliştirmeden önceki tasarım aşamasından itibaren karmaşık bir süreç gerektirir.
Goroutine, "belleği paylaşarak iletişim kurmayın, iletişim kurarak belleği paylaşın" felsefesine uygun olarak kanal (Channel) üzerinden veri aktarımını önerir; SELECT ise kanallarla birlikte kullanılarak veri hazır olan kanaldan işlem yapılmasını sağlayan bir özelliği de destekler. Ayrıca sync.WaitGroup kullanılarak birden fazla goroutine'in tamamının bitmesi basitçe beklenebilir ve iş akışı kolayca yönetilebilir. Bu araçlar sayesinde iş parçacıkları arasındaki veri yarışı (data race) sorunları önlenir ve eşzamanlılık işlemleri daha güvenli bir şekilde gerçekleştirilebilir.
Ayrıca bağlam (context) kullanılarak yaşam döngüsü, iptal, zaman aşımı, son tarih ve istek kapsamı kullanıcı seviyesinde (User-Level) kontrol edilebilir; bu da belirli bir düzeyde kararlılığın sağlanmasına olanak tanır.
Goroutine'de paralel çalışma (GOMAXPROCS)
Goroutine'in eşzamanlılık açısından sağladığı avantajlardan bahsedildi, ancak paralelliği desteklemiyor mu? sorusu akla gelmiş olabilir. Son dönemde CPU çekirdek sayıları geçmişten farklı olarak iki haneli sayılara ulaşmış durumda ve ev tipi bilgisayarlar da çok sayıda çekirdeğe sahip olabiliyor.
Ancak Goroutine paralel işlemleri de gerçekleştirir; bunu sağlayan şey GOMAXPROCStir.
GOMAXPROCS ayarlanmazsa, sürüme göre farklı biçimde belirlenir.
1.5 öncesi: Varsayılan değer 1'dir; 1'den fazla kullanmak gerektiğinde
runtime.GOMAXPOCS(runtime.NumCPU())gibi bir yöntemle ayarlanması zorunludur.1.5 ~ 1.24: Kullanılabilir tüm mantıksal çekirdek sayısına değiştirilmiştir. Bu tarihten itibaren, belirli kısıtlamaların gerekli olduğu durumlar dışında geliştiricinin bunu ayarlaması gerekmez.
1.25: Konteyner ortamlarında adını duyurmuş bir dil olarak, Linux üzerindeki cGroup değerlerini kontrol ederek konteynere tanımlanmış
CPU sınırıdeğerini belirler.Örneğin mantıksal çekirdek sayısı 10 ve CPU sınırı değeri 5 ise
GOMAXPROCSdaha düşük olan 5 değerine ayarlanır.
1.25 sürümündeki değişiklik çok büyük bir güncellemedir. Bunun nedeni, dilin konteyner ortamlarındaki kullanım alanının artmasıdır. Bu sayede gereksiz iş parçacığı oluşturma ve bağlam değiştirme azaltılarak CPU kısıtlaması (throttling) önlenebilir hale gelmiştir.
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "time"
8)
9
10func exe(name int, wg *sync.WaitGroup) {
11 defer wg.Done()
12
13 fmt.Printf("Goroutine %d: 시작\n", name)
14 time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연
15 fmt.Printf("Goroutine %d: 시작\n", name)
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용
20 wg := sync.WaitGroup();
21 goroutineCount := 10
22 wg.Add(goroutineCount)
23
24 for i := 0; i < goroutineCount; i++ {
25 go exe(i, &wg)
26 }
27
28 fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...")
29 wg.Wait()
30 fmt.Println("모든 작업이 완료되었습니다.")
31
32}
33
Goroutine'in zamanlayıcısı (M:N modeli)
Önceki bölümde bahsedilen, M:N modeli kullanılarak OS iş parçacıklarına atanarak yönetim konusu, goroutine GMP modeli ile daha ayrıntılı açıklanabilir.
- G (Goroutine): Go'da çalıştırılan en küçük iş birimi
- M (Machine): OS iş parçacığı (gerçek işin yürütüldüğü yer)
- P (Processor): Go çalışma zamanının yönettiği mantıksal işlemci
şeklindedir. P ayrıca yerel çalıştırma kuyruğuna (Local Run Queue) sahiptir ve atanan G'yi bir M'ye yerleştiren zamanlayıcı görevini üstlenir. Kısaca goroutine'in GMP işleyişi aşağıdaki gibidir:
- G (Goroutine) oluşturulduğunda P (Processor)'nin yerel çalıştırma kuyruğuna atanır.
- P (Processor), yerel çalıştırma kuyruğundaki G (Goroutine)'yi bir M (Machine)'ye atar.
- M (Machine), G (Goroutine)'nin durumunu, yani block, complete veya preempted durumlarını geri döndürür.
- İş çalma (Work-Stealing): Bir P'nin yerel çalıştırma kuyruğu boşalırsa, diğer P'ler önce global kuyruğu kontrol eder. Orada da G (Goroutine) yoksa diğer yerel P (Processor)'lerin işlerini çalarak tüm M'lerin durmadan çalışmasını sağlar.
- Sistem çağrısı işleme (Blocking): G (Goroutine) çalışırken bloke olursa M (Machine) bekleme durumuna geçer; bu durumda P (Processor), bloke olan M (Machine)'den ayrılarak başka bir M (Machine) ile birleşir ve bir sonraki G (Goroutine)'yi çalıştırır. Bu sayede I/O işlemi sırasındaki bekleme süresinde bile CPU israf edilmez.
- Bir G (Goroutine) uzun süre çalışıp önleme (preempted) yapılırsa, diğer G (Goroutine)'lere çalışma fırsatı verilir.
Golang'ın çöp toplayıcısı (GC, Garbage Collector) de Goroutine'ler üzerinde çalışır; böylece uygulamanın yürütülmesini en az düzeyde durdurarak (STW) belleği paralel biçimde temizleyebilir ve sistem kaynaklarını verimli kullanabilir.
Son olarak, Golang dilin güçlü avantajlarından biridir ve bunun yanında daha birçok özelliği vardır; birçok geliştiricinin Go ile keyifli vakit geçirmesini dileriz.
Teşekkür ederim.