동시성 프로그래밍에서 가장 중요한 문제는 같은 메모리 영역에 동시에 접근해서 생기는 race condition 문제이다. 이 문제를 해결하기 위해 mutex, semaphore등 특정 영역에 대한 lock과 unlock을 걸어 특정 메모리 영역에 동시에 접근하지 못하게 한다.
Go에서도 mutex.lock, mutex.unlock을 사용할 수 있지만 이 경우 해당 영역을 읽고 쓰는 모든 영역에 lock, unlock을 해줘야 한다. 코드 복잡성을 늘릴 수 있다. 그렇다고 무조건적으로 channel만 사용해야 하는건 아니다. 다음 글들을 통해 channel만 사용하는 것은 좋지 않다는 것을 알 수 있다.
Mutex 와 Channel 의 차이
Mutex 와 Channel 은 고루틴에서 원자성과 동시성을 지키기 위한 방법론이다. Mutex 는 개발자가 특정 ...
blog.naver.com
mutex에 비해 channel의 성능은 더 좋지 않다. 그러므로 무조건 적인 channel의 사용은 지양하자.
channel 사용법 및 차이
채널을 선언하는 방법은 크게 두가지다
1. 채널 크기 미지정
2. 채널 크기 지정
ch := make(chan int)
ch := make(chan int, 2)
두 차이는 다음의 상황을 통해 확인할 수 있다 ( <- 기호를 통해 channel에 value를 집어넣음)
func main(){
ch := make(chan int)
go square2()
ch <- 9
fmt.Println("is it print?")
}
func square2() {
for {
time.Sleep(2 * time.Second)
fmt.Println("sleep")
}
}
이 경우 main go루틴이 끝나기 때문에 화면에 "is it print?"를 뱉고 끝내는게 맞다. 하지만 코드를 실행해보면 그렇지 않다. 오히려 "sleep"이 무한히 출력된다. 다음의 경우를 보자.
func main(){
ch := make(chan int,2)
go square2()
ch <- 9
fmt.Println("is it print?")
}
func square2() {
for {
time.Sleep(2 * time.Second)
fmt.Println("sleep")
}
}
이제 정상적으로 "is it print?"가 출력되고 프로그램이 종료된다. 차이는 channel에 크기를 줬다는 것이다. 이게 무슨 의미이길래 이런 차이가 발생할까?
channel의 크기
channel에 넣는 데이터를 "바닥에 둘 수 없는 상자"로 비유하면 channel의 크기는 "상자 한 개를 둘 수 있는 책상(공간)"이다. 즉, 위의 상황에서 ch <- 9를 통해 택배원(프로그램)이 ch에 9를 들고 왔는데 상자를 둘곳이 없어서 계속 들고 서있는 것이다. 누군가 와서 상자를 가져갈때까지 택배원은 기다린다... 밑의 코드를 통해 확인해보자
func main(){
ch := make(chan int)
go square2(ch)
ch <- 9
fmt.Println("is it print?")
}
func square2() {
for {
time.Sleep(2 * time.Second)
fmt.Println("sleep")
nine := <-ch
fmt.Println("i take it!!", nine)
}
}
이 경우 square2()에서 nine := <-ch를 통해 프로그램이 들고있던 9를 가져와서 프로그램이 자유의 몸이 됐다. 그 결과 "is it print?"가 출력되며 프로그램이 종료된다.
그렇다고 무조건 크기를 선언했다고 프로그램이 자유의 몸이 되는건 아니다. 밑의 상황을 보자
func main(){
ch := make(chan int,2)
go square2()
ch <- 9
ch <- 10
ch <- 11
fmt.Println("is it print?")
}
func square2() {
for {
time.Sleep(2 * time.Second)
fmt.Println("sleep")
}
}
아까는 크기를 줘서 "is it print?"가 출력 됐지만 ch에 두개의 값을 더 추가함으로 다시 "sleep"의 굴레에 빠졌다...
이미 앞서 들어온 두 9,10이 책상 위에 올려져 있어서 11을 들고온 프로그램이 또 택배 들고 서있어서 그렇다.. 그럼 딱 한번만 책상 위 값을 빼주면 정상적으로 수행됨을 예상할 수 있다.
func main(){
ch := make(chan int, 2) // 하지만 이렇게 큐에 사이즈를 줘서 빈공간을 만들어주면 대기하지 않고 종료됨. 즉, 빈공간이 없이 재고가 쌓이면 대기함.
go square2(ch)
ch <- 9
ch <- 10
ch <- 11
fmt.Println("is it print?")
}
func square2(ch chan int) {
nine := <-ch
fmt.Println("i take it!", nine)
for {
time.Sleep(2 * time.Second)
fmt.Println("sleep")
}
}
잘 출력된다 ㅎㅎ channel의 크기가 프로그램에 어떤 영향을 미치는지 알아봤다. 그럼 이제 정상적으로 channel을 사용하는 법을 알아보자
channel에서 값 계속 가져오기!
func main(){
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go square3(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
wg.Wait()
}
func square3(wg *sync.WaitGroup, ch chan int) {
for n := range ch { //무한반복을 돌면서 ch에 데이터가 올때까지 대기함. 데이터가 들어오면 출력하고 다시 대기.
fmt.Println("Square : ", n*n)
time.Sleep(time.Second)
}
wg.Done()
}
for n := range ch {} 를 통해 channel의 값을 무한히 받아온다. 코드를 실행해보자!!
잘 출력되다가 deadlock에 걸려버린다...
square3에서 무한 반복되는 channel읽기에 wg.Done()이 수행되지 않아 main 고루틴도 square3 고루틴도 묶여버린 것이다... 어떻게 해결해줘야 할까?
channel close() !!
func main(){
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go square3(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
close(ch)
wg.Wait()
}
func square3(wg *sync.WaitGroup, ch chan int) {
for n := range ch { //무한반복을 돌면서 ch에 데이터가 올때까지 대기함. 데이터가 들어오면 출력하고 다시 대기.
fmt.Println("Square : ", n*n)
time.Sleep(time.Second)
}
wg.Done()
}
위에서의 square3과 같은 고루틴을 좀비 고루틴이라 한다. 이런 좀비 고루틴을 방지하기 위해 특정 channel을 다 사용하게 된 경우 close()로 채널을 닫아준다. 이를 통해 위의 문제를 해결할 수 있다! (정상 종료 됨!)
channel select!
하나의 함수에서 여러 채널의 값을 읽어와야 하는 경우 사용하는 방법이다. Tick과 After 채널을 이용해 테스트 해보자!
func main(){
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go square4(&wg, ch)
for i := 0; i < 10; i++ {
ch <- i * 2
}
//채널을 닫아주지 않아도 무한 루트를 After를 통해 빠져나옴.
wg.Wait()
}
func square4(wg *sync.WaitGroup, ch chan int) {
tick := time.Tick(time.Second) // 1초에 한번씩 신호를 주는 channel tick
terminate := time.After(10 * time.Second) // 10초 후에 신호를 주는 channel terminate
for {
select { //select에서 여러 case가 동시에 걸리는 경우에는 random하게 case를 실행함. 그래서 출력 결과가 다를 수 있음.(Tick이 매 초마다 나오지 않음)
case <-tick:
fmt.Println("Tick")
case <-terminate:
fmt.Println("Terminated")
wg.Done()
return
case n := <-ch:
fmt.Println("Square: ", n*n)
time.Sleep(time.Second)
}
}
}
코드의 주석을 보지 않고 그냥 실행하면 한가지 의문이 생긴다.
"close() 안했는데 어떻게 정상 종료했지?"
주석을 보면 time.After를 통해 10초 후에 terminate 채널에 값을 넣는 것을 알 수 있다. 이 경우 인자로 전달받은 wg에 Done을 넣어 wait중이던 wg이 wait 상태에서 벗어나며 프로그램이 정상 종료되는 것이다!
헷갈릴 수 있는 부분이 tick, terminate 변수인데 얘네는 ch처럼 각각 채널이다. 즉, 저 상황은 3가지의 채널에서 값을 받아와 수행되고 있는 상황이다.
기본적인 channel 사용법은 여기까지 쓰도록 하겠다~
'IT 일기 > GO' 카테고리의 다른 글
Go 컨텍스트란? Cancle, Deadline, Timeout, Value (0) | 2023.04.03 |
---|---|
What is GoRoutine? (0) | 2023.03.31 |
Go Slice 사용법, 구조 그리고 append() 원리까지 Deep Dive (0) | 2023.03.29 |
Understand SOLID in Go (0) | 2023.03.24 |
vscode Terminal 설정 변경 (Go에서 sqlite3 사용하기) (0) | 2023.03.23 |