Go/Golang - Channel & Context
Channel
Channel 이란 무엇인가?
Channel(채널)
이란 고루틴끼리 메세지를 전달받을 수 있는 메세지 큐를 의미한다.
Channel 사용업
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
| func square(wg *sync.WaitGroup, ch chan int) {
// square goroutine은 빈 채널이다. 따라서 이 channel에 데이터가 들어오기까지
// 기다린다.
n := <- ch
time.Sleep(time.Second) // 그럼 이건 왜있는거? -> 없어도 정상동작함
fmt.Printf("Square: %d\n", n*n)
// 출력 동작 후 wg -1한다.
wg.Done()
}
func main() {
var wg sync.WaitGroup
// 채널 생성
// var testCh chan string = make(chan string)
// 채널변수 키워드 메세지타입 ( 채널 타입 )
// 여기서 채널타입은 채널_키워드 + 메세지_타입 을 의미한다.
ch := make(chan int) // make함수로 만든다.
wg.Add(1)
// waitgroup에 1개 추가하고, go routine 하나 만든다.
go square(&wg, ch)
// main thread에서 9를 넣으면 기다리고 있던 square가 동작한다.
ch <- 9
// wg 끝날때까지 기다렸다가 종료한다.
wg.Wait()
}
|
Channel에 버퍼(Buffer) 부여하기
Channel을 위와 같이 선언하게 되면 channel의 공간크기는 0
이 된다. 그래서 Buffer가 다 차게 되는 경우에는 make() 함수를 사용해 버퍼의 크기를 적어준다.
1
| var chan string messages = make(chan string, 2)
|
Channel에서 데이터 대기하기
channel은 제떄 여는 것 만큼, 제때 닫아주는 것 또한 중요
하다. 이렇게 channel에서 제때 닫아주지 않아서 고루틴에서 데이터를 기다리며 무한 대기하는 상태를 GoRoutine Leak(릭)
이라고 한다.
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
| func square(wg *sync.WaitGroup, ch chan int) {
// 데이터가 들어오길 기다렸다가, 데이터를 빼내고 for문 실행
// channel을 전부 소비한 뒤에도 닫히지 않고 계속 열려있으므로
// 계속 데이터 대기상태를 유지한다 -> DeadLock 발생한다.
for n := range ch { // 데이터 다 쓰고, channel close되면 range 빠져나간다.
fmt.Printf("square : %d\n", n*n)
time.Sleep(time.Second)
}
wg.Done()
}
func main() {
var wg sync.WaitGroup // wg 선언
ch := make(chan int) // channel 생성
wg.Add(1)
go square(&wg, ch) // 고루틴 생성
for i := 0; i < 10; i++ {
ch <- i * 2 // channel 안에 0,2,4,6,8,10,12,14,16,18
}
// 채널을 닫는다. 채널에서 데이터를 모두 빼낸 상태이고, 채널이 닫혔다면 for range문을 빠져나간다.
close(ch)
wg.Wait()
}
|
select
channel에 데이터가 들어오지 않고 있는 상황에서 다른 동작을 시키거나, 여러 채널을 동시에 대가하고 싶을떄는 select
를 사용한다. 단 하나의 channel에서 데이터를 가져오더라도 해당 구문은 실행되고 select 구문은 종료된다.
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
| func square(wg *sync.WaitGroup, ch chan int, quit chan bool) {
for {
select { // channel ch와 quit을 기다린다.
case n := <- ch: // ch에서 읽을 수 있다면 먼저 읽는다.
fmt.Printf("Square: %d\n", n*n)
time.Sleep(time.Second)
case <- quit: // ch에서 더이상 가지고 올 데이터가 없을 때 Done() 을 실핼시켜 종료한다.
wg.Done()
return
}
}
}
func main() {
var wg sync.WaitGroup
ch := make(chan int)
quit := make(chan bool)
wg.Add(1)
go square(&wg, ch, quit)
for i := 0; i < 10; i++ {
ch <- i * 2
}
quit <- true
wg.Wait()
}
|
select 이용한 signal
이 select를 사용하면, 처리순서를 정할 수 있다. 같은 main() 을 사용한다는 가정하에 다음과 같이 tick, terminates, ch 의 세가지 channel에 대한 처리 순서는 tick > terminate > ch 순이다. tick과 ch의 채널에 대한 소비가 번갈아 이루어지다가(tick은 초당 시그널이 생기는데 버퍼가 없으므로 ch 호출) 10초가 지난 뒤 terminate 호출이 걸린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| func square(wg *sync.WaitGroup, ch chan int) {
tick := time.Tick(time.Second) // 1초 간격 시그널을 주는 채널 반환
terminate := time.After(10*time.Second) // 10초 이후 시그널
for {
select {
case <- tick:
fmt.Println("Tick")
case <- terminate:
fmt.Println("Terminated!")
wg.Done()
return
case n := <- ch:
fmt.Printf("Square: %d\n", n * n)
time.Sleep(time.Second)
}
}
}
|
channel을 이용한 GoRoutine
channel을 이용하면, Go Routine을 작성할 때 마치 컨테이너 벨트 시스템처럼 역할을 나누어 분배할 수 있다. 처음 한 루틴을 처리하는 시간은 linear하게 동작하므로 일반 방식과 동일하게 소요되지만, 이후의 단계들은 pararell하게 진행되고 있었기 때문에 한 step씩만의 간격을 두고 빠르게 처리가 가능하다. 마치 카드돌려막기의 느낌이랄까. 이런 방식을 사용하면 뮤택스도 필요없을 뿐더러 고루틴 하나를 사용한 경우보다 빠르게 작업을 처리할 수 있다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| package main
import (
"fmt"
"sync"
"time"
)
type Car struct {
Body string
Tire string
Color string
}
var wg sync.WaitGroup
var startTime = time.Now()
func main() {
tireCh := make(chan *Car)
paintCh := make(chan *Car)
fmt.Printf("start Factory\n")
wg.Add(3)
go MakeBody(tireCh)
go InstallTire(tireCh, paintCh)
go PaintCar(paintCh)
wg.Wait()
fmt.Println("close the factory")
}
// step 1. 차체 제작
func MakeBody(tireCh chan *Car) {
tick := time.Tick(time.Second)
after := time.After(10 * time.Second)
for {
select {
case <- tick: // 1초간격으로 차체 생산
car := &Car{}
car.Body = "Sports Car"
tireCh <- car
case <- after:
close(tireCh)
wg.Done()
return
}
}
}
// step 2. 타이어 설치
func InstallTire(tireCh, paintCh chan *Car) {
for car := range tireCh { // tireCh 종료될때까지 대기하면서 동작
time.Sleep(time.Second)
car.Tire = "Winter tire"
paintCh <- car
}
wg.Done()
close(paintCh) // 끝날때 paintCh 종료
}
// step 3. 도색
func PaintCar(paintCh chan *Car) {
for car := range paintCh { // paintCh 종료될때까지 동작
time.Sleep(time.Second)
car.Color = "Red"
duration := time.Now().Sub(startTime)
fmt.Printf("%.2f Complete Car: %s %s %s\n", duration.Seconds(),
car.Body, car.Tire, car.Color)
}
wg.Done()
}
|
Context
Context란 무엇인가?
Context는 작업을 지시할 때, 작업 가능 시간, 취소 요건등 작업 조건을 명세하는 context 패키지의 기능을 의미한다.
Context 작업취소
작업취소 기능의 Context. 작업을 지시한 지시자가 원할 때 작업취소를 알릴 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| func PrintEverySecond(ctx context.Context) {
tick := time.Tick(time.Second)
for {
select {
case <- ctx.Done():
wg.Done()
return
case <- tick:
fmt.Println("Tick")
}
}
}
func main() {
wg.Add(1)
// 컨텍스트 생성
// 상위 컨텍스트가 없다면 가장 기본적인 context인 background를 넣어준다.
ctx, cancel := context.WithCancel(context.Background())
go PrintEverySecond(ctx)
time.Sleep(5*time.Second)
// 컨텍스트에서 반환받은 cancel함수 실행
cancel()
wg.Wait()
}
|
Context 작업시간 설정
비슷한 방식으로 일정 시간동안만 작업할 수 있는 컨텍스트
1
| ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
Context 특정값 설정
비슷한 방식으로 특정값을 추가하는 컨텍스트
1
| ctx := context.WithValue(context.Background(), "number", 9)
|
[참고] Producer/Consumer vs Publish/Subscribe Patterns
비슷해 보이지만 분명 다른 패턴이다. > stackoverflow 정리글 Producer/Consumer는 한쪽에서 생산해 다른쪽에서 소비하는 만큼 ‘분배’의 개념인 반면, Pub/Sub은 모든 구독자가 같은 메세지를 받게 하는 ‘구독’의 느낌이 강하다.
Questions
Q1. Channel 이란 무엇인가?
1
| `Channel(채널)` 이란 고루틴끼리 메세지를 전달받을 수 있는 메세지 큐를 의미한다.
|
Channel의 생성, 입력, 출력을 예시를 들어 설명하라
1
2
3
4
5
6
7
8
9
| 1) 생성
ch := make(chan int) // make함수로 만든다.
// orElse var ch chan int = make(chan int)
2) 입력
ch <- 9
3) 출력
n := <- ch
|
Q2. 아래 코드를 주석 후 실행하면 어떨게 되는가? 이유는?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| func square(wg *sync.WaitGroup, ch chan int) {
n := <- ch
// time.Sleep(time.Second) -> 주석처리
fmt.Printf("Square: %d\n", n*n)
wg.Done()
}
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go square(&wg, ch)
ch <- 9
wg.Wait()
}
// 1) 정상 실행된다.
// 2) 굳이 sleep이 없어도 ch에서는 값이 들어올때까지 대기한다.
|
Q3. 아래 코드는 어떤 부분이 잘못되었는가?
2가지 방법(채널 닫기, select 활용)으로 고쳐보아라
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
| // 문제, 1번답
func square(wg *sync.WaitGroup, ch chan int) {
for n := range ch {
fmt.Printf("square : %d\n", n*n)
time.Sleep(time.Second)
}
wg.Done()
}
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go square(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
// close(ch)
wg.Wait()
}
// 1) DeadLock에 빠진다 > 고루틴(Leak) 발생한 상태이다 -> square의 range함수를 빠져나오지 못하므로 결론적으로 wg.Done을 칠 수가 없다.
// 2) channel을 닫아준다. -> close(ch)
// 3) 2가지 channel을 만들고 select 구문 > case 에 Done()을 삽입한다.
|
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
29
| // 2번 답
func square(wg *sync.WaitGroup, ch chan int, quit chan bool) {
for {
select { // channel ch와 quit을 기다린다.
case n := <- ch: // ch에서 읽을 수 있다면 먼저 읽는다.
fmt.Printf("Square: %d\n", n*n)
time.Sleep(time.Second)
case <- quit: // ch에서 더이상 가지고 올 데이터가 없을 때 Done() 을 실핼시켜 종료한다.
wg.Done()
return
}
}
}
func main() {
var wg sync.WaitGroup
ch := make(chan int)
quit := make(chan bool)
wg.Add(1)
go square(&wg, ch, quit)
for i := 0; i < 10; i++ {
ch <- i * 2
}
quit <- true
wg.Wait()
}
|
Q4. 생산자-소비자-패턴 (Producer Consumer Pattern) 은 무엇인가?
1
2
3
4
| 한쪽에서 데이터를 생성해서 넣어주면 다른쪽에서 데이터를 뺴서 사용하는 방식을 말한다.
producer/consumer와 publish/subscribe 패턴 간 비교
// https://stackoverflow.com/questions/42471870/publish-subscribe-vs-producer-consumer
|
Q5. context란 무엇인가, 다음 코드는 무엇을 의미하는가?
1
2
3
4
5
| ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "number", 9)
// 1) Context는 작업을 지시할 때, 작업 가능 시간, 취소 요건등 작업 조건을 명세하는 context 패키지의 기능을 의미
// 2) 취소 + 값설정을 한 context
|