Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.
채널(Channel)
- 고루틴끼리 메시지를 전달할 수 있는 메시지 큐(FIFO).
- Thread Safe Queue라고도 하며, 멀티스레드 환경에서 Lcok 없이 사용할 수 있음.
- 메시지 큐에 메시지들은 차례대로 쌓이게 되고 메시지를 읽을 때 맨 처음 온 메시지부터 차례대로 읽게됨.
* 채널 인스턴스 생성
- 채널을 사용하기 위해서는 먼저 채널 인스턴스를 만들어야 함.
var message chan string = make(chan string)
- 슬라이스, 맵 등과 같이 make() 함수로 만들 수 있고, 채널 타입은 채널(Channel)을 의미하는 Chan과 메시지 타입을 합쳐서 표현
- chan string은 string 타입 메시지를 전달하는 채널의 타입
* 채널에 데이터 넣기(Push & Pop)
message <- "This is a message"
- 채널에 데이터를 넣는 데 <- 연산을 이용
- <- 연산자 좌변에 채널 인스턴스를 놓고 우변에 넣을 데이터를 놓으면, 우변 데이터를 좌변 채널에 넣음.
- messages 채널 인스턴스는 chan string 타입으로 만들었기 때문에 문자열 데이터를 넣음.
* 채널에서 데이터 빼기
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) // 1초 대기
fmt.Printf("Square: %d\n", n*n)
wg.Done()
}
// 결과
Square: 81
- 채널을 먼저 생성하고, square() 함수를 실행하는 고루틴 생성
= square() 함수는 main() 루틴이 아닌 새로운 고 루틴에서 동시에 실행됨.
- square() 함수는 앞서 생성한 채널 인스턴스를 인수로 받아 먼저 채널에서 빼오려고 시도함.
= 현재 채널에는 데이터가 없어서 데이터가 들어올 때까지 대기함.
- main() 루틴에서 채널에 9를 넣고, 데이터가 들어왔기 때문에 square()함수에서 데이터를 빼서 결괏값을 출력함.
- square() 함수에서 wg.Done()을 통해 작업이 완료될 때 까지 기다렸다 프로그램 종료.
채널 크기
- 일반적으로 채널을 생성하면 크기가 0인 채널*이 생성됨.
- 크키가 0이라는 것은 채널에 들어온 데이터를 담아둘 곳이 없다는 것.
- 크기는 make() 함수 끝에 숫자를 적으면 기재한 크기 만큼 공간이 생기고, 기재하지 않으면 크기가 0인 채널이 생성됨.
package main
import "fmt"
func main() {
ch := make(chan int) // 채널이 0인 채널 생성
ch <- 9 // main() 함수가 멈춤
fmt.Println("Never Print") // 실행되지 않음
}
// 결과
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox571263458/prog.go:8 +0x28
- 채널에 데이터를 넣었지만, 보관할 곳이 없기 때문에 다른 고루틴에서 데이터를 빼가기를 기다림.
- 어떤 고루틴도 데이터를 빼가지 않기 때문에 모든 고루틴이 영원히 대기하게 됨.
- deadlock 메시지를 출력하고 프로그램이 강제 종료 됨.
버퍼를 가진 채널
- 내부에 데이터를 보관할 수 있는 메모리 영역을 버퍼(Buffer)라고 함.
- 보관함을 가지고 있는 채널을 버퍼를 가진 채널이라고 말함.
var chan string messages = make(chan string, 2)
- 위와 같이 채널을 생성하면 버퍼가 2개인 채널이 만들어져서, 데이터를 2개까지 보관할 수 있음.
- 만약 버퍼가 다 찼을 경우 보관함에 빈자리가 생길 때까지 대기함.
채널에서 데이터 대기
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 // 데이터를 넣음
}
wg.Wait() // 작업 완료를 기다림
}
// 결과
Square: 0
Square: 4
Square: 16
Square: 36
Square: 64
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324
fatal error: all goroutines are asleep - deadlock!
...
- 채널에 데이터를 10번 넣고, for range 구문을 사용하여 채널에서 데이터를 계속 기다리게 함 = wg.Wait().
- ch 채널 인스턴스로부터 데이터가 들어오길 기다렸다 데이터가 들어오면 데이터를 빼내서 n 변수에 값을 복사하고 for문을 실행.
for n := range ch {
...
}
- wg.Wait() 메서드로 작업이 완료되기를 기다리고, for range 구문은 채널에 데이터가 들어오기를 계속 기다리고 있고,
그 다음 구문이 실행되지 않아 모든 고루틴이 멈추게 되어 deadlock이 표시됨.
= main 고루틴은 wg.Wait()에서, square 고루틴은 for n := range ch 구문에서 멈춰 있음 = 모든 고루틴이 멈춰 있음.
* close()로 채널을 닫아주기
- 위 예제의 문제를 해결하기 위해서는 close()로 채널을 닫아주면 됨.
• 좀비 고루틴: 채널을 닫아주지 않아서 무한대기하는 고루틴을 좀비 고루틴 또는 고루틴 릭(Leak)이라고 함.
→ 좀비 루틴이 많아지면 프로그램 자원을 소모하게 되어 프로그램이 느려지거나 메모리 부족으로 강제 종료될 수 있음.
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()
}
// 결과
Square: 0
Square: 4
Square: 16
Square: 36
Square: 64
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324
- 데이터를 10번 넣고 채널이 더 이상 필요하지 않기 때문에 close(ch)를 호출해 닫음.
- for range에서 데이터를 모두 처리하고 채널이 닫힌 상태이면 for문을 빠져나와 프로그램이 정상적으로 종료 됨.
따라서, 결과가 10개가 나옴.
Select 문
- 여러 채널에서 동시에 데이터를 기다릴 때 사용함.
select {
case n := <-ch1:
... // ch1 채널에서 데이터를 빼낼 수 있을 때 실행
case n2 := <-ch2:
... // ch2 채널에서 데이터를 빼낼 수 있을 때 실행
case ...
}
- 어떤 채널이라도 하나의 채널에서 데이터를 읽어오면 해당 구문을 실행하고 select문이 종료됨.
= 하나의 case만 처리되면 종료되기 때문에 반복해서 데이터를 처리하고 싶다면 for문과 함께 사용해야 함.
package main
import (
"fmt"
"sync"
"time"
)
func square(wg *sync.WaitGroup, ch chan int, quit chan bool) {
for {
select { // ch와 quit 양쪽 모두를 기다림
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()
}
- quit 종료 채널을 만들어서 square() 루틴을 만들 때 알려줌.
- 이제 select 문에서 ch와 quit 채널을 모두 기다리고, ch 채널을 먼저 시도하기 때문에 ch 채널에서 데이터를 읽을 수 있으면 계속 읽음.
→ 따라서, 10개의 제곱이 모두 출력되고, quit 채널에서 데이터를 읽어온 다음 square() 함수가 종료됨.
일정 간격으로 실행
- time 패키지의 Tick() 함수로 원하는 시간 간격으로 신호를 보내는 채널을 반환
- After()는 일정 시간 대기 후 한 번만 신호를 주는 채널을 반환
package main
import (
"fmt"
"sync"
"time"
)
func square(wg *sync.WaitGroup, ch chan int) {
tick := time.Tick(time.Second) // 1초 간격 시그널
terminate := time.After(10 * time.Second) // 10초 이후 시그널
for {
select { // tick, terminate, ch 순서로 처리
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)
}
}
}
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
}
wg.Wait()
}
// 결과
Square: 0
Tick
Square: 4
Tick
Square: 16
Tick
Square: 36
Tick
Square: 64
Tick
Square: 100
Tick
Square: 144
Tick
Square: 196
Square: 256
Square: 324
Tick
Terminated!
// 결과는 실행할 때마다 조금씩 달라질 수 있음.
- time.Tick()은 일정 시간 간격 주기로 신호를 보내주는 채널을 생성해서 반환하는 함수
함수가 반환한 채널에서 데이터를 읽어오면 일정 시간 간격으로 현재 시각을 나타내는 Time 객체를 반환함.
- time.After()는 현재 시간 이후로 일정 시간 경과 후에 신호를 보내주는 채널을 생성해 반환하는 함수
함수가 반환한 채널에서 데이터를 읽으면 일정 시간 경과 후에 현재 시각을 나타내는 Time 객체를 반환함.
- Select문을 이용해 tick, terminate, ch 순서로 채널에서 데이터 읽기를 시도함.
- tick에서 메시지를 읽어오면 Tick을 출력하고, terminate에서 읽어오면 함수를 종료함.
- tick과 terminate에서 신호를 못 읽으면 ch에서 읽어오게 되고, tick은 1초 간격으로 신호를 보내고,
10초 이후에는 terminate 신호가 와서 함수가 종료됨.
채널로 생산자(Producer) / 소비자(Consumer) 패턴 구현
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.After(10 * time.Second)
for {
select {
case <-tick:
// Make a body
car := &Car{}
car.Body = "Sports car"
tireCh <- car
case <-after: // 10초 뒤 종료
close(tireCh)
wg.Done()
return
}
}
}
func InstallTire(tireCh, paintCh chan *Car) { // 바퀴 설치
for car := range tireCh {
// Make a body
time.Sleep(time.Second)
car.Tire = "Winter tire"
paintCh <- car
}
wg.Done()
close(paintCh)
}
func PaintCar(paintch chan *Car) { // 도색
for car := range paintch {
// Make a body
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()
}
// 결과
Start Factory
3.00 Complete Car: Sports car Winter tire Red
4.00 Complete Car: Sports car Winter tire Red
5.00 Complete Car: Sports car Winter tire Red
6.00 Complete Car: Sports car Winter tire Red
7.00 Complete Car: Sports car Winter tire Red
8.00 Complete Car: Sports car Winter tire Red
9.00 Complete Car: Sports car Winter tire Red
10.00 Complete Car: Sports car Winter tire Red
11.00 Complete Car: Sports car Winter tire Red
Close the factory
- MakeBody(), InstallTire(), PaintCar() 고루틴을 생성하고, main() 루틴은 모든 고루틴이 종료될 때까지 대기.
- MakeBody()는 1초 간격으로 차제를 생성해 tireCh 채널에 데이터를 넣고, 10초 이후에 tireCh 채널을 닫아주고 루틴 종료.
- InsatllTire() 루틴은 채널에서 데이터를 읽어 바퀴를 설치하고 paintCh 채널에 넣고,
만약, tireCh 채널이 닫히면 루틴을 종료하고 paintCh 채널을 닫음.
- PaintCar() 루틴은 paintCh 채널에서 데이터를 읽어서 도색을 하고 완성된 차를 출력하며, paintCh 채널이 닫히면 루틴을 종료함.
- 차가 완성되면 현재 시각에서 시작 시간을 뺀 경과 시간을 출력
→ 채널을 이용해 역할을 나누면, 고루틴 하나를 사용할 때보다 더 빠르게 작업을 완료할 수 있고, 뮤텍스를 사용하지 않아도 됨.
✓ 한 쪽에서 데이터를 생성해서 넣어주면 다른 쪽에서 생성된 데이터를 빼서 사용하는 방식을 생산자 소비자 패턴*이라고 함.
* 생산자 소비자 패턴(Producer Consumer Pattern), 생산자와 소비자가 연속적으로 연결된 형태를 파이프라인 패턴이라고 부르기도 함.
컨텍스트(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")
}
}
}
//결과
tick
tick
tick
tick
tick
- context.WithCancel() 함수로 취소 가능한 컨텍스트를 생성
- 상위 컨텍스트를 인수로 넣으면 그 컨텍스트를 감싼 새로운 컨텍스트를 만듦
= 상위 컨텍스트가 없으면 가장 기본적인 컨텍스트인 context.Background()를 넣어줌.
- context.WithCancel() 함수는 값을 두 개 반환하는데 첫 번째가 컨텍스트 객체이고, 두 번째가 취소 함수로
두 번째 취소 함수를 사용해서 원할 때 취소할 수 있음.
- main() 함수에서 5초 이후 취소 함수를 호출해 작업 취소를 알리고, 컨텍스트의 Done() 채널에 시그널을 보내 작업자가
작업을 취소할 수 있도록 알림.
- PrintEverySecond() 루틴에서 인수로 받은 컨텍스트의 Done() 채널의 시그널이 있는지 검사함.
- 컨텍스트가 완료될 때 Done() 채널에 시그널을 넣기 때문에 여기서 메시지를 수신하면 고루틴을 종료함.
작업 시간을 설정한 컨텍스트
- 일정한 시간 동안만 작업을 지시할 수 있는 컨텍스트
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
- context 패키지의 WithTimeout() 함수를 사용해서 작업 시간을 설정할 수 있음.
- 두 번째 인수로 시간을 설정하면 그 시간이 지난 뒤 컨텍스트의 Done() 채널에 시그널을 보내 작업 종료를 요청함.
- WithTimeout() 함수도 두 번째 반환값으로 cancel 함수를 반환하기에 작업 시간 전에 원하면 언제든지 작업 취소 가능함.
특정 값을 설정한 컨텍스트
- 작업자에게 작업을 지시할 때 별도 지시사항을 추가 할 수 있음
- 컨텍스트에 특정 키로 값을 읽어올 수 있도록 설정할 수 있음
- context.WithValue() 함수를 이용해 컨텍스트에 값을 설정할 수 있음.
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()
}
// 결과
Square:81
- "number"를 키로 값을 9로 설정한 컨텍스트를 만들고, square() 함수 인수로 넘겨 값을 사용할 수 있도록 함.
- 컨텍스트의 ctx의 Value() 메서드로 값을 읽어오고, Value() 메서드의 반환 타입은 빈 인터페이스라,
int 타입으로 변환하여 사용
컨텍스트 랩핑
ctx, cancel := context.WithCancel(context.Background()) // 1
ctx = context.WithValue(ctx, "number", 9) // 2
ctx = context.WithValue(ctx, "keyword", "Lilly") //3
- 먼저 취소 기능이 있는 컨텍스트를 만들고, 이를 감싸서 값을 설정한 컨텍스트를 만듦.
- 컨텍스트를 여러 번 감싸 여러 값을 설정할 수 있음.
'Language > Golang' 카테고리의 다른 글
Golang (Go언어) Go루틴 (0) | 2024.12.07 |
---|---|
Golang (Go언어) 에러 핸들링 (2) | 2024.12.05 |
Golang (Go언어) 자료 구조(Data Structure) 2/2 (0) | 2024.12.03 |
Golang (Go언어) 자료 구조(Data Structure) 1/2 (1) | 2024.12.02 |
Golang (Go언어) 함수고급편 (1) | 2024.11.29 |