Skip to content

cond

sync.Cond 는 Go 표준 라이브러리의 조건 변수로, 수동으로 초기화해야 하는 유일한 동기화 도구입니다. 다른 동기화 프리미티브와 달리 sync.Cond 는 공유 리소스 액세스를 보호하기 위해 뮤텍스 (sync.Mutex) 를 전달해야 합니다. 이는 코루틴이 특정 조건이 충족되기 전까지 대기 상태에 들어가고, 조건이 충족되면 깨어날 수 있게 합니다.

예제 코드

go
package main

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

var i = 0

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

    // 조건 변수를 생성하고 뮤텍스 전달
    cd := sync.NewCond(&mu)

    // 4 개의 대기 코루틴 추가
    wg.Add(4)

    // 3 개의 코루틴 생성, 각 코루틴은 조건이 충족되기를 대기
   	for j := range 3 {
		go func() {
			defer wg.Done()

			mu.Lock()
			for i <= 100 {
                 // 조건이 충족되지 않으면 코루틴이 여기서 블록됨
				cd.Wait()
			}
			fmt.Printf("%d wake up\n", j)
			mu.Unlock()
		}()
	}

    // 조건을 업데이트하고 다른 코루틴을 깨우는 코루틴 생성
    go func() {
        defer wg.Done()
        for {
            mu.Lock()
            i++ // 공유 변수 업데이트
            mu.Unlock()
            if i > 100 {
                cd.Broadcast() // 조건이 충족되면 모든 대기 코루틴 깨움
                break
            }
            time.Sleep(time.Millisecond * 10) // 작업 부하 시뮬레이션
        }
    }()

    // 모든 코루틴 완료 대기
    wg.Wait()
}

위 예제에서 공유 변수 i 는 여러 코루틴에 의해 동시에 액세스하고 수정됩니다. 뮤텍스 mu 를 사용하여 동시성 조건에서 i 에 대한 액세스가 안전하도록 보장합니다. 그런 다음 sync.NewCond(&mu) 를 사용하여 조건 변수 cd 를 생성하며, 이는 mu 락에 의존하여 대기 시 공유 리소스 액세스가 동기화되도록 보장합니다.

  • 세 개의 대기 코루틴: 각 코루틴은 cd.Wait() 를 통해 블록되며, 조건이 충족될 때까지 (i > 100) 대기합니다. 이 코루틴들은 공유 리소스 i 의 값이 업데이트되기 전까지 블록 상태로 유지됩니다.
  • 조건을 업데이트하고 다른 코루틴을 깨우는 코루틴: 조건이 충족되면 (즉, i > 100), 이 코루틴은 cd.Broadcast() 를 사용하여 모든 대기 코루틴을 깨워 계속 실행되도록 합니다.

구조

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
}

구조는 복잡하지 않습니다:

  • L, 뮤텍스로, 여기서는 구체적인 락 유형이 아닌 Locker 인터페이스입니다.
  • notify, 대기 코루틴의 알림 링크드 리스트

가장 중요한 것은 runtime.notifyList 구조입니다.

  • wait, 원자값으로, 몇 개의 대기 코루틴이 있는지 기록합니다.
  • notify, 다음에 깨어날 코루틴을 가리키며, 0 부터 시작하여 증가합니다.
  • lock, 뮤텍스로, 우리가 전달한 락이 아닌 runtime 내부에서 구현한 락입니다.
  • head, tail, 링크드 리스트 포인터입니다.

총 세 가지 메서드가 있습니다.

  • Wait, 블록 대기
  • Signal, 대기 코루틴 하나 깨움
  • Broadcast, 모든 대기 코루틴 깨움

대부분의 구현은 runtime 라이브러리 아래에 숨겨져 있으며, 이러한 구현은 runtime/sema.go 파일에 있어 표준 라이브러리에서 코드가 매우 짧습니다. 기본 원리는 락이 추가된 블록 큐입니다.

Wait

Wait 메서드는 코루틴이 깨어날 때까지 자체적으로 블록 대기 상태에 들어갑니다.

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

먼저 자신을 notifyList 에 추가하지만, 실제로는 notifyList.wait 을 1 증가시키는 것뿐입니다. 여기 작업은 len(notifyList)-1 과 같으며, 마지막 요소의 인덱스를 얻습니다.

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

실제 추가 작업은 notifyListWait 함수에서 완료됩니다.

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

이 함수에서 먼저 링크드 리스트에 락을 건 후, 현재 코루틴이 이미 깨어났는지 빠르게 판단합니다. 이미 깨어났으면 바로 반환하여 블록 대기할 필요가 없습니다.

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

깨어나지 않았다면 sudog 를 구성하여 큐에 추가한 후 gopark 을 통해 중단합니다.

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)

깨어난 후 sudog 구조를 해제합니다.

go
releaseSudog(s)

Signal

Signal 은 큐의 선입선출 순서에 따라 블록된 코루틴을 깨웁니다.

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

플로우는 다음과 같습니다.

  1. 락 없이 직접 l.waitl.notify 와 같은지 판단합니다. 같으면 모든 코루틴이 이미 깨어난 것입니다.

    go
    if l.wait.Load() == atomic.Load(&l.notify) {
    	return
    }
  2. 락을 건 후 다시 모두 깨어났는지 판단합니다.

    go
    lockWithRank(&l.lock, lockRankNotifyList)
    t := l.notify
    if t == l.wait.Load() {
    	unlock(&l.lock)
    	return
    }
  3. l.notify 를 1 증가시킵니다.

    go
    atomic.Store(&l.notify, t+1)
  4. 링크드 리스트를 순회하며 깨어날 코루틴을 찾고, 마지막으로 runtime.goready 를 통해 코루틴을 깨웁니다.

    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 는 모든 블록된 코루틴을 깨웁니다.

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

플로우는 기본적으로 동일합니다.

  1. 락 없이 모두 깨어났는지 확인합니다.

    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. 락을 걸고 링크드 리스트를 비운 후 락을 해제합니다. 이후 새로 도착하는 코루틴은 링크드 리스트 헤드에 추가됩니다.

    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. 링크드 리스트를 순회하며 모든 코루틴을 깨웁니다.

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

요약

sync.Cond 의 가장 일반적인 사용 시나리오는 여러 코루틴 간에 특정 조건을 동기화해야 하는 경우로, 일반적으로 생산자 - 소비자 모델, 작업 스케줄링 등의 시나리오에 적용됩니다. 이러한 시나리오에서 여러 코루틴은 특정 조건이 충족되어야 계속 실행되거나, 조건이 변경될 때 여러 코루틴에 알림을 보내야 합니다. 이는 코루틴 간 동기화를 관리하는 유연하고 효율적인 방식을 제공합니다. 뮤텍스와 함께 사용하여 sync.Cond 는 공유 리소스 액세스 안전을 보장하고, 특정 조건이 충족될 때 코루틴 실행 순서를 제어할 수 있습니다. 내부 구현 원리를 이해하면 복잡한 조건 동기화를 다룰 때 특히 유용하며, 동시성 프로그래밍 기술을 더 잘 습득하는 데 도움이 됩니다.

Golang by www.golangdev.cn edit