Skip to content

cond

sync.Cond Go'nun standart kütüphane koşul değişkenidir ve manuel başlatma gerektiren tek senkronizasyon aracıdır. Diğer senkronizasyon ilkelilerinden farklı olarak, sync.Cond paylaşılan kaynaklara erişimi korumak için bir mutex kilidi (sync.Mutex) geçirilmesini gerektirir. Goroutine'lerin belirli bir koşul sağlanana kadar bekleme durumuna girmesine ve koşul sağlandığında uyandırılmasına olanak tanır.

Örnek Kod

go
package main

import (
    "fmt"
    "sync"
    "time"
)

var i = 0

func main() {
    var mu sync.Mutex
    var wg sync.WaitGroup

    // Create a condition variable, passing in the mutex
    cd := sync.NewCond(&mu)

    // Add 4 goroutines to be processed
    wg.Add(4)

    // Create 3 goroutines, each waiting for the condition to be met
   	for j := range 3 {
		go func() {
			defer wg.Done()

			mu.Lock()
			for i <= 100 {
                 // When condition is not met, goroutine blocks here
				cd.Wait()
			}
			fmt.Printf("%d wake up\n", j)
			mu.Unlock()
		}()
	}

    // Create a goroutine to update the condition and wake up other goroutines
    go func() {
        defer wg.Done()
        for {
            mu.Lock()
            i++ // Update shared variable
            mu.Unlock()
            if i > 100 {
                cd.Broadcast() // Wake up all waiting goroutines when condition is met
                break
            }
            time.Sleep(time.Millisecond * 10) // Simulate workload
        }
    }()

    // Wait for all goroutines to complete
    wg.Wait()
}

Yukarıdaki örnekte, paylaşılan değişken i çoklu goroutine'ler tarafından eşzamanlı olarak erişilir ve değiştirilir. Mutex kilidi mu aracılığıyla, i'ye erişimin eşzamanlı koşullar altında güvenli olduğu sağlanır. Sonra, sync.NewCond(&mu) aracılığıyla, bir koşul değişkeni cd oluşturulur, bu beklerken paylaşılan kaynaklara erişimin senkronize edilmesini sağlamak için mu kilidine bağlıdır.

  • Üç bekleyen goroutine: Her goroutine cd.Wait() aracılığıyla kendini engeller, koşul sağlanana kadar (i > 100). Bu goroutine'ler paylaşılan kaynak i'nin değeri güncellenene kadar engellenmiş kalır.
  • Koşulu güncelleyen ve diğerlerini uyandıran bir goroutine: Koşul sağlandığında (yani i > 100), bu goroutine cd.Broadcast() aracılığıyla tüm bekleyen goroutine'leri uyandırır, onların yürütmeye devam etmesini sağlar.

Yapı

go
type Cond struct {
	// L is held while observing or changing the condition
	L Locker

	notify  notifyList
}

type notifyList struct {
	// wait is the ticket number of the next waiter. It is atomically
	// incremented outside the lock.
	wait atomic.Uint32

	notify uint32

	// List of parked waiters.
	lock mutex
	head *sudog
	tail *sudog
}

Yapısı karmaşık değildir:

  • L, mutex kilidi, burada tip Locker arayüzüdür, somut bir kilit tipi değil
  • notify, bekleyen goroutine'ler için bildirim bağlı listesi

Daha önemli kısım runtime.notifyList yapısıdır:

  • wait, atomik değer, kaç tane bekleyen goroutine olduğunu kaydeder
  • notify, uyandırılacak bir sonraki goroutine'e işaret eder, 0'dan başlar ve artar
  • lock, mutex kilidi, geçirdiğimiz kilit değil, runtime tarafından dahili olarak uygulanan bir kilit
  • head, tail, bağlı liste işaretçileri

Sadece üç metodu vardır:

  • Wait, engelle ve bekle
  • Signal, bir bekleyen goroutine'i uyandır
  • Broadcast, tüm bekleyen goroutine'leri uyandır

Uygulamasının çoğu runtime kütüphanesi altında gizlenmiştir. Bu uygulamalar runtime/sema.go dosyasında bulunur, bu yüzden standart kütüphanedeki kodu çok kısadır. Temel prensibi kilitli engelleme kuyruğudur.

Wait

Wait metodu goroutine'in kendisinin uyanana kadar engelleme beklemeye girmesine neden olur.

go
func (c *Cond) Wait() {
    t := runtime_notifyListAdd(&c.notify)
    c.L.Unlock()
    runtime_notifyListWait(&c.notify, t)
    c.L.Lock()
}

İlk olarak kendini notifyList'e ekler, ancak aslında sadece notifyList.wait'i bir artırır. Buradaki işlem len(notifyList)-1'e eşdeğerdir, son elemanın indeksini alır.

go
func notifyListAdd(l *notifyList) uint32 {
	return l.wait.Add(1) - 1
}

Gerçek ekleme işlemi notifyListWait fonksiyonunda tamamlanır:

go
func notifyListWait(l *notifyList, t uint32) {
	...
}

Bu fonksiyonda, ilk olarak bağlı listeyi kilitler, sonra mevcut goroutine'in zaten uyanıp uyanmadığını hızlıca kontrol eder. Eğer zaten uyandırılmışsa, doğrudan döner, engelleme ve bekleme yapmaz.

go
lockWithRank(&l.lock, lockRankNotifyList)
// Return right away if this ticket has already been notified.
if less(t, l.notify) {
	unlock(&l.lock)
	return
}

Eğer uyandırılmamışsa, kuyruğa katılmak için bir sudog oluşturur, sonra gopark aracılığıyla askıya alır.

go
s := acquireSudog()
s.g = getg()
s.ticket = t
s.releasetime = 0
if l.tail == nil {
	l.head = s
} else {
	l.tail.next = s
}
l.tail = s
goparkunlock(&l.lock, waitReasonSyncCondWait, traceBlockCondWait, 3)

Uyandırıldıktan sonra, sudog yapısını serbest bırakır:

go
releaseSudog(s)

Signal

Signal engellenmiş goroutine'leri FIFO kuyruk sırasına göre uyandırır.

go
func (c *Cond) Signal() {
	runtime_notifyListNotifyOne(&c.notify)
}

Akışı şöyledir:

  1. Kilitlemeden l.wait'in l.notify'a eşit olup olmadığını kontrol eder. Eğer eşitse, tüm goroutine'lerin uyandırıldığı anlamına gelir.

    go
    if l.wait.Load() == atomic.Load(&l.notify) {
    	return
    }
  2. Kilitledikten sonra, tekrar tümünün uyanıp uyanmadığını kontrol eder.

    go
    lockWithRank(&l.lock, lockRankNotifyList)
    t := l.notify
    if t == l.wait.Load() {
    	unlock(&l.lock)
    	return
    }
  3. l.notify'ı bir artırır.

    go
    atomic.Store(&l.notify, t+1)
  4. Bağlı listeyi dolaşır, uyandırılacak goroutine'i bulur ve son olarak runtime.goready aracılığıyla goroutine'i uyandırır.

    go
    for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
    	if s.ticket == t {
    		n := s.next
    		if p != nil {
    			p.next = n
    		} else {
    			l.head = n
    		}
    		if n == nil {
    			l.tail = p
    		}
    		unlock(&l.lock)
    		s.next = nil
    		readyWithTime(s, 4)
    		return
    	}
    }
    unlock(&l.lock)

Broadcast

Broadcast tüm engellenmiş goroutine'leri uyandırır.

go
func (c *Cond) Broadcast() {
    runtime_notifyListNotifyAll(&c.notify)
}

Akışı temelde aynıdır:

  1. Kilitsiz kontrol, tümü uyandırılmış mı.

    go
    // Fast-path: if there are no new waiters since the last notification
    // we don't need to acquire the lock.
    if l.wait.Load() == atomic.Load(&l.notify) {
    	return
    }
  2. Kilitledikten sonra, bağlı listeyi temizler, sonra kilidi serbest bırakır. Sonraki yeni gelen goroutine'ler liste başına eklenecektir.

    go
    lockWithRank(&l.lock, lockRankNotifyList)
    s := l.head
    l.head = nil
    l.tail = nil
    atomic.Store(&l.notify, l.wait.Load())
    unlock(&l.lock)
  3. Bağlı listeyi dolaşır, tüm goroutine'leri uyandırır.

    go
    for s != nil {
    	next := s.next
    	s.next = nil
    	readyWithTime(s, 4)
    	s = next
    }

Özet

sync.Cond'un en yaygın kullanım durumları, çoklu goroutine'ler arasında belirli koşulların senkronize edilmesini gerektiren senaryolardır, tipik olarak üretici-tüketici modelleri, görev zamanlaması ve diğer senaryolara uygulanır. Bu senaryolarda, çoklu goroutine'ler yürütmeye devam etmeden önce belirli koşulların sağlanmasını beklemelidir veya koşullar değiştiğinde çoklu goroutine'leri bildirmelidir. Goroutine'ler arasındaki senkronizasyonu yönetmek için esnek ve verimli bir yol sağlar. Mutex kilitleriyle çalışarak, sync.Cond paylaşılan kaynaklara güvenli erişimi sağlayabilir ve belirli koşullar sağlandığında goroutine'lerin yürütme sırasını kontrol edebilir. İç uygulama prensiplerini anlamak, özellikle karmaşık koşul senkronizasyonu ile uğraşırken, eşzamanlı programlama tekniklerini daha iyi kavramamıza yardımcı olur.

Golang by www.golangdev.cn edit