Programming Language/go

Go 언어 프로그래밍 - 채널과 컨텍스트

김크리 2021. 8. 9. 23:10

『Tucker의 Go 언어 프로그래밍』 스터디 요약 노트입니다.


채널

채널은 고루틴간 메시지 큐이다.(FIFO) 고루틴끼리 메시지를 전달할 수 있도록 한다. Thread-safe queue 라고도 한다.multi thread 환경에서 lock 없이 사용할 수 있는 큐이다.

사용방법

//1. make()로 채널 인스턴스 생성
var messages chan string = make(chan string)
//chan 은 channel 키워드

//2. 채널에 데이터 넣기
messages <- "This is a message"
//<- 연산자를 이용하여 채널 인스턴스에 데이터를 삽입

//3. 채널에서 데이터 빼기
var msg string = <- messages
//<- 연산자를 이용하여 채널 인스턴스에서 뺀 데이터를 변수에 삽입
package main

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

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

	wg.Add(1)
	//main 이 아닌 go루틴
	//lock 필요없이 채널 간 데이터 공유
	go square(&wg, ch)
	ch <- 9
	wg.Wait()
}

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

	time.Sleep(time.Second)
	fmt.Println("Square : ", n*n)
	wg.Done()
}

채널의 크기

기본 크기는 0이다. 

package main

import (
	"fmt"
	"time"
)

func main() {
	//0개짜리 채널생성(기본값이 0)
	ch := make(chan int)

	go square()
	//채널에 값을 계속 넣음...근데 채널의 값을 가져가는 것이 없다.
	//채널 값을 가져가는 것이 없어 대기중
	//무한대기
	ch <- 9
	//채널 무한대기로 출력안됌
	fmt.Println("Never print")
}
func square() {
	for {
		time.Sleep(2 * time.Second)
		fmt.Println("sleep")
	}
}

채널에서 데이터대기

데이터를 계속 읽어오는 경우가 발생 한다. 이럴경우 데드락이 발생한다.

좀비 고루틴 : 채널을 닫아주지 않아서 무한 대기를 하는 고루틴을 좀비 고루틴/고루틴 릭(leak)이라고 한다.

이런 경우 close로 채널을 닫아준다.

package main

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

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

	wg.Add(1)
	go square(&wg, ch)

	for i := 1; i < 10; i++ {
		ch <- i * 2
	}
    //3. 좀비고루틴을 막기 위해 채널을 닫아줌
    close(ch)
	wg.Wait() //1.메인 고루틴의 중단
}
func square(wg *sync.WaitGroup, ch chan int) {
	//2.무한하게 데이터를 채널에서 가져오려고 대기
	for n := range ch {
		fmt.Println("Square : ", n*n)
		time.Sleep(time.Second)
	}
	wg.Done()
}

//데드락 발생

select문

여러 채널에서 동시에 데이터를 기다릴때 사용한다. switch 문과 비슷하게 생겼지만 다르다.

select {
	case n:= <-ch1
    ...
	case n2:= <-ch1
    ...
    case ...
}

select 문은 일정 간격으로 실행한다.

package main

import (
	"fmt"
	"sync"
	"time"
)
/*
time 패키지의 tick()은 일정 간적으로 신호를 주는 채널을 반환한다.
After()는 일정 시간 대기 후 한번만 신호를 주는 채널을 반환한다.
*/
func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 1; i < 10; i++ {
		ch <- i * 2
	}
	close(ch)
	wg.Wait() //메인 고루틴의 중단
}
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: //tick에서 데이터를 뽑을 수 있는 경우,
			fmt.Println("tick")
		case <-terminate:
			fmt.Println("Terminated")
			wg.Done() //10초 지나면 프로그램 종료
			return
		case n := <-ch:
			fmt.Println("Square : ", n*n) //채널에 들어온 데이터 출력
			time.Sleep(time.Second)
		}
	}
	wg.Done()
}

채널로 생산자/소비자 패턴 구현

자원을 관리하기 위해 역할을 나누는 방법으로 생산자(producer)/소비자(consumer) 패턴이 있다. 예제 참고(ex25.7.go)

컨텍스트(Context)

작업을 지시할 때 작업 가능 시간, 작업 취소 등의 조건을 지시할 수 있는 작업 명세서 역할을 한다.

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	//취소 가능한 컨텍스트 생성
	ctx, cancel := context.WithCancel(context.Background())
	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")
		}
	}
}

특정 값 설정

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)
	}
	wg.Done()
}

컨텍스트 래핑

컨텍스트를 만들때 래핑하여 어러가지 긴으을 만들 수 있다.

채널로 발행(Publisher)/구독(Subscriber) 패턴 구현

옵져버 패턴과 거의 유사하다.

옵져버 패턴이란, 어떠한 이벤트 subject 가 있다면 해당 이벤트가 언제 끝나는지 감시한다. subject 가 이벤트가 발생함에 따라 notify를 준다. 이러한 패턴이 옵저버 패턴이다.

옵저버 패턴은 기본적으로 동기식이다. 이때 동기로 인해 발생하는 문제를 해결하기 위해서 pub/sub 패턴이 확장되어 나왔다.

코드참고(ex25/pubsub.go)

참고

Tucker의 Go언어 프로그래밍 - Go가 온당

https://www.youtube.com/c/TuckerProgramming/videos

https://github.com/tuckersGo