동시성과 병행성의 차이 그리고 고루틴의 간단한 사용법을 이전 포스팅에 남겼다. 이 포스팅은 조금 더 고루틴을 맛깔나게 쓰기위한 Channel에 대해 알아보려 한다.
(
고루틴의 간단한 사용법은 이전 포스팅 글에서 확인하시길 바란다.
)
Channel이란?
Channel은 Golang의 고루틴 간의 통신을 위한 수단.
Channel은 Thread Safe한 Queue이다.
Channel은 Golang에서 기본 자료형으로 주어지며 일급 객체의 역할을 한다.
Channel의 표기법은 chan [자료형]이며 주고 받는 역할을 하는 양방향 channel, 주기만 또는 받기만 하는 단방향 channel을 지정할 수 있다.
Channel은 레퍼런스 자료형이다.
Channel은 Block 형태의 처리방식을 가진다.
Block 처리방식
Block 처리방식을 더 쉽게 이해하기 위해 간단한 예제부터 살펴보자.
예제1
코드
package main
import (
"fmt"
"time"
)
func main() {
go goTest()
fmt.Println("Finish!")
}
func goTest() {
time.Sleep(1000 * time.Millisecond)
fmt.Println("Done!")
}
결과
Finish!
역시나 이전에 확인했듯 main 역시 고루틴이기에 goTest가 먼저 실행될 것이라 확증하지 못한다.
거기다 Sleep까지 줬으니 main 고루틴이 Finish!를 출력하고 죽어버린다. 메인 고루틴이 죽었기에 서브 고루틴 역시 일도 해보기전에 자다가 죽어버린다 ㅡㅜ
예제2
코드
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go goTest(ch)
ch <- "Done!"
fmt.Println("Finish!")
}
func goTest(ch chan string) {
time.Sleep(time.Second)
fmt.Println(<-ch)
}
결과
Done!
Finish!
Channel을 사용한다고 어찌 이런 결과가 나올까?
이는 Channel이 Block 처리방식을 가지기 때문이다.
그렇다면 Block 처리방식은 또 무엇인가..
간단히 말하자면 함수를 호출하는 호출자가 함수의 흐름을 책임지는 제어권을 호출한 함수로 넘겨버리는 것이다. 만약 싱글 쓰레드의 프로세스라면 Block 처리방식으로 함수를 호출한다면 메인 흐름에서의 제어권을 함수로 넘기기 때문에 메인 흐름은 함수가 제어권을 넘길 때 까지 진행하지 않는 것이다.
그렇다면 이해가 갈 것이다. 어찌 1초라는 시간을 기다리면서 Channel ch에 넘겨준 Done! 을 먼저 출력하는지!!
Channel의 사용 방법
Channel의 선언
var c1 chan int // 양방향의 채널
var c2 <-chan int // sender 채널
var c3 chan <-int // recever 채널
//화살표가 channel에서 나가는지 channel을 향하는지를 보고 구분하면 헷갈리지 않는다!
Channel의 초기화
//make를 통해 초기화 하여야하는 레퍼런스형이다.
c1 := make(chan int)
c2 := make(chan<- int)
c3 := make(<-chan int)
c4 := c1 // c4와 c1은 같은 주소를 가르키기 때문에 c1과 c4는 같은 객체이다.
c5 := make(chan int, 3) // 채널은 크기를 지정해 줄 수 있으며 지정하지 않으면 default 0 값을 가진다.
Channel 기본 사용 예제
코드
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int, 2)
wg.Add(1)
go calculateSquareArea(&wg, ch)
ch <- 5
wg.Wait()
}
func calculateSquareArea(wg *sync.WaitGroup, ch <-chan int) {
length := <-ch
fmt.Println(length * length)
wg.Done()
return
}
코드 설명
위의 Channel의 특징에서도 말했듯 Channel은 Thread Safe한 자료형이기에 DeadLock에 대한 걱정 없이 사용할 수 있는 모습을 볼 수 있다.
여기서 주의깊게 보아야할 내용은 자료형이 channel int인 ch들이다.
이 코드로 알 수 있는것은 다양하다.
- WaitGroup자료형은 &를 달아 주소값을 매개변수로 넘겨주었지만 ch는 그렇지 않다. 기본적으로 channel은 Map과 같은 레퍼런스형이기 때문이다.
- 양방향 channel인 chan int가 <-chan int 형으로 받아진다. 양방향의 경우 단 방향으로의 전환이 가능하다.
- calculateSquareArea 고루틴과 main의 메인 고루틴 간의 데이터 교환이 이루어 지는것을 알 수 있다.
for문을 사용한 Channel 사용 예제
코드
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go receiver(ch)
for i := 1; i <= 10; i++ {
ch <- i
}
fmt.Println("Finish!")
}
func receiver(ch <-chan int) {
for i := range ch {
fmt.Printf("%d ", i)
time.Sleep(time.Second)
}
}
코드 설명
receiver함수를 보자. channel을 반복문을 이용하여 받는다. ch가 들어올 때 마다 반복문을 실행할 수 있다는 것이다.
아주 쉽지 않은가. 그렇지만 이 코드는 위험할 수 있다. channel이 열린 채로 계속 유지되기 때문이다. 이렇게 channel을 관리한다면 결국 예상치 못한 좀비 고루틴이 생길 수 있고 그렇지 않다 해도 channel이 쓸데없는 메모리를 차지하게 된다.
해결 방법
더 간단하다.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go receiver(ch)
for i := 1; i <= 10; i++ {
ch <- i
}
close(ch) // 요거 추가!
fmt.Println("Finish!")
}
func receiver(ch <-chan int) {
for i := range ch {
fmt.Printf("%d ", i)
time.Sleep(time.Second)
}
}
channel을 닫아버리자!
마무리
고 이쁘다.. 고루틴 멋지다..
다음엔 고루틴을 더 효과적으로 사용할 수 있는 select와 고루틴 활용한 디자인 패턴을 살펴보겠다.
'Study > Go' 카테고리의 다른 글
[Go] Heap 자료구조를 사용하는 우선순위 큐 (Priority Queue) (0) | 2021.10.04 |
---|---|
[Go] 클로저(Closure)를 활용하여 생성기(generator) 제작 (0) | 2021.10.02 |
[Go] 고루틴(Goroutine) 알고쓰기(1) - 동시성, 병렬성 프로그래밍 그리고 고루틴 (0) | 2021.09.01 |
[Go] Json 활용 방법 (0) | 2021.08.12 |
[Go] 웹 서버(Web Server) 열기 (0) | 2021.07.27 |