Featured image of post Golang: 13. Go 채널

Golang: 13. Go 채널

Go 언어에서 채널은 고루틴을 연결해주는 통로(파이프)다. 기본적으로 채널은 양방향이고 고루틴이 아래 이미지와 같이 동일한 채널을 통해 데이터를 보내거나 받을 수 있다.

Go 채널은 그 채널을 통하여 데이터를 주고 받는 통로라 볼 수 있다. 채널은 make() 함수를 통해 미리 생성되어야 하며, 채널 연산자 <- 을 통해 데이터를 보내고 받는다.

채널은 흔이 고루틴들 사이에 데이터를 주고 받을때 사용되는데, 상대편이 준비될 때까지 채널에서 대기함으로써 별도로 lock을 걸지 않고 데이터를 동기화하는데 사용된다.

아래 예제는 정수형 채널을 생성하고, 한 고루틴에서 그 채널에 123이란 정수 데이터를 보낸 후, 이를 다시 메인 루틴에서 채널로부터 123 데이터를 받는 코드이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main
 
func main() {
  // 정수형 채널을 생성한다 
  ch := make(chan int)
 
  go func() {
    ch <- 123   //채널에 123을 보낸다
  }()
 
  var i int
  i = <- ch  // 채널로부터 123을 받는다
  println(i)
}

채널을 생성할 때는 make()함수에 어떤 타입 데이터를 채널에서 주고받을 지 미리 지정해 주어야 한다. 채널로 데이터를 보낼 때는 채널명 <- 데이터 와 같이 사용하고, 채널로부터 데이터를 받을 경우에는 <- 채널명 와 같이 사용한다.

메인 루틴은 마지막에서 채널로부터 데이터를 받고 있는데, 상대편 고루틴에서 데이터를 전송할 때까지는 계속 대기하게 된다. 따라서, 이 예제에서는 time.Sleep()이나 fmt.Scanf() 같이 고루틴이 끝날 때까지 기다리는 코드를 적지 않는다.

Go 채널의 수신자와 송신자가 서로를 기다리는 속성을 이용하여 Go루틴이 끝날 때까지 기다리는 기능을 구현할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main
 
import "fmt"
 
func main() {
    done := make(chan bool)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        done <- true
    }()
 
    // 위의 Go루틴이 끝날 때까지 대기
    <-done
}

익명함수를 사용한 고루틴에서 어떤 작업이 실행되고 있을 때, 메인 루틴은 ←done에서 계속 수신하며 대기하고 있게 된다.

익명함수 고루틴에서 작업이 끝난 후, done채널에 true를 보내면, 수신자 메인루틴은 이를 받고 프로그램을 끝내게 된다.

Go 채널 버퍼링

Go 채널은 Unbufferd Channel과 Buffered Channel 2가지 형태가 있다.

Unbufferd Channel

위 예제에서 Go 채널은 Unbuffered Channel로서 이 채널에서는 하나의 수신자가 데이터를 받을 때까지 송신자가 데이터를 보내는 채널에 묶여있게 된다. (결과를 반환할 때까지 기다린다. blocking?)

Bufferd Channel

Bufferd Channel을 사용하면 수신자가 받을 준비가 되어 있지 않아도 지정된 버퍼만큼 데이터를 보내고 계속 다른 일을 수행할 수 있다.(unblocking?) 버퍼 크기까지 입력 작업이 블락되지 않는다.

버퍼 채널은 make(chan type, N) 함수를 통해 생성되는데, 두번째 파라미터 N에 사용할 버퍼 갯수를 넣는다.

예를들어 make(chan int, 10)은 10개의 정수형을 갖는 버퍼 채널을 만든다.

1
2
3
4
5
6
7
8
9
package main
 
import "fmt"
 
func main() {
  c := make(chan int)
  c <- 1   //수신루틴이 없으므로 데드락 
  fmt.Println(<-c) //코멘트해도 데드락 (별도의 Go루틴없기 때문)
}

버퍼 채널을 이용하지 않는 경우, 위 코드는 데드락 에러를 발생시킨다. 메인루틴에서 채널에 1을 보내면서 상대편 수신자를 기다리고 있는데, 이 채널을 받는 수신자 고루틴이 없기 때문이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main
 
import "fmt"
 
func main() {
    ch := make(chan int, 1)
 
    //수신자가 없더라도 보낼 수 있다.
    ch <- 101
 
    fmt.Println(<-ch)
}

위처럼 버퍼채널을 사용하면, 수신자가 당장 없더라도 최대버퍼 수까지 데이터를 보낼 수 있으므로 에러가 발생하지 않는다

버퍼에 적재된 데이터를 언젠가 가져갈 것이라 판단하고 최대 버퍼수를 넘지 않을때까지 보내도 데드락 에러를 발생시키지 않는다.

Unbufferd Channel 예제코드 오류 분석

1
2
3
4
5
6
7
8
package main
 
import "fmt"
 
func main() {
  c := make(chan int)
  c <- 1
}

unbuffered channel에서 발생하는 일반적인 데드락의 의미인 2개 이상의 작업 서로의 작업이 완료되기를 대기하는 교착 상태와는 약간 다르다고 볼 수도 있다.

왜냐하면 sender와 receiver중 누군가가 먼저 작업을 끝내야지 그 다음으로 누군가가 작업을 수행할 수 있는 것이 아니라 서로 동시에 협력해야만 unbuffered channel에 대한 대기를 끝낼 수 있는데 이 경우는 동시에 협력해줄 그 누군가(receiver)가 없어 무한정 기다리게되는 상황이다.

1
2
3
4
5
6
7
8
9
package main
 
import "fmt"
 
func main() {
  c := make(chan int)
  c <- 1
  <- c
}

그렇다고해서 위와 같이 자기 혼자 send와 receive를 하려 해도 Unbuffered channel은 sender와 receiver가 모두 ready여야 작업을 진행할 수 있기 때문에 불가능하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main
 
import "fmt"
 
func main() {
  c := make(chan int)
  go func() {
    <- c
  }()
  c <- 1
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main
 
import "fmt"
 
func main() {
  c := make(chan int)
  go func() {
    c <- 1
  }()
  <- c
}

따라서 다른 goroutine에서 A에 대한 receiver or sender 역할을 해주면 된다.

채널 파라미터

채널을 함수의 파라미터로 전달할 때, 일반적으로 송수신을 모두 하는 채널을 전달하지만, 특별히 해당 채널로 송신만 할 것인지 혹은 수신만 할 것 인지를 지정할 수 있다.

  • 송신 파라미터: chan<-
  • 수신 파라미터: <-chan

파라미터에 다른 송수신을 바꿔 넣게 되면 에러가 발생한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main
 
import "fmt"
 
func main() {
    ch := make(chan string, 1)
    sendChan(ch)
    receiveChan(ch)
}
 
func sendChan(ch chan<- string) {
    ch <- "Data"
    // x := <-ch // 에러발생
}
 
func receiveChan(ch <-chan string) {
    data := <-ch
    fmt.Println(data)
}

채널 닫기

채널을 열어 데이터를 보낸 후, close()함수를 사용하여 채널을 닫을 수 있다. (송신자만 가능) 채널을 닫게되면, 해당 채널로는 더이상 송신할 수 없지만, 계속 수신은 가능하다.

채널 수신에 사용되는 <- ch채널 메시지, 정상 수신 여부 2개의 반환값을 갖는다. 채널이 닫혔을 경우 두번째 리턴값은 false이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main
 
func main() {
    ch := make(chan int, 2)
     
    // 채널에 송신
    ch <- 1
    ch <- 2
     
    // 채널을 닫는다
    close(ch)
 
    // 채널 수신
    println(<-ch)
    println(<-ch)
     
    if _, success := <-ch; !success {
        println("더이상 데이타 없음.")
    }
}

채널 range 문

채널에서 송신자가 송신을 한 후, 채널을 닫을 수 있다. 수신자는 임의의 갯수의 데이터를 채널이 닫힐 때까지 수신할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
 
func main() {
    ch := make(chan int, 2)
 
    // 채널에 송신
    ch <- 1
    ch <- 2
 
    // 채널을 닫는다
    close(ch)
 
    // 방법1
    // 채널이 닫힌 것을 감지할 때까지 계속 수신(무한 for 루프)
    for {
        if i, success := <-ch; success {
            println(i)
        } else {
            break
        }
    }
 
    // 방법2
    // 위 표현과 동일한 채널 range 문 (방법 1의 간결한 표현 세부 동작은 같음)
    for i := range ch {
        println(i)
    }
}

채널 range문은 range 키워드 다음에 명시한 채널로부터 데이터를 계속 수신하다가 채널이 닫힌 것을 감지하면 for 루프를 종료한다.

채널 select 문

select문은 복수 채널들을 기다리면서 준비된 채널을 실행하는 기능을 제공한다.

select문은 여러개의 case문에서 각각 다른 채널을 기다리다가 준비가 된 채널 case를 실행한다. select문은 case 채널들이 준비되지 않으면 계속 대기하게 되고, 가장 먼저 도착한 채널의 case를 실행한다. 만약 복수 채널에 신호가 오면, Go 런타임이 랜덤하게 그중 한 개를 선택하게 된다. select문에 default문이 있으면, case문 채널이 준비되지 않더라도 계속 대기하지 않고 바로 default문을 실행한다.