cond
sync.Cond 는 Go 표준 라이브러리의 조건 변수로, 수동으로 초기화해야 하는 유일한 동기화 도구입니다. 다른 동기화 프리미티브와 달리 sync.Cond 는 공유 리소스 액세스를 보호하기 위해 뮤텍스 (sync.Mutex) 를 전달해야 합니다. 이는 코루틴이 특정 조건이 충족되기 전까지 대기 상태에 들어가고, 조건이 충족되면 깨어날 수 있게 합니다.
예제 코드
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()를 사용하여 모든 대기 코루틴을 깨워 계속 실행되도록 합니다.
구조
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 메서드는 코루틴이 깨어날 때까지 자체적으로 블록 대기 상태에 들어갑니다.
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 과 같으며, 마지막 요소의 인덱스를 얻습니다.
func notifyListAdd(l *notifyList) uint32 {
return l.wait.Add(1) - 1
}실제 추가 작업은 notifyListWait 함수에서 완료됩니다.
func notifyListWait(l *notifyList, t uint32) {
...
}이 함수에서 먼저 링크드 리스트에 락을 건 후, 현재 코루틴이 이미 깨어났는지 빠르게 판단합니다. 이미 깨어났으면 바로 반환하여 블록 대기할 필요가 없습니다.
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 을 통해 중단합니다.
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 구조를 해제합니다.
releaseSudog(s)Signal
Signal 은 큐의 선입선출 순서에 따라 블록된 코루틴을 깨웁니다.
func (c *Cond) Signal() {
runtime_notifyListNotifyOne(&c.notify)
}플로우는 다음과 같습니다.
락 없이 직접
l.wait가l.notify와 같은지 판단합니다. 같으면 모든 코루틴이 이미 깨어난 것입니다.goif l.wait.Load() == atomic.Load(&l.notify) { return }락을 건 후 다시 모두 깨어났는지 판단합니다.
golockWithRank(&l.lock, lockRankNotifyList) t := l.notify if t == l.wait.Load() { unlock(&l.lock) return }l.notify를 1 증가시킵니다.goatomic.Store(&l.notify, t+1)링크드 리스트를 순회하며 깨어날 코루틴을 찾고, 마지막으로
runtime.goready를 통해 코루틴을 깨웁니다.gofor 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 는 모든 블록된 코루틴을 깨웁니다.
func (c *Cond) Broadcast() {
runtime_notifyListNotifyAll(&c.notify)
}플로우는 기본적으로 동일합니다.
락 없이 모두 깨어났는지 확인합니다.
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 }락을 걸고 링크드 리스트를 비운 후 락을 해제합니다. 이후 새로 도착하는 코루틴은 링크드 리스트 헤드에 추가됩니다.
golockWithRank(&l.lock, lockRankNotifyList) s := l.head l.head = nil l.tail = nil atomic.Store(&l.notify, l.wait.Load()) unlock(&l.lock)링크드 리스트를 순회하며 모든 코루틴을 깨웁니다.
gofor s != nil { next := s.next s.next = nil readyWithTime(s, 4) s = next }
요약
sync.Cond 의 가장 일반적인 사용 시나리오는 여러 코루틴 간에 특정 조건을 동기화해야 하는 경우로, 일반적으로 생산자 - 소비자 모델, 작업 스케줄링 등의 시나리오에 적용됩니다. 이러한 시나리오에서 여러 코루틴은 특정 조건이 충족되어야 계속 실행되거나, 조건이 변경될 때 여러 코루틴에 알림을 보내야 합니다. 이는 코루틴 간 동기화를 관리하는 유연하고 효율적인 방식을 제공합니다. 뮤텍스와 함께 사용하여 sync.Cond 는 공유 리소스 액세스 안전을 보장하고, 특정 조건이 충족될 때 코루틴 실행 순서를 제어할 수 있습니다. 내부 구현 원리를 이해하면 복잡한 조건 동기화를 다룰 때 특히 유용하며, 동시성 프로그래밍 기술을 더 잘 습득하는 데 도움이 됩니다.
