이번 게시글에서는 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
수신대기하는 고루틴은 채널을 닫아주지 않으면 영원히 수신대기하며 리소스를 사용합니다.
채널을 닫아주면 블락이 풀리고 고루틴이 남은 로직을 진행하게 할 수 있습니다.
'golang' 카테고리의 다른 글
[golang] go context의 활용1(go context사용법 및 예제1) (0) | 2021.12.07 |
---|---|
[golang] go context란 (0) | 2021.12.06 |
[golang] 고루틴(go routine)이란 (0) | 2021.11.15 |
[golang] golang defer 사용법 및 예제(golang의 catch는 defer안에서) (0) | 2021.11.15 |
[golang] go언어를 객체지향언어처럼 사용하는방법 (0) | 2021.08.16 |
댓글