GO언어 - 채널 방향, 버퍼링, Select

채널 방향 (channel directions)

채널은 고루틴 간의 통신 매체로 서로 데이터를 주고받을 수 있다. 기본적으로 채널은 양방향이지만 단방향 채널도 만들 수 있는데, 단방향 채널을 만들때는 make() 함수를 사용한다.

// 수신용 채널
c1 := make(<-chan Type)

// 송신용 채널
c2 := make(chan<- Type)

함수 인자를 사용하여 양방향 채널을 단방향 채널로 변환할 수 있다.
양방향 채널을 수신 전용 또는 송신 전용 채널로 변환할 수 있지만 그 반대의 경우는 불가능하다.

package main

import "fmt"

func sending(s chan<- string) {
	s <- "Hello"
}

func main() {
  // 양방향 채널 생성
  myChannel := make(chan string)
  
  // sending 함수를 호출하여 양방향 채널을 송신용 채널로 변경
  go sending(myChannel)

  fmt.Println(<-myChannel)
}

Output:

Hello

단방향 채널을 사용하면 프로그램의 타입 안전성을 높일 수 있다.

채널 버퍼링(channel buffering)

go 언어에서 채널은 unbuffered channel, buffered channel 두 가지 종류가 있다.

Unbuffered Channel

기본적으로 채널은 Unbuffered이다. Unbufferd 채널은 버퍼링 되지 않기 때문에 수신자가 데이터를 받을 때까지 송신자는 block이 걸리기 때문에 동기화가 보장된다.

package main

import "fmt"

func main() {
  // Unbuffered 채널 생성
  done := make(chan bool)

  fmt.Println("Main start")
  go func() {
    done <- ture
    for i:= 0; i < 3; i++ {
      fmt.Println("Goroutine1: ", i)
    }
  } ()

  time.Sleep(100 * time.Millisecond)
  go func() {
    for i:= 0; i < 3; i++ {
      fmt.Println("Goroutine2: ", i)
    }
  <-done
  } () 

  time.Sleep(100 * time.Millisecond)
  fmt.Println("Main end")
}

Output:

Main start
Goroutine2:  0
Goroutine2:  1
Goroutine2:  2
Goroutine1:  0
Goroutine1:  1
Goroutine1:  2
Main end

고루틴 1은 고루틴 2에서 데이터를 수신하기 전까지 block이 걸리기 때문에 위와 같은 순서로 출력을 하게 된다.

Buffered Channel

Buffered 채널은 수신자가 받을 준비가 되어 있지 않아도 지정된 버퍼만큼 데이터를 보내고 계속 다른 일을 수행할 수 있기 때문에 비동기적으로 동작할 수 있다.
buffered 채널에서 송신 측은 버퍼가 가득 찬 경우에만 차단되고 수신 측에서는 버퍼가 비어 있을 때만 차단된다.

Bufferd channel은 make(chan Type, N) 함수를 사용하여 만들 수 있다.
두 번째 매개변수 N에 사용할 버퍼 개수를 넣으면 된다.

myChannel := make(chan Type, N)
N이 0이면 Unbuffered Channel과 같다.

앞에서 썼던 형태를 bufferd channel 방식으로 변경해보면

package main

import "fmt"

func main() {
  // Bufferd 채널 생성
  done := make(chan bool, 1)

  fmt.Println("Main start")
  go func() {
    done <- ture
    for i:= 0; i < 3; i++ {
      fmt.Println("Goroutine1: ", i)
    }
  } ()

  time.Sleep(100 * time.Millisecond)
  go func() {
    for i:= 0; i < 3; i++ {
      fmt.Println("Goroutine2: ", i)
    }
  <-done
  } () 

  time.Sleep(100 * time.Millisecond)
  fmt.Println("Main end")
}

Output:

Main start
Goroutine1:  0
Goroutine1:  1
Goroutine1:  2
Goroutine2:  0
Goroutine2:  1
Goroutine2:  2
Main end

고루틴 1에서 메시지를 보내고 block 되지 않고 바로 다음 일을 수행하기 때문에 위와 같은 순서로 출력이 된다.

Select

Go 언어에서 select 문은 switch문과 비슷하지만 select 문에서 case문은 채널에서 전송 또는 수신 작업을 의미한다.

select는 case들 중 하나가 실행될 때까지 대기한다. 만약 다수의 case가 준비되는 경우에는 select가 무작위로 하나를 선택한다.
select문에 default문이 있으면, case문 채널이 준비되지 않더라도 대기하지 않고 바로 default문을 실행한다.

select {
  case <-ch1:
    // 채널1에 값이 들어왔을 때 수행
  case <-ch2:
    // 채널2에 값이 들어왔을 때 수행
  default:
    // 모든 채널에 값이 들어오지 않았을 때 수행
}

보통 select를 계속 처리하기 위해 for 반복문을 사용한다.

package main

import (
  "fmt"
  "time"
)

func send1(channel1 chan string) {

  time.Sleep(100 * time.Millisecond)
  channel1 <- "message1"
}

func send2(channel2 chan string) {

  time.Sleep(200 * time.Millisecond)
  channel2 <- "message2"
}

func main() {

  ch1 := make(chan string)
  ch2 := make(chan string)

  go send1(ch1)
  go send2(ch2)

  for {
    select {
      case msg1 := <-ch1: // ch1에 값이 들어오면 메세지 출력
        fmt.Println("recv: ", msg1)

      case msg2 := <-ch2: // ch2에 값이 들어오면 메세지 출력후 종료
        fmt.Println("recv: ", msg2)
        return
      }
  }
}

Output:

recv:  message1
recv:  message2

Categories:

Updated:

Comments