[Go] 18. 채널

2024. 3. 12. 18:07Go

반응형

channel은 고루틴간에 메시지를 전달하는 메시지 큐입니다.


1. 기본 문법

채널 인스턴스는 make() 내장함수로 만들 수 있습니다.

var 변수명 chan 채널타입 = make(chan 채널타입)

채널에 데이터를 넣기 위해서는 <- 연산자를 사용하고,
채널로부터 데이터를 뺄 때에도 <- 연산자를 사용합니다.

아래는 messages 라는 이름의 채널에 데이터를 넣고, 빼는 예제입니다.

var messages chan string = make(chan string)
messages <- "apple"

데이터를 빼려고 시도할 때, 만약 messages 에 데이터가 들어있지 않을 경우, 데이터가 들어올 때까지 대기합니다.

var message string = <- messages

채널은 프로그래밍적으로 안전하게 코딩하기 위해
잘못 작성된 상황을 방지하기 위한 용도로 아래와 같이 작성할 수 있습니다.
수신/송신만 하는 채널

  • func sendChan(ch chan <- string)
  • func receiveChan(ch <-chan string)

1.1. 간단한 예제

채널 인스턴스 ch를 만들고, 그 채널 인스턴스를 활용하는 고루틴을 생성하는 예제입니다.
go power(&wg, ch) 코드에서 아직 ch 인스턴스에 데이터가 들어있지 않아, 대기상태로 기다리다가
데이터가 들어오면 고루틴을 실행합니다

package main

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

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)
	wg.Add(1)
	go power(&wg, ch)
	fmt.Println("data insert before")
	ch <- 5
	fmt.Println("data insert after")
	wg.Wait()
}

func power(wg *sync.WaitGroup, ch chan int) {
	fmt.Println("power function start")
	num := <-ch
	time.Sleep(time.Second)
	fmt.Printf("%d의 거듭제곱은 %d\n", num, num*num)
	fmt.Println("power function end")
	wg.Done()
}
data insert before
power function start
data insert after
5의 거듭제곱은 25
power function end

채널 인스턴스에 데이터가 들어올 때까지, power 함수 내의 fmt.Println 가 출력되지 않고 대기하고 있습니다.


2. 채널 크기

ch := make(chan string)

채널 생성시 위와 같이 make 함수내에 인자를 하나만 설정할 경우, 크기가 0인 채널이 만들어집니다.
크기가 0인 채널은 들어온 데이터를 담아두지 않고 수신자가 소비할 때까지 대기합니다.

채널 크기가 0인 채널 인스턴스에 데이터를 담아두고 소비하지 않을 경우, deadlock이 걸리면서 프로그램이 멈춥니다.

위의 예제코드에서, 채널에 넣은 데이터를 소비하지 않도록 코드를 아래와 같이 변경할 경우, 데드락 발생 후 프로그램이 강제종료 되는 것을 확인할 수 잇습니다.

이 때, 채널 인스턴스에 데이터를 할당하는 코드 아후의 로직은 동작되지 않습니다.
아래 예제에서도 data insert after 는 출력되지 않은 것을 확인할 수 있습니다.

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)
	wg.Add(1)
	go power(&wg, ch)
	fmt.Println("data insert before")
	ch <- 5
	fmt.Println("data insert after")
	wg.Wait()
}

func power(wg *sync.WaitGroup, ch chan int) {
	fmt.Println("power function start")
	time.Sleep(time.Second)
	fmt.Println("power function end")
	wg.Done()
}
data insert before
power function start
power function end
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /Users/sherry/jini_box/jinispaces/go/goplayground/ch25/ex25.1/ex25.1.go:15 +0xe8

3. 버퍼를 가진 채널

버퍼는 내부에 데이터를 보관할수 있는 메모리 영역을 말합니다.
버퍼를 가진 채널을 생성하고 싶다면, make 함수로 채널 인스턴스를 생성할 때, 두번째 인자값에 버퍼 크기를 설정하면 됩니다.

ch := make(chan string, 2)

버퍼 크기가 있는 채널의 경우, 버퍼 크기만큼 데이터를 담아둘 수 있습니다.
버퍼에 담기지 않은 데이터가 있지 않는다면 버퍼에 들어있는 데이터를 수신할 때까지 무한정 대기하지 않습니다.

버퍼를 초과하는 경우에는 버퍼가 없는 채널과 동일하게 데이터 소비를 무한정 기다립니다. (제때 소비되지 않으면 deadlock이 발생됩니다)


4. 채널 닫기

채널 인스턴스에 들어있는 값들을 처리하는 코드를 작성할 때에는 주의해야할 점이 있습니다.

아래 예제를 보며 설명을 이어가겠습니다.

package main

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

func power(wg *sync.WaitGroup, ch chan int) {
	for num := range ch {
		fmt.Printf("%d의 거듭제곱은 %d\n", num, num*num)
		time.Sleep(200 * time.Millisecond)
	}
	wg.Done()
}

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

	wg.Add(1)
	go power(&wg, ch)
	for i := 0; i < 10; i++ {
		ch <- i
	}
	wg.Wait()
}
0의 거듭제곱은 0
1의 거듭제곱은 1
2의 거듭제곱은 4
3의 거듭제곱은 9
4의 거듭제곱은 16
5의 거듭제곱은 25
6의 거듭제곱은 36
7의 거듭제곱은 49
8의 거듭제곱은 64
9의 거듭제곱은 81
fatal error: all goroutines are asleep - deadlock!

위의 코드는 문제없이 종료될것 같지만, 위와 같이 데드락이 발생되었습니다.

ch 인스턴스에 10개의 데이터를 넣엏고, power 고루틴에서 10개에 대해서 처리할것을 예상하겠지만

power 고루틴에서는 정확하게 ch인스턴스의 데이터가 몇개일지 예측할 수 없기 때문에, 10개의 입력을 모두 마친 후에도 여전히 데이터의 입력을 무한 대기하는 상태가 되어, 데드락이 발생됩니다.

채널을 제때에 닫아주지 않아 고루틴에서 데이터를 무한 대기하는 상태에 대해 좀비 루틴 또는 고루틴 leak 이라고 부릅니다.


여러개의 데이터를 입력받고, 입력받은 데이터들에 대한 처리를 하고자 할 때에는, 해당 채널의 입력을 마치기 위해 채널을 닫아줘야 합니다

for 루프에서 데이터를 입력을 모두 마친 후, close(ch) 로 채널을 닫아주면 데드락 이슈를 해결할 수 있습니다.

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

	wg.Add(1)
	go power(&wg, ch)
	for i := 0; i < 10; i++ {
		ch <- i
	}
	close(ch)
	wg.Wait()
}

5. select ... case

select 문을 이용하여 여러 채널을 대기하며, 각 채널에 데이터가 들어왔을시에 대한 처리를 한번에 할 수 있습니다.

package main

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

func power(wg *sync.WaitGroup, nums chan int, quit chan bool) {
	for {
		select {
		case num := <-nums:
			fmt.Printf("%d의 거듭제곱은 %d\n", num, num*num)
			time.Sleep(time.Second)
		case <-quit:
			wg.Done()
			return
		}
	}
}

func main() {
	var wg sync.WaitGroup
	nums := make(chan int)
	quit := make(chan bool)
	wg.Add(1)
	go power(&wg, nums, quit)
	for i := 1; i <= 5; i++ {
		nums <- i
	}
	quit <- true
	wg.Wait()
}
1의 거듭제곱은 1
2의 거듭제곱은 4
3의 거듭제곱은 9
4의 거듭제곱은 16
5의 거듭제곱은 25

6. context

컨텍스트는 context 패키지에서 제공하는 기능으로,
작업을 지시할 때 작업 가능시간, 작업 취소 등의 조건을 지시할 수 있는 작업 명세서 역할을 하며, 작업 설정에 대한 데이터를 전달할 수 있습니다.

  • 새로운 고루틴으로 작업을 시작할 때, 일정 시간동안만 작업을 지시할 때
  • 외부에서 작업을 취소하고자할 때

6.1. 작업 취소가 가능한 context

작업이 취소될 때까지 1초마다 메시지를 출력하는 고루틴

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background())
	go printTickEverySecond(ctx)

	time.Sleep(5 * time.Second)
	cancel()
	wg.Wait()
}

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

context.WithCancel() 는 취소가능한 컨텍스트를 생성합니다.
상위 컨텍스트가 있다면 인자로 상위컨텍스트를 설정하면 됩니다. 예제코드에서는 상위 컨텍스트가 없기 때문에 기본 컨텍스트인 **context.Background()**를 인자로 설정하였습니다.

context.WithCancel() 가 반환하는 cancel 함수를 호출할 경우, 컨텍스트가 종료됩니다.

5초 대기후, cacel() 함수가 호출되도록 설정하였고,
고루틴에서 1초간격으로 Tick! 메시지가 출력되도록 설정하였습니다.
그리고 만약 작업취소 가 실행될 경우(ctx.Done()) 작업그룹이 종료되도록 하였습니다.

6.2. 작업 시간을 설정한 context

context.WithTimeout 의 두번째 인자에 작업시간을 설정할 수 있습니다.
이 경우, 컨텍스트 생성 후, 작업시간이 지난 후, Done() 채널에 시그널을 보냅니다.

아래 코드는, 5초가 지난 후 컨텍스트를 종료시키는 예제코드입니다.

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
	go printTickEverySecond(ctx)
	wg.Wait()
}

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

6.3. 특정 값을 설정한 context

context.WithValue 함수를 이용하여 컨텍스트에 특정 키를 설정할 수 있습니다. (반환값은 context.Context)

컨텍스트에 설정한 값은 Value(키)를 이용하여 조회할 수 있습니다. 존재하지 않는 키를 조회시도할 경우 nil을 반환합니다.

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx := context.WithValue(context.Background(), "num", 5)
	go power(ctx)
	wg.Wait()
}

func power(ctx context.Context) {
	if v := ctx.Value("num"); v != nil {
		num := v.(int)
		fmt.Printf("%d의 거듭제곱은 %d\n", num, num*num)
	}
	wg.Done()
}

728x90
반응형

'Go' 카테고리의 다른 글

[Go] 19. 테스트와 벤치마크  (0) 2024.03.13
[Go] 17. 고루틴  (0) 2024.03.11
[Go] 16. 에러 핸들링  (0) 2024.03.10
[Go] 15. 자료구조 - list, ring, map  (0) 2024.02.05
[Go] 14. 함수 고급편  (0) 2024.02.04