-
golang: 고루틴(Goroutine) 연습해보기 초급편Back-End/Golang 2024. 11. 23. 14:58반응형
고루틴 학습 교과서: Go로 배우는 동시성 프로그래밍
서문
Go 언어의 핵심인 고루틴은 경량 스레드로, 효율적이고 강력한 동시성 프로그래밍을 가능하게 합니다. 이 교과서는 고루틴의 기초부터 심화된 패턴까지 체계적으로 학습할 수 있도록 설계되었습니다. 각 단계별로 학습 포인트와 실습 예제를 통해 실력을 점진적으로 향상시킬 수 있습니다.
1단계: 고루틴 기초
학습 포인트
- 고루틴의 정의 및 기본 사용법
- 동기 실행과 비동기 실행의 차이점
설명
고루틴은 go 키워드를 사용하여 비동기로 함수를 실행할 수 있는 Go의 기본 단위입니다. OS 스레드와 비교해 메모리 오버헤드가 작으며, 런타임 스케줄러가 고루틴을 관리합니다.
예제
package main import ( "fmt" "time" ) func printMessage(msg string) { for i := 0; i < 3; i++ { fmt.Println(msg) time.Sleep(500 * time.Millisecond) } } func main() { go printMessage("Hello from Goroutine!") printMessage("Hello from Main!") }
추가 연습
- time.Sleep을 제거하면 두 출력의 순서가 어떻게 달라지는지 확인하세요.
- 고루틴 없이 실행했을 때 결과를 비교해 보세요.
해설
- go printMessage("Hello from Goroutine!")는 새로운 고루틴으로 실행되며, 메인 고루틴과 병렬로 동작합니다.
- time.Sleep이 제거되면 main 고루틴이 종료되면서 다른 고루틴도 중단됩니다.
추가 학습 개념
- OS 스레드와의 차이점: 고루틴은 런타임 스케줄러에 의해 관리됩니다. 더 깊게 배우려면 Go의 M스케줄링 모델을 공부하세요.
- 동기적 코드와 비동기적 코드 비교.
2단계: 채널을 사용한 고루틴 통신
학습 포인트
- 채널의 기본 동작 이해
- 채널을 이용해 고루틴 간 데이터 전달
설명
채널은 고루틴 간 데이터를 안전하게 주고받기 위한 도구입니다. Go에서는 채널을 사용하여 동기화된 데이터 전송이 가능합니다.
예제
package main import "fmt" func calculateSquare(num int, ch chan int) { ch <- num * num } func main() { ch := make(chan int) go calculateSquare(4, ch) result := <-ch fmt.Println("Square:", result) }
추가 연습
- 채널을 buffered로 변경한 후 차이를 확인하세요.
- 두 개 이상의 고루틴에서 데이터를 처리하도록 확장하세요.
해설
- ch <- num * num은 채널에 데이터를 보냅니다.
- <-ch는 채널에서 데이터를 받습니다. 이 동작은 데이터를 받을 때까지 블로킹됩니다.
- make(chan int)은 unbuffered 채널을 생성하므로, 송신과 수신이 동시에 이루어져야 합니다.
추가 학습 개념
- Buffered vs Unbuffered 채널: make(chan int, 5)와 같이 버퍼 크기를 지정하여 동작 차이를 실험해 보세요.
- 채널 닫기: 채널을 닫을 때 사용하는 close(ch)와 닫힌 채널에 데이터를 보낼 때의 동작을 확인하세요.
3단계: Select 문으로 다중 채널 처리
학습 포인트
- Select 문을 사용해 여러 채널의 데이터를 동시에 처리
- Default 구문으로 비동기 동작 추가
설명
select는 다중 채널 작업을 동시에 처리하거나 우선순위를 제어할 수 있습니다.
예제
package main import ( "fmt" "time" ) func sendData(ch chan string, msg string, delay time.Duration) { time.Sleep(delay) ch <- msg } func main() { ch1 := make(chan string) ch2 := make(chan string) go sendData(ch1, "Data from ch1", 2*time.Second) go sendData(ch2, "Data from ch2", 1*time.Second) for i := 0; i < 2; i++ { select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) default: fmt.Println("No data received yet") time.Sleep(500 * time.Millisecond) } } }
추가 연습
- 추가적인 채널을 생성하고 작업을 확장하세요.
- Default 구문을 제거하고 실행 결과를 확인하세요.
해설
- select 문은 다중 채널의 데이터를 동시에 기다립니다.
- default 구문은 어떤 채널도 준비되지 않은 경우 실행됩니다.
- 데이터가 ch2에서 먼저 도착하므로 Data from ch2가 먼저 출력됩니다.
추가 학습 개념
- 채널 우선순위: select 구문에서 여러 채널이 준비된 경우, Go 스케줄러가 임의로 선택합니다.
- 타임아웃 추가: time.After를 사용해 대기 시간이 초과된 경우 처리하도록 확장하세요.
4단계: Mutex를 사용한 데이터 동기화
학습 포인트
- 고루틴 간 데이터 경합 방지
- sync.Mutex와 sync.WaitGroup의 사용법
설명
데이터 레이스는 동시성 프로그래밍에서 흔히 발생하는 문제로, 이를 방지하려면 Mutex로 공유 자원을 보호해야 합니다.
예제
package main import ( "fmt" "sync" ) var count = 0 var mutex sync.Mutex func increment(wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 1000; i++ { mutex.Lock() count++ mutex.Unlock() } } func main() { var wg sync.WaitGroup wg.Add(2) go increment(&wg) go increment(&wg) wg.Wait() fmt.Println("Final count:", count) }
추가 연습
- Mutex를 제거하면 어떤 문제가 발생하는지 실험해 보세요.
- 고루틴 수를 늘려 처리 속도를 확인하세요.
해설
- mutex.Lock()과 mutex.Unlock()로 공유 변수 count에 대한 데이터 레이스를 방지했습니다.
- sync.WaitGroup은 모든 고루틴이 완료될 때까지 main 함수의 종료를 지연시킵니다.
추가 학습 개념
- 데이터 레이스 탐지: go run -race 명령어로 데이터 레이스 발생 여부를 확인하세요.
- Atomic 연산: sync/atomic 패키지를 사용해 경량 동기화를 학습하세요.
5단계: Context로 고루틴 관리
학습 포인트
- 고루틴 타임아웃 설정
- Context를 사용한 고루틴 취소
설명
컨텍스트는 고루틴을 효과적으로 관리할 수 있는 Go의 내장 기능입니다. 시간 초과, 취소 신호 등을 전파할 수 있습니다.
예제
package main import ( "context" "fmt" "time" ) func work(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("Work canceled") return default: fmt.Println("Working...") time.Sleep(500 * time.Millisecond) } } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go work(ctx) time.Sleep(3 * time.Second) fmt.Println("Main finished") }
추가 연습
- context.WithCancel을 사용해 수동으로 고루틴을 종료해 보세요.
- 여러 고루틴이 하나의 Context를 공유하도록 수정해 보세요.
해설
- context.WithTimeout은 2초 후에 컨텍스트가 종료됩니다.
- ctx.Done() 채널을 통해 고루틴 종료 신호가 전파됩니다.
추가 학습 개념
- Context 계층 구조: 부모 컨텍스트가 취소되면 하위 컨텍스트도 자동으로 취소됩니다.
- Context Value: 컨텍스트에 키-값 데이터를 저장하고 전달하는 방법.
6단계: 고루틴 풀 구현
학습 포인트
- 고루틴 수를 제한해 자원 최적화
- 작업 큐 처리 방식
설명
고루틴을 무분별하게 생성하면 시스템에 과부하를 줄 수 있습니다. 고루틴 풀은 제한된 개수의 고루틴으로 작업을 처리합니다.
예제
package main import ( "fmt" "sync" ) func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) { defer wg.Done() for job := range jobs { fmt.Printf("Worker %d processing job %d\n", id, job) results <- job * 2 } } func main() { const numWorkers = 3 const numJobs = 10 jobs := make(chan int, numJobs) results := make(chan int, numJobs) var wg sync.WaitGroup for i := 1; i <= numWorkers; i++ { wg.Add(1) go worker(i, jobs, results, &wg) } for j := 1; j <= numJobs; j++ { jobs <- j } close(jobs) wg.Wait() close(results) for result := range results { fmt.Println("Result:", result) } }
추가 연습
- 실패한 작업을 재시도하는 로직을 추가해 보세요.
- 작업 우선순위에 따라 처리를 제어하도록 수정하세요.
해설
- 작업이 jobs 채널로 전달되고, 워커가 이를 처리합니다.
- 고루틴 풀은 제한된 수의 워커를 사용해 자원을 효율적으로 활용합니다.
추가 학습 개념
- Task Queue 관리: 작업 실패 시 재시도 로직을 추가하세요.
- Worker Pool 확장: 워커 수를 동적으로 조절하는 로직을 구현해 보세요.
7단계: 실무 사례 - 병렬 처리
학습 포인트
- 실무 서비스에서 고루틴 사용 사례 이해
- 병렬 데이터 처리로 성능 최적화
설명
요청을 병렬로 처리하여 속도를 높이는 방법을 학습합니다.
예제
package main import ( "fmt" "time" ) func fetchIndexing() string { time.Sleep(2 * time.Second) return "Indexing result" } func fetchImages() string { time.Sleep(1 * time.Second) return "Images result" } func fetchRecommendations() string { time.Sleep(3 * time.Second) return "Recommendations result" } func main() { results := make(chan string, 3) go func() { results <- fetchIndexing() }() go func() { results <- fetchImages() }() go func() { results <- fetchRecommendations() }() for i := 0; i < 3; i++ { fmt.Println(<-results) } }
추가 연습
- 네트워크 실패 상황을 시뮬레이션하고 복구 로직을 추가하세요.
- 채널 대신 sync.WaitGroup을 사용하여 동기화해 보세요.
해설
- 고루틴은 각 검색 작업을 병렬로 처리하여 응답 속도를 최적화합니다.
- 채널을 사용해 작업 결과를 비동기로 수집합니다.
추가 학습 개념
- 비동기 결과 처리: 채널 대신 sync.WaitGroup을 사용하여 작업 완료를 대기해 보세요.
- 실패 처리: 작업 실패 시 복구를 시뮬레이션합니다.
8단계: 고급 주제
추가 개념 1: Goroutine Leak 탐지
- 고루틴이 중단되지 않으면 리소스 누수가 발생합니다.
- Leak을 방지하려면 항상 고루틴에 종료 조건을 추가하세요.
func worker(ch chan struct{}) { for { select { case <-ch: return // 고루틴 종료 default: fmt.Println("Working...") } } }
추가 개념 2: pprof로 프로파일링
- net/http/pprof 패키지를 사용해 고루틴 병목 현상과 메모리 사용량을 분석합니다.
import _ "net/http/pprof" go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
반응형'Back-End > Golang' 카테고리의 다른 글
golang: 런타임(Runtime)이란? (1) 2024.11.27 golang: init functions (0) 2024.11.21 golang: godoc 철학과 사용방법 (1) 2024.11.18 golang: 고언어의 철학(Clear is better than clever) (0) 2024.11.16 golang: 제네릭(Generic) 심화과정 (1) 2024.11.10