Home 8. channel & context
Post
Cancel

8. channel & context

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
This post is licensed under CC BY 4.0 by the author.