Go 언어 동시성 동기화: 한 번에 모두 배우기
Wenhao Wang
Dev Intern · Leapcell

Go 언어 동시성 동기화: 한 번에 모두 배우기
Go 언어 프로그래밍 시스템에서 경량 스레드인 고루틴은 낮은 리소스 소비와 낮은 전환 비용이라는 상당한 장점 덕분에 동시 작업을 효율적으로 구현하는 데 강력한 지원을 제공합니다. 그러나 이러한 동시에 실행되는 고루틴을 효과적으로 제어하는 방법은 개발자가 직면해야 할 중요한 문제가 되었습니다.
Go 언어의 고루틴 동시성 제어 방법 분석
Go 언어 프로그래밍 시스템에서 경량 스레드인 고루틴은 낮은 리소스 소비와 낮은 전환 비용이라는 상당한 장점 덕분에 동시 작업을 효율적으로 구현하는 데 강력한 지원을 제공합니다. 그러나 이러한 동시에 실행되는 고루틴을 효과적으로 제어하는 방법은 개발자가 직면해야 할 중요한 문제가 되었습니다.
동시성 제어와 관련하여 잠금 메커니즘이 종종 고려되는 첫 번째 수단입니다. Go 언어에서는 뮤텍스 잠금 sync.Mutex과 읽기-쓰기 잠금 sync.RWMutex을 포함한 관련 잠금 메커니즘도 제공되며, 또한 원자적 연산 sync/atomic도 있습니다. 그러나 이러한 작업은 주로 고루틴 자체를 직접 대상으로 하기보다는 동시성 중 데이터 보안에 중점을 둔다는 점을 분명히 해야 합니다.
이 기사에서는 고루틴의 동시 동작을 제어하는 일반적인 방법을 소개하는 데 중점을 둘 것입니다. Go 언어에는 sync.WaitGroup, channel 및 Context라는 세 가지 가장 일반적으로 사용되는 방법이 있습니다.
I. sync.WaitGroup
sync.WaitGroup은 Go 언어에서 매우 실용적인 동기화 기본 요소이며, 주요 기능은 개발자가 고루틴 그룹이 실행을 완료할 때까지 기다리도록 돕는 것입니다. 일반적으로 sync.WaitGroup은 다음 시나리오에서 사용됩니다.
- 메인 함수에서 프로그램이 종료되기 전에 고루틴 그룹이 모두 실행되었는지 확인해야 하는 경우.
- 함수 내에서 여러 고루틴이 시작되고 이러한 고루틴이 모두 완료될 때까지 결과를 반환해서는 안 되는 시나리오.
- 함수가 여러 고루틴을 시작하고 모두 완료된 후 특정 작업을 실행해야 하는 경우.
- 함수가 여러 고루틴을 시작하고 모두 완료된 후 특정 리소스를 닫아야 하는 상황.
- 함수가 여러 고루틴을 시작하고 모두 완료될 때까지 루프를 종료할 수 없는 경우.
sync.WaitGroup을 사용하는 경우 구체적인 단계는 다음과 같습니다. 먼저 sync.WaitGroup 객체를 생성합니다. 그런 다음 이 객체의 Add 메서드를 사용하여 대기할 고루틴 수를 지정합니다. 이후 go 키워드를 사용하여 여러 고루틴을 시작하고 각 고루틴 내에서 sync.WaitGroup 객체의 Done 메서드를 호출하여 고루틴이 실행을 완료했음을 나타냅니다. 마지막으로 sync.WaitGroup 객체의 Wait 메서드를 호출하여 모든 고루틴이 완료될 때까지 기다립니다.
다음은 간단한 예입니다. 이 예에서는 각각 0초, 1초, 2초 동안 절전 모드로 전환되는 3개의 고루틴을 시작하고 메인 함수는 이 3개의 고루틴이 종료된 후에 종료됩니다.
package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(i int) { defer wg.Done() fmt.Printf("sub goroutine sleep: %ds\n", i) time.Sleep(time.Duration(i) * time.Second) }(i) } wg.Wait() fmt.Println("main func done") }
II. channel
Go 언어에서 channel은 개발자가 고루틴의 동시성을 더 잘 제어하는 데 도움이 되는 강력한 도구입니다. 다음은 channel을 사용하여 고루틴의 동시성을 제어하는 몇 가지 일반적인 방법입니다.
(I) 동기화를 위해 버퍼링되지 않은 채널 사용
버퍼링되지 않은 channel은 생산자-소비자 패턴을 구현하는 데 사용할 수 있습니다. 이 패턴에서 하나의 고루틴은 데이터 생성을 담당하고 다른 고루틴은 데이터 소비를 담당합니다. 생산자 고루틴이 channel에 데이터를 보내면 소비자 고루틴은 차단 상태로 들어가 데이터가 도착할 때까지 기다립니다. 이러한 방식으로 생산자와 소비자 간의 데이터 동기화를 보장할 수 있습니다.
다음은 간단한 예제 코드입니다.
package main import ( "fmt" "sync" "time" ) func producer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 10; i++ { ch <- i fmt.Println("produced", i) time.Sleep(100 * time.Millisecond) } close(ch) } func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() for i := range ch { fmt.Println("consumed", i) time.Sleep(150 * time.Millisecond) } } func main() { var wg sync.WaitGroup ch := make(chan int) wg.Add(2) go producer(ch, &wg) go consumer(ch, &wg) wg.Wait() }
이 예에서는 버퍼링되지 않은 channel을 생성하여 생산자 고루틴과 소비자 고루틴 간에 데이터를 전송합니다. 생산자 고루틴은 channel에 데이터를 보내고 소비자 고루틴은 channel에서 데이터를 수신합니다. 생산자 고루틴에서는 time.Sleep 함수를 사용하여 데이터를 생성하는 데 필요한 시간을 시뮬레이션합니다. 소비자 고루틴에서는 time.Sleep 함수를 사용하여 데이터를 소비하는 데 필요한 시간을 시뮬레이션합니다. 마지막으로 sync.WaitGroup을 사용하여 모든 고루틴이 완료될 때까지 기다립니다.
(II) 속도 제한을 위해 버퍼링된 채널 사용
버퍼링된 channel은 동시 고루틴 수를 제한하는 데 사용할 수 있습니다. 구체적인 접근 방식은 channel의 용량을 원하는 최대 동시 고루틴 수로 설정하는 것입니다. 각 고루틴을 시작하기 전에 channel에 값을 보냅니다. 고루틴이 실행을 마치면 channel에서 값을 받습니다. 이러한 방식으로 동시에 실행되는 고루틴 수가 지정된 최대 동시 수를 초과하지 않도록 할 수 있습니다.
다음은 간단한 예제 코드입니다.
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup maxConcurrency := 3 semaphore := make(chan struct{}, maxConcurrency) for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() semaphore <- struct{}{} fmt.Println("goroutine", i, "started") // do some work fmt.Println("goroutine", i, "finished") <-semaphore }() } wg.Wait() }
이 예에서는 버퍼 크기가 3인 버퍼링된 channel을 만듭니다. 그런 다음 10개의 고루틴을 시작합니다. 각 고루틴에서 빈 구조체를 channel에 보내 고루틴이 실행을 시작했음을 나타냅니다. 고루틴이 완료되면 빈 구조체를 channel에서 받아 고루틴이 실행을 완료했음을 나타냅니다. 이러한 방식으로 동시에 실행되는 고루틴 수가 3개를 초과하지 않도록 할 수 있습니다.
III. Context
Go 언어에서 Context는 고루틴의 동시성을 제어하는 중요한 수단입니다. 다음은 Context를 사용하여 고루틴의 동시성을 제어하는 몇 가지 일반적인 방법입니다.
(I) 시간 초과 제어
경우에 따라 프로그램의 장기 차단 또는 교착 상태와 같은 문제를 방지하기 위해 고루틴의 실행 시간을 제한해야 합니다. Context는 개발자가 고루틴의 실행 시간을 더 잘 제어하는 데 도움이 될 수 있습니다. 구체적인 작업은 시간 초과 기간이 있는 Context를 생성하여 고루틴에 전달하는 것입니다. 고루틴이 시간 초과 기간 내에 실행을 완료하지 못하면 Context의 Done 메서드를 사용하여 고루틴의 실행을 취소할 수 있습니다.
다음은 간단한 예제 코드입니다.
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go func() { for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } }() time.Sleep(3 * time.Second) }
이 예에서는 시간 초과 기간이 있는 Context를 만들어 고루틴에 전달합니다. 고루틴 내부에서는 select 문을 사용하여 Context의 Done 메서드를 수신 대기합니다. Context 시간이 초과되면 고루틴의 실행이 취소됩니다.
(II) 취소 작업
프로그램 실행 과정에서 특정 고루틴의 실행을 취소해야 하는 경우가 있습니다. Context는 개발자가 고루틴의 취소 작업을 더 잘 제어하는 데 도움이 될 수 있습니다. 구체적인 접근 방식은 취소 기능이 있는 Context를 생성하여 고루틴에 전달하는 것입니다. 고루틴의 실행을 취소해야 하는 경우 Context의 Cancel 메서드를 호출할 수 있습니다.
다음은 간단한 예제 코드입니다.
package main import ( "context" "fmt" "sync" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } }() time.Sleep(2 * time.Second) cancel() wg.Wait() }
이 예에서는 취소 기능이 있는 Context를 만들어 고루틴에 전달합니다. 고루틴 내부에서는 select 문을 사용하여 Context의 Done 메서드를 수신 대기합니다. Context가 취소되면 고루틴의 실행이 취소됩니다. 메인 함수에서는 time.Sleep 함수를 사용하여 프로그램 실행 과정에서 고루틴의 실행을 취소해야 하는 특정 시점을 시뮬레이션한 다음 Context의 Cancel 메서드를 호출합니다.
(III) 리소스 관리
경우에 따라 리소스 누수 또는 경합 상태와 같은 문제를 방지하기 위해 고루틴에서 사용하는 리소스를 관리해야 합니다. Context는 개발자가 고루틴에서 사용하는 리소스를 더 잘 관리하는 데 도움이 될 수 있습니다. 구체적인 작업은 리소스를 Context와 연결하고 Context를 고루틴에 전달하는 것입니다. 고루틴이 실행을 마치면 Context를 사용하여 리소스를 해제하거나 다른 리소스 관리 작업을 수행할 수 있습니다.
다음은 간단한 예제 코드입니다.
package main import ( "context" "fmt" "sync" "time" ) func worker(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go worker(ctx, &wg) time.Sleep(2 * time.Second) cancel() wg.Wait() }
이 예에서는 취소 기능이 있는 Context를 만들어 고루틴에 전달합니다. 고루틴 내부에서는 select 문을 사용하여 Context의 Done 메서드를 수신 대기합니다. Context가 취소되면 고루틴의 실행이 취소됩니다. 메인 함수에서는 time.Sleep 함수를 사용하여 프로그램 실행 과정에서 고루틴의 실행을 취소해야 하는 특정 시점을 시뮬레이션한 다음 Context의 Cancel 메서드를 호출합니다.
Leapcell: Golang 앱 호스팅을 위한 차세대 서버리스 플랫폼
마지막으로 Go 서비스를 배포하는 데 가장 적합한 플랫폼인 **Leapcell**을 추천합니다.

1. 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발합니다.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하고 요청이나 요금은 없습니다.
3. 타의 추종을 불허하는 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불합니다.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장합니다.
- 운영 오버헤드가 없어 구축에만 집중할 수 있습니다.

Leapcell 트위터: https://x.com/LeapcellHQ

