paint-brush
Go의 sync.Cond 이해: 초보자를 위한 가이드by@ivanlemeshev
1,871
1,871

Go의 sync.Cond 이해: 초보자를 위한 가이드

Ivan Lemeshev9m2024/04/28
Read on Terminal Reader

결론적으로, sync.Cond는 특정 조건에 따라 고루틴 간의 동기화 및 조정을 허용하는 Go 프로그래밍 언어의 유용한 유형입니다. 조건변수를 생성하고 관리하는 방법을 제공합니다. 조건을 기다리는 방법, 신호를 보내는 방법, 브로드캐스트하는 방법이 있습니다. `sync.Cond`를 사용하면 Go에서 더 제어되고 동기화된 동시 프로그램을 작성할 수 있습니다. sync.Cond는 Go 표준 라이브러리에서 제공하는 동기화 기본 요소 중 하나일 뿐이며 그 사용법은 동시 프로그램의 특정 요구 사항에 따라 달라집니다. 어떤 경우에는 채널이나 sync.WaitGroup과 같은 다른 동기화 기본 요소가 더 적합할 수 있습니다.
featured image - Go의 sync.Cond 이해: 초보자를 위한 가이드
Ivan Lemeshev HackerNoon profile picture
0-item

소개

sync.Cond 유형과 사용 사례, 사용 시기를 논의하고 싶습니다.

sync.Cond 은(는) 무엇인가요?

Go 프로그래밍 언어에서 sync.Cond 는 조건 변수를 나타내는 sync 패키지에 정의된 유형입니다. 조건 변수는 진행하기 전에 특정 조건이 참이 될 때까지 기다릴 수 있도록 하여 고루틴을 조정하는 데 사용되는 동기화 프리미티브입니다.


sync.Cond 유형은 조건 변수를 생성하고 관리하는 방법을 제공합니다. 여기에는 세 가지 주요 방법이 있습니다.


  1. Wait() : 이 메서드는 호출하는 고루틴이 다른 고루틴이 조건 변수에 신호를 보낼 때까지 기다리게 합니다. 고루틴이 Wait() 호출하면 연관된 잠금을 해제하고 다른 고루틴이 동일한 sync.Cond 변수에 대해 Signal() 또는 Broadcast() 호출할 때까지 실행을 일시 중지합니다.


  2. Signal() : 이 메서드는 조건 변수를 기다리고 있는 하나의 고루틴을 깨웁니다. 여러 개의 고루틴이 대기 중인 경우 그중 하나만 깨어납니다. 어떤 고루틴을 깨울지 선택하는 것은 임의적이며 보장되지 않습니다.


  3. Broadcast() : 이 메서드는 조건 변수를 기다리는 모든 고루틴을 깨웁니다. Broadcast() 가 호출되면 대기 중인 모든 고루틴이 깨어나 진행할 수 있습니다.


sync.Cond 에는 조건 변수에 대한 액세스를 동기화하기 위해 연관된 sync.Mutex 가 필요합니다.


sync.Cond 사용하면 특정 조건에 따라 고루틴 실행을 조정할 수 있으므로 Go에서 더 제어되고 동기화된 동시 프로그래밍이 가능해집니다.

일반적인 사용 사례

sync.Cond 는 고루틴이 특정 조건에 따라 서로 조정하고 통신해야 하는 시나리오에서 일반적으로 사용됩니다. sync.Cond 의 일반적인 사용 사례를 고려해 보겠습니다.

고루틴 동기화

sync.Cond 여러 고루틴의 실행을 동기화하는 데 사용할 수 있습니다. 예를 들어, 진행하기 전에 특정 조건이 충족될 때까지 기다려야 하는 다양한 고루틴이 있을 수 있습니다. 대기 중인 고루틴은 cond.Wait() 호출할 수 있고, 신호를 보내는 고루틴은 조건이 충족되면 대기 중인 고루틴을 깨우기 위해 cond.Signal() 또는 cond.Broadcast() 호출할 수 있습니다.


 package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(2) go func() { fmt.Println("Goroutine 1 is started") defer wg.Done() cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 1 is waiting for condition") cond.Wait() fmt.Println("Goroutine 1 met the condition") fmt.Println("Goroutine 1 is done") }() go func() { fmt.Println("Goroutine 2 is started") defer wg.Done() time.Sleep(5 * time.Second) // Simulating some work cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 2 is signaling condition") cond.Signal() fmt.Println("Goroutine 2 completed signaling") fmt.Println("Goroutine 2 is done") }() wg.Wait() }


이 예에는 두 개의 고루틴이 있습니다. 첫 번째 고루틴은 cond.Wait() 사용하여 조건을 기다리는 반면, 두 번째 고루틴은 cond.Signal() 사용하여 조건에 신호를 보냅니다.


프로그램이 실행되면 첫 번째 고루틴은 잠금을 획득한 다음 cond.Wait() 호출합니다. 조건이 아직 충족되지 않았기 때문에 첫 번째 고루틴은 잠금을 해제하고 실행을 일시 중지합니다.


한편, 두 번째 고루틴은 5초 동안 절전 모드로 전환하여 일부 작업을 시뮬레이션합니다. 잠금을 획득한 다음 cond.Signal() 을 호출합니다. 대기 중인 고루틴을 깨운 다음 잠금을 획득하고 실행합니다.


sync.Cond 를 사용하면 두 번째 고루틴이 조건 신호를 보낼 때까지 첫 번째 고루틴이 대기하여 두 고루틴 간의 동기화 및 조정이 가능해집니다.

생산자-소비자 문제

sync.Cond 공통 고정 크기 버퍼 또는 대기열을 공유하는 두 가지 유형의 프로세스, 생산자 및 소비자와 관련된 고전적인 동기화 문제인 생산자-소비자 문제를 해결하는 데 유용할 수 있습니다. 생산자 고루틴은 cond.Signal() 또는 cond.Broadcast() 사용하여 새로운 데이터를 사용할 수 있을 때 소비자 고루틴에 알릴 수 있습니다.


 package main import ( "fmt" "sync" "time" ) const MaxMessageChannelSize = 5 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) messageChannel := NewMessageChannel(MaxMessageChannelSize) producer := NewProducer(cond, messageChannel) consumer := NewConsumer(cond, messageChannel) wg.Add(2) go func() { defer wg.Done() for i := range 10 { producer.Produce(fmt.Sprintf("Message %d", i)) } }() go func() { defer wg.Done() for range 10 { consumer.Consume() } }() wg.Wait() } type MessageChannel struct { maxBufferSize int buffer []string } func NewMessageChannel(size int) *MessageChannel { return &MessageChannel{ maxBufferSize: size, buffer: make([]string, 0, size), } } func (mc *MessageChannel) IsEmpty() bool { return len(mc.buffer) == 0 } func (mc *MessageChannel) IsFull() bool { return len(mc.buffer) == mc.maxBufferSize } func (mc *MessageChannel) Add(message string) { mc.buffer = append(mc.buffer, message) } func (mc *MessageChannel) Get() string { message := mc.buffer[0] mc.buffer = mc.buffer[1:] return message } type Producer struct { cond *sync.Cond messageChannel *MessageChannel } func NewProducer(cond *sync.Cond, messageChannel *MessageChannel) *Producer { return &Producer{ cond: cond, messageChannel: messageChannel, } } func (p *Producer) Produce(message string) { time.Sleep(500 * time.Millisecond) // Simulating some work p.cond.L.Lock() defer p.cond.L.Unlock() for p.messageChannel.IsFull() { fmt.Println("Producer is waiting because the message channel is full") p.cond.Wait() } p.messageChannel.Add(message) fmt.Println("Producer produced the message:", message) p.cond.Signal() } type Consumer struct { id int cond *sync.Cond messageChannel *MessageChannel } func NewConsumer(cond *sync.Cond, messageChannel *MessageChannel) *Consumer { return &Consumer{ cond: cond, messageChannel: messageChannel, } } func (c *Consumer) Consume() { time.Sleep(1 * time.Second) // Simulating some work c.cond.L.Lock() defer c.cond.L.Unlock() for c.messageChannel.IsEmpty() { fmt.Println("Consumer is waiting because the message channel is empty") c.cond.Wait() } message := c.messageChannel.Get() fmt.Println("Consumer consumed the message:", message) c.cond.Signal() }


이 예에는 메시지를 생성하여 메시지 채널에 추가하는 생산자 고루틴과 메시지를 소비하는 소비자 고루틴이 있습니다. 메시지 채널에는 MaxMessageChannelSize 로 정의된 최대 크기가 있습니다.


생산자 고루틴은 메시지 채널에 메시지를 추가하고 cond.Signal() 사용하여 새 데이터를 사용할 수 있을 때 소비자 고루틴에 알립니다. 메시지 채널이 가득 차면 생산자 고루틴은 소비자가 일부 데이터를 소비하고 메시지 채널의 공간을 확보할 때까지 cond.Wait() 사용하여 기다립니다.


마찬가지로 소비자 고루틴은 메시지 채널의 메시지를 소비하고 cond.Signal() 사용하여 메시지 채널에 공간을 사용할 수 있게 되면 생산자 고루틴에 알립니다. 비어 있으면 소비자 고루틴은 생산자가 일부 데이터를 생성하여 메시지 채널에 추가할 때까지 cond.Wait() 사용하여 기다립니다.


여기에서 sync.Cond 생산자와 소비자 고루틴 간의 조정과 동기화를 허용합니다. 메시지 채널이 비어 있을 때 소비자가 기다리고, 가득 차면 생산자가 기다리도록 하여 생산자-소비자 문제를 해결합니다.

리소스 동기화

여러 고루틴이 공유 리소스에 대한 독점적인 액세스가 필요하다고 가정해 보겠습니다. sync.Cond 사용하여 액세스를 조정할 수 있습니다. 예를 들어 작업자 고루틴 풀은 처리를 시작하기 전에 특정 수의 리소스를 사용할 수 있을 때까지 기다려야 할 수 있습니다. 고루틴은 cond.Wait() 을 사용하여 조건 변수를 기다릴 수 있고 cond.Signal() 또는 cond.Broadcast() 사용하여 리소스 해제에 대해 알릴 수 있습니다.


 package main import ( "fmt" "sync" "time" ) const MaxResources = 3 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) resourceProvider := NewResourceProvider(cond, MaxResources) wg.Add(10) for i := range 10 { go func(workerID int) { defer wg.Done() worker := NewWorker(workerID, cond, resourceProvider) worker.Run() }(i) } wg.Wait() } type ResourceProvider struct { maxResources int availableResources int cond *sync.Cond } func NewResourceProvider(cond *sync.Cond, maxResources int) *ResourceProvider { return &ResourceProvider{ cond: cond, availableResources: maxResources, } } func (rp *ResourceProvider) AvailableResources() int { return rp.availableResources } func (rp *ResourceProvider) AcquireResoirce() { rp.availableResources-- } func (rp *ResourceProvider) ReleaseResource() { rp.availableResources++ } type Worker struct { id int cond *sync.Cond rp *ResourceProvider } func NewWorker(workerID int, cond *sync.Cond, rp *ResourceProvider) *Worker { return &Worker{ id: workerID, cond: cond, rp: rp, } } func (w *Worker) Run() { w.cond.L.Lock() for w.rp.AvailableResources() == 0 { fmt.Printf("Worker %d is waiting for resources\n", w.id) w.cond.Wait() } w.rp.AcquireResoirce() fmt.Printf("Worker %d acquired resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.L.Unlock() time.Sleep(1 * time.Second) // Simulating work w.cond.L.Lock() defer w.cond.L.Unlock() w.rp.ReleaseResource() fmt.Printf("Worker %d released resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.Signal() }


이 예에는 제한된 리소스에 독점적으로 액세스해야 하는 여러 작업자 고루틴이 있습니다. 작업자 고루틴은 다른 작업자와 조정하기 위해 cond.Signal() 사용하여 리소스를 획득하고 해제합니다. 사용 가능한 리소스가 없으면 작업자 고루틴은 다른 고루틴이 리소스를 해제할 때까지 using cond.Wait() 기다립니다.


이 예에서 sync.Cond 작업자 고루틴 간의 동기화 및 조정을 허용하여 사용 가능한 리소스가 없을 때 작업자 고루틴이 대기하도록 보장함으로써 리소스 액세스를 효과적으로 동기화합니다.

이벤트 알림

sync.Cond 시스템의 특정 이벤트나 변경 사항에 대해 고루틴에 알리는 데 사용할 수 있습니다. 예를 들어, 특정 이벤트를 기다리는 고루틴을 가질 수 있습니다. 이벤트가 발생하면 신호 고루틴은 cond.Signal() 또는 cond.Broadcast() 사용하여 대기 중인 고루틴을 깨우고 이벤트를 처리하도록 허용할 수 있습니다.


 package main import ( "fmt" "sync" "time" ) const maxWorkersCount = 10 func main() { var counter int32 var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(maxWorkersCount) for i := range maxWorkersCount { go func(workerID int) { defer wg.Done() fmt.Printf("Worker %d performing work\n", workerID) time.Sleep(1 * time.Second) // Simulate work cond.L.Lock() defer cond.L.Unlock() counter++ if counter == maxWorkersCount { fmt.Println("All workers have reached the barrier") cond.Broadcast() } else { fmt.Printf("Worker %d is waiting at the barrier\n", workerID) cond.Wait() } fmt.Printf("Worker %d passed the barrier\n", workerID) }(i) } wg.Wait() }


여기에는 작업을 수행하고 장벽 지점에서 동기화하는 여러 작업자 고루틴이 있습니다. 작업자 고루틴은 카운터를 증가시킨 다음 장벽에서 기다리거나 장벽에 도달한 작업자 수에 따라 cond.Wait()cond.Broadcast() 사용하여 장벽에 신호를 보냅니다.


각 작업자 고루틴은 일부 작업을 수행한 다음 잠금을 획득하여 카운터 변수를 증가시킵니다. 현재 작업자가 장벽에 마지막으로 도달한 경우 cond.Broadcast() 사용하여 장벽 조건을 브로드캐스트하여 대기 중인 모든 작업자를 깨웁니다. 그렇지 않으면 cond.Wait() 사용하여 장벽에서 마지막 작업자의 알림을 기다립니다.


장벽 동기화는 모든 작업자 고루틴이 장벽을 넘어 진행하기 전에 장벽에 도달하도록 보장합니다. 워크플로의 특정 지점에서 여러 고루틴 실행을 동기화해야 하는 시나리오에 유용할 수 있습니다.


이 예에서는 간단한 카운터를 사용하여 장벽을 구현했습니다. 그러나 보다 복잡한 시나리오에서는 올바른 동기화를 보장하고 경합 조건을 방지하기 위해 추가 동기화 메커니즘이나 조건을 고려해야 할 수도 있습니다.

결론

결론적으로, sync.Cond 특정 조건에 따라 고루틴 간의 동기화 및 조정을 허용하는 Go 프로그래밍 언어의 유용한 유형입니다. 조건변수를 생성하고 관리하는 방법을 제공합니다. 조건을 기다리는 방법, 신호를 보내는 방법, 브로드캐스트하는 방법이 있습니다. `sync.Cond`를 사용하면 Go에서 더 제어되고 동기화된 동시 프로그램을 작성할 수 있습니다.


sync.Cond 는 Go 표준 라이브러리에서 제공하는 동기화 기본 요소 중 하나일 뿐이며 그 사용법은 동시 프로그램의 특정 요구 사항에 따라 달라집니다. 어떤 경우에는 채널이나 sync.WaitGroup 과 같은 다른 동기화 기본 요소가 더 적합할 수 있습니다.