본문 바로가기

IT 일기/GO

Go 채널이란?

728x90

동시성 프로그래밍에서 가장 중요한 문제는 같은 메모리 영역에 동시에 접근해서 생기는 race condition 문제이다. 이 문제를 해결하기 위해 mutex, semaphore등 특정 영역에 대한 lock과 unlock을 걸어 특정 메모리 영역에 동시에 접근하지 못하게 한다.

 

Go에서도 mutex.lock, mutex.unlock을 사용할 수 있지만 이 경우 해당 영역을 읽고 쓰는 모든 영역에 lock, unlock을 해줘야 한다. 코드 복잡성을 늘릴 수 있다. 그렇다고 무조건적으로 channel만 사용해야 하는건 아니다. 다음 글들을 통해 channel만 사용하는 것은 좋지 않다는 것을 알 수 있다.

https://blog.naver.com/PostView.naver?blogId=sjc02183&logNo=222283500624&parentCategoryNo=&categoryNo=&viewDate=&isShowPopularPosts=false&from=postView

 

Mutex 와 Channel 의 차이

Mutex 와 Channel 은 고루틴에서 원자성과 동시성을 지키기 위한 방법론이다. Mutex 는 개발자가 특정 ...

blog.naver.com

 

mutex에 비해 channel의 성능은 더 좋지 않다. 그러므로 무조건 적인 channel의 사용은 지양하자.

 

channel 사용법 및 차이

채널을 선언하는 방법은 크게 두가지다

 

1. 채널 크기 미지정

2. 채널 크기 지정

ch := make(chan int)

ch := make(chan int, 2)

두 차이는 다음의 상황을 통해 확인할 수 있다 ( <- 기호를 통해 channel에 value를 집어넣음)

 

func main(){
	ch := make(chan int)
	go square2()
	ch <- 9
	fmt.Println("is it print?")
}

func square2() {
	for {
		time.Sleep(2 * time.Second)
		fmt.Println("sleep")
	}
}

이 경우 main go루틴이 끝나기 때문에 화면에 "is it print?"를 뱉고 끝내는게 맞다. 하지만 코드를 실행해보면 그렇지 않다. 오히려 "sleep"이 무한히 출력된다. 다음의 경우를 보자.

func main(){
	ch := make(chan int,2)
	go square2()
	ch <- 9
	fmt.Println("is it print?")
}

func square2() {
	for {
		time.Sleep(2 * time.Second)
		fmt.Println("sleep")
	}
}

이제 정상적으로 "is it print?"가 출력되고 프로그램이 종료된다. 차이는 channel에 크기를 줬다는 것이다. 이게 무슨 의미이길래 이런 차이가 발생할까?

 

channel의 크기

channel에 넣는 데이터를 "바닥에 둘 수 없는 상자"로 비유하면 channel의 크기는 "상자 한 개를 둘 수 있는 책상(공간)"이다. 즉, 위의 상황에서 ch <- 9를 통해 택배원(프로그램)이 ch에 9를 들고 왔는데 상자를 둘곳이 없어서 계속 들고 서있는 것이다. 누군가 와서 상자를 가져갈때까지 택배원은 기다린다...  밑의 코드를 통해 확인해보자

func main(){
	ch := make(chan int)
	go square2(ch)
	ch <- 9
	fmt.Println("is it print?")
}

func square2() {
	for {
		time.Sleep(2 * time.Second)
		fmt.Println("sleep")
        	nine := <-ch
        	fmt.Println("i take it!!", nine)
	}
}

이 경우 square2()에서 nine := <-ch를 통해 프로그램이 들고있던 9를 가져와서 프로그램이 자유의 몸이 됐다. 그 결과 "is it print?"가 출력되며 프로그램이 종료된다.

 

그렇다고 무조건 크기를 선언했다고 프로그램이 자유의 몸이 되는건 아니다. 밑의 상황을 보자

func main(){
	ch := make(chan int,2)
	go square2()
	ch <- 9
    	ch <- 10
    	ch <- 11
	fmt.Println("is it print?")
}

func square2() {
	for {
		time.Sleep(2 * time.Second)
		fmt.Println("sleep")
	}
}

아까는 크기를 줘서 "is it print?"가 출력 됐지만 ch에 두개의 값을 더 추가함으로 다시 "sleep"의 굴레에 빠졌다...

이미 앞서 들어온 두 9,10이 책상 위에 올려져 있어서 11을 들고온 프로그램이 또 택배 들고 서있어서 그렇다.. 그럼 딱 한번만 책상 위 값을 빼주면 정상적으로 수행됨을 예상할 수 있다.

 

func main(){
	ch := make(chan int, 2) // 하지만 이렇게 큐에 사이즈를 줘서 빈공간을 만들어주면 대기하지 않고 종료됨. 즉, 빈공간이 없이 재고가 쌓이면 대기함.
	go square2(ch)
	ch <- 9
	ch <- 10
	ch <- 11
	fmt.Println("is it print?")
}

func square2(ch chan int) {
	nine := <-ch
	fmt.Println("i take it!", nine)
	for {
		time.Sleep(2 * time.Second)
		fmt.Println("sleep")
	}
}

잘 출력된다 ㅎㅎ channel의 크기가 프로그램에 어떤 영향을 미치는지 알아봤다. 그럼 이제 정상적으로 channel을 사용하는 법을 알아보자

 

channel에서 값 계속 가져오기!

func main(){
	ch := make(chan int)
	var wg sync.WaitGroup
	wg.Add(1)
	go square3(&wg, ch)
	for i := 0; i < 10; i++ {
		ch <- i * 2
	}
    wg.Wait()
}

func square3(wg *sync.WaitGroup, ch chan int) {
	for n := range ch { //무한반복을 돌면서 ch에 데이터가 올때까지 대기함. 데이터가 들어오면 출력하고 다시 대기.
		fmt.Println("Square : ", n*n)
		time.Sleep(time.Second)
	}
	wg.Done()
}

for n := range ch {} 를 통해 channel의 값을 무한히 받아온다. 코드를 실행해보자!!

뀨?

잘 출력되다가 deadlock에 걸려버린다...

square3에서 무한 반복되는 channel읽기에 wg.Done()이 수행되지 않아 main 고루틴도 square3 고루틴도 묶여버린 것이다... 어떻게 해결해줘야 할까?

 

channel close() !!

func main(){
	ch := make(chan int)
	var wg sync.WaitGroup
	wg.Add(1)
	go square3(&wg, ch)
	for i := 0; i < 10; i++ {
		ch <- i * 2
	}
    close(ch)
    wg.Wait()
}

func square3(wg *sync.WaitGroup, ch chan int) {
	for n := range ch { //무한반복을 돌면서 ch에 데이터가 올때까지 대기함. 데이터가 들어오면 출력하고 다시 대기.
		fmt.Println("Square : ", n*n)
		time.Sleep(time.Second)
	}
	wg.Done()
}

위에서의 square3과 같은 고루틴을 좀비 고루틴이라 한다. 이런 좀비 고루틴을 방지하기 위해 특정 channel을 다 사용하게 된 경우 close()로 채널을 닫아준다. 이를 통해 위의 문제를 해결할 수 있다! (정상 종료 됨!)

 

channel select!

하나의 함수에서 여러 채널의 값을 읽어와야 하는 경우 사용하는 방법이다. Tick과 After 채널을 이용해 테스트 해보자!

func main(){
	ch := make(chan int)
	var wg sync.WaitGroup
	wg.Add(1)
	go square4(&wg, ch)
	for i := 0; i < 10; i++ {
		ch <- i * 2
	}
	//채널을 닫아주지 않아도 무한 루트를 After를 통해 빠져나옴.
	wg.Wait()
}

func square4(wg *sync.WaitGroup, ch chan int) {

	tick := time.Tick(time.Second)            // 1초에 한번씩 신호를 주는 channel tick
	terminate := time.After(10 * time.Second) // 10초 후에 신호를 주는 channel terminate

	for {
		select { //select에서 여러 case가 동시에 걸리는 경우에는 random하게 case를 실행함. 그래서 출력 결과가 다를 수 있음.(Tick이 매 초마다 나오지 않음)
		case <-tick:
			fmt.Println("Tick")
		case <-terminate:
			fmt.Println("Terminated")
			wg.Done()
			return
		case n := <-ch:
			fmt.Println("Square: ", n*n)
			time.Sleep(time.Second)
		}
	}
}

코드의 주석을 보지 않고 그냥 실행하면 한가지 의문이 생긴다.

"close() 안했는데 어떻게 정상 종료했지?"

주석을 보면 time.After를 통해 10초 후에 terminate 채널에 값을 넣는 것을 알 수 있다. 이 경우 인자로 전달받은 wg에 Done을 넣어 wait중이던 wg이 wait 상태에서 벗어나며 프로그램이 정상 종료되는 것이다!

 

헷갈릴 수 있는 부분이 tick, terminate 변수인데 얘네는 ch처럼 각각 채널이다. 즉, 저 상황은 3가지의 채널에서 값을 받아와 수행되고 있는 상황이다.

 

 

 

기본적인 channel 사용법은 여기까지 쓰도록 하겠다~

728x90