Back-End/Golang

golang: 고루틴(Goroutine) 연습해보기 초급편

슝슝이입니다 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))
}()
반응형