-
[Golang] 채널언어/Golang 2017. 3. 2. 20:53
Go 채널은 그 채널을 통하여 고루틴끼리 데이터를 주고 받는 통로라 볼 수 있는데, 채널은 make(chan 자료형) 함수를 통해 미리 생성되어야 하며, 채널 연산자 <- 을 통해 데이타를 보내고 받습니다. 채널은 흔히 goroutine들 사이 데이터를 주고 받는데 사용되는데, 상대편이 준비될 때까지 채널에서 대기함으로써 별도의 lock을 걸지 않고 데이타를 동기화하는데 사용됩니다. 채널은 실행 흐름을 제어하는 기능이 있고 모든 타입을 채널로 사용할 수 있습니다(이 때문에 일급시민(first class citizen)이라 할 수 있음). 그리고 채널 자체는 값이 아닌 레퍼런스 타입입니다. 채널을 그림으로 표현하면 다음과 같습니다.
아래 예제는 정수형 채널을 생성하고, 한goroutine 에서 그 채널에 123이란 정수 데이터를 보낸 후, 이를 다시 메인 루틴에서 채널로부터 123 데이터를 받는 코드입니다. 채널을 생성할 때는 make() 함수에 어떤 타입의 데이터를 채널에서 주고 받을지를 미리 지정해 주어야 합니다. 채널로 데이터를 보낼 때는 채널명 <- 데이터 와 같이 사용하고, 채널로부터 데이터를 받을 경우는 <- 채널명 와 같이 사용합니다.
package main func main() { // 정수형 채널을 생성 ch := make(chan int) go func() { ch <- 123 //채널에 123을 보냄 }() var i int i = <- ch // 채널로부터 123을 받음 println(i) }
아래는 고루틴과 채널을 이용하여 두 정수 값을 더하는 예제입니다.
package main import "fmt" func sum(a int, b int, c chan int) { c <- a + b // 채널에 a와 b의 합을 보냄 } func main() { c := make(chan int) // int형 채널 생성 go sum(1, 2, c) // sum을 고루틴으로 실행한 뒤 채널을 매개변수로 넘겨줌 n := <-c // 채널에서 값을 꺼낸 뒤 n에 대입 fmt.Println(n) // 3 }
채널을 사용하기 전에는 반드시
make
함수로 공간을 할당해야 합니다. 그리고 이렇게 생성하면 동기 채널(synchronous channel)이 생성됩니다.아래는 고루틴A와 고루틴B 간 채널로 통신하는 그림입니다.
동기 채널
Go 채널은 수신자와 송신자가 서로를 기다리는 속성때문에, 이를 이용하여 (다음 예제와 같이) Go루틴이 끝날 때까지 기다리는 기능을 구현할 수 있습니다. 즉, 익명함수를 사용한 한 Go 루틴에서 어떤 작업이 실행되고 있을 때, 메인루틴은 <-done 에서 계속 수신하며 대기하고 있게 됩니다. 익명함수 Go 루틴에서 작업이 끝난 후, done채널에 true를 보내면, 수신자 메인루틴은 이를 받고 프로그램을 끝내게 됩니다.
package main import "fmt" func main() { done := make(chan bool) go func() { for i := 0; i < 10; i++ { fmt.Println(i) } done <- true }() // 위의 Go루틴이 끝날 때까지 대기 <-done }
다음예제는 고루틴과 메인함수를 번갈아가면서 실행하는 예제입니다.
package main import ( "fmt" "time" ) func main() { done := make(chan bool) // 동기 채널 생성 count := 3 // 반복할 횟수 go func() { for i := 0; i < count; i++ { done <- true // 고루틴에 true를 보냄, 값을 꺼낼 때까지 대기 fmt.Println("고루틴 : ", i) // 반복문의 변수 출력 time.Sleep(1 * time.Second) // 1초 대기 } }() for i := 0; i < count; i++ { <-done // 채널에 값이 들어올 때까지 대기, 값을 꺼냄 fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력 } }
make(chan bool)처럼 채널을 생성합니다. 여기서는 채널로 값을 주고 받아도 실제로 사용하지 않으므로 자료형은 큰 의미가 없습니다. make 함수에 매개 변수를 하나만 지정했으므로 동기 채널을 생성됩니다. 비동기 채널과 버퍼는 뒤에서 설명하겠습니다.
다음 고루틴 내 for문에서 먼저 고루틴을 생성하고, 반복문을 실행할 때마다 채널 done에 true 값을 보낸 뒤 1초를 기다립니다.
동기 채널이므로 done에 값을 보내면 다른 쪽에서 값을 꺼낼 때까지 대기합니다. 따라서 반복문도 실행되지 않으므로 “고루틴 : 숫자”가 계속 출력되지 않습니다. 이제 메인 함수에서는 일반 반복문을 실행할 때마다 채널 done에서 값을 꺼냅니다.
<-done 부분에서 채널에 값이 들어올 때까지 대기합니다. 먼저 앞의 고루틴에서 done에 값을 보냈기 때문에 값을 꺼낸 뒤 다음 코드로 진행합니다. 그리고 고루틴 쪽의 대기도 종료되면서 다시 반복문이 실행된 뒤 채널에 값을 보냅니다. 그리고 메인 함수는 채널에서 값을 꺼내고, 다시 고루틴도 채널에 값을 보냅니다. 따라서 다음과 같이 고루틴 → 메인 함수 → 고루틴 → 메인 함수 순서로 실행됩니다.
// 결과 고루틴 : 0 메인 함수 : 0 고루틴 : 1 메인 함수 : 1 메인 함수 : 2 고루틴 :
동기 채널은 보내는 쪽에서는 값을 받을 때까지 대기하고, 받는 쪽에서는 채널에 값이 들어올 때까지 대기합니다. 따라서, 동기 채널을 활용하면 고루틴의 코드 실행 순서를 제어할 수 있습니다.
채널 버퍼링
Go 채널은 2가지의 채널이 있는데, Unbuffered Channel과 Buffered Channel이 있습니다. 위에서 설명한 예제에서의 Go 채널은 Unbuffered Channel로서 이 채널에서는 하나의 수신자가 데이타를 받을 때까지 송신자가 데이터를 보내는 채널에 묶여 있게 됩니다. 하지만, Buffered Channel을 사용하면 비록 수신자가 받을 준비가 되어 있지 않을 지라도 지정된 버퍼만큼 데이터를 보내고 계속 다른 일을 수행할 수 있습니다. 버퍼 채널은 make(chan type, N) 함수를 통해 생성되는데, 두번째 파라미터 N에 사용할 버퍼 개수를 넣습니다. 예를 들어, make(chan int, 10)은 10개의 정수형을 갖는 버퍼 채널을 만듭니다.
다음은 채널의 버퍼가 가득차면 값을 꺼내서 출력하는 예제입니다.
package main import ( "fmt" "runtime" ) func main() { runtime.GOMAXPROCS(1) done := make(chan bool, 2) // 버퍼가 2개인 비동기 채널 생성 count := 4 // 반복할 횟수 go func() { for i := 0; i < count; i++ { done <- true // 채널에 true를 보냄, 버퍼가 가득차면 대기 fmt.Println("고루틴 : ", i) // 반복문의 변수 출력 } }() for i := 0; i < count; i++ { <-done // 버퍼에 값이 없으면 대기, 값을 꺼냄 fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력 } } // 결과 고루틴 : 0 고루틴 : 1 고루틴 : 2 메인 함수 : 0 메인 함수 : 1 메인 함수 : 2 메인 함수 : 3
채널에 버퍼를 1개 이상 설정하면 비동기 채널(asynchronous channel)이 생성됩니다. 비동기 채널은 보내는 쪽에서 버퍼가 가득 차면 실행을 멈추고 대기하며 받는 쪽에서는 버퍼에 값이 없으면 대기합니다.
- 이번 예제에서는 채널에 값을 연달아서 보내고 꺼냈지만 시간차를 두고 보내거나 꺼낼 수도 있습니다. 이렇게 되면 채널에서 값을 꺼냈을 때 버퍼에 공간이 생기므로 다른 쪽에서 다시 값을 보낼 수 있습니다. 버퍼 개수를 수정하고, 고루틴과 메인 함수의 반복문에서 time.Sleep 함수를 사용하여 여러 가지 형태로 실행해보기 바랍니다.
- 이번 예제에서는 runtime.GOMAXPROCS(1)를 설정하여 CPU 코어를 하나만 사용합니다. CPU 코어를 여러 개 사용하면 결과가 달라질 수 있는데, 여러 곳에서 채널에 값을 보내고 꺼내기 때문입니다. 그러므로 멀티코어에서 비동기 채널을 사용할 때는 실행 순서나 채널 사용 방법에 좀 더 신경써야 합니다.
- Go 1.4에서 채널 버퍼링 예제를 실행하면 고루틴 2번, 메인 함수 2번씩 출력됩니다. 하지만 Go 1.5 이상부터는 고루틴 스케쥴링 구현이 바뀌었기 때문에 이렇게 출력되지 않을 수 있습니다. 단, 비동기 채널의 구현과 동작은 바뀐 것이 없으므로 보내는 쪽에서 버퍼가 가득 차면 실행을 멈추고 대기하며 받는 쪽에서는 버퍼에 값이 없으면 대기한다는 점만 기억하면 됩니다.
버퍼 채널을 이용하지 않는 경우, 아래와 같은 코드는 에러 (fatal error: all goroutines are asleep - deadlock!) 를 발생시킵니다. 왜냐하면 메인루틴에서 채널에 1을 보내면서 상대편 수신자를 기다리고 있는데, 이 채널을 받는 수신자 Go루틴이 없기 때문입니다.
package main import "fmt" func main() { c := make(chan int) c <- 1 //수신루틴이 없으므로 데드락 fmt.Println(<-c) //코멘트해도 데드락 (별도의 Go루틴없기 때문) }
하지만 아래와 같이 버퍼채널을 사용하면, 수신자가 당장 없더라도 최대버퍼 수까지 데이터를 보낼 수 있으므로, 에러가 발생하지 않습니다.
package main import "fmt" func main() { ch := make(chan int, 1) //수신자가 없더라도 보낼 수 있다. ch <- 101 fmt.Println(<-ch) }
채널 파라미터 (보내기 전용 및 받기 전용 채널 사용)
채널을 함수의 파라미터도 전달할 때, 일반적으로 송수신을 모두 하는 채널을 전달하지만, 특별히 해당 채널로 송신만 할 것인지 혹은 수신만 할 것인지를 지정할 수도 있습니다. 송신 파라미터는 (p chan<- int)와 같이 chan<- 을 사용하고, 수신 파라미터는 (p <-chan int)와 같이 <-chan 을 사용합니다. 만약 송신 채널 파라미터에서 수신을 한다거나, 수신 채널에 송신을 하게되면, 에러가 발생합니다.
다음은 0부터 4까지 채널에 값을 보내고, 다시 채널에서 값을 꺼내서 출력합니다. 그리고 반복문이 끝난 뒤 채널에 100을 보낸 뒤 다시 꺼내서 출력합니다.
package main import "fmt" func producer(c chan<- int) { // 보내기 전용 채널 for i := 0; i < 5; i++ { c <- i } c <- 100 // 채널에 값을 보냄 //fmt.Println(<-c) // 채널에서 값을 꺼내면 컴파일 에러 } func consumer(c <-chan int) { // 받기 전용 채널 for i := range c { fmt.Println(i) } fmt.Println(<-c) // 채널에 값을 꺼냄 // c <- 1 // 채널에 값을 보내면 컴파일 에러 } func main() { c := make(chan int) go producer(c) go consumer(c) fmt.Scanln() } // 결과 0 1 2 3 4 100
보내기 전용 및 받기 전용 채널은 채널 앞 뒤로
<-
연산자를 붙여서 만듭니다. 보통 함수의 매개변수로 사용하거나, 구조체의 필드로 사용합니다.- 보내기 전용(send-only):
chan<- 자료형
형식입니다. c chan<- int는 int형 보내기 전용 채널 c를 뜻합니다. 보내기 전용 채널은 값을 보낼 수만 있으며 값을 가져오려고 하면 컴파일 에러가 발생합니다. - 받기 전용(receive-only):
<-chan 자료형
형식입니다. c <-chan int는 int형 받기 전용 채널 c를 뜻합니다. 받기 전용 채널은 range 키워드 또는<- 채널
형식으로 값을 꺼낼 수만 있으며 값을 보내려고 하면 컴파일 에러가 발생합니다.
chan
키워드를 기준으로 <- (화살표)가 붙은 방향을 보면 보내기 전용인지 받기 전용인지 알 수 있습니다. 즉 chan<-은 chan 키워드로 <-가 들어가므로 보내기 전용, <-chan은 chan 키워드에서 <-가 나오고 있으므로 받기 전용 채널입니다.여기서는 producer 함수는 매개변수로 보내기 전용 채널을 사용하고, consumer 함수는 매개변수로 받기 전용 채널을 사용합니다. 따라서 producer 함수는 값을 보내기만 하고, consumer 함수는 값을 꺼내기만 합니다. 이번에는 채널을 리턴값으로 사용해보겠습니다. 다음은 두 수를 더한 뒤 채널로 리턴합니다.
package main import "fmt" // ↓ 함수의 리턴 값은 int 형 받기 전용 채널 func sum(a, b int) <-chan int { out := make(chan int) // 채널 생성 go func() { out <- a + b // 채널에 a와 b의 합을 보냄 }() return out // 채널 변수 자체를 리턴 } func main() { c := sum(1, 2) // 채널을 리턴값으로 받아서 c에 대입 fmt.Println(<-c) // 3: 채널에서 값을 꺼냄 } // 결과 3
sum 함수는 받기 전용 채널을 리턴하도록 만들었습니다. 채널을 리턴하려면 먼저 make 함수로 채널을 생성합니다. 그리고 고루틴 안에서 채널에 값을 보낸 뒤 고루틴 바깥에서 채널을 리턴합니다. sum 함수를 사용하여 채널을 리턴값으로 받았으면 <-c처럼 값을 꺼내면 됩니다.
이번에는 채널만 사용하여 값을 더해보겠습니다.
package main import "fmt" // ↓ 함수의 리턴 값은 int 형 받기 전용 채널 func num(a, b int) <-chan int { out := make(chan int) // int형 채널 생성 go func() { out <- a // 채널에 a의 값을 보냄 out <- b // 채널에 b의 값을 보냄 close(out) // 채널을 닫음 }() return out // 채널 변수 자체를 리턴 } // ↓ 함수의 매개변수는 int형 받기 전용 채널 func sum(c <-chan int) <-chan int { // ↑ 함수의 리턴 값은 int형 받기 전용 채널 out := make(chan int) // int형 채널 생성 go func() { r := 0 for i := range c { // range를 사용하여 채널이 닫힐 때까지 값을 꺼냄 r = r + i // 꺼낸 값을 모두 더함 } out <- r // 더한 결과를 채널에 보냄 }() return out // 채널 변수 자체를 리턴 } func main() { c := num(1, 2) // 1과 2가 들어있는 채널이 리턴됨 out := sum(c) // 채널 c를 매개변수에 넘겨서 모두 더함, 더한 값이 들어있는 out 채널을 리턴 fmt.Println(<-out) // 3: out 채널에서 값을 꺼냄 } // 결과 3
아래 예제에서는 sendChan() 함수 안에서 x := <- ch 를 실행하면 송신전용 채널에 수신을 시도하므로 에러가 발생합니다.
package main import "fmt" func main() { ch := make(chan string, 1) sendChan(ch) receiveChan(ch) } func sendChan(ch chan<- string) { ch <- "Data" // x := <-ch // 에러발생 } func receiveChan(ch <-chan string) { data := <-ch fmt.Println(data) }
채널 닫기 (close)
채널을 오픈한 후 데이타를 송신한 후, close()함수를 사용하여 채널을 닫을 수 있습니다. 채널을 닫게 되면, 해당 채널로는 더이상 송신을 할 수 없지만, 채널이 닫힌 이후에도 계속 수신은 가능합니다. 채널 수신에 사용되는 <- ch 은 두개의 리턴 값을 갖는데, 첫째는 채널 메시지이고, 두번째는 수신이 제대로 되었는가를 나타냅니다. 만약 채널이 닫혔다면, 두번째 리턴값은 false를 리턴합니다.
package main func main() { ch := make(chan int, 2) // 채널에 송신 ch <- 1 ch <- 2 // 채널을 닫는다 close(ch) // 채널 수신 println(<-ch) println(<-ch) if _, success = <-ch; !success { println("더이상 데이타 없음.") } }
채널 range 문
채널에서 송신자가 송신을 한 후, 채널을 닫을 수 있습니다. 그리고 수신자는 임의의 개수의 데이터를 채널이 닫힐 때까지 계속 수신할 수 있습니다. 아래 예제는 이러한 송수신 방법을 표현한 것으로, 수신자는 채널이 닫히는 것을 체크하면서 계속 루프를 돌게 됩니다. 첫 번째 방법은 무한 for 루프 안에서 if 문으로 수신 채널의 두번째 파라미터를 체크하는 방식이고, 두 번째 방법는 첫 번째 방법과 동일한 표현이지만, for range문으로 보다 간결하게 표현한 것입니다. 채널 range문은 range 키워드 다음의 채널로부터 계속 수신하다가 채널이 닫힌 것을 감지하면 for 루프를 종료합니다.
package main func main() { ch := make(chan int, 2) // 채널에 송신 ch <- 1 ch <- 2 // 채널을 닫는다 close(ch) // 방법1 // 채널이 Close된 때 까지 계속 수신 /* for { if i, success := <-ch; success { println(i) } else { break } } */ // 방법2 // 위 표현과 동일한 채널 range 문 for i := range ch { println(i) } }
다음은 range와 close 함수의 특징입니다.
- 이미 닫힌 채널에 값을 보내면 패닉이 발생
- 채널을 닫으면 range 루프가 종료
- 채널이 열려있고, 값이 들어오지 않는다면 range는 실행되지 않고 계속 대기합니다. 만약 다른 곳에서 채널에 값을 보냈다면(채널에 값이 들어오면) 그때부터 range가 계속 반복
select문 사용
Go의 select문은 복수 채널들을 기다리면서 준비된 (데이터를 보내온) 채널을 실행하는 기능을 제공합니다. 즉, select문은 여러 개의 case문에서 각각 다른 채널을 기다리다가 준비가 된 채널 case를 실행하는 것입니다. select문은 case 채널들이 준비되지 않으면 계속 대기하게 되고, 가장 먼저 도착한 채널의 case를 실행합니다. 만약 복수 채널에 신호가 오면, Go 런타임이 랜덤하게 그 중 한 개를 선택합니다. 하지만, select문에 default 문이 있으면, case문 채널이 준비되지 않더라도 계속 대기하지 않고 바로 default문을 실행합니다.
select { case <-채널1: // 채널1에 값이 들어왔을 때 실행할 코드를 작성합니다. case <-채널2: // 채널2에 값이 들어왔을 때 실행할 코드를 작성합니다. default: // 모든 case의 채널에 값이 들어오지 않았을 때 실행할 코드를 작성합니다. }
select 분기문은 switch 분기문과 비슷하지만 select 키워드 뒤에 검사할 변수를 따로 지정하지 않으며 각 채널에 값이 들어오면 해당 case가 실행됩니다(close 함수로 채널을 닫았을 때도 case가 실행됩니다). 그리고 보통 select를 계속 처리할 수 있도록 for로 반복해줍니다(반복하지 않으면 한 번만 실행하고 끝냅니다).
switch 분기문과 마찬가지로 select 분기문도 default 케이스를 지정할 수 있으며 case에 지정된 채널에 값이 들어오지 않았을 때 즉시 실행됩니다. 단, default에 적절한 처리를 하지 않으면 CPU 코어를 모두 점유하므로 주의합니다.
다음은 채널 2개를 생성하고 100밀리초, 500밀리초 간격으로 숫자와 문자열을 보낸 뒤 꺼내서 출력합니다.
package main import ( "fmt" "time" ) func main() { c1 := make(chan int) // int형 채널 생성 c2 := make(chan string) // string 채널 생성 go func() { for { c1 <- 10 // 채널 c1에 10을 보낸 뒤 time.Sleep(100 * time.Millisecond) // 100 밀리초 대기 } }() go func() { for { c2 <- "Hello, world!" // 채널 c2에 Hello, world!를 보낸 뒤 time.Sleep(500 * time.Millisecond) // 500 밀리초 대기 } }() go func() { for { select { case i := <-c1: // 채널 c1에 값이 들어왔다면 값을 꺼내서 i에 대입 fmt.Println("c1 :", i) // i 값을 출력 case s := <-c2: // 채널 c2에 값이 들어왔다면 값을 꺼내서 s에 대입 fmt.Println("c2 :", s) // s 값을 출력 } } }() time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행 } // 결과 c2 : Hello, world! c1 : 10 c1 : 10 c1 : 10 c1 : 10 c1 : 10 c2 : Hello, world! c1 : 10 ... (생략)
- case에서는 case i := <-c1:처럼 채널에서 값을 꺼낸 뒤 변수에 바로 저장할 수 있습니다. 만약 꺼낸 값을 사용하지 않는다면 case <-c1:처럼 변수를 생략해도 됩니다.
- time.After 함수를 사용하면 시간 제한 처리를 할 수 있습니다. time.After는 특정 시간이 지나면 현재 시간을 채널로 보냅니다.
위의 예제와는 다르게 select에서 매번 채널 c1에 값을 보내지만 채널 c2에 값이 들어오면 c2에서 값을 꺼내서 출력합니다.
package main import ( "fmt" "time" ) func main() { c1 := make(chan int) // int형 채널 생성 c2 := make(chan string) // string 채널 생성 go func() { for { i := <-c1 // 채널 c1에서 값을 꺼낸 뒤 i에 대입 fmt.Println("c1 :", i) // i 값을 출력 time.Sleep(100 * time.Millisecond) // 100 밀리초 대기 } }() go func() { for { c2 <- "Hello, world!" // 채널 c2에 Hello, world!를 보냄 time.Sleep(500 * time.Millisecond) // 500 밀리초 대기 } }() go func() { for { // 무한 루프 select { case c1 <- 10: // 매번 채널 c1에 10을 보냄 case s := <-c2: // c2에 값이 들어왔을 때는 값을 꺼낸 뒤 s에 대입 fmt.Println("c2 :", s) // s 값을 출력 } } }() time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행 } //결과 c2 : Hello, world! c1 : 10 c1 : 10 c1 : 10 c1 : 10 c1 : 10 c2 : Hello, world! c1 : 10 ... (생략)
아래 또 다른 예제는 for 루프 안에 select 문을 쓰면서 두개의 goroutine이 모두 실행되기를 기다리고 있습니다. 첫번째 run1()이 1초간 실행되고 done1 채널로부터 수신하여 해당 case를 실행하고, 다시 for 루프를 돕니다. for루프를 다시 돌면서 다시 select문이 실행되는데, 다음 run2()가 2초후에 실행되고 done2 채널로부터 수신하여 해당 case를 실행하게 됩니다. done2 채널 case문에 break EXIT 이 있는데, 이 문장으로 인해 for 루프를 빠져나와 EXIT 레이블로 이동하게 됩니다. Go의 "break 레이블" 문은 C/C# 등의 언어에서의 goto 문과 다른데, Go에서는 해당 레이블로 이동한 후 자신이 빠져나온 루프 다음 문장을 실행하게 됩니다. 따라서, 여기서는 for 루프 다음 즉 main() 함수의 끝에 다다르게 됩니다.
package main import "time" func main() { done1 := make(chan bool) done2 := make(chan bool) go run1(done1) go run2(done2) EXIT: for { select { case <-done1: println("run1 완료") case <-done2: println("run2 완료") break EXIT } } } func run1(done chan bool) { time.Sleep(1 * time.Second) done <- true } func run2(done chan bool) { time.Sleep(2 * time.Second) done <- true }