알고나면 스트레스가 줄어드는 golang channel (with select)

타미·2021년 2월 28일
3

Hello Golang

목록 보기
5/7
post-thumbnail

channel에 send할 때는 receive할 준비가 되어 있어야 한다.

c := make(chan string)
c <- "Hello"
<-c // 오류 발생: fatal error: all goroutines are asleep - deadlock!

위와 같은 경우, 코드가 순차적으로 실행된다.
2번째 라인이 실행된 후에 3번째 라인이 실행되는데,
2번째 라인에서 send하려는 시점에 3번째 라인에서 receive할 준비가 되지 않아서 보낼 수가 없다.


c := make(chan string, 1)
c <- "Hello"
fmt.Println(<-c) // 출력: Hello

버퍼가 있는 채널이라면 정상적으로 동작할 수 있다.
send하려는 시점에 receive할 준비는 되어있지 않지만, 버퍼에 넣어두면 되니까 괜찮다.


c := make(chan string)
go func() {
	c <- "Hello"
}()
fmt.Println(<-c) // 출력: Hello

정상적으로 실행된다. 왜 가능할까?
고루틴은 스케줄링되어 실행되기 때문이다.

실행 순서
1. 고루틴이 스케줄링 된다.
2. <-c에 왔지만 send된 데이터가 없어서 대기
3. 어느 시점에 고루틴이 실행된다. c <- "Hello"
4. fmt.Println(<-c)


range는 channel이 close되어야 끝난다.

channel에서 데이터를 receive 받는 방법으로 range가 있다. 이때, range는 channel이 close되어야 끝난다. 고로, channel을 close해주지 않는다면 영원히 기다리면서 deadlock이 발생한다.

ch := make(chan int, 1)
ch <- 101
for value := range ch {
	fmt.Println(value)
}
Output:
101
fatal error: all goroutines are asleep - deadlock!

101을 보낸 것은 잘 받아서 처리했지만, 그 후에 close되지 않아서 deadlock이 발생한다.


close된 channel에 send 할 수 없다.

ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel

close된 channel에서 receive 할 수 있다.

ch := make(chan int, 2)

var wg sync.WaitGroup
wg.Add(1)
go func() {
	ch <- 10
	ch <- 11
	wg.Done()
}()

wg.Wait()
close(ch)
fmt.Println(<-ch) // 10
fmt.Println(<-ch) // 11
fmt.Println(<-ch) // 0

waitGroup에 대해 잘 모르는 분이라면...
버퍼가 2개인 채널에 10, 11을 send할 때까지 기다린 후에,
channel을 close하고 receive하는 코드라고 이해하자.

close된 channel에 send 할 수는 없지만 receive할 수는 있다.
버퍼에 저장된 값은 고대로 읽어오고 값이 없다면 zero value를 가져온다.


nil 채널에 send/receive하면 영구 대기


select

select문은 무한루프가 아니지만, case가 올 때까지 기다린다.

select는 무한루프가 아니기 때문에 for-select 형식으로 사용된다.
하지만, case가 발생할 때까지 대기한다.

ch := make(chan int)
takeSomeTime := func()  {
	go func() {
		time.Sleep(time.Second * 2)
		ch <- 1
	}()
}

start := time.Now()
takeSomeTime()
		
select {
case <-ch:
	fmt.Println(time.Since(start)) // 2.004948752s
}

takeSometime은 2초간 대기한 후에 채널로 데이터를 보내는 함수이다. 요 데이터를 보내야 case는 값을 receive할 수 있다. 만약 select문에서 <-ch 발생을 기다리지 않고 넘어간다면 Output이 출력되지 않았을 것이다. 하지만 <-ch이 발생할 때까지 기다렸기 때문에 대략 2초 후에 출력되었다.

default가 있다면 고놈을 실행한다.

select에서 case는 순차적으로 실행되지 않는다.

select에서 case는 무작위로 실행되며, 각 case는 모두 비슷한 확률로 실행된다. (균일한 의사 무작위 선택)

c1 := make(chan interface{})
close(c1)
c2 := make(chan interface{})
close(c2)

var c1Count, c2Count int
for i := 1000; i >= 0; i-- {
	select {
	case <-c1:
		c1Count++
	case <-c2:
		c2Count++
	}
}

fmt.Printf("c1Count: %d\nc2Count: %d\n", c1Count, c2Count)
Output
c1Count: 519
c2Count: 482

대략 비슷하게 case가 실행된다.
(아까 위에서 언급했듯이, close해도 receive할 수 있기 때문에 위의 예는 문제가 없이 동작한다.)

case에 함수가 있다면 그 함수가 끝난 후, 다른 case를 검사한다.

select {
case <-ch:
case ch <- doSomething:
}

channel로 값이 오는 것을 기다리는 것 외에도 channel에 값을 전달하는 식으로도 select문을 이용할 수 있다. 이때, doSoemthing이 끝날 때까지 다른 case는 검사하지 않는다.

longFunction := func() interface{} {
	defer fmt.Println("end long function")
	fmt.Println("start long function")
	time.Sleep(time.Second)
	return nil
}

shortFunction := func() interface{} {
	defer fmt.Println("end short function")
	fmt.Println("start short function")
	return nil
}

chan1 := make(chan interface{},10)
chan2 := make(chan interface{},10)

for i := 0; i < 10; i++ {
	select {
	case chan1 <- longFunction():
	case chan2 <- shortFunction():
	}
}
start long function
end long function
start short function
end short function
start long function
end long function
start short function
end short function
start long function
...

소요시간이 긴 long function이 끝난 후에 다른 case의 short function이 실행되는 것을 볼 수 있다. select case에서 값을 send하는 경우에는 말 그대로 해당 case line이 실행된다고 이해하면 되겠다.


case에 있는 함수는 끝까지 실행하지만, 적절하지 않은 상황이라면 다른 case를 실행한다.

select {
case chan1 <- funcA():
case chan2 <- funcB():
}

위와 같은 상황에서 funcA를 먼저 처리한다고 가정하자. 고러면 funcA 함수는 끝까지 실행된다. 그런데 끝까지 실행하고 보니 chan1이 데이터를 받지 못하는 상황(버퍼가 꽉찬 채널)일 수 있다. 그러면 funcB()로 차례가 넘어간다.

chan1 := make(chan interface{})
chan2 := make(chan interface{},10)

functionA := func() interface{} {
	defer fmt.Println("end a function")
	fmt.Println("start a function")
	return nil
}

functionB := func() interface{} {
	defer fmt.Println("end b function")
	fmt.Println("start b function")
	return nil
}

for i := 0; i < 10; i++ {
	select {
	case chan1 <- functionA():
		fmt.Println("case A running")
	case chan2 <- functionB():
		fmt.Println("case B running")
	}
}

위 예제를 보면 channel에 데이터를 보내기는 하지만 받는 부분이 없다. ch1의 경우는 버퍼가 없기 때문에 데이터를 보낼 수 없고, ch2의 경우에는 버퍼가 넉넉하게 있기 때문에 데이터를 보낼 수 있다. 위 코드를 실행하면 다음과 같은 결과가 나온다.

start a function
end a function

start b function
end b function

case B running

start a function
end a function

start b function
end b function

case B running

case문에 있는 funtionA(), functionB() 모두 실행되지만 실질적으로 채널에 데이터를 보낸 것은 functionB()만 가능하다.

profile
IT's 호기심 천국

0개의 댓글