Go에서 Concurrency 파트 2: Channel
Source: Dev.to
Channel
“메모리를 공유해서 통신하지 말고, 통신을 통해 메모리를 공유하라.”
Go에서는 goroutine이 보통 채널을 통해 데이터를 교환하며, 공유 변수의 값을 직접 변경하지 않는다.
- 값 보내기:
c <- v - 값 받기:
v := <-c
c는 타입이 지정된 채널이며, 요소 타입은 채널을 만들 때 고정된다.
func multiply(v1, v2 int, c chan int) {
c <- v1 * v2
}
func main() {
c := make(chan int)
go multiply(1, 2, c)
go multiply(3, 4, c)
a := <-c
b := <-c
fmt.Println(a * b) // prints 24
}
채널의 내장 블로킹 동작 덕분에 명시적인 WaitGroup이 필요하지 않다: 송신은 수신자가 준비될 때까지 블록되고, 수신은 값이 들어올 때까지 블록된다.
간단한 동기화에 채널 사용
채널은 WaitGroup과 비슷하게 한 번만 사용되는 신호 역할을 할 수 있다.
ready := make(chan bool)
go func() {
// do some work...
ready <- true // signal completion
}()
<-ready // wait until the goroutine signals
받은 값은 보통 무시한다; 프로그램은 단지 송신이 일어날 때까지 기다린다.
for range 로 스트림 받기
생산자가 채널을 닫으면, 수신자는 채널이 닫힐 때까지 반복할 수 있다.
func producer(c chan int) {
for i := 1; i <= 5; i++ {
fmt.Println("Sending", i)
c <- i
time.Sleep(500 * time.Millisecond)
}
close(c)
}
func main() {
c := make(chan int)
go producer(c)
for v := range c {
fmt.Print("Receiving ", v)
}
fmt.Println("\nDone")
}
Important: 더 이상 값이 전송되지 않을 경우 송신자가 채널을 닫아야 한다. 수신 측에서 채널을 닫으면 송신자가 아직 동작 중일 때 패닉이 발생할 수 있다.
Buffered vs Unbuffered Channels
기본적으로 채널은 용량 0(버퍼 없음)이며, 각 송신을 대응되는 수신과 동기화한다.
버퍼가 있는 채널은 송신자를 블록하지 않고 제한된 수의 값을 큐에 저장할 수 있다:
c := make(chan int, 3) // capacity 3
- 처음 세 번의 송신은 즉시 성공한다.
- 네 번째 송신은 수신자가 값을 읽을 때까지 블록된다.
- 수신은 버퍼가 비어 있을 때만 블록된다.
버퍼 채널은 생산자와 소비자의 속도가 다를 때, 일시적인 불일치를 완화하는 데 유용하다.
Directional Channels
채널을 함수 매개변수로 전달할 때 방향을 제한할 수 있다:
- 송신 전용:
func producer(c chan<- int) - 수신 전용:
func consumer(c <-chan int)
이렇게 하면 의도된 사용을 명시적으로 드러내고, 컴파일러가 오용을 잡아준다.
Select Statement
select는 채널 연산을 위한 switch와 같다. 케이스 중 하나가 진행 가능해질 때까지 블록한다.
기본 사용법
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "A"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "B"
}()
select {
case res := <-c1:
fmt.Println("Received from c1:", res)
case res := <-c2:
fmt.Println("Received from c2:", res)
}
}
프로그램은 먼저 준비된 채널(c1이 예시에서는 먼저 준비됨)에서 값을 받아 출력한다.
송신과 수신을 동시에 선택하기
select {
case a := <-inChan:
fmt.Println("Received:", a)
case outChan <- b:
fmt.Println("Sent:", b)
}
흔히 쓰이는 패턴
타임아웃
select {
case res := <-c1:
fmt.Println("Received:", res)
case <-time.After(5 * time.Second):
fmt.Println("Timeout")
}
논블로킹 기본값
select {
case res := <-c1:
fmt.Println("Received:", res)
default:
fmt.Println("No message")
}
중단 신호
select {
case res := <-c1:
fmt.Println("Received:", res)
case <-abortChan:
fmt.Println("Abort")
return
}