Go sync 패키지 탐구: 동시성 조정을 위한 Cond 심층 분석
Grace Collins
Solutions Engineer · Leapcell

Go의 sync
패키지는 Mutex
, RWMutex
, WaitGroup
, Once
와 같은 근본적인 빌딩 블록을 제공하며 Go 언어의 동시성 프로그래밍의 초석입니다. 이들 중에서 sync.Cond
는 특정 조건이 참이 될 때까지 기다려야 하는 고루틴을 조정하는 강력한 기본 요소로 두드러집니다. 이 기사는 sync.Cond
를 깊이 파고들어 작동 방식, 뮤텍스와의 관계, 그리고 실제 예제를 통한 사용법을 보여줍니다.
조건 변수에 대한 소개
Go의 sync.Cond
에서 제공하는 조건 변수는 그 자체로 불리언 플래그나 카운터가 아닙니다. 대신, 고루틴이 조건이 충족될 때까지 대기하고 다른 고루틴이 조건이 변경되었을 수 있음을 신호할 수 있도록 하는 메커니즘입니다. sync.Cond
는 항상 sync.Locker
(일반적으로 sync.Mutex
또는 sync.RWMutex
)와 함께 사용된다는 것을 이해하는 것이 중요합니다. 락커는 조건 변수가 모니터링하는 공유 상태를 보호합니다.
핵심 아이디어는 다음과 같습니다:
- 고루틴이 진행하기를 원하지만 조건이 충족되지 않았습니다. 해당 뮤텍스를 획득하고, 조건을 확인하고, 거짓이면
Cond.Wait()
를 호출합니다. Cond.Wait()
는 세 가지 중요한 작업을 원자적으로 수행합니다: a. 해당 뮤텍스를 해제합니다. b. 고루틴을 일시 중지하고 대기열에 추가합니다. c. 신호를 받으면 반환하기 전에 해당 뮤텍스를 재획득합니다.- 다른 고루틴이 공유 상태를 변경하여 잠재적으로 조건을 만족합니다. 해당 뮤텍스를 획득하고, 공유 상태를 수정하고,
Cond.Signal()
또는Cond.Broadcast()
를 호출하여 대기 중인 고루틴에 알립니다.
sync.Cond
의 구조
sync.Cond
구조체와 주요 메서드를 살펴보겠습니다:
type Cond struct { noCopy noCopy // ensures Cond is not copied L NoCopyLocker // the locker associated with c. // contains filtered or unexported fields }
L sync.Locker
:Cond
가 바인딩된 뮤텍스(또는sync.RWMutex
)입니다.Wait
가 호출될 때, 그리고 공유 조건이 확인되거나 수정될 때 반드시 보유해야 합니다.
주요 메서드
-
func NewCond(l Locker) *Cond
: 제공된Locker
와 연결된 새Cond
변수를 생성하고 반환합니다. -
func (c *Cond) Wait()
:c.L
를 잠근 상태에서 호출해야 합니다.- 원자적으로
c.L
를 해제하고, 호출하는 고루틴을 일시 중지하고, 신호를 받고 고루틴이 깨어날 때c.L
를 다시 잠급니다. - 중요한 것은
Wait
가 반환될 때 조건이 여전히 거짓일 수 있다는 것입니다. 이를 허위 웨이크업(spurious wakeup)이라고 합니다. 따라서Wait
는 항상 조건을 재확인하는 루프 내에서 호출해야 합니다.
-
func (c *Cond) Signal()
:c
에서 대기 중인 고루틴 하나를 최대치로 깨웁니다.- 대기 중인 고루틴이 없으면 아무 작업도 하지 않습니다.
- 호출자가
c.L
를 잠글 필요는 없지만, 공유 상태(신호가 필요했던)가 방금 수정되었을 때 종종c.L
가 잠겨 있을 때 호출됩니다.
-
func (c *Cond) Broadcast()
:c
에서 대기 중인 모든 고루틴을 깨웁니다.- 대기 중인 고루틴이 없으면 아무 작업도 하지 않습니다.
Signal
과 마찬가지로, 호출자가c.L
를 잠글 필요는 없습니다.
왜 Cond
와 Mutex
인가? 어떤 시너지가 있는가?
Mutex
는 상호 배제를 제공하여 한 번에 하나의 고루틴만 공유 데이터에 접근할 수 있도록 하여 데이터 수정 중 경쟁 상태를 방지합니다. 그러나 Mutex
만으로는 고루틴이 바쁜 대기(CPU 사이클을 소비하는 루프에서 계속 뮤텍스를 획득/해제) 없이 효율적으로 특정 조건이 참이 될 때까지 기다릴 수 있는 방법을 제공하지 않습니다.
여기서 Cond
가 등장합니다. 조건 문제를 해결합니다:
Mutex
는 공유 상태를 보호합니다. 조건에 의존하는 상태를 읽거나 수정할 때 뮤텍스를 보유합니다.Cond
는 대기/알림을 처리합니다. 고루틴이 상태 변경을 기다릴 필요가 있을 때Cond.Wait()
를 사용합니다. 고루틴이 다른 고루틴을 차단 해제할 수 있는 방식으로 상태를 변경할 때Cond.Signal()
또는Cond.Broadcast()
를 사용합니다.
이렇게 생각해보세요. Mutex
는 회의실에 대한 접근을 보호합니다. Cond
는 대기 공간에 있는 초인종으로, 소파에 있는 사람들에게 의제의 회의가 시작될 수 있음을 알립니다.
실제 예제 1: 프로듀서-컨슈머 문제
조건 변수의 고전적인 사용 사례는 프로듀서가 버퍼에 항목을 추가하고 컨슈머가 항목을 제거하는 프로듀서-컨슈머 문제입니다. 버퍼가 가득 차면 프로듀서는 기다려야 합니다. 버퍼가 비어 있으면 컨슈머는 기다려야 합니다.
package main import ( "fmt" "sync" "time" "math/rand" ) const ( bufferCapacity = 5 numProducers = 2 numConsumers = 3 itemsPerProducer = 10 ) // 공유 상태 var ( buffer []int cond *sync.Cond mu sync.Mutex itemCount int ) func producer(id int) { for i := 0; i < itemsPerProducer; i++ { // 버퍼 확인/수정 전에 뮤텍스 획득 cond.L.Lock() // cond.L이 mu이므로 mu.Lock()과 동일 // 버퍼가 가득 차면 대기 for len(buffer) == bufferCapacity { fmt.Printf("Producer %d: Buffer full, waiting...\n", id) cond.Wait() // mu 해제, 대기, mu 재획득 } // 항목 생산 item := rand.Intn(100) buffer = append(buffer, item) itemCount++ fmt.Printf("Producer %d: Produced item %d. Buffer: %v\n", id, item, buffer) // 소비자가 항목이 사용 가능하다는 것을 알림 cond.Signal() // 잠재적으로 소비자 하나를 깨움 // cond.Broadcast() // 대기 중인 모든 소비자를 깨우면 (여기서는 덜 효율적) cond.L.Unlock() // 뮤텍스 해제 time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) // 작업 시뮬레이션 } fmt.Printf("Producer %d finished.\n", id) } func consumer(id int) { for { cond.L.Lock() // 뮤텍스 획득 // 버퍼가 비어 있으면 대기 for len(buffer) == 0 { if itemCount >= numProducers*itemsPerProducer && len(buffer) == 0 { fmt.Printf("Consumer %d: No more items expected, exiting.\n", id) cond.L.Unlock() return // 모든 항목이 생산되고 소비됨 } fmt.Printf("Consumer %d: Buffer empty, waiting...\n", id) cond.Wait() // mu 해제, 대기, mu 재획득 } // 항목 소비 item := buffer[0] buffer = buffer[1:] fmt.Printf("Consumer %d: Consumed item %d. Buffer: %v\n", id, item, buffer) // 프로듀서에게 공간이 사용 가능함을 알림 cond.Signal() // 잠재적으로 대기 중인 프로듀서 하나를 깨움 cond.L.Unlock() // 뮤텍스 해제 time.Sleep(time.Duration(rand.Intn(300)) * time.Millisecond) // 작업 시뮬레이션 } } func main() { rand.Seed(time.Now().UnixNano()) cond = sync.NewCond(&mu) // cond를 뮤텍스와 연관시킴 fmt.Println("Starting producer-consumer simulation...") go func() { // 프로듀서 시작 for i := 0; i < numProducers; i++ { go producer(i + 1) } }() go func() { // 컨슈머 시작 for i := 0; i < numConsumers; i++ { go consumer(i + 1) } }() // 작동이 완료될 충분한 시간을 기다립니다. // 실제 애플리케이션에서는 정상적인 종료를 위해 WaitGroup이나 채널을 사용할 수 있습니다. time.Sleep(5 * time.Second) fmt.Println("\nSimulation finished.") }
프로듀서-컨슈머 예제 설명:
buffer
와mu
(뮤텍스)는 공유 리소스입니다.itemCount
는 컨슈머가 종료 시점을 알도록 돕습니다.cond = sync.NewCond(&mu)
는 조건 변수를 뮤텍스와 바인딩합니다.- 프로듀서 로직:
mu
를 잠급니다(cond.L.Lock()
사용).for len(buffer) == bufferCapacity
루프에 들어갑니다. 이것이 중요한 재확인 루프입니다. 버퍼가 가득 차 있으면cond.Wait()
를 호출합니다.Wait
는mu
를 해제하고, 고루틴을 일시 중지하고, 깨어날 때mu
를 다시 잠급니다. 깨어날 때 조건을 다시 평가합니다.- 버퍼가 가득 차 있지 않으면 항목을 추가하고
itemCount
를 증가시킵니다. cond.Signal()
을 호출하여 대기 중인 컨슈머 하나에게 항목이 사용 가능함을 알립니다.- 마지막으로
mu.Unlock()
를 호출합니다.
- 컨슈머 로직:
- 유사한 구조:
mu
를 잠급니다. for len(buffer) == 0
루프에 들어갑니다. 버퍼가 비어 있으면cond.Wait()
를 호출합니다.- 모든 항목이 생산되고 소비되었는지 확인하여 정상적인 종료를 허용하는 추가 검사를 포함합니다.
- 항목이 사용 가능하면 소비합니다.
cond.Signal()
을 호출하여 대기 중인 프로듀서 하나에게 공간이 사용 가능함을 알립니다.mu
를 잠금 해제합니다.
- 유사한 구조:
이 예제는 조건이 충족되지 않았을 때 Cond.Wait()
가 CPU를 효율적으로 양보하고, 조건이 변경되었을 때 Cond.Signal()
이 대기 중인 고루틴을 효율적으로 재개하는 방법을 명확하게 보여줍니다.
실제 예제 2: 순차 실행 (간단한 배리어)
때로는 고루틴이 모두 진행되기 전에 특정 수의 태스크가 완료되거나 특정 상태에 도달할 때까지 기다리도록 해야 합니다. 이는 간단한 배리어와 유사합니다.
package main import ( "fmt" "sync" "time" ) const numWorkers = 5 var ( mu sync.Mutex cond *sync.Cond readyCount int // 진행 준비가 된 작업자 수 allReady bool ) func worker(id int) { fmt.Printf("Worker %d: Initializing...\n", id) time.Sleep(time.Duration(id*100) * time.Millisecond) // 준비 작업 시뮬레이션 cond.L.Lock() // 공유 상태(readyCount, allReady) 수정을 위해 잠금 readyCount++ fmt.Printf("Worker %d: Ready. Total ready: %d\n", id, readyCount) // 이 작업자가 마지막으로 준비된 경우, 모두에게 신호 if readyCount == numWorkers { allReady = true fmt.Printf("Worker %d: All workers are ready! Signaling everyone.\n", id) cond.Broadcast() // 대기 중인 모든 작업자 깨우기 } else { // 그렇지 않으면 다른 모든 작업자가 준비될 때까지 대기 for !allReady { fmt.Printf("Worker %d: Waiting for others to be ready...\n", id) cond.Wait() // mu 해제, 대기, mu 재획득 } } cond.L.Unlock() // 잠금 해제 fmt.Printf("Worker %d: Proceeding with synchronized task!\n", id) // 동기화된 작업 시뮬레이션 time.Sleep(time.Duration(100) * time.Millisecond) fmt.Printf("Worker %d: Synchronized task completed.\n", id) } func main() { cond = sync.NewCond(&mu) var wg sync.WaitGroup fmt.Println("Starting workers...") for i := 0; i < numWorkers; i++ { wg.Add(1) go func(id int) { defer wg.Done() worker(id) }(i + 1) } wg.Wait() // 모든 작업자가 작업을 완료할 때까지 기다립니다. fmt.Println("All workers finished. Exiting.") }
순차 실행 예제 설명:
readyCount
는 동기화 지점에 도달한 작업자의 수를 추적합니다.allReady
는 모든 작업자가 조건(모두 준비됨)을 충족했음을 나타내는 불리언 플래그입니다.- 각
worker
고루틴은- 예비 작업을 수행합니다.
- 뮤텍스를 획득합니다(
cond.L.Lock()
). readyCount
를 증가시킵니다.- 중요한 로직:
- 마지막 작업자가 준비되면(
readyCount == numWorkers
),allReady = true
로 설정하고cond.Broadcast()
를 호출합니다. 이는cond.Wait()
를 호출 중인 다른 모든 작업자를 깨웁니다. - 마지막 작업자가 아니면
for !allReady
루프에 들어가cond.Wait()
를 호출합니다. 마지막 작업자가allReady
를 참으로 만들고 신호할 때까지 기다립니다.
- 마지막 작업자가 준비되면(
cond.Wait()
반환 후(및 뮤텍스 재획득 후) 또는 마지막 작업자로서 브로드캐스트한 경우, 뮤텍스를 해제하고 동기화된 작업으로 진행합니다.
이는 단일 트리거 이벤트에 대해 여러 고루틴을 동시에 해제해야 하는 시나리오에 Broadcast
를 보여줍니다.
중요 고려 사항 및 모범 사례
- 항상 루프 내에서
Wait
를 사용하세요: 언급했듯이Wait
는 허위 웨이크업(신호 또는 브로드캐스트 없이 깨어나는 것)을 경험할 수 있습니다. 조건 확인(for !condition { cond.Wait() }
)은 이 문제를 처리하고 상태를 재평가하는 데 필수적입니다. Wait
호출 시 뮤텍스를 보유하세요:cond.Wait()
는Cond
의 연관된Locker
가 호출자에 의해 보유될 것으로 예상합니다. 자동으로 해제하고 다시 획득합니다.- 조건 확인/수정 시 뮤텍스를 보유하세요: 조건에 의존하는 공유 상태의 모든 읽기 또는 쓰기는
Cond
의 연관된Locker
로 보호되어야 합니다. Signal
대Broadcast
:- **
Signal()
**을 사용하여 하나의 고루틴만 진행하거나 상태 변경의 이점을 얻을 수 있는 경우(예: 버퍼에 항목 하나가 사용 가능하므로 하나의 컨슈머만 이를 가져갈 수 있음) 사용합니다. - **
Broadcast()
**를 사용하여 모든 대기 중인 고루틴이 반응해야 하는 경우(예: 종료 신호, 모든 작업자가 중지해야 함; 또는 모든 사람에게 영향을 미치는 전역 상태 변경) 사용합니다.Broadcast
는 일반적으로 모든 고루틴이 깨어나 뮤텍스를 경쟁하고 대부분이 다시 잠드는 상황 때문에 덜 효율적입니다.
- **
Signal
/Broadcast
의 배치: 뮤텍스를 해제하기 전 또는 후에Signal
/Broadcast
를 호출할 수 있습니다.- 뮤텍스를 해제하기 전에 호출하면 깨어난
Wait
고루틴이 깨어나자마자 뮤텍스를 경쟁하게 됩니다. 뮤텍스가 즉시 사용 가능한 경우 약간 더 빠를 수 있습니다. - 뮤텍스를 해제한 후에 호출하면 깨어난 고루틴을 위해 뮤텍스가 이미 비어 있도록 합니다.
- 정확성 측면에서는 일반적으로 큰 차이가 없지만, 높은 경쟁 시나리오에서의 성능 영향을 고려하십시오. 단순성과 신호 고루틴이 다른 사람들을 깨우기 전에 중요 섹션을 완료할 수 있도록 허용하기 위해 많은 패턴은 잠금 해제 후에 신호를 배치하지만, 의존성을 해제한 상태 변경 후에 배치합니다. 제 위의 예제들은 잠금 해제 전에 호출하는 일반적인 패턴을 보여줍니다.
- 뮤텍스를 해제하기 전에 호출하면 깨어난
- 데드락 방지: 고루틴이 기다리는 경우, 궁극적으로 신호하는 다른 고루틴이 있거나 정상적인 종료 메커니즘이 있는지 확인하십시오. 모든 고루틴이 기다리고 아무도 신호하지 않는 것이 일반적인 실수입니다.
context.Context
고려: 더 복잡한 시나리오, 특히 장기 실행 작업 또는 네트워크 상호 작용의 경우context.Context
를select
문 및 채널과 통합하면sync.Cond
와 함께 시간 초과 및 취소를 처리하는 더 강력한 방법을 제공할 수 있습니다.
결론
sync.Cond
는 Go의 동시성 도구 상자에서 필수적인 도구로, 특정 조건이 충족되는지에 의존하는 고루틴 간의 효율적인 조정을 가능하게 합니다. sync.Locker
(특히 sync.Mutex
)와의 긴밀한 관계를 이해하고 루프 기반 대기 및 Signal
대 Broadcast
의 신중한 사용과 같은 모범 사례를 따르면 강력하고 성능이 뛰어난 동시성 애플리케이션을 구축할 수 있습니다. 고루틴이 실제로 필요할 때까지 잠잘 수 있도록 하여 CPU 사이클을 절약하고 Go 프로그램의 전반적인 효율성을 향상시킵니다. 더 복잡한 동시성 디자인으로 나아가면서 sync.Cond
가 제공하는 섬세한 제어는 매우 유용할 것입니다.