>  기사  >  백엔드 개발  >  Go sync.Cond, 가장 간과되는 동기화 메커니즘

Go sync.Cond, 가장 간과되는 동기화 메커니즘

DDD
DDD원래의
2024-10-30 06:43:28235검색

게시물 일부 발췌입니다. 전체 게시물은 여기에서 볼 수 있습니다: https://victoriametrics.com/blog/go-sync-cond/

이 게시물은 Go의 동시성 처리에 관한 시리즈의 일부입니다.

  • 동기화.Mutex: 일반 및 기아 모드
  • go sync.WaitGroup 및 정렬 문제
  • Sync.Pool과 그 뒤에 숨은 메커니즘
  • 가장 간과되는 동기화 메커니즘인 sync.Cond를 만나보세요(우리가 왔습니다)
  • Go sync.Map: 올바른 작업을 위한 올바른 도구
  • Go Singleflight는 DB가 아닌 코드에 녹아있습니다

Go에서 sync.Cond는 동기화 기본 요소이지만 sync.Mutex 또는 sync.WaitGroup과 같은 형제만큼 일반적으로 사용되지는 않습니다. 대부분의 프로젝트나 심지어 다른 동기화 메커니즘이 대신 사용되는 표준 라이브러리에서도 이를 거의 볼 수 없습니다.

즉, Go 엔지니어로서 sync.Cond를 사용하는 코드를 읽고 무슨 일이 일어나고 있는지 전혀 모르고 싶지 않을 것입니다. 왜냐하면 이 코드는 결국 표준 라이브러리의 일부이기 때문입니다.

따라서 이 토론은 격차를 줄이는 데 도움이 될 것이며 더 나아가 실제로 실제로 어떻게 작동하는지 더 명확하게 이해할 수 있게 될 것입니다.

sync.Cond란 무엇인가요?

이제 sync.Cond가 무엇인지 자세히 살펴보겠습니다.

고루틴이 일부 공유 데이터 변경과 같은 특정 작업이 발생할 때까지 기다려야 하는 경우 "차단"할 수 있습니다. 즉, 계속 진행하라는 명령을 받을 때까지 작업을 일시 중지한다는 의미입니다. 이를 수행하는 가장 기본적인 방법은 루프를 사용하거나 시간을 추가하는 것입니다. CPU가 바쁜 대기로 인해 미쳐가는 것을 방지하려면 절전 모드를 사용하세요.

다음과 같습니다.

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

아무 것도 변경되지 않은 경우에도 해당 루프가 여전히 백그라운드에서 실행되어 CPU 주기를 소모하므로 이는 실제로 효율적이지 않습니다.

여기서 sync.Cond가 개입하여 고루틴이 작업을 조정할 수 있는 더 나은 방법을 제공합니다. 기술적으로 좀 더 학문적인 배경을 갖고 있는 경우에는 "조건 변수"입니다.

  • 하나의 고루틴이 어떤 일이 발생하기를 기다릴 때(특정 조건이 true가 되기를 기다리는 경우) Wait()를 호출할 수 있습니다.
  • 또 다른 고루틴은 조건이 충족될 수 있다는 것을 알게 되면 Signal() 또는 Broadcast()를 호출하여 대기 중인 고루틴을 깨우고 계속 진행할 시간임을 알릴 수 있습니다.

sync.Cond가 제공하는 기본 인터페이스는 다음과 같습니다.

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond 개요

알겠습니다. 간단한 의사 예시를 확인해 보겠습니다. 이번에는 포켓몬 테마가 진행 중입니다. 특정 포켓몬을 기다리고 있다고 가정하고 포켓몬이 나타나면 다른 고루틴에 알리고 싶습니다.

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

이 예에서 하나의 고루틴은 피카츄가 나타날 때까지 기다리는 반면, 다른 고루틴(생산자)은 목록에서 무작위로 포켓몬을 선택하고 새 포켓몬이 나타날 때 소비자에게 신호를 보냅니다.

생산자가 신호를 보내면 소비자는 깨어나서 올바른 포켓몬이 나타났는지 확인합니다. 그렇다면 포켓몬을 잡게 되고, 그렇지 않으면 소비자는 다시 잠에 빠져 다음 포켓몬을 기다립니다.

문제는 신호를 보내는 생산자와 실제로 깨어나는 소비자 사이에 간격이 있다는 것입니다. 그 동안 소비자 고루틴이 1ms보다 늦게 깨어나거나(드물게) 다른 고루틴이 공유 포켓몬을 수정하기 때문에 포켓몬이 변경될 수 있습니다. 따라서 sync.Cond는 기본적으로 다음과 같이 말합니다. '야, 뭔가 달라졌어! 일어나서 확인해 보세요. 너무 늦으면 또 바뀔 수도 있어요.'

소비자가 늦게 ​​일어나면 포켓몬이 도망갈 수 있고, 고루틴은 다시 잠들게 됩니다.

"어, 채널을 이용해 포켓몬 이름이나 신호를 다른 고루틴에 보낼 수 있겠네요"

물론이죠. 실제로 채널은 일반적으로 Go에서 sync.Cond보다 선호됩니다. 그 이유는 채널이 더 간단하고 관용적이며 대부분의 개발자에게 친숙하기 때문입니다.

위의 경우 채널을 통해 쉽게 포켓몬 이름을 보내거나, 데이터를 보내지 않고 빈 구조체를 사용하여{} 신호를 보낼 수 있습니다. 하지만 우리의 문제는 채널을 통해 메시지를 전달하는 것뿐만 아니라 공유 상태를 처리하는 것에도 관한 것입니다.

우리의 예는 매우 간단하지만 여러 고루틴이 공유 포켓몬 변수에 액세스하는 경우 채널을 사용하면 어떤 일이 일어나는지 살펴보겠습니다.

  • 채널을 사용하여 포켓몬 이름을 전송하는 경우에도 공유 포켓몬 변수를 보호하기 위한 뮤텍스가 필요합니다.
  • 단지 신호를 보내기 위해 채널을 사용하는 경우에도 공유 상태에 대한 액세스를 관리하려면 뮤텍스가 필요합니다.
  • 프로듀서에서 피카츄를 확인한 다음 채널을 통해 보내는 경우에도 뮤텍스가 필요합니다. 게다가 우리는 실제로 소비자에게 속한 논리를 생산자가 떠맡는 우려 분리 원칙을 위반하게 됩니다.

즉, 여러 고루틴이 공유 데이터를 수정하는 경우 이를 보호하기 위해 뮤텍스가 여전히 필요합니다. 이러한 경우 적절한 동기화와 데이터 안전을 보장하기 위해 채널과 뮤텍스의 조합을 자주 볼 수 있습니다.

"그럼 방송신호는요?"

좋은 질문입니다! 실제로 채널을 닫으면(close(ch)) 채널을 사용하여 대기 중인 모든 고루틴에 대한 브로드캐스트 신호를 흉내낼 수 있습니다. 채널을 닫으면 해당 채널에서 수신하는 모든 고루틴이 알림을 받습니다. 하지만 닫힌 채널은 재사용할 수 없다는 점을 명심하세요. 일단 닫힌 채널은 닫힌 상태로 유지됩니다.

그런데 실제로 Go 2에서 sync.Cond를 제거하는 것에 대한 이야기가 있었습니다: Proposal: sync: Cond 유형을 제거합니다.

"그럼 sync.Cond는 뭐에 좋은가요?"

음, sync.Cond가 채널보다 더 적합한 특정 시나리오가 있습니다.

  1. 채널을 사용하면 값을 전송하여 하나의 고루틴에 신호를 보내거나 채널을 닫아 모든 고루틴에 알릴 수 있지만 둘 다 수행할 수는 없습니다. sync.Cond를 사용하면 더욱 세밀하게 제어할 수 있습니다. Signal()을 호출하여 단일 고루틴을 깨우거나 Broadcast()를 호출하여 모든 고루틴을 깨울 수 있습니다.
  2. 그리고 필요한 만큼 여러 번 Broadcast()를 호출할 수 있는데, 채널이 닫힌 후에는 수행할 수 없는 작업입니다(닫힌 채널을 닫으면 패닉이 발생합니다).
  3. 채널은 공유 데이터를 보호하는 기본 제공 방법을 제공하지 않습니다. 뮤텍스를 사용하여 별도로 관리해야 합니다. 반면에 sync.Cond는 잠금과 신호를 하나의 패키지에 결합하여 더 나은 성능을 제공하는 보다 통합된 접근 방식을 제공합니다.

"sync.Cond에 왜 Lock이 내장되어 있나요?"

이론적으로 sync.Cond와 같은 조건 변수는 신호가 작동하기 위해 잠금에 연결될 필요가 없습니다.

사용자가 조건 변수 외부에서 자신의 잠금을 관리하도록 할 수 있는데, 이는 더 많은 유연성을 제공하는 것처럼 들릴 수 있습니다. 실제로는 기술적 한계가 아니라 인간의 실수에 관한 것입니다.

수동으로 관리하면 패턴이 실제로 직관적이지 않기 때문에 쉽게 실수로 이어질 수 있습니다. Wait()를 호출하기 전에 뮤텍스를 잠금 해제한 다음 고루틴이 깨어날 때 다시 잠가야 합니다. 이 프로세스는 어색하게 느껴질 수 있으며 적시에 잠그거나 잠금 해제하는 것을 잊어버리는 등 오류가 발생하기 쉽습니다.

그런데 왜 패턴이 조금 어긋난 것 같나요?

일반적으로 cond.Wait()를 호출하는 고루틴은 다음과 같이 루프에서 일부 공유 상태를 확인해야 합니다.

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

sync.Cond에 포함된 잠금은 잠금/잠금 해제 프로세스를 처리하는 데 도움이 되어 코드를 더 깔끔하게 만들고 오류 가능성을 줄입니다. 패턴에 대해서는 곧 자세히 논의하겠습니다.

그것을 사용하는 방법?

이전 예를 자세히 살펴보면 소비자에서 일관된 패턴을 발견할 수 있습니다. 조건을 기다리기(.Wait())하기 전에 항상 뮤텍스를 잠그고 조건이 충족된 후에 잠금을 해제합니다.

또한 대기 조건을 루프 내에 래핑합니다. 다시 한 번 복습해 보겠습니다.

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}

조건.기다려()

sync.Cond에서 Wait()를 호출하면 현재 고루틴이 특정 조건이 충족될 때까지 기다리도록 지시합니다.

비하인드 스토리는 다음과 같습니다.

  1. 고루틴은 동일한 조건을 기다리고 있는 다른 고루틴 목록에 추가됩니다. 이러한 모든 고루틴은 차단됩니다. 즉, Signal() 또는 Broadcast() 호출로 "깨어나기" 전까지는 계속할 수 없습니다.
  2. 여기서 중요한 부분은 Wait()를 호출하기 전에 뮤텍스를 잠가야 한다는 것입니다. Wait()는 중요한 일을 하기 때문에 고루틴을 절전 모드로 전환하기 전에 자동으로 잠금을 해제합니다(Unlock() 호출). 이를 통해 원래 고루틴이 기다리는 동안 다른 고루틴이 잠금을 잡고 작업을 수행할 수 있습니다.
  3. 대기 중인 고루틴이 깨어났을 때(Signal() 또는 Broadcast()에 의해) 즉시 작업을 재개하지 않습니다. 먼저 잠금(Lock())을 다시 획득해야 합니다.

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond.Wait() 메소드

Wait()이 내부적으로 어떻게 작동하는지 살펴보겠습니다.

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}

간단하지만 4가지 핵심을 짚어볼 수 있습니다.

  1. Cond 인스턴스 복사를 방지하는 검사기가 있는데 그렇게 하면 패닉 상태가 됩니다.
  2. cond.Wait()를 호출하면 즉시 뮤텍스가 잠금 해제되므로 cond.Wait()를 호출하기 전에 뮤텍스를 잠가야 합니다. 그렇지 않으면 패닉이 발생합니다.
  3. 깨어난 후 cond.Wait()는 뮤텍스를 다시 잠급니다. 즉, 공유 데이터 작업을 마친 후 다시 잠금을 해제해야 합니다.
  4. sync.Cond의 기능 대부분은 알림을 위해 티켓 기반 시스템을 사용하는 informList라는 내부 데이터 구조를 사용하여 Go 런타임에서 구현됩니다.

이러한 잠금/잠금 해제 동작 때문에 sync.Cond.Wait()를 사용할 때 일반적인 실수를 피하기 위해 따라야 할 일반적인 패턴이 있습니다.

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond.Wait() 사용의 일반적인 패턴

"루프 없이 c.Wait()를 직접 사용하면 어떨까요?"


게시물 일부 발췌입니다. 전체 게시물은 여기에서 볼 수 있습니다: https://victoriametrics.com/blog/go-sync-cond/

위 내용은 Go sync.Cond, 가장 간과되는 동기화 메커니즘의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.