1 Gorutinlere Giriş
1.1 Eş Zamanlılık ve Paralelizm Temel Kavramları
Eş zamanlılık ve paralelizm, çoklu iş parçacıklı programlamada yaygın olarak kullanılan iki kavramdır. Aynı anda gerçekleşebilecek olayları veya program yürütmesini tanımlamak için kullanılırlar.
- Eş zamanlılık, aynı zamanda birden fazla görevin işlendiği ancak her seferinde sadece bir görevin yürütüldüğü bir durumu ifade eder. Görevler hızla birbirleri arasında geçiş yaparak kullanıcıya eş zamanlı yürütme yanılsamasını verir. Eş zamanlılık, tek çekirdekli işlemciler için uygundur.
- Paralelizm, gerçekten aynı anda aynı anda gerçekleşen birden fazla görevi ifade eder ve bu durum çoklu çekirdekli işlemcilerden destek gerektirir.
Go dilinin temel amaçlarından biri olarak eş zamanlı programlama modellerini etkili bir şekilde elde etmiştir. Bu modelleri, Gorutinler ve Kanallar aracılığıyla etkili şekilde gerçekleştirmiştir. Go'nun çalışma zamanı, Gorutinlerin yönetimini üstlenir ve bu Gorutinleri çeşitli sistem iş parçacıklarında planlayarak paralel işlemler elde eder.
1.2 Go Dilinde Gorutinler
Gorutinler, Go dilinde eş zamanlı programlamayı gerçekleştirmenin temel kavramlarından biridir. Go'nun çalışma zamanı tarafından yönetilen hafif iş parçacıklarıdır. Kullanıcı perspektifinden bakıldığında, onlar iş parçacıklarına benzerler ancak daha az kaynak tüketirler ve daha hızlı başlarlar.
Gorutinlerin özellikleri şunlardır:
- Hafif: Gorutinler, geleneksel iş parçacıklarına kıyasla daha az yığın belleği kaplar ve ihtiyaca göre yığın boyutları dinamik olarak genişleyebilir veya küçülebilir.
- Düşük maliyetli: Gorutinlerin oluşturulması ve yok edilmesi için gerekli maliyet, geleneksel iş parçacıklarına kıyasla çok daha düşüktür.
- Basit iletişim mekanizması: Kanallar, Gorutinler arasında basit ve etkili bir iletişim mekanizması sağlar.
- Engelleme olmayan tasarım: Belirli işlemlerde Gorutinlerin diğer Gorutinlerin çalışmasını engellemediği tasarımı. Örneğin, bir Gorutin giriş/çıkış işlemlerini beklerken diğer Gorutinlerin yürütmesine devam edebilir.
2 Gorutinlerin Oluşturulması ve Yönetilmesi
2.1 Bir Gorutin Nasıl Oluşturulur
Go dilinde, go
anahtar kelimesini kullanarak kolayca bir Gorutin oluşturabilirsiniz. Bir işlev çağrısını go
anahtar kelimesi ile önce eklediğinizde, işlev yeni bir Gorutinde eşzamansız şekilde yürütülür.
Basit bir örneğe bakalım:
package main
import (
"fmt"
"time"
)
// Merhaba yazdırmak için bir işlev tanımla
func sayHello() {
fmt.Println("Merhaba")
}
func main() {
// go anahtar kelimesini kullanarak yeni bir Gorutin başlat
go sayHello()
// main Gorutini, sayHello'un yürütülmesine izin vermek için bir süre bekler
time.Sleep(1 * time.Second)
fmt.Println("Ana işlev")
}
Yukarıdaki kodda, sayHello()
işlevi yeni bir Gorutinde eşzamansız olarak yürütülecektir. Bu, main()
işlevinin, devam etmeden önce sayHello()
un bitmesini beklemeyeceği anlamına gelir. Bu nedenle, main Gorutini, sayHello
işlevindeki yazdırma ifadesinin yürütülmesine izin vermek için time.Sleep
kullanılır. Bu sadece bir gösterim amaçlıdır. Gerçek geliştirmede genellikle farklı Gorutinlerin yürütülmesini koordine etmek için kanallar veya diğer senkronizasyon yöntemlerini kullanırız.
Not: Uygulamalarda, bir Gorutinin bitmesini beklemek için
time.Sleep()
kullanmak güvenilir bir senkronizasyon mekanizması olmadığından kullanılmamalıdır.
2.2 Gorutin Zamanlama Mekanizması
Go'da, Gorutinlerin zamanlaması, Go'nun çalışma zamanının zamanlayıcısı tarafından ele alınır ve bu, mevcut mantıksal işlemciler üzerinde yürütme zamanı ayırmaktan sorumludur. Go zamanlayıcısı, çoklu çekirdekli işlemcilerde daha iyi performans elde etmek için M:N
zamanlama teknolojisini (çoklu Gorutinlerin çoklu işletim sistemi iş parçacıklarına eşlemesi) kullanır.
GOMAXPROCS ve Mantıksal İşlemciler
GOMAXPROCS
, çalışma zamanı zamanlayıcısı için kullanılabilen maksimum CPU sayısını tanımlayan bir ortam değişkenidir ve varsayılan değerini makinedeki CPU çekirdek sayısı alır. Go çalışma zamanı, her bir mantıksal işlemci için bir işletim sistemi iş parçacığı atar. GOMAXPROCS
ayarlayarak, çalışma zamanı tarafından kullanılan çekirdek sayısını sınırlayabiliriz.
import "runtime"
func init() {
runtime.GOMAXPROCS(2)
}
Yukarıdaki kod, Gorutinlerin planlanması için maksimum iki çekirdeği ayarlar, hatta daha fazla çekirdekli bir makinede program çalıştırılsa bile.
Zamanlayıcı İşlemi
Zamanlayıcı, M (makine), P (işlemci) ve G (Goroutine) olmak üzere üç önemli varlık kullanarak çalışır. M, bir makine veya iş parçacığını temsil eder; P, zamanlama bağlamını temsil eder ve her P'nin yerel Goroutine kuyruğu bulunur; G ise belirli bir Goroutine'u temsil eder.
- M: Bir makine veya iş parçacığını temsil eder ve OS çekirdeği iş parçacıklarının bir soyutlaması olarak hizmet verir.
- P: Bir Goroutine'u yürütmek için gereken kaynakları temsil eder. Her P'nin yerel Goroutine kuyruğu bulunur.
- G: Bir Goroutine'u temsil eder ve yürütme yığını, komut seti ve diğer bilgileri içerir.
Go zamanlayıcısının çalışma prensipleri şunlardır:
- M'nin G'yi yürütebilmesi için bir P'ye sahip olması gerekir. Eğer bir P yoksa, M iş parçacığı önbelleğine geri döner.
- G, diğer G'ler tarafından engellenmediğinde (örneğin, sistem çağrılarında), mümkün olduğunca aynı M üzerinde çalışır ve G'nin yerel verilerinin daha verimli CPU önbelleği kullanımı için 'sıcak' kalmasına yardımcı olur.
- Bir G engellendiğinde, M ve P ayrılır ve P, yeni bir M arar veya yeni bir M'yi uyandırarak diğer G'lere hizmet etmesini sağlar.
go func() {
fmt.Println("Goroutine'dan Merhaba")
}()
Yukarıdaki kod, yeni bir Goroutine başlatmayı gösterir ve bu yeni G'nin çalıştırılma kuyruğuna eklenmesi için zamanlayıcıyı tetikler.
Goroutine'ların Öncelikli Olarak Zamanlanması
Başlangıç aşamalarında, Go, Goroutine'ların gönüllü olarak kontrolü bırakmadan uzun süre çalıştıklarında diğer Goroutine'ları olumsuz etkileyebileceği kooperatif zamanlama kullanıyordu. Şimdi ise, Go zamanlayıcısı, uzun süren G'lerin diğer G'lere yürütme şansı vermek için duraklatılmasını sağlayan öncelikli zamanlama uygular.
2.3 Goroutine Yaşam Döngüsü Yönetimi
Go uygulamanızın sağlamlığını ve performansını sağlamak için Goroutine'ların yaşam döngüsünü anlamak ve bunları uygun bir şekilde yönetmek kritiktir. Goroutine'ları başlatmak basittir, ancak uygun yönetim olmadan, hafıza sızıntıları ve yarış koşulları gibi sorunlara yol açabilirler.
Güvenli Bir Şekilde Goroutine Başlatma
Bir Goroutine'u başlatmadan önce, iş yükünü ve çalışma zamanı karakteristiklerini anlamak önemlidir. Bir Goroutine'un açık bir başlangıcı ve sonu olmalıdır, aksi takdirde sonlandırma koşulları olmadan "Goroutine yetimleri" oluşturabilir.
func worker(done chan bool) {
fmt.Println("Çalışılıyor...")
time.Sleep(time.Second) // pahalı bir işlemi taklit et
fmt.Println("Çalışma Bitti.")
done <- true
}
func main() {
// Burada, Go'daki kanal mekanizması kullanılır. Kanalı temel bir ileti kuyruğu olarak düşünebilir ve "<-" operatörünü kullanarak kuyruğun veri okuması ve yazılması yapılabilir.
done := make(chan bool, 1)
go worker(done)
// Goroutine'un bitmesini bekle
<-done
}
Yukarıdaki kod, done
kanalını kullanarak bir Goroutine'un bitmesini beklemenin bir yolunu gösterir.
Not: Bu örnek, Go'daki kanal mekanizmasını kullanmaktadır, bu mekanizma daha sonraki bölümlerde detaylandırılacaktır.
Goroutine'ları Durdurma
Genel olarak, tüm programın sonuçlanması tüm Goroutine'ları zımnen sonlandıracaktır. Ancak, uzun süre çalışan hizmetlerde, Goroutine'ları etkin bir şekilde durdurmak gerekebilir.
- Durdurma sinyali göndermek için kanalları kullanın: Goroutine'lar, durdurma sinyallerini kontrol etmek için kanalları sürekli olarak kontrol edebilir.
stop := make(chan struct{})
go func() {
for {
select {
case <-stop:
fmt.Println("Durdurma sinyali alındı. Kapatılıyor...")
return
default:
// normal işlemi gerçekleştir
}
}
}()
// Durdurma sinyali gönder
stop <- struct{}{}
-
Yaşam döngüsünü yönetmek için
context
paketini kullanın:
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Durdurma sinyali alındı. Kapatılıyor...")
return
default:
// normal işlemi gerçekleştir
}
}
}(ctx)
// Goroutine'u durdurmak istediğinizde
cancel()
context
paketini kullanmak, Goroutine'ların daha esnek bir kontrolünü sağlar, zaman aşımı ve iptal yetenekleri sunar. Büyük uygulamalarda veya mikro servislerde, Goroutine yaşam döngülerini kontrol etmek için context
, önerilen yoldur.