일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- go 마스코트
- go 패닉
- go 맥
- gin recovery
- 고루틴 채널
- go 맥 air 환경변수
- go 환경변수
- clean architecture middleware
- gin logger
- gopath 환경변수
- golang gopher
- go디자인패턴
- go air 환경변수
- git
- go 대기그룹
- 개발자
- 좀비고루틴
- go air
- go 맥 air
- gin middleware
- go recover
- go 캐릭터
- 신입개발자
- go
- go clean architecture
- 골랑 고퍼
- go channel
- air 환경변수
- go panic
- go middleware
- Today
- Total
뽀미의 개발노트
Go 고루틴과 채널 본문
멀티쓰레드
실행 흐름이 여러개인 것. 하나의 CPU가 여러 thread를 빠르게 번갈아가며 실행 -> 마치 동시에 여러 작업을 실행하는 것처럼 보임.

그런데 쓰레드를 전환할때 비용이 발생하고, 이를 컨텍스트 스위치라고 함. 멀티쓰레드는 여러 작업을 병렬적으로 처리할 수 있어 효율적이지만, 쓰레드가 CPU 갯수를 넘어서서 지나치게 많아지면 오히려 성능 문제를 일으킬 수 있음. 따라서 멀티쓰레드를 활용할 때는 쓰레드가 CPU 갯수를 넘지 않도록 주의해서 사용해야 함.
고루틴
고루틴은 OS의 쓰레드를 ‘이용’하는 경량 쓰레드임. (OS의 쓰레드와는 다른 개념!)

코어 - OS 스레드 - 고루틴 이렇게 연결해서 명령을 실행함. 고루틴 끝나고 제거되면 OS 스레드가 놀게됨. 그럼 대기하고 있던 고루틴이 그 빈자리로 가서 실행됨. 고루틴 중 하나가 네트워크 상태로 들어가면 대기하던 고루틴과 자리 교체됨. 이런식으로 스케쥴링 하며 효율적으로 관리됨.
물론 고루틴에서도 컨텍스트 스위칭 비용이 들기는 하지만 멀티쓰레드에서보다 스위칭 비용 부담이 훨씬 덜함. 왜냐하면 고루틴의 스택 사이즈가 적기 때문! 그래서 멀티쓰레드에 비해 고루틴은 자유롭게 많이 쓸 수 있음. (스택 메모리란?? 함수 호출 시에 함수에 자동으로 할당되고 함수가 끝나면 자동으로 정리되는 메모리)

main()함수도 고루틴을 가지고 있어서 고로 만들어진 프로그램은 무조건 하나 이상의 고루틴을 가질 수밖에 없음!! 고루틴을 사용하면 동시성 작업을 단순하고 가볍게 처리할 수 있음. 수천 개의 작업을 병렬로 실행하기 적합함.
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("② 다른 루틴")
say("① 이 루틴")
}
함수 앞에 고만 붙이면 비동기로 작동해서 C#에서 멀티쓰레드 or 비동기를 구현하는 것에 비해 간단함. 함수 실행하면 이 루틴과 다른 루틴이 번갈아가면서 나옴(랜덤으로) 근데 go say go say 두번 하면 아무것도 출력이 안되는데, 왜일까?? 바로바로 go 쓰면 비동기라 바로 다음 줄로 넘어가는데 고만 있으면 두개 실행하기도 전에 main 함수 실행이 끝나서 미처 실행할 타이밍이 안되어서! 그래서 맨 아래 좀 한참 걸리는 함수를 써줘야만 go 함수 두번 실행할 시간이 우연히 되면 출력됨!!! 근데 이건 우연이므로 Sync 패키지에서 제공하는 대기 그룹의 구조체와 함수를 사용하는 것이 일반적.
대기 그룹
wg.Add() : 대기그룹에 고루틴 추가.
wg.Done() : 대기그룹에 고루틴 뺌
wg.Wait() : 대기그룹의 모든 고루틴이 끝날 때까지 기다림.
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // 모든 CPU 사용
wg := new(sync.WaitGroup) // 대기 그룹 생성
for i := 0; i < 10; i++ {
wg.Add(1) // 반복할 때마다 wg.Add 함수로 1씩 추가
go func(n int) { // 고루틴 10개 생성
fmt.Println(n)
wg.Done() // 고루틴이 끝났다는 것을 알려줌
}(i)
}
wg.Wait() // 모든 고루틴이 끝날 때까지 기다림
fmt.Println("the end")
}
뮤텍스와 데드락
서로 다른 고루틴이 힙메모리의 동일한 메모리 자원에 접근할 시 오류 발생할 수 있음!! (힙 메모리란? 프로그래머가 수동으로 할당하는 메모리. 수동으로 할당하기 때문에 수동으로 해제해야함. 안 그러면 메모리 낭비됨. 힙은 접근과 해제가 스택에 비해 느림.)
뮤텍스
이를 해결하기 위해 뮤텍스(Mutual exclusion)의 락(Lock)을 사용할 수 있음. 락을 건 고루틴만 메모리 자원에 접근할 수 있고 락을 풀면 그제서야 다른 고루틴이 메모리에 접근할 수 있는 방식임. 한번에 한 고루틴만 메모리 자원에 접근 가능.
근데 사실 이럴거면 동시성 프로그래밍을 하는 의미가 없음. 그리고 락 거는것 자체도 시간이 걸려서 과도한 락킹은 성능 저하를 일으킬 수 있음. 또한 데드락(Deadlock)을 일으킬 수도 있음.
데드락
데드락이란 : 두개 이상의 작업이 서로의 작업이 끝나기만을 기다리는 상태 (교착상태)
사다리에서 올라가는 사람과 내려가는 사람이 마주쳐 둘다 못 움직이는 상태같은 것. 데드락이 일어나면 프로그램이 완전히 멈춰버리기도 함. 조건문 잘못 처리했을때 순서 잘못 물려서 데드락 일어나는 경우 많음. 근데 고루틴 100개중에 2개는 데드락이고 98개 멀쩡히 돌아가면 → 프로그램이 멈추지는 알아서 데드락 상태라는걸 알 수가 없고 디버깅도 힘듬. 그래서 뮤텍스는 매우 조심히 사용해야함!! 자원 보호를 위한 심플하고 좋은 방법이긴한데 적은 범위 내에서만 사용해야함.
채널
채널 : 고루틴간 메세지 큐(FIFO)임.
thread-safe한 queue임!! 멀티쓰레드 환경에서 락을 안 잡아도 쓸 수 있음. make()로 채널 인스턴스 생성.(마치 맵처럼) ‘채널 <- 데이터’ : 이게 push임. 데이터를 채널에 넣겠다는 소리이고, ‘변수 := <- 채널’ : 이건 pop임. 채널에서 데이터를 빼서 변수에 담겠다는 소리임!!!
package main
import "fmt"
func main() {
// 채널 생성
c := make(chan int)
// 데이터를 보내는 고루틴
go func() {
c <- 42 // 채널로 값 42 전송
}()
// 데이터를 수신
value := <-c // 채널에서 값 수신
fmt.Println(value) // 출력: 42
}
여기서 value에 값이 들어와야 그 다음이 실행됨!!! 채널에 데이터를 보내면 받을 때까지 기다림. 이 특징을 활용하여 고루틴이 끝날 때까지 기다리는 기능을 구현할 수 있음.
for n := range ch 이렇게 쓰면 채널에 데이터 들어올때까지 기다렸다가 들어오면 for문 실행되게 할 수 있음!!!
채널로 Producer / Consumer 패턴 구현하여 역할을 나누어 컨베이어 벨트처럼 하나의 고루틴이 하나의 작업만 하도록 할 수 있음. 고루틴과 채널을 연결시킨 컴파운드 패턴을 활용하는 방법은 무궁무진함!! 계속 만들어보고 써보고 해야함.
채널 버퍼링
채널 길이가 0이면? : 마치 택배 보관함 없을때는 받을 사람이 올때까지 택배차가 기다리는 것처럼 채널도 받을 고루틴을 기다림. 그냥 채널 만들기만 하고(기본 길이 0) 채널에 값 집어넣고 받는 애가 없으면 계속 기다리느라 다음 코드가 실행이 안될 수도 있음. 이런 채널을 Unbuffered channel이라고 하고 채널에 데이터를 보냈는데 수신자가 없는 경우 deadlock! 에러 발생함. 근데 채널 길이 지정해주면 택배보관함에 택배를 놓고 택배차는 떠날 수 있음. 그래서 받는 고루틴이 없어도 다음 코드가 실행될 수 있음. (근데 채널이 가득 차면 또 똑같은 상황 발생)
좀비 고루틴(고루틴 Leak)
for문으로 채널에 데이터가 들어오길 기다렸다가 작업을 수행하는 반복문 짤때, 채널을 닫아주지 않으면 고루틴이 무한 대기를 할 수 있음. close(채널)로 채널 닫아주며 더이상 보낼 데이터가 없음을 알려야함. 데드락 발생할 경우 어디서 채널 안 닫혔는지 찾기 힘드니까 for문 짤때 꼭 채널 close 신경쓰기!! val, ok := <- ch로 채널이 닫혔는지 확인할 수도 있음.
'Go lang' 카테고리의 다른 글
클라우드와 VM 이해하기 (0) | 2024.12.20 |
---|---|
Go 프레임워크 + 테스트코드 라이브러리 (0) | 2024.12.20 |
Go는 exception이 없다 (0) | 2024.12.20 |
Go 컴파일 언어 (0) | 2024.12.20 |
Go 포인터와 GC (0) | 2024.12.20 |