효율적인 Go 동시성, select 사용
Min-jun Kim
Dev Intern · Leapcell

서문
Go 프로그래밍 언어에서 Goroutine과 채널은 동시성 프로그래밍의 필수적인 개념입니다. 이는 동시성과 관련된 다양한 문제를 해결하는 데 도움이 됩니다. 이 기사에서는 여러 채널을 조정하는 다리 역할을 하는 select
에 중점을 둡니다.
select
소개
select
란 무엇인가
select
는 여러 통신 작업 중에서 실행 가능한 작업을 선택하는 데 사용되는 Go의 제어 구조입니다. 여러 채널에서 읽기 및 쓰기 작업을 조정하여 여러 채널에서 비차단 데이터 전송, 동기화 및 제어를 가능하게 합니다.
왜 select
가 필요한가
Go의 select
문은 채널을 멀티플렉싱하는 메커니즘을 제공합니다. 이를 통해 여러 채널에서 메시지를 기다리고 처리할 수 있습니다. 채널을 반복하기 위해 for
루프를 사용하는 것보다 select
는 여러 채널을 관리하는 데 더 효율적인 방법입니다.
select
사용에 대한 몇 가지 일반적인 시나리오는 다음과 같습니다.
-
여러 채널에서 메시지 대기(멀티플렉싱) 여러 채널에서 메시지를 기다려야 하는 경우
select
를 사용하면 데이터 수신을 위해 여러 Goroutine을 사용하여 동기화 및 대기할 필요 없이 데이터 수신을 기다리는 것이 편리합니다. -
채널 메시지 대기 시간 초과 특정 기간 내에 채널에서 메시지를 기다려야 하는 경우
select
를time
패키지와 결합하여 시간 제한 대기를 구현할 수 있습니다. -
채널에서 비차단 읽기/쓰기 채널에 데이터나 공간이 없으면 채널에서 읽거나 쓰는 것이 차단됩니다.
default
분기와 함께select
를 사용하면 비차단 작업을 수행하여 교착 상태 또는 무한 루프를 방지할 수 있습니다.
따라서 select
의 주요 목적은 여러 채널을 처리하기 위한 효율적이고 사용하기 쉬운 메커니즘을 제공하여 Goroutine 동기화 및 대기를 단순화하고 프로그램을 더 읽기 쉽고 효율적이며 안정적으로 만드는 것입니다.
select
의 기본 사항
구문
select { case <- channel1: // channel1 준비 완료 case data := <- channel2: // channel2 준비 완료, 데이터 읽을 수 있음 case channel3 <- data: // channel3 준비 완료, 데이터 쓸 수 있음 default: // 채널 준비 안 됨 }
여기서 <- channel1
은 channel1
에서 읽는 것을 의미하고, data := <- channel2
는 data
로 데이터를 수신하는 것을 의미합니다. channel3 <- data
는 data
를 channel3
에 쓰는 것을 의미합니다.
select
의 구문은 switch
와 유사하지만 채널 작업에만 사용됩니다. select
문에서 여러 case
블록을 정의할 수 있으며, 각 블록은 데이터를 읽거나 쓰는 채널 작업입니다. 여러 case가 동시에 준비되면 하나가 무작위로 선택됩니다. 준비된 것이 없으면 default
분기(있는 경우)가 실행됩니다. 그렇지 않으면 하나 이상의 case가 준비될 때까지 select
가 차단됩니다.
기본 사용법
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) ch2 := make(chan int) go func() { time.Sleep(1 * time.Second) ch1 <- 1 }() go func() { time.Sleep(2 * time.Second) ch2 <- 2 }() for i := 0; i < 2; i++ { select { case data, ok := <-ch1: if ok { fmt.Println("ch1에서 받음:", data) } else { fmt.Println("채널 닫힘") } case data, ok := <-ch2: if ok { fmt.Println("ch2에서 받음:", data) } else { fmt.Println("채널 닫힘") } } } select { case data, ok := <-ch1: if ok { fmt.Println("ch1에서 받음:", data) } else { fmt.Println("채널 닫힘") } case data, ok := <-ch2: if ok { fmt.Println("ch2에서 받음:", data) } else { fmt.Println("채널 닫힘") } default: fmt.Println("데이터를 받지 못했습니다. 기본 분기가 실행되었습니다.") } }
실행 결과
ch1에서 받음: 1
ch2에서 받음: 2
데이터를 받지 못했습니다. 기본 분기가 실행되었습니다.
위의 예에서는 두 개의 채널 ch1
과 ch2
가 생성됩니다. 분리된 Goroutine은 서로 다른 지연 후에 이러한 채널에 씁니다. 기본 Goroutine은 select
문을 사용하여 두 채널을 모두 수신합니다. 채널에 데이터가 도착하면 데이터를 인쇄합니다. ch1
이 ch2
보다 먼저 데이터를 수신하므로 메시지 "ch1에서 받음: 1"
이 먼저 인쇄되고 그 다음에 "ch2에서 받음: 2"
가 인쇄됩니다.
default
분기를 보여주기 위해 프로그램에는 두 번째 select
블록이 포함되어 있습니다. 이 시점에서 ch1
과 ch2
는 모두 비어 있으므로 default
분기가 실행되어 "데이터를 받지 못했습니다. 기본 분기가 실행되었습니다."
가 인쇄됩니다.
select
와 채널 결합 시나리오
시간 초과 제어 구현
package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { time.Sleep(3 * time.Second) ch <- 1 }() select { case data, ok := <-ch: if ok { fmt.Println("데이터 받음:", data) } else { fmt.Println("채널 닫힘") } case <-time.After(2 * time.Second): fmt.Println("시간 초과됨!") } }
실행 결과: 시간 초과됨!
이 예에서 프로그램은 3초 후에 ch
채널로 데이터를 보냅니다. 그러나 select
블록은 2초의 시간 초과를 설정합니다. 그 시간 안에 데이터가 수신되지 않으면 시간 초과 case가 트리거됩니다.
다중 작업 동시 제어 구현
package main import ( "fmt" ) func main() { ch := make(chan int) for i := 0; i < 10; i++ { go func(id int) { ch <- id }(i) } for i := 0; i < 10; i++ { select { case data, ok := <-ch: if ok { fmt.Println("작업 완료:", data) } else { fmt.Println("채널 닫힘") } } } }
실행 결과 (각 실행마다 순서가 다를 수 있습니다).
작업 완료: 1
작업 완료: 5
작업 완료: 2
작업 완료: 3
작업 완료: 4
작업 완료: 0
작업 완료: 9
작업 완료: 6
작업 완료: 7
작업 완료: 8
이 예에서는 10개의 Goroutine이 시작되어 작업을 동시에 실행합니다. 단일 채널은 작업 완료 알림을 수신하는 데 사용됩니다. 기본 함수는 select
를 사용하여 이 채널을 수신하고 수신 시 완료된 각 작업을 처리합니다.
여러 채널 수신
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) ch2 := make(chan int) // ch1으로 데이터를 보내는 Goroutine 1 시작 go func() { for i := 0; i < 5; i++ { ch1 <- i time.Sleep(time.Second) } }() // ch2로 데이터를 보내는 Goroutine 2 시작 go func() { for i := 5; i < 10; i++ { ch2 <- i time.Sleep(time.Second) } }() // 기본 Goroutine은 ch1과 ch2에서 데이터를 수신하여 인쇄합니다. for i := 0; i < 10; i++ { select { case data := <-ch1: fmt.Println("ch1에서 받음:", data) case data := <-ch2: fmt.Println("ch2에서 받음:", data) } } fmt.Println("완료.") }
실행 결과 (각 실행마다 순서가 다를 수 있습니다).
ch2에서 받음: 5
ch1에서 받음: 0
ch1에서 받음: 1
ch2에서 받음: 6
ch1에서 받음: 2
ch2에서 받음: 7
ch1에서 받음: 3
ch2에서 받음: 8
ch1에서 받음: 4
ch2에서 받음: 9
완료.
이 예에서 select
를 사용하면 여러 채널에서 데이터를 멀티플렉싱할 수 있습니다. 이 프로그램을 사용하면 동기화를 위해 분리된 Goroutine이 필요 없이 ch1
과 ch2
를 동시에 수신할 수 있습니다.
default
를 사용하여 비차단 읽기 및 쓰기 구현
import ( "fmt" "time" ) func main() { ch := make(chan int, 1) go func() { for i := 1; i <= 5; i++ { ch <- i time.Sleep(1 * time.Second) } close(ch) }() for { select { case val, ok := <-ch: if ok { fmt.Println(val) } else { ch = nil } default: fmt.Println("준비된 값이 없습니다.") time.Sleep(500 * time.Millisecond) } if ch == nil { break } } }
실행 결과 (각 실행마다 순서가 다를 수 있습니다).
준비된 값이 없습니다.
1
준비된 값이 없습니다.
2
준비된 값이 없습니다.
준비된 값이 없습니다.
3
준비된 값이 없습니다.
준비된 값이 없습니다.
4
준비된 값이 없습니다.
준비된 값이 없습니다.
5
준비된 값이 없습니다.
준비된 값이 없습니다.
이 코드는 default
분기를 사용하여 비차단 채널 읽기 및 쓰기를 구현합니다. select
문에서 채널을 읽거나 쓸 준비가 되면 해당 분기가 실행됩니다. 준비된 채널이 없으면 default
분기가 실행되어 차단을 방지합니다.
select
사용 시 참고 사항
select
를 사용할 때 명심해야 할 몇 가지 중요한 사항은 다음과 같습니다.
select
문은 채널에서 읽거나 쓰는 것과 같은 통신 작업에만 사용할 수 있습니다. 일반적인 계산이나 함수 호출에는 사용할 수 없습니다.select
문은 하나 이상의 case가 준비될 때까지 차단됩니다. 여러 case가 준비되면 그 중 하나가 무작위로 선택됩니다.- 준비된 case가 없고
default
분기가 있으면default
분기가 즉시 실행됩니다. select
에서 채널을 사용할 때는 채널이 올바르게 초기화되었는지 확인하십시오.- 채널이 닫히면 비어 있을 때까지 계속 읽을 수 있습니다. 닫힌 채널에서 읽으면 요소 유형의 0값과 채널의 닫힌 상태를 나타내는 부울이 반환됩니다.
요약하면 select
를 사용할 때는 교착 상태 및 기타 문제를 방지하기 위해 각 case의 조건과 실행 순서를 신중하게 고려하십시오.
Leapcell은 Go 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다중 언어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무료로 무제한 프로젝트 배포
- 사용량에 대해서만 비용을 지불하십시오. 요청도 없고 요금도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 손쉬운 설정을 위한 직관적인 UI
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리하기 위한 자동 확장
- 운영 오버헤드가 없어 구축에만 집중할 수 있습니다.
설명서에서 자세히 알아보십시오!
X에서 팔로우하세요: @LeapcellHQ