Goroutines Temelleri
Goroutine
Gopher'lara golang'in avantajları sorulduğunda sıklıkla bahsedilen Eşzamanlılık (Concurrency) ile ilgili bir yazı bulunmaktadır. Bu içeriğin temelini hafif ve basitçe işlenebilen goroutine oluşturmaktadır. Buna dair kısa bir derleme yaptım.
Eşzamanlılık (Concurrency) vs Paralellik (Parallelism)
Goroutine'i anlamadan önce, sıklıkla karıştırılan iki kavramı açıklığa kavuşturmak istiyorum.
- Eşzamanlılık: Eşzamanlılık, birçok işi bir kerede ele almakla ilgilidir. İlla ki fiilen eş zamanlı olarak yürütüldüğü anlamına gelmez; birden fazla görevi küçük birimlere ayırıp sırayla çalıştırarak, kullanıcının gözünde birden fazla işin aynı anda işleniyormuş gibi görünmesini sağlayan yapısal ve mantıksal bir kavramdır. Tek çekirdekte bile eşzamanlılık mümkündür.
- Paralellik: Paralellik, "birden fazla çekirdekte birden fazla işin aynı anda işlenmesi" demektir. Kelimenin tam anlamıyla işlerin paralel olarak ilerlemesi ve farklı görevlerin aynı anda yürütülmesidir.
Goroutine, Go runtime scheduler aracılığıyla eşzamanlılığın kolayca uygulanmasını sağlar ve GOMAXPROCS
ayarı ile paralelliği doğal bir şekilde kullanır.
Yaygın olarak kullanılan Java'nın Multi-thread (Çoklu iş parçacığı) yapısı, paralelliğin tipik bir örneğidir.
Goroutine neden iyidir?
Hafiftir (lightweight)
Oluşturma maliyeti diğer dillere göre çok düşüktür. Burada akla "golang neden az kullanır?" sorusu gelmektedir; bunun nedeni oluşturma yerinin Go runtime içinde yönetilmesidir. Çünkü yukarıda bahsedilen hafif mantıksal bir iş parçacığıdır. OS iş parçacığı biriminden daha küçüktür, başlangıçta yaklaşık 2KB boyutunda bir yığına ihtiyaç duyar ve kullanıcının uygulamasına bağlı olarak yığın eklenerek dinamik olarak değişkenlik gösterebilir.
Yığın birimleri halinde yönetildiği için oluşturulması ve kaldırılması çok hızlı ve düşük maliyetli bir şekilde gerçekleştirilebilir, bu sayede milyonlarca goroutine çalıştırılsa bile yük oluşturmayacak bir işlem sağlanır. Bu durum sayesinde Goroutine, runtime scheduler sayesinde OS kernel müdahalesini minimuma indirebilir.
Performanslıdır (performance)
Öncelikle Goroutine, yukarıdaki açıklamada olduğu gibi OS kernel müdahalesi az olduğu için kullanıcı düzeyinde (User-Level) bağlam anahtarlaması (context switching) yaparken OS iş parçacığı birimine göre daha düşük maliyetle hızlı bir şekilde iş geçişi yapabilir.
Ayrıca, M:N modeli kullanılarak OS iş parçacıklarına atanarak yönetilir. Bir OS iş parçacığı havuzu (thread pool) oluşturulur ve çok sayıda iş parçacığına gerek kalmadan az sayıda iş parçacığı ile işlem yapılabilir. Örneğin, sistem çağrısı gibi bir bekleme durumuna girildiğinde, Go runtime, OS iş parçacığında başka bir goroutine çalıştırarak OS iş parçacığının boş durmamasını ve CPU'yu verimli bir şekilde kullanarak hızlı işlem yapılmasını sağlar.
Bu nedenle Golang, özellikle I/O işlemlerinde diğer dillere göre daha yüksek performans sergileyebilir.
Sade ve Anlaşılırdır (concise)
Eşzamanlılık gerektiğinde go
anahtar kelimesi ile bir fonksiyonun kolayca işlenebilmesi de büyük bir avantajdır.
Mutex
, Semaphore
gibi karmaşık Kilitlerin kullanılması gerekir ve Kilit kullanıldığında kaçınılmaz olarak dikkate alınması gereken Deadlock (Kilitlenme) durumu nedeniyle, geliştirme öncesindeki tasarım aşamasından itibaren karmaşık aşamalara ihtiyaç duyulur.
Goroutine, "belleği paylaşarak iletişim kurmayın, iletişim kurarak belleği paylaşın" felsefesine uygun olarak Channel
(Kanal) aracılığıyla veri iletimini teşvik eder ve SELECT
, Channel ile birleşerek verinin hazır olduğu kanaldan işlem yapma imkanı sunan bir özellik bile destekler. Ayrıca, sync.WaitGroup
kullanılarak birden fazla goroutine'in tamamlanması basitçe beklenebilir, bu da iş akışının kolayca yönetilmesini sağlar. Bu araçlar sayesinde iş parçacıkları arasındaki veri rekabeti sorunları önlenir ve daha güvenli bir eşzamanlılık işlemi mümkün olur.
Ayrıca, context (bağlam) kullanılarak yaşam döngüsü, iptal, zaman aşımı, son teslim tarihi ve istek kapsamı kullanıcı düzeyinde (User-Level) kontrol edilebilir, bu da belirli bir düzeyde istikrarı garanti edebilir.
Goroutine'in Paralel Çalışması (GOMAXPROCS)
Goroutine'in eşzamanlılığının iyi olduğu söylense de, "paralelliği desteklemiyor mu?" diye bir soru akla gelebilir. Çünkü günümüz CPU'larının çekirdek sayısı geçmişten farklı olarak ondan fazladır ve ev bilgisayarlarında bile çekirdek sayısı az değildir.
Ancak Goroutine paralel çalışmayı da gerçekleştirir, bu da GOMAXPROCS
ile sağlanır.
GOMAXPROCS
ayarlanmazsa, sürüme göre farklı şekilde ayarlanır.
1.5 öncesi: Varsayılan değer 1'dir. 1'den fazlasına ihtiyaç duyulursa
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 dönemden itibaren, geliştiricinin büyük kısıtlamalara ihtiyaç duymadığı sürece ayarlama yapmasına gerek kalmamıştır.
1.25: Konteyner ortamlarında popüler bir dil olması nedeniyle, Linux üzerindeki cGroup'u kontrol ederek konteynere atanmış
CPU kısıtlamasını
doğrular.Eğer mantıksal çekirdek sayısı 10 ise ve CPU kısıtlama değeri 5 ise,
GOMAXPROCS
daha düşük olan 5 olarak ayarlanır.
1.25'teki bu değişiklik oldukça büyük bir iyileştirme getirmiştir. Bu, konteyner ortamlarında dilin kullanım oranının artmasından kaynaklanmaktadır. Bu sayede gereksiz iş parçacığı oluşturulması ve bağlam anahtarlaması azaltılarak CPU kısıtlaması (throttling) önlenebilmiş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) // Goroutine %d: Başlangıç
14 time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 // İş simülasyonu için gecikme
15 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Başlangıç
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 // Sadece 2 CPU çekirdeği kullan
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이 끝날 때까지 대기합니다...") // Tüm goroutine'ler bitene kadar bekleniyor...
29 wg.Wait()
30 fmt.Println("모든 작업이 완료되었습니다.") // Tüm işlemler tamamlandı.
31
32}
33
Goroutine'in Planlayıcısı (M:N Modeli)
Yukarıdaki "M:N modeli kullanılarak OS iş parçacıklarına atanarak yönetilir" kısmına biraz daha yakından bakarsak, goroutine GMP modeli bulunmaktadır.
- G (Goroutine): Go'da yürütülen en küçük iş birimi
- M (Machine): OS İş Parçacığı (Gerçek çalışma konumu)
- P (Processor): Go runtime tarafından yönetilen mantıksal işlemci
olarak tanımlanır. P ek olarak yerel bir yürütme kuyruğuna (Local Run Queue) sahiptir ve atanmış G'leri M'ye dağıtan zamanlayıcı (scheduler) rolünü üstlenir. Kısaca goroutine,
GMP'nin çalışma süreci aşağıdaki gibidir:
- G (Goroutine) oluşturulduğunda, P'nin (Processor) yerel yürütme kuyruğuna atanır.
- P (Processor), yerel yürütme kuyruğundaki G'yi (Goroutine) M'ye (Machine) atar.
- M (Machine), G'nin (Goroutine) durumunu (block, complete, preempted) geri döndürür.
- Work-Stealing (İş Çalma): Eğer P'nin yerel yürütme kuyruğu boşalırsa, diğer P küresel kuyruğu kontrol eder. Orada da G (Goroutine) yoksa, diğer yerel P'nin (Processor) işini çalarak tüm M'lerin boş durmadan çalışmasını sağlar.
- Sistem Çağrısı İşlemi (Blocking): G (Goroutine) çalışırken Block durumu oluşursa, M (Machine) bekleme durumuna geçer. Bu durumda P (Processor), Block olan M'den ayrılır ve başka bir M (Machine) ile birleşerek bir sonraki G'yi (Goroutine) çalıştırır. Bu sayede I/O işlemi sırasındaki bekleme süresinde bile CPU israfı olmaz.
- Bir G (Goroutine) uzun süre önalım (preempted) yaparsa, diğer G'lere (Goroutine) yürütme şansı verilir.
Golang'de GC (Garbage Collector) de Goroutine üzerinde çalıştığı için, uygulamanın yürütülmesini minimum düzeyde durdurarak (STW) belleği paralel olarak temizleyebilir ve sistem kaynaklarını verimli bir şekilde kullanır.
Sonuç olarak Golang, dilin güçlü yönlerinden biridir ve bunun dışında da pek çok avantajı vardır; umarım birçok geliştirici Go dilinden keyif alır.
Teşekkür ederim.