ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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))
    }()
    반응형
Designed by Tistory.