Meandering Trajectory

Go 채널 본문

컴퓨터/GoLang

Go 채널

latentis 2017. 8. 9. 23:28
09-go-chan.html

Go에 대한 인상

Go 언어는 처음에 걸었던 기대와 달리 약간 실망스럽다. 문법이 난잡하게 느껴지고 그래서 Go 언어로 작성된 프로그램도 지져분하게 느끼진다. 혁신적으로 보이는 특징도 별로 안 보인다. 21세기에 설계된 언어로 작성된 프로그램에서 세그멘테이션 폴트 에러를 보는 것도 별로 유쾌하지 않고 말이지.

하지만 한가지 맘에 드는 것이 있다. 그것은 Go 루틴과 채널이다. 이 둘을 빼면 Go는 그냥 그저 그런 명령형 언어였을 것이다. 이번 포스팅에서는 채널에 대해 간략히 살펴보려고 한다.

Go 루틴과 채널

C와 같은 전통적인 프로그래밍 언어에서 쓰이던 스레드 대신, Go 언어는 동시성 프로그램밍을 위해 Go 루틴이라는 특이한 기능을 제공한다. Go 루틴을 이용해 작성된 프로그램은 각 Go 루틴 별로 일정시간 스레드를 점유할 기회를 얻는다.(물론 각 스레드가 어떤 시점에 실제로 CPU를 점유하고 실행될지 말지는 OS가 정하기 때문에 스레드를 점유한다고 무조건 실제로 실행되는 것은 아니다.) Go 루틴은 과거 한때 유행하던 유져-레벨 스레드(user-level thread)와 유사하다.

Go 루틴 간에 정보를 주고받을 일이 전혀 없다면 정말 행복하겠지만 애석하게도 Go 루틴 간에 정보를 주고 받는 일은 흔하다. 채널은 이렇게 Go 루틴간 정보 교환에 쓰이는 데이터 타입이다.

채널 생성 및 사용

q := make(chan string)

이 문장을 이용해 1개의 문자열을 교환할 수 있는 채널을 생성할 수 있다.

채널로 문자열을 전달하는 방법은 다음과 같다.

q <- "super fast channel"

채널에서 문자열을 가져오고 싶다면 이렇게 하면 된다.

str <- q

일기전용/쓰기전용 채널

채널은 엄연히 Go의 타입 시스템의 한 자리를 차지하고 있는 정식 타입니다. 따라서 함수의 파라미터로 쓰일 수 있다.

func printStrFromChan(q chan string) {
    fmt.Println(<-q);
}

이 함수는 채널을 통해 받은 문자열을 화면에 출력한다.

함수의 파라미터로 쓰일 때 채널은 읽기 전용이나 쓰기 전용으로 선언될 수 있다. 아래 함수의 파라미터는 읽기 전용 파라미터이다.

func printStrFromChan(q <-chan string) {
    fmt.Println(<-q)
}

한편 다음과 같이 쓰면 쓰기전용 파라미터가 된다.

func strFromChan(q chan<- string) {
    q <- "String from channel!"
}

뭔가 헷깔리기 쉬운 문법이다. 하지만 타입에 있는 화살표(<-)의 위치를 잘 보면 읽기 전용과 쓰기 전용 타입을 왜 저렇게 만들었는지 이해할 수 있다.

우선 읽기 전용 채널의 경우

<-chan string

이 타입 선언을 오른쪽에서 부터 왼쪽으로 읽어나가면서 해석하면 다음과 같다. 문자열(string)이 채널(chan)을 통해 빠져나올 수만(<-) 있다.

쓰기 전용 채널의 경우도 같은 방식으로 이해할 수 있다.

chan<- string

역시 오른쪽에서부터 왼쪽으로 읽어 나가며 해석한다. 문자열(string) 들어갈 수만(<-) 있는 채널(chan)이다.

참고로 쓰기(읽기) 전용 채널에서 읽기(쓰기)를 수행하는 코드는 컴파일이 되지 않는다.

예를 들어 아래 코드는

func strFromChan(q chan<- string) {
    fmt.Println(<-q)
}

이런 오류와 함께 컴파일이 실패한다.

./test.go:14: invalid operation: <-q (receive from send-only type chan<- string)

코드 예

아래 코드는 채널을 사용한 간단한 예제 프로그램이다.

package main

import "fmt"
import "sync"

var wg sync.WaitGroup

func printStrFromChan(q chan string) {
    defer wg.Done()
    fmt.Println(<-q)
}

func main() {
    q := make(chan string)
    wg.Add(1)

    go printStrFromChan(q)
    q <- "Hello!"

    wg.Wait()
}

main 함수에서 채널 q에 문자열을 쓰고 고루틴으로 실행된 printStrFromChan()은 채널에서 문자열을 읽어 화면에 출력하는 예제다.

버퍼드(buffered) vs 언버퍼드(unbuffered)

앞서 소개한 채널들은 모두 언퍼버드 채널이다. 즉 채널에 한번에 1개의 데이터 아이템만 쓸 수 있다.

반면 버퍼드 채널은 동시에 넣을 수 있는 데이터 아이템이 여러개인 채널로 다음과 같은 방법으로 생성할 수 있다.

q := make(chan string, 10)

동시에 쓸 수 있는 슬롯이 10개 있는 채널을 생성한 예다.

언버퍼드 채널의 경우 채널의 데이터를 누군가 읽어가기 전에는 쓰기가 대기하게 된다. 예를 들어 앞선 예제 프로그램의 go 루틴을 호출하는 문장을 채널에 문자열을 쓰는 문장 뒤로 다음과 같이 옮겼다고 하자.

package main

import "fmt"
import "sync"

var wg sync.WaitGroup

func printStrFromChan(q chan string) {
    defer wg.Done()
    fmt.Println(<-q)
}

func main() {
    q := make(chan string)
    wg.Add(1)

    q <- "Hello!"          // (1)
    go printStrFromChan(q) // (2)

    wg.Wait()
}

이 코드를 빌드 후 실행하면 이런 에러를 출력하면 프로그램이 비정상 종료된다.

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /home/qcollapse/work/md/blog/2017/08/test.go:17 +0x96

이런 일이 발생하는 이유는 main 함수의 실행이 (1)번 문장에서 멈추기 때문이다. (2)번 문장이 실행되어야 채널에서 문자열을 읽어갈텐데 프로그램은 (1)번 문장에서 대기를 하고 있으니 (2)번 문장이 실행될 수가 없는 상황이다. 이런 상황을 교착상태(deadlock)이라고 하는데 에러 메시지의 첫번째 출을 보면 deadlock이란 단어가 떡하니 찍혀 있다. Go 언어는 이런 데드락 상태를 자동으로 감지해 프로그램을 종료해 주는 기능을 가지고 있다.

반면 버퍼드 채널을 쓰면 상황이 달라진다.

package main

import "fmt"
import "sync"

var wg sync.WaitGroup

func printStrFromChan(q chan string) {
    defer wg.Done()
    fmt.Println(<-q)
}

func main() {
    q := make(chan string, 1) // 버퍼드 채널!!!
    wg.Add(1)

    q <- "Hello!"             //(1)
    go printStrFromChan(q)    //(2)

    wg.Wait()
}

앞의 예제와 마찬가지로 채널에 데이터를 쓰는 (1)번 문장이 go 루틴을 실행하는 (2)번 문장보다 앞에 있다. 하지만 이번에는 채널 q가 버퍼드 채널이란 점이 다르다. 버퍼드 채널은 슬롯에 여유가 있는 경우 블로킹 되지 않기 대문에 위 프로그램은 정상적으로 실행된다.

하지만 이번에도 역시 채널에 2개의 문자열을 쓰도록 코드를 수정하면 여지없이 deadlock 에러가 발생한다.

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

클로저(Closure)와 데코레이터(Decorator) 패턴  (0) 2017.08.14
아니 Go의 상태가…  (0) 2017.08.12
Go: 리플렉션 #2  (0) 2017.07.16
Go: 리플렉션 #1  (0) 2017.07.15
Go 언어 - 벤치마크  (0) 2017.07.08
Comments