Meandering Trajectory

select 문: 여러 채널에서 데이터 읽어 처리하기 본문

컴퓨터/GoLang

select 문: 여러 채널에서 데이터 읽어 처리하기

latentis 2017. 11. 10. 23:33

고루틴과 채널은 Go의 대표적인 기능이다. 고루틴은 서로 다른 작업이 함께 진행될 수 있도록 해주고 채널은 고루틴들이 통신을 통해 협력할 수 있게 한다.

select 문을 이용하면 여러 채널을 모니터링하다가 먼저 데이터가 도착한 채널의 데이터를 읽도록 할 수 있다. 이런 기능이 없다면 프로그래머는 각 채널별로 별도의 고루틴을 할당해야 한다. Go 스케쥴러의 특성상 지나치게 많은 고루틴을 사용하는 것은 부작용이 있을 수 있으므로 한개의 고루틴으로 여러 가지 일을 할 수 있다는 것은 여러 모로 좋은 일이다.

이렇게 한개의 스레드를 이용해 여러 데이터 소스(이 경우 채널)를 한꺼번에 들여다 보고 있다가 먼저 도착한 데이터를 우선 읽어 처리하는 방식의 프로그래밍을 이벤트 기반 프로그래밍(event-driven programming)이라고 한다.

select의 사용법

아래 코드는 a, b 두개의 채널에 대해 select 문을 사용한 예로 이렇게 하면 둘 중 어느 것이든 먼저 데이터가 도착한 채널의 데이터를 읽을 수 있다.

package main

import "fmt"

func main() {
    a := make(chan int32)
    b := make(chan string)

    select {
    case x := <-a:
        fmt.Printf("a 채널에서 읽은 데이터: %v\n", x)
    case y := <-b:
        fmt.Printf("b 채널에서 읽은 데이터: %v\n", y)
    }
}

하지만 이 코드를 컴파일하고 실행하면 다음과 같이 에러가 발생한다.

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
    /Users/qcollapse/work/md/blog/2017/11/main.go:17 +0x26f

그 이유는 채널에 쓰기를 하는 고루틴이 없기 때문이다. 그러니까 a, b 어느 채널에도 데이터가 배달되지 않으니 프로그래밍 영원한 대기 상태(deadlock)에 빠지는 것을 막기 위해 스스로 종료된 것이다.

이 에러를 통해 select 문의 중요한 동작을 알 수 있다:

select 문은 모니터링 중인 채널에 데이터가 도착할 때까지 영원히 기다린다.[각주:1]

제대로 실행되게 코드 수정하기

다음과 같이 코드를 수정하면 프로그램이 정상적으로 실행된다.

package main

import "fmt"

func main() {
    a := make(chan int32)
    b := make(chan string)

    go func() {
        a <- int32(7)
    }()

    go func() {
        b <- "일곱개"
    }()

    select {
    case x := <-a:
        fmt.Printf("a 채널에서 읽은 데이터: %v\n", x)
    case y := <-b:
        fmt.Printf("b 채널에서 읽은 데이터: %v\n", y)
    }
}

그런데 이 코드를 컴파일한 뒤 실행하면

b 채널에서 읽은 데이터: 일곱개

a 채널에서 읽은 데이터: 7

중 하나만 출력된다.

여기서 주목해야 할 점은 다음과 같다.

  1. 둘 중 한쪽 채널의 데이터만 출력된다.
  2. a 채널의 데이터가 출력될 때가 있고 b 채널의 데이터가 출력될 떄가 있다.

1번 현상의 원이은 select 문이 모니터링 중이던 채널 중 먼저 데이터가 도착하는 쪽의 case 절만 수행한 뒤 실행을 끝내기 때문이다. 즉 select 문은 최초의 데이터가 독찰할 때 딱 한번만 실행된다. (select 문은 loop가 아니다.)

2번과 같은 현상이 생기는 원인은 조금 더 복잡하다. 위 프로그램이 실행되면 main 함수와 두 개의 고루틴은 기본적으로 모두 다른 스레드에서 실행된다. 이때 채널에 데이터를 기록하는 두 개의 고루틴 중 어느 고루틴이 채널에 쓰기를 먼저 수행할 것인지는 운영체제의 상황에 따라 그때그떄 다르다. 그래서 a 채널에 데이터가 먼저 출력될 때가 있고 b 채널의 데이터가 먼저 출력될 때가 있는 것이다.

두 채널의 데이터가 모두 출력되게 하기

다음과 같이 select 문이 두번 실행되도록 하면 각 채널의 데이터가 모두 출력된다.

package main

import "fmt"

func main() {
    a := make(chan int32)
    b := make(chan string)

    go func() {
        a <- int32(7)
    }()

    go func() {
        b <- "일곱개"
    }()

    for i := 0; i < 2; i++ {
        select {
        case x := <-a:
            fmt.Printf("a 채널에서 읽은 데이터: %v\n", x)
        case y := <-b:
            fmt.Printf("b 채널에서 읽은 데이터: %v\n", y)
        }
    }
}

loop를 이용해 select 문이 두 번 실행되게 했다.



  1. 고루틴(혹은 스레드) 간이든 네트워크를 통한 것이든 통신과 관련하여 이렇게 대기하는 상황을 흔히 블로킹(blocking) 된다고 이야기 한다. select 문이 실행되는 시점에 읽을 데이터가 없을 때 대기하지 않고 select 문을 빠져나오게 하고 싶다면 select에 default case를 추가하면 된다. 물론 이경우 데이터가 읽힐 때까지 select 문을 적절히 반복해서 수행하는 것이 필요하다. [본문으로]

'컴퓨터 > GoLang' 카테고리의 다른 글

Go 코드에서의 에러 처리  (0) 2017.11.18
new와 make  (0) 2017.11.12
Go 언어: 빈 슬라이스와 nil  (0) 2017.10.20
클로저(Closure)와 데코레이터(Decorator) 패턴  (0) 2017.08.14
아니 Go의 상태가…  (0) 2017.08.12
Comments