Go Eşzamanlılığı Geleneksel İş Parçacığı Modellerinden Daha İyi Nasıl Ele Alır?

Go Eşzamanlılığı Geleneksel İş Parçacığı Modellerinden Daha İyi Nasıl Ele Alır?

Modern yazılım mühendisliğinde, aynı anda birden fazla görevi yerine getirebilecek uygulamalar oluşturmak artık bir lüks değil; temel bir gerekliliktir. Yüksek verimli web sunucularından gerçek zamanlı akış hizmetlerine kadar eşzamanlılık, performansın merkezinde yer alır.

Onlarca yıldır C++, Java ve Python gibi geleneksel programlama dilleri, eşzamanlı görevleri yerine getirmek için işletim sisteminin yerel iş parçacığı modellerine güveniyordu. Ancak Google 2000’li yılların sonlarında Go’yu (Golang) tasarladığında tamamen farklı bir yol izledi. Go, ham işletim sistemi iş parçacıklarını açığa çıkarmak yerine Goroutines ve özel bir M:N Zamanlayıcı‘yı tanıttı.

Bu makalede, geleneksel iş parçacığı modellerinin mimari sınırlamalarını inceleyeceğiz ve Go’nun eşzamanlılık tasarımının neden önemli ölçüde daha verimli, ölçeklenebilir ve geliştirici dostu olduğunu keşfedeceğiz.


1. Geleneksel Diş Açmanın Darboğazları (1:1 Modeli)

Çoğu geleneksel çalışma zamanı sistemi 1:1 iş parçacığı modeli kullanır. Bu modelde, kullanıcı alanı kodunda oluşturulan her iş parçacığı, doğrudan İşletim Sistemi (OS) tarafından yönetilen bir çekirdek alanı iş parçacığına eşlenir.

Basit olmasına rağmen, bu 1:1 haritalama üç kritik darboğaza neden olur:

A. Bellek Yükü (Büyük Yığın Boyutları)

Bir işletim sistemi iş parçacığı ağır bir kaynaktır. Varsayılan olarak, işletim sistemleri her iş parçacığına sabit, bitişik bir yığın boyutu ayırır (genellikle 1MB ila 8MB).

  • 10.000 eşzamanlı bağlantıyı yönetmek istiyorsanız, 10.000 işletim sistemi iş parçacığının tahsis edilmesi, yalnızca iş parçacığı yığını belleği için 10 GB ila 80 GB RAM gerektirir.
  • Bu, yüksek bağlantı sayıları altında yüksek eşzamanlılığın işlenmesini son derece pahalı hale getirir ve standart sunucu donanımında neredeyse imkansız hale getirir.

B. Yüksek Bağlam Değiştirme Maliyetleri

İşletim sistemi, yürütmeyi bir iş parçacığından diğerine geçirdiğinde, bağlam değişikliği gerçekleştirir. İşletim sistemi iş parçacıkları çekirdek tarafından yönetildiği için bağlam anahtarı, kullanıcı çekirdeği sınırını geçmeyi gerektirir. CPU şunları yapmalıdır:

  1. Mevcut kayıtların durumunu kaydedin.
  2. CPU önbellek satırlarını temizleyin ve Sayfa Tablolarını (Çeviri Bakış Arabellekleri) güncelleyin.
  3. Çekirdek moduna geçin, sonraki iş parçacığını seçin ve kayıtlı durumunu yükleyin.
  4. Kullanıcı moduna geri dönün.

Bu gidiş-dönüş yolculuğunun tamamı yaklaşık 1 ila 2 mikrosaniye sürer; bu, gerçek iş mantığını yürütmek yerine yalnızca yönetim yüküne harcanan yüzlerce veya binlerce CPU döngüsünü temsil eder.

C. İşletim Sistemi Planlama Sınırları

İşletim sistemi zamanlayıcısı genel amaçlıdır. Veritabanı iş parçacıklarını, kullanıcı arayüzü iş parçacıklarını ve hafif ağ bağlantılarını aynı zamanlama buluşsal yöntemiyle ele alır. Uygulama düzeyindeki bağlamı anlamadığından, bir iş parçacığının bir ağ yuvasında engellenmesine, dahili bir kilit beklenmesine veya etkin hesaplama gerçekleştirilmesine bağlı olarak zamanlamayı optimize edemez.


2. Go’nun Çözümü: M:N Zamanlayıcı (Goroutines)

Go, Goroutines‘i tanıtarak ve kendi çalışma zamanı planlayıcısını uygulayarak işletim sistemi iş parçacığının sınırlamalarını aşar. Go, iş parçacıklarını 1:1 eşlemek yerine, M hafif goroutinlerin N fiziksel işletim sistemi iş parçacıklarına çoğullandığı bir M:N planlama modeli kullanır.

Geleneksel 1:1 İş Parçacığı ve Go M:N Zamanlayıcı Model Diyagramı

Bu mimari, genellikle GMP modeli olarak anılan üç temel yapısal öğe tarafından yönetilir:

  • G (Goroutine): Tek bir goroutine’i temsil eder. Goroutinin yürütme yığınını, program sayacını ve durumunu içerir. Bir G bir işletim sistemi iş parçacığı değildir; bu, başlatılması yalnızca 2 KB kadar belleğe mal olan basit bir Go yapısıdır.
  • M (Makine): Fiziksel bir işletim sistemi/çekirdek iş parçacığını temsil eder. İşletim sistemi zamanlayıcısı tarafından yönetilir ve goroutinlerin makine kodu talimatlarının yürütülmesinden sorumludur.
  • P (İşlemci): Mantıksal bir işlemciyi veya yürütme bağlamını temsil eder. P örneklerinin sayısı varsayılan olarak ana makinedeki (GOMAXPROCS tarafından kontrol edilen) mantıksal CPU çekirdeği sayısına eşittir. M makinesinin Go kodunu çalıştırabilmesi için P mantıksal işlemcisini edinmesi gerekir.

GMP Modeli Neden Üstündür

Goroutinler tamamen kullanıcı alanında Go çalışma zamanı tarafından yönetildiğinden:

  1. Dinamik Yığınlar: Bir goroutine 2KB’lik küçük bir yığınla başlar. Yürütmenin gerektirdiği şekilde yığın dinamik olarak büyür (yığında daha büyük bitişik bellek bölümleri tahsis edilir) ve küçülür. Bu, Go’nun tek bir dizüstü bilgisayarda aynı anda yüz binlerce goroutine çalıştırmasına olanak tanır.
  2. Hızlı Bağlam Anahtarları: Goroutinler arasındaki geçiş, çekirdek bağlam anahtarlarını çağırmadan tamamen kullanıcı alanında gerçekleşir. Yalnızca program sayacı ve birkaç CPU kaydı kaydedilir. Bu kullanıcı alanı bağlam anahtarı yalnızca 10 ila 100 nanosaniye sürer; bu, bir işletim sistemi iş parçacığı anahtarından yaklaşık 10 kat ila 100 kat daha hızlıdır.

3. İş Çalma ve Engellenmeyen G/Ç

Go çalışma zamanı, fiziksel CPU çekirdeklerinin hiçbir zaman gereğinden az kullanılmamasını sağlamak için iki gelişmiş planlama mekanizması kullanır: İş Çalma ve Sistem Çağrısı Aktarma.

A. İş Çalma Algoritması

Her mantıksal işlemci P, goroutinlerin kendi yerel çalıştırma kuyruğunu korur. Ek olarak taşma için genel bir çalıştırma kuyruğu vardır. Eğer bir P işlemcisi yerel çalıştırma kuyruğundaki tüm goroutinleri tüketirse uykuya geçmez. Bunun yerine iş hırsızlığı gerçekleştirir: diğer mantıksal işlemcileri kontrol eder ve tüm CPU çekirdeklerindeki iş yükünü dengelemek için sıradaki goroutinlerin yarısını çalar.

Local Queue P1: [ G1, G2, G3 ]  ---> Running on Thread M1
Local Queue P2: [ ]            ---> Running on Thread M2 (Idle)
                                     P2 steals G3 and G2 from P1!

B. Ağ Poller ve Engellemeyen G/Ç

Bir goroutine ağ G/Ç işlemi gerçekleştirdiğinde (örneğin, bir veritabanı bağlantısından okuma veya HTTP çağrısı yapma), Go, temeldeki işletim sistemi iş parçacığını M engellemez.

Bunun yerine Go çalışma zamanı, bloğu özel bir Network Poller ile kaydeder (bu, Linux’ta epoll, macOS’ta kqueue veya Windows’ta IOCP gibi işletim sistemine özgü verimli çoğullama API’lerini kullanır). Engellenen G goroutine M iş parçacığından ayrılır ve ağ yoklayıcıya park edilir.

M iş parçacığı, kuyruktan hemen başka bir çalıştırılabilir goroutine alır. G/Ç olayı tamamlandığında ağ yoklayıcı, yürütmeye devam etmek için G öğesini tekrar etkin çalıştırma kuyruğuna taşır.


4. Kanallar ve Paylaşılan Bellek (CSP Modeli)

Geleneksel iş parçacığı modelleri, belleği paylaşarak (örneğin, iş parçacıkları arasında işaretçilerin geçirilmesi) eşzamanlı görevleri koordine eder. Veri yarışlarını önlemek için geliştiricilerin kilitleri, muteksleri ve koşul değişkenlerini manuel olarak yönetmesi gerekir:

// Traditional Java Shared Memory Approach
synchronized(lock) {
    sharedResource.updateState();
}

Bu modelin hataya açık olduğu biliniyor ve sıklıkla kilitlenmelere, yarış koşullarına ve önbellek tutarlılığı darboğazlarına neden oluyor.

Go, ünlü Golang atasözüyle özetlenen İletişim Sıralı Süreçler (CSP) resmi modelini uygular:

“Hafızayı paylaşarak iletişim kurmayın; bunun yerine, iletişim kurarak hafızayı paylaşın.”

Go, birinci sınıf temel öğeler olarak Kanalları sağlar. Kanallar, goroutine’lerin yürütmeyi senkronize etmek ve veri sahipliğini aktarmak için mesaj gönderip almasına olanak tanıyan, tür açısından güvenli kuyruklar görevi görür.


5. Pratik Uygulama: Goroutinler ve Kanallar İş Başında

Burada, birden fazla çalışan goroutine’in görevleri eş zamanlı olarak nasıl işleyebildiğini ve sonuçları bir kanal aracılığıyla nasıl döndürebildiğini, veri akışını kilitlenmeler olmadan güvenli bir şekilde yönetebildiğini gösteren pratik bir Go programı bulunmaktadır.

package main

import (
	"fmt"
	"time"
)

// Task represents a unit of work
type Task struct {
	ID       int
	Duration time.Duration
}

// Result represents the outcome of a processed task
type Result struct {
	TaskID int
	Value  string
}

// worker processes incoming tasks from the jobs channel and sends results
func worker(id int, jobs <-chan Task, results chan<- Result) {
	for job := range jobs {
		fmt.Printf("Worker %d: Started processing job %d\n", id, job.ID)
		time.Sleep(job.Duration) // Simulate CPU/IO delay
		
		results <- Result{
			TaskID: job.ID,
			Value:  fmt.Sprintf("Processed by worker %d in %v", id, job.Duration),
		}
	}
}

func main() {
	// Create tasks
	tasks := []Task{
		{ID: 101, Duration: 200 * time.Millisecond},
		{ID: 102, Duration: 400 * time.Millisecond},
		{ID: 103, Duration: 100 * time.Millisecond},
		{ID: 104, Duration: 300 * time.Millisecond},
	}

	// Buffered channels prevent sender blocking
	jobs := make(chan Task, len(tasks))
	results := make(chan Result, len(tasks))

	// Spawn 3 concurrent worker goroutines
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}

	// Feed tasks into the job channel
	for _, task := range tasks {
		jobs <- task
	}
	close(jobs) // Closing tells workers no more jobs are coming

	// Collect and aggregate results
	for i := 0; i < len(tasks); i++ {
		res := <-results
		fmt.Printf("[RESULT] Job %d: %s\n", res.TaskID, res.Value)
	}
	close(results)
}

6. Mimari Karşılaştırması: İşletim Sistemi İş Parçacıkları ve Goroutinler

Metrik / Özellik Geleneksel İşletim Sistemi Konuları (1:1 Model) Goroutine’lere Git (M:N Modeli)
Başlangıç ​​Belleği Sabit (genellikle 1 MB - 8 MB) Dinamik (~2KB’den başlar)
Bağlam Değiştirme Süresi Yavaş (1.000 - 2.000 nanosaniye) Hızlı (10 - 100 nanosaniye)
Alan Değiştirme Çekirdek alanı (yoğun içerik anahtarı) Kullanıcı alanı (çalışma zamanı planlayıcısı)
Oluşturma Maliyeti Pahalı (işletim sistemi sistem çağrılarını içerir) Son derece ucuz (basit tahsis)
İletişim Paylaşılan hafıza (Muteksler, Semafor) Kanallar (CSP mesaj geçişi)
Kilitlenme Riski Yüksek (manuel olarak takip edilmesi zor) Kanal tasarımı ve derleme çalışma zamanı tespiti ile azaltıldı

Çözüm

Go, eşzamanlılığı işletim sisteminin ham iş parçacığı modelinden ayırarak, modern arka uç mimarilerinin temel ölçeklendirme sorunlarını çözdü.

Goroutinler, minimum bellek yüküyle büyük eşzamanlılığa olanak tanır, M:N zamanlayıcı, çekirdek bağlamı değiştirme maliyetleri olmadan iş çalma yoluyla CPU kullanımını optimize eder ve kanallar güvenli, etkileyici bir eşzamanlılık paradigması sağlar.

İster mikro hizmetler ister büyük dağıtılmış sistemler oluşturuyor olun, Go’nun yerleşik eşzamanlılık mimarisi, uygulamanızın duyarlı, kaynak açısından verimli ve bakımı kolay kalmasını sağlar.


Ghaznix Blogunda daha fazla yazılım geliştirme ve arka uç mühendisliği öngörülerini keşfedin →