효율적인 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



