본문 바로가기
golang

[golang] golang 채널(channel) 사용법, 사용예제

by devjh 2021. 12. 6.
반응형

이번 게시글에서는 channel 에 대해 정리합니다.

 

1. channel이란

일반적인 프로그래밍언어의 스레드는 전역화된 변수나, heap에 메모리를 잡거나 콜백을 제공하는 라이브러리를 이용해서 동기화작업 및 데이터를 공유하는 경우가 많습니다.

 

그러나 고 언어는 일반적인 스레드가 아닌 고루틴을 사용하며 channel이라는 고루틴끼리의 통로를 이용하여 동기화작업이나 데이터를 교환합니다.

 

고루틴의 channel은 일반적인 동기화 방식보다 저렴하며, 스레드에 안전하다는 장점이 있습니다.

 

2. channel을 통한 동기화 및 데이터교환

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	go doSomething(ch)
	result := <-ch
	fmt.Println(result)
}

func doSomething(ch chan string) {
	for i := 0; i < 3; i++ {
		fmt.Println("do something")
		time.Sleep(time.Second)
	}
	ch <- "finish"
}

원하는 데이터타입의 채널을 선언해주고 "<-" 연산자를 이용해 고루틴끼리 데이터를 주고받습니다.

 

고루틴은 커널레벨 스레드에 직접 붙는 형태가 아니라 고 스케줄러에서 관리하므로

메인루틴(메인스레드)이 끝나면 고루틴의 작업이 남아있더라도 종료되어버립니다.

 

그러나 메인루틴에 doSomething에 넘긴 채널의 수신대기(<-ch)를 하면  

두개의 고루틴을 동기화 할 수 있습니다.(채널에 데이터가 올때까지 메인루틴 블락)

3. select를 통한 데이터 교환

package main

import (
	"fmt"
	"time"
)

func sendHello(ch1 chan string) {
	time.Sleep(time.Second * 1)
	ch1 <- "hello"
}

func sendWorld(ch2 chan string) {
	time.Sleep(time.Second * 3)
	ch2 <- " world"
}

func receive(ch1 chan string, ch2 chan string, done chan string) {
	var msg string
	for {
		select {
		case msg1 := <-ch1:
			fmt.Println("msg: ", msg1)
			msg += msg1

		case msg2 := <-ch2:
			fmt.Println("msg: ", msg2)
			msg += msg2
			done <- msg
			return
		default:
			fmt.Println("default는 계속 호출됩니다.")
			time.Sleep(time.Second)
		}
	}
}

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	done := make(chan string)
	go sendHello(ch1)
	go sendWorld(ch2)
	go receive(ch1, ch2, done)
	result := <-done
	fmt.Println("result: ", result)
}

 

output

default는 계속 호출됩니다.
default는 계속 호출됩니다.
msg:  hello
default는 계속 호출됩니다.
msg:   world
result:  hello world

select를 사용하면 하나의 루틴에서 여러 채널의 수신대기작업을 진행시킬 수 있습니다.

채널들을 여러 고루틴에 전달하고, 하나의 리시브루틴에서 select를 사용하면 여러 고루틴과 통신할 수 있습니다.

 

 

4. 송신용, 수신용 채널

package main

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan string)
   go sendToChannel(ch)
   go receiveFromChannel(ch)
   <-time.After(time.Second * 1)
}

// 송신용
func sendToChannel(ch chan<- string) {
   ch <- "hello"
}

// 수신용
func receiveFromChannel(ch <-chan string) {
   result := <-ch
   fmt.Println(result)
}

아규먼트 패싱시 채널을 송신용이나 수신용 채널로 변경해 안전하게 사용할 수 있습니다.

 

 

5. 버퍼채널

package main

import (
   "fmt"
   "time"
)

func main() {
   // 버퍼채널은 두번째 매개변수로 버퍼의 갯수를 지정하며 생성합니다.
   ch := make(chan string, 1)
   //ch := make(chan string)
   go sendToChannel(ch)
   
   <-time.After(time.Second)
   fmt.Println("main routine finish")
}

func sendToChannel(ch chan string) {
   // 버퍼채널이 아니라면 수신대기가 없다면 블락
   // 버퍼채널은 수신대기가 없어도 블락되지않고 로직은 진행
   ch <- "hello"
   fmt.Println("send finish")
}

채널은 수신대기하는 루틴이 없다면 송신시 블락됩니다.

버퍼채널을 사용하면 수신대기가 없는 채널에 데이터를 보내더라도 블락되지 않습니다.

버퍼채널은 FIFO로 동작합니다.

 

 

버퍼 채널을 사용할시 output

$ go run main.go
send finish
main routine finish

 

일반 채널의 output

$ go run main.go
main routine finish

송신에 블락되지 않고 데이터를 쌓을 수 있다는 장점을 활용하면

버퍼채널을 이용하여 비동기큐를 구현할 수 있습니다.

 

6. 수신대기에 블락된 고루틴 종료

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	ch := make(chan string)
	for i := 0; i < 10; i++ {
		go someWork(ch)
	}
	<-time.After(time.Second)
	fmt.Println(runtime.NumGoroutine())
	close(ch)
	<-time.After(time.Second)
	fmt.Println(runtime.NumGoroutine())
}

func someWork(ch chan string) {
	someMsg := <-ch
	fmt.Println(someMsg)
}
output
11
1

수신대기하는 고루틴은 채널을 닫아주지 않으면 영원히 수신대기하며 리소스를 사용합니다.

채널을 닫아주면 블락이 풀리고 고루틴이 남은 로직을 진행하게 할 수 있습니다.

반응형

댓글