Post

23. 채널과 컨텍스트

23. 채널과 컨텍스트

✅ 1. 채널 사용하기

  • 채널(channel)이란 고루틴끼리 메시지를 전달할 수 있는 메시지 큐이다. 메시지 큐에 메시지들이 차례대로 쌓이고 메시지를 읽을 때는 들어온 순서대로 읽게 된다

1.1 채널 인스턴스 생성

1
var messages chan string = make(chan string)
  • 채널은 make() 함수로 생성한다
  • 채널 타입은 chan 키워드와 메시지 타입을 합쳐서 표현한다 chan string

1.2 채널에 데이터 넣기

1
messages <- "This is a message"
  • <- 연산자를 통해 채널에 데이터를 넣는다

1.3 채널에서 데이터 빼기

1
var msg string = <- messages
  • 데이터를 넣을 때와 마찬가지로 <- 연산자를 사용한다.
  • 좌변에 빼낸 데이터를 담을 변수가 오며 우변에 채널이 온다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func square(wg *sync.WaitGroup, ch chan int) {
	n := <-ch // 5. 데이터가 들어올 때까지 대기

	fmt.Printf("Square: %d\n", n*n)
	wg.Done() // 6. 작업 수행 후 종료
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int) // 1. 채널 생성
	wg.Add(1)
	go square(&wg, ch) // 2. 고루틴 실행
	ch <- 9            // 3. 채널에 데이터 넣음
	wg.Wait()          // 4. 작업 완료 대기
}

1.4 채널 크기

  • make(chan int)와 같이 채널을 생성하면 크기가 0인 채널이 생성된다
  • 크기가 0인 채널은 데이터가 들어오면 다른 고루틴에서 빼갈 때까지 대기한다
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)

	ch <- 7 // 무한 대기 -> deadlock
	// fatal error: all goroutines are asleep - deadlock!
	fmt.Println(<-ch)
}

1.5 버퍼를 가지는 채널

1
var chan string messages = make(chan string, 2)
  • make() 함수의 두 번째 인자로 버퍼의 크기를 적어준다
  • 버퍼가 가득 찬 상태로 데이터가 더 들어오면 크기가 0인 채널과 마찬가지로 빈 자리가 생길 때까지 대기한다

1.6 채널에서 데이터 대기

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
package main

import (
	"fmt"
	"sync"
	"time"
)

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 := range 10 {
		ch <- i * 2
	}
	// close(ch)
	wg.Wait()
}
  • for range 구문은 채널에서 데이터를 무한 대기하며 데이터를 빼내서 n에 값을 복사한다
  • wg.Wait()이 작업 완료를 기다리지만, 채널에 데이터가 들어오지 않아 무한 대기하게 되고 wg.Done()를 실행하지 못한다고 판단하여 데드락에 걸린다
  • close(ch)를 호출하여 채널이 닫혔음을 알려야 한다

이렇게 채널을 제때 닫아주지 않아 고루틴이 무한대기하는 경우, 좀비 루틴 또는 고루틴 락이라고 한다. 경량 스레드라고 해도 고루틴 또한 메모리를 사용하기 때문에 좀비 루틴을 조심해야 한다.

1.7 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 {
		case n := <-ch:
			fmt.Printf("Square: %d\n", n*n)
			time.Sleep(time.Second)
		case <-quit:
			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 := range 5 {
		ch <- i * 2
	}

	quit <- true
	wg.Wait()
}
  • select 문은 여러 채널에서 동시에 기다릴 수 있으며, 어떤 채널에서 데이터를 읽어왔다면 해당 구문을 실행하고 select문은 종료된다
  • 하나의 case만 처리하고 종료되기 때문에 반복하고 싶다면 for문과 함께 사용해야 한다

1.8 일정 간격으로 실행

  • time 패키지의 Tick() 함수로 원하는 시간 간격으로 신호를 보내주는 채널을 만들 수 있다
    • Tick()으로 생성한 채널은 일정 시간마다 현재 시각을 알려주는 Time 객체를 반환한다
  • After() 함수로 일정 시간 경과 후에 신호를 보내주는 채널을 생성할 수 있으며 반환된 채널은 Time 객체를 반환한다
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
          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)
              }
          }
    

1.9 채널로 생산자 소비자 패턴 구현하기

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
package main

import (
	"fmt"
	"sync"
	"time"
)

type Car struct {
	Body  string
	Tire  string
	Color string
}

var (
	wg        sync.WaitGroup
	startTime = time.Now()
)

func main() {
	tireCh := make(chan *Car)

	fmt.Println("Start Factory")
	wg.Add(2)

	go MakeBody(tireCh)
	go InstallTire(tireCh)
	wg.Wait()
	fmt.Println("Finish")
}

func MakeBody(tireCh chan *Car) {
	tick := time.Tick(time.Second)
	after := time.After(5 * time.Second)

	for {
		select {
		case <-tick:
			// Make a body
			car := &Car{}
			car.Body = "Sports Car"
			tireCh <- car
		case <-after:
			close(tireCh)
			wg.Done()
			return
		}
	}
}

func InstallTire(tireCh chan *Car) {
	for car := range tireCh {
		// Make a Tire
		time.Sleep(time.Second)
		car.Tire = "Winter tire"

		duration := time.Since(startTime)
		fmt.Printf("%.6f Complete Car: %v\n", duration.Seconds(), car)
	}
	wg.Done()
}

/*
Start Factory
2.001219 Complete Car: &{Sports Car Winter tire }
3.001570 Complete Car: &{Sports Car Winter tire }
4.001832 Complete Car: &{Sports Car Winter tire }
5.002079 Complete Car: &{Sports Car Winter tire }
6.002416 Complete Car: &{Sports Car Winter tire }
Finish
*/
  • 한 쪽에서 데이터를 생성해서 넣어주면 다른 곳에서 데이터를 빼서 사용하는 패턴을 생산자 소비자 패턴이라고 한다.
  • 채널을 여러 개 이용해서 생산자 소비자 패턴을 구현할 수 있다

✅ 2. 컨텍스트 사용하기

  • context 패키지에서 제공하는 기능으로, 작업을 지시할 때 작업 가능 시간, 작업 취소 등의 조건을 지시하는 명세서 역할을 한다

2.1 작업 취소가 가능한 컨텍스트

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
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func PrintSecond(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)
	ctx, cancel := context.WithCancel(context.Background())
	go PrintSecond(ctx)
	time.Sleep(5 * time.Second)
	cancel()

	wg.Wait()
}
  • context.WithCancel() 함수로 취소 가능한 컨텍스트를 생성한다
    • 상위 컨텍스트를 인수로 넣으면, 그 컨텍스트를 감싼 새로운 컨텍스트를 만들어준다
    • 상위 컨테스트가 없다면 기본 컨텍스트 context.Background()를 넣어준다
  • context.WithCancel()은 컨텍스트 객체와 cancel() 함수를 반환한다
    • 취소 함수를 이용해서 원할 때 취소할 수 있다
  • cancel()함수를 호출하면 컨텍스트의 Done() 채널을 닫는다

2.2 작업 시간을 설정한 컨텍스트

1
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  • 인자로 넣은 요청 시간이 지나면 컨텍스트의 Done() 채널을 닫는다
  • cancel()를 호출하면 작업 시간 전에도 취소 가능하다

2.3 특정 값을 설정한 컨텍스트

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
package main

import (
	"context"
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx := context.WithValue(context.Background(), "number", 9)
	go square(ctx)

	wg.Wait()
}

func square(ctx context.Context) {
	if v := ctx.Value("number"); v != nil {
		n := v.(int)
		fmt.Printf("Square:%d\n", n*n)
	}
	wg.Done()
}
  • context.WithValue()의 인자로 key, value를 넣을 수 있다
  • Value() 메서드로 값을 읽어올 수 있으며 반환 타입은 any이다
  • 외부에서 읽어올 때는 값 복사가 이루어진다
1
2
3
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WirthValue(ctx, "number", 9)
ctx = context.WithValue(ctx, "keyword", "Lilly")
  • 취소와 값 설정 모두 가능한 컨텍스트를 만들기 위해서는 상위 컨텍스트를 취소 가능한 컨텍스트로 넣어줘야 한다
This post is licensed under CC BY 4.0 by the author.