채널(Channel)과 컨택스트(Context)는 GO 언어에서 동시성 프로그래밍을 도와주는 기능
채널과 컨텍스트를 사용해 특정 데이터를 전달하거나 특정 시간 동안만 작업을 요청하거나 작업 도중에 작업 취소를 요청할 수 있음
채널이란 고루틴끼리 메시지를 전달할 수 있는 메시지 큐인데, 메시지 큐에 메시지들은 차례대로 쌓이게 되고 메시지를 읽을 때는 맨 처음 온 메시지부터 차례대로 읽음 (FIFO)
//타입 채널인스턴스변수명 chan 채널 타입 = make(채널키워드 메세지타입)
var messages chan string = make(chan string)
채널은 슬라이스, 맵과 같이 make()함수로 생성
채널 타입은 Channel을 의미하는 chan과 메시지 타입을 합쳐서 표현
chan string은 string 타입 메시지를 전달하는 채널 타입
//채널_인스턴스 <-(연산자) 넣을_데이터
messages <- "This is a message"
채널에 데이터를 넣을 때는 <-연산자 사용. <-연산자 좌변에 채널 인스턴스를 놓고 우변에 넣을 데이터를 놓으면 우변 데이터를 좌변 채널에 넣게 됨
messages 채널 인스턴스는 앞서 string을 받는 채널인 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
Goroutine이 생성되지 않으면 채널에서 데이터를 받을 준비가 안 된 상태입니다.
이 경우 데이터를 송신하려고 하면 송신 Goroutine(여기서는 main Goroutine)이 블로킹 상태가 되고, Goroutine이 생성되지 않았으므로 데드락이 발생합니다.
채널과 Goroutine의 송수신 관계를 명확히 설계하여, 항상 송신자와 수신자가 준비 상태에 있도록 보장해야 합니다.
처음 채널을 생성하면 크기가 0인 채널이 생성 (들어온 데이터를 담을 공간이 없음)
즉, 데이터를 넣을 때 보관할 공간이 없기 때문에 데이터를 빼갈 때까지 대기
package main
import (
"fmt"
)
func main() {
ch := make(chan int) //크기 0 인 채널 생성
ch <- 9 //메인 함수가 여기서 멈춤
fmt.Println("Never print")
}
//결과
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
채널을 만들고 데이터를 넣었지만, 보관할 곳이 없기 때문에 다른 고루틴에서 데이터를 빼가기를 기다림. 하지만 어떤 고루틴도 데이터를 빼가지 않기 때문에 fmt.Println("Never print")는 실행되지 않고 모든 고루틴이 영원히 대기.
따라서 deadlock 메시지를 출력하고 프로그램 강제 종료
내부에 데이터를 보관할 수 있는 메모리 영역을 버퍼라고 함
그래서 보관함을 가진 채널을 버퍼를 가진 채널이라고 함
var chan string message = make(chan string, 2) //버퍼 2
이렇게 되면 2개까지는 데이터를 보관할 수 있음.
보관함이 다 차면? 버퍼가 없을 때와 마찬가지로 보관함에 빈자리가 생길 때까지 대기. 그래서 데이터를 제때 빼가지 않으면 버퍼가 없을 때처럼 고루틴 멈춤
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 := 0; i < 10; i++ {
ch <- i * 2 // 1. 데이터를 넣음
}
wg.Wait() // 3. 작업이 완료되길 기다림
}
func square(wg *sync.WaitGroup, ch chan int) {
for n := range ch { // 2. 데이터를 계속 기다림
fmt.Printf("Square: %d\n", n*n)
time.Sleep(time.Second)
}
wg.Done() // 4. 실행되지 않음
}
//결과
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!
for n:= range ch {
...
}
위와 같이 하면 ch 채널 인스턴스로부터 데이터가 들어오길 기다렸다가 데이터가 들어오면 데이터를 빼내서 n 변수에 값을 복사하고 for 본문을 실행
wg.Wait() 메서드로 작업이 완료되길 기다리지만 for range 구문은 채널에 데이터가 들어오기를 계속 대기하므로 절대 wg.Done() 메서드가 실행되지 않고 모든 고루틴이 멈추게 되어 deadlock에 걸림
해결 방법은,
채널을 다 사용하면 close(ch)를 호출해 채널을 닫고 채널이 닫혔음을 알려줘야 함
채널에서 데이터를 모두 빼낸 상태이고 채널이 닫혔으면 for range문을 빠져나감
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 := 0; i < 10; i++ {
ch <- i * 2
}
close(ch) //채널 닫음
wg.Wait()
}
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()
}
//결과
Square: 0
Square: 4
Square: 16
Square: 36
Square: 64
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324
데이터를 모두 넣고 채널이 더는 필요없기 때문에 close(ch)를 호출해서 닫아줌.
그럼 for range에서 데이터를 모두 처리하고 난 다음에 채널이 닫힌 상태면 for문을 종료해서 프로그램이 정상 종료됨.
좀비 루틴 / 고루틴 릭(leak)
채널을 제때 닫아주지 않아서 고루틴에서 데이터를 기다리며 무한 대기하는 경우를 좀비 루틴 또는 고루틴릭 이라고 함. 아무리 경량 스레드라고 해도 고루틴 또한 메모리와 성능을 차지하기 때문에 이런 좀비 루틴이 많아지면 프로그램 자원을 소모하게 되고 프로그램이 느려지거나 메모리 부족으로 강제 종료될 수 있음. 그래서 놀고 있는 고루틴이 없는지 잘 살펴봐야 함
채널에서 데이터가 들어오기를 대기하는 상황에서 만약 데이터가 들어오지 않으면 다른 작업을 하거나, 아니면 여러 채널을 동시에 대기하고 싶을 때 사용
select {
case n := <- ch1:
... //ch1 채널에서 데이터를 빼낼 수 있을 때 실행
case n2 := <- ch2:
... //ch2 채널에서 데이터를 빼낼 수 있을 때 실행
case ...
}
select문은 위와 같이 여러 채널을 동시에 기다릴 수 있음. 만약 어떤 채널이라도 하나의 채널에서 데이터를 읽어오면 해당 구문을 실행하고 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()
}
//결과
Square : 0
Square : 4
Square : 16
Square : 36
Square : 64
Square : 100
Square : 144
Square : 196
Square : 256
Square : 324
메시지가 있으면 메시지를 빼와서 실행하고 그렇지 않으면 1초 간격으로 다른 일을 수행해야 한다고 가정할 때, 이런 경우 time 패키지의 Tick() 함수로 원하는 시간 간격으로 신호를 보내주는 채널을 만들 수 있음
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("Terminate!")
wg.Done()
return
case n := <-ch:
fmt.Printf("Square : %d\n", n*n)
time.Sleep(time.Second)
return
}
}
}
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
Square : 100
Tick
Square : 144
Square : 196
Tick
Square : 256
Square : 324
Terminate!
Tick을 이용한 예제 중, 2초 간격, 5초 간격 시그널을 받는 Tick을 사용할때 10초에서 충돌나는데, 그 경우 하나의 case만 실행되고 select문을 탈출함
package main
import (
"fmt"
"sync"
"time"
)
func testTick(wg *sync.WaitGroup) {
defer wg.Done()
tick1 := time.Tick(2 * time.Second) // 2초 간격 시그널
tick2 := time.Tick(5 * time.Second) // 5초 간격 시그널
for {
select {
case <-tick1:
fmt.Printf("[%s] Tick1\n", time.Now().Format("15:04:05"))
case <-tick2:
fmt.Printf("[%s] Tick2\n", time.Now().Format("15:04:05"))
}
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go testTick(&wg)
// 프로그램을 12초 동안 실행
time.Sleep(12 * time.Second)
wg.Wait()
fmt.Println("Main function finished.")
}
//결과 예시 -
[12:00:02] Tick1
[12:00:04] Tick1
[12:00:05] Tick2
[12:00:06] Tick1
[12:00:08] Tick1
[12:00:10] Tick1
[12:00:12] Tick1
or
[16:52:31] Tick2
[16:52:32] Tick1
[16:52:34] Tick1
[16:52:36] Tick1
[16:52:36] Tick2 //충돌
[16:52:38] Tick1
[16:52:40] Tick1
[16:52:41] Tick2
[16:52:42] Tick1
[16:52:44] Tick1
[16:52:46] Tick1
[16:52:46] Tick2 //충돌
[16:52:48] Tick1
[16:52:50] Tick1
[16:52:51] Tick2
[16:52:52] Tick1
[16:52:54] Tick1
[16:52:56] Tick1
[16:52:56] Tick2 //충돌
select는 한 번에 하나의 case만 처리합니다.
여러 case가 동시에 실행 가능하다면, Go 런타임은 랜덤하게 하나를 선택하여 처리합니다.
따라서 Tick1과 Tick2가 충돌하는 순간에도 Tick2가 무조건 무시되지 않고 실행될 수 있습니다.
고루틴에서 뮤텍스를 사용하지 않는 방법 중 첫 번째 방법인 영역을 나누는 방법에 이어 두 번째 방법인 채널을 이용해서 역할을 나누는 방법에 대한 예제
-시나리오-
자동차 공장에서 자동차를 차체 생산 → 바퀴 설치 → 도색 → 완성 단계를 거쳐 생산한다고 가정. 각 공정에 1초가 걸린다고 보면 자동차 한 대를 만드는 데 3초가 걸림. 그런데 3명이 공정 하나씩 처리하면 첫 차 생산에만 3초가 걸리고 그 뒤론 1초마다 하나씩 생산할 수 있음 (컨베이어 벨트 시스템)

작업자 간 자동차 전달은 컨베이어 벨트를 통해서만 이뤄지고, 자동차는 데이터, 컨베이어 벨트는 채널로 볼 수 있음
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.Printf("Close the Factory")
}
func makeBody(tireCh chan *Car) {
tick := time.Tick(time.Second) //차체 생산
after := time.After(10 * time.Second)
for {
select {
case <-tick:
//makeBody
car := &Car{}
car.Body = "Sports car"
tireCh <- car
case <-after: //10초뒤 종료
close(tireCh)
wg.Done()
return
}
}
}
func InstallTire(tireCh chan *Car, 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
12.00 Complete Car : Sports car Winter tire Red
Close the Factory
결과를 보면 첫 차만 3초가 경과됐고 그 뒤론 1초 간격으로 차가 완성됨. 이렇게 채널을 이용해서 역할을 나누면 고루틴 하나를 사용할 때보다 더 빠르게 작업을 완료할 수 있고, 뮤텍스도 필요 없음
이와 같이 한쪽에서 데이터를 생성해서 넣어주면 다른 쪽에서 생성된 데이터를 빼서 사용하는 방식을 생산자 소비자 패턴이라고 함. 이번 예제에서는 MakeBody() 루틴이 생산자, InstallTire() 루틴이 소비자. 또한 InstallTire()는 PaintCar() 루틴에 대해서는 생산자가 되는 구조