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