본문 바로가기
Go

[Go] 채널과 컨텍스트

by weero 2022. 3. 6.

[Tucker의 Go 언어 프로그래밍] 책을 참고

 

 

채널이란 고루틴끼리 메시지를 전달할 수 있는 메시지 큐로, 채널을 사용하면 뮤텍스 없이 동시성 프로그래밍이 가능하다. 컨텍스트는 고루틴에 작업을 요청할 때 작업 취소나 작업 시간, 추가 데이터 등을 설정할 수 있는 작업 명세서 역할을 한다.

→ 채널과 컨텍스트를 사용해 특정 데이터를 전달하거나, 특정 시간 동안만 작업을 요청하거나, 작업 도중에 작업 취소를 요청할 수 있다.

 

 

01 채널

채널 인스턴스 생성

var messages chan string = make(chan string)

chan string은 string 타입의 메세지를 전달하는 채널 타입이다.

 

 

채널에 데이터 넣기

messages <- "This is a message"
// 채널 인스턴스 <- 넣을 데이터

 

 

채널에서 데이터 빼기

var msg string = <- messages

데이터를 빼올 때 만약 채널 인스턴스에 데이터가 없으면 데이터가 들어올 때까지 대기한다.

 

package main

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

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)
	ch <- 9
	wg.Wait()
}

func square(wg &sync.WaitGroup, ch chan int) {
	n := <- ch

	time.Sleep(time.Second)
	fmt.Printf("Square: %d\n", n*n)
	wg.Done()
}

 

 

채널 크기

채널을 생성하면 크기가 0인 채널이 생성되고, 이는 채널에 들어온 데이터를 담아둘 곳이 없다는 뜻이다. 데이터를 넣을 때 보관할 곳이 없기 때문에 데이터를 빼갈 때까지 대기하게 된다.

 

 

버퍼를 가진 채널

내부에 데이터를 보관할 수 있는 메모리 영역을 버퍼라고 부른다. 채널은 내부에 버퍼를 가질 수 있다.

var chan string message = make(chan string, 2) // 버퍼를 가진 채널

버퍼가 2개인 채널이 만들어진다. 버퍼가 다 차면 버퍼가 없을 때와 마찬가지로 보관함에 빈 자리가 생길 때까지 대기한다. 그래서 데이터를 제때 빼가지 않으면, 버퍼가 없을 때처럼 고루틴이 멈추게 된다.

 

 

채널에서 데이터 대기 (영역 나누기)

고루틴에서 데이터를 계속 기다리면서 데이터가 들어오면 작업을 수행할 때(for n  := range ch), 데이터가 들어오길 계속 기다리기 때문에 결국 모든 고루틴이 멈추는 데드록(Deadlock)이 발생할 수 있다. 채널을 다 사용하면 close(ch)를 호출해 채널을 닫고 채널이 닫혔음을 알려줘야 한다. 채널에서 데이터를 모두 빼낸 상태이고 채널이 닫혔으면 for range문을 빠져나오게 된다.

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 := 0; i<10; i++ {
		ch <- i * 2
	}
	close(ch)
	wg.Wait()
}

 

 

select 문

채널에서 데이터가 들어오기를 대기하는 상황에서 만약 다른 데이턱가 들어오지 않으면 다른 작업을 하거나, 아니면 여러 채널을 동시에 대기할 수 있다.

select {
case n := <-ch1:
	...              // ch1 채널에서 데이터를 빼낼 수 있을 때 실행
case n2 := <-ch2
	...              // ch2 채널에서 데이터를 빼낼 수 있을 때 실행
}

select문은 여러 채널을 동시에 기다릴 수 있다. 만약 어떤 채널이라도 하나의 채널에서 데이터를 읽어오면 해당 구문을 실행하고 select문이 종료된다. 하나의 case만 처리되면 종료되기 때문에 반복해서 데이터를 처리하고 싶으면 for문과 함께 사용해야 한다.

 

package main

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

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 := 0; i < 10; i++ {
		ch <- i * 2
	}

	quit <- true
	wg.Wait()
}

 

 

채널을 이용해 역할을 나누는 방법

채널로 생성자-소비자 패턴 구현하는 예제를 보자.

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")
}

func MakeBody(tireCh chan *Car) {
	tick := time.Tick(time.Second)
	after := time.Tick(10 * time.Second)
	for {
		select {
		case <-tick:
			car := &Car{}
			car.Body = "Sports car"
			tireCh <- car
		case <-after:
			close(tireCh)
			wg.Done()
			return
		}
	}
}

func InstallTire(tireCh, paintCh chan *Car) {
	for car := range tireCh {
		time.Sleep(time.Second)
		car.Tire = "Winter tire"
		paintCh <- car
	}
	wg.Done()
	close(paintCh)
}

func PaintCar(paintCh chan *Car) {
	for car := range 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()
}
  • time.Tick() : 일정 시간 간격 주기로 신호를 보내주는 채널을 생성해 반환하는 함수
  • time.After() : 현재 시간 이후로 일정 시간 경과 후 신호를 보내주는 채널을 생성해 반환한다.

 

채널을 이용해 역할을 나누면 고루틴 하나를 사용할 때보다 더 빠르게 작업을 완료할 수 있고, 뮤텍스도 필요 없다. 이와 같이 한쪽에서 데이터를 생성해서  넣어주면 다른 쪽에서 생성된 데이터를 빼서 사용하는 방식을 생성자 소비자 패턴이라고 한다.

 

 

 

02 컨텍스트

컨텍스트 사용하기

컨텍스트(context)는 context 패키지에서 제공하는 기능으로 작업을 지시할 때 작업 가능 시간, 작업 취소 등의 조건을 지시할 수 있는 작업 명세서 역할을 한다. 새로운 고루틴으로 작업을 시작할 때 일정 시간 동안만 작업을 지시하거나 외부에서 작업을 취소할 때 사용한다. 또 작업 설정에 관한 데이터를 전달할 수도 있다.

 

 

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background()) // context 생성
	go PrintEverySecond(ctx)
	time.Sleep(5*time.Second)
	cancel()
	
	wg.Wait()
}

func PrintEverySecond(ctx context.Context) {
	tick := time.Tick(time.Second)
	for {
		select {
		case <- ctx.Done():
			wg.Done()
			return
		case <-tick:
			fmt.Println("Tick")
		}
	}
}

취소 가능한 컨텍스트를 생성한다. 두번째 취소 함수(cancel())를 사용해 원할 때 취소할 수 있다.

 

 

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

일정 시간 동안만 작업을 지시할 수 있는 컨텍스트

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

두 번째 인수로 시간을 설정하면 그 시간이 지난 뒤의 컨텍스트의 Done() 채널에 시그널을 보내서 작업 종료를 요청한다. 두번째 반환값으로 cancel 함수를 반환하기 때문에 작업 시간 전에 원하면 언제든지 작업 취소를 할 수도 있다.

 

 

특정 값을 설정한 컨텍스트

작업자에게 별도 지시사항을 추가해 작업을 지시할 때, 컨텍스트에 특정 키로 값을 읽어올 수 있도록 설정할 수 있다.

ctx := context.WithValue(context.Background(), "number", 9)
=> ctx.Value("number")