• Feed
  • Explore
  • Ranking
/
/
    Go

    Goroutine에 대하여

    goroutine에 대해 알아보자
    GoDevOps
    A
    Antonio
    2025.03.10
    ·
    14 min read

    3859

    goroutine이란

    goroutine은 Go 언어에서 제공하는 경량 스레드(lightweight thread) 개념으로, OS 수준 스레드와 달리 매우 적은 비용으로 생성·관리할 수 있는 동시성(concurrency) 실행 단위입니다. Go 런타임이 직접 스케줄링하여, 개발자는 go 키워드만 붙이면 별도의 고루틴에서 해당 함수를 실행하도록 만들 수 있습니다.

    특징

    • 가벼움: 고루틴은 일반적인 스레드보다 생성·스위칭 비용이 훨씬 낮으며, 시작 시 약 2KB 정도의 작은 스택을 사용하고, 필요한 경우에만 동적으로 확장합니다.

    • 간단한 동시성: 고루틴을 생성(go funcName())하고, 채널(channel)을 사용하면, 복잡한 락(lock) 없이도 안전하고 간결한 동시성 프로그래밍을 할 수 있습니다.

    • M:N 스케줄링: 고루틴 여러 개(G)를 실제 운영체제 스레드(M)에 매핑해주는 방식으로, Go 런타임이 내부적으로 효율적인 스케줄링을 수행합니다.

    goroutine이 생겨난 이유

    1. 동시성(Concurrency)과 병렬성(Parallelism)에 대한 요구

      Go 언어는 2009년 구글에서 개발을 시작했으며, 현대 프로그래밍 환경에서 점점 중요해지고 있는 동시성(Concurrency) 문제를 간단하고 효율적으로 해결하고자 설계되었습니다. 전통적인 스레드(Thread)를 이용한 동시성 프로그래밍 방식은 다음과 같은 문제가 있었습니다.

      • 오버헤드(Overhead) 문제: OS 레벨 스레드는 생성할 때 비용이 많이 들고, 스케줄링 또한 무겁습니다.

      • 복잡한 동기화: 동시성 프로그래밍에서 스레드 간 공유 자원을 다룰 때 Mutex, Semaphore, Condition variable 등을 사용해야 하며, 이는 러닝타임(런타임)에 구현이 복잡하고 코드 가독성도 떨어집니다.

      이러한 문제를 보다 단순하게 해결하기 위해 Go는 동시성 모델로 CSP(Communicating Sequential Processes) 모델을 채택하였고, 이를 더 쉽게 구현하기 위해 ‘작고 가벼운 스레드’인 고루틴을 도입하였습니다.

    2. 경량 스레드(Lightweight Thread)에 대한 요구

      전통적인 OS 스레드는 생성 시나 스케줄링 시에 무거운 비용이 발생합니다. 반면에 고루틴은 Go 런타임 레벨에서 스케줄링되어 수많은(수천~수백만 개까지) 고루틴을도 운영체제 스레드에 비해 훨씬 적은 비용으로 관리할 수 있도록 설계되었습니다. 이러한 경량성은 Go 언어가 빠르고 효율적인 동시성 처리를 가능하게 만드는 핵심 요소입니다.

    goroutine의 필요성

    1. 고성능 서버 및 네트워크 프로그램

      Go는 네트워크 서버, 분산 시스템, 마이크로서비스 등에 활용될 때 뛰어난 성능과 간단한 코드 구현 방식을 제공합니다. 예를 들어 HTTP 서버를 구성할 때, 요청마다 고루틴을 생성하여 동시성을 극대화할 수 있습니다.

    2. 간단한 동시성 코드 구현

      Go 언어는 “동시성은 복잡하지만, 코드는 단순해야 한다”라는 기조를 지향합니다. go 키워드를 사용하여 쉽게 고루틴을 만들고, 채널(channel) 이라는 Go만의 동시성 원리를 통해 고루틴 간 데이터를 안전하게 전달하고 동기화할 수 있습니다.

    3. 효율적인 리소스 활용

      고루틴은 필요할 때만 생성되고, 실행이 일시 중단(sleep 등) 상태가 되면 다른 고루틴들에게 CPU 자원을 양보합니다. 또한 최소 2KB(Go 1.4 이전에는 스택 분할(Segmented stack)을 사용했으나, 현재는 동적으로 스택이 확장되는 방식을 사용)에 불과한 스택으로 시작해 필요할 때 점진적으로 스택을 키워나가므로 메모리를 효율적으로 관리할 수 있습니다.

    goroutine의 활용 방법

    1. goroutine 생성

    func main() {
        go func() {
            fmt.Println("Hello from a goroutine!")
        }()
    
        // 메인 함수(고루틴)가 종료되면 모든 고루틴이 함께 종료되므로
        // 잠시 대기하도록 Time.Sleep 또는 sync.WaitGroup 등을 사용할 수 있음
        time.Sleep(1 * time.Second)
    }
    
    1. 동기화 및 데이터 교환: 채널(Channels)

      Go에서 고루틴 간 데이터 전달 및 동기화를 위해 가장 널리 사용되는 방법이 채널(channel) 입니다. 채널은 타입 안전(type-safe)한 FIFO 큐와 같은 역할을 하며, <- 연산자를 통해 데이터를 주고받습니다.

    func main() {
        ch := make(chan int)
    
        go func() {
            // 채널에 값 보내기
            ch <- 42
        }()
    
        // 채널에서 값 받기 (동기화 발생)
        val := <-ch
        fmt.Println(val) // 42
    }
    
    1. WaitGroup, Mutex 등 동기화 패키지

      표준 라이브러리인 sync 패키지에는 여러 동기화 기법들이 제공됩니다.

      • sync.WaitGroup: 여러 고루틴이 완료될 때까지 기다릴 수 있음

      • sync.Mutex: 상호 배제(Mutual Exclusion)

      • sync.RWMutex: 읽기/쓰기에 대한 락 분리

      • sync.Once: 한 번만 실행되는 로직 보장

    goroutine의 내부 동작 원리 및 내부 동작 코드

    고루틴은 Go 런타임에 의해 M:N 스케줄링(다수의 고루틴을 소수의 OS 스레드에 매핑) 방식으로 동작합니다. 이를 이해하기 위해선 Go 런타임이 사용하는 3가지 핵심 개념을 알아야 합니다.

    1. G(Goroutine): 고루틴 자체를 나타냅니다. 각 고루틴은 자신만의 스택, 명령 포인터, 스케줄러 상태 등을 가지고 있습니다.

    2. M(Machine): 실제로 OS 레벨에서 존재하는 스레드를 나타냅니다. 고루틴은 하나 이상의 M에서 실행됩니다.

    3. P(Processor): 스케줄링에 필요한 런타임 논리를 캡슐화한 추상 개념입니다. M은 P를 붙잡고 있을 때만 G를 실행할 수 있습니다. Go 런타임은 GOMAXPROCS 값을 통해 사용할 P(논리 프로세서)의 개수를 제한할 수 있습니다.

    M:N 스케줄링 동작 개요

    • G는 큐(또는 deque)에 들어 있으며, 유휴(idle) 상태의 M이 P를 소유하고 G를 dequeue하여 실행합니다.

    • G가 블로킹 연산(예: 시스템 호출) 등으로 인해 기다려야 하는 상황이 되면, 런타임은 해당 M을 다른 G가 사용하도록 만들거나, 다른 M을 찾아 G를 실행하게 합니다.

    • Go 런타임은 이러한 작업을 자동으로 처리하며, 개발자는 단지 고루틴과 채널을 사용해 동시성 로직을 설계하기만 하면 됩니다.

    goroutine 스택 메모리 구조

    Go 1.4 이전에는 분할 스택(segmented stack) 을 사용했으나, Go 1.4 이후에는 스택이 동적으로 확장(resize)되는 방식을 취합니다.

    초기 스택 크기는 약 2KB로 매우 작습니다. 필요 시 런타임이 자동으로 스택을 재할당(re-allocate)하여 확장합니다. 이는 많은 고루틴을 동시에 생성해도 메모리 사용량을 크게 늘리지 않게 해줍니다.

    goroutine 생성 코드 내부(Runtime 레벨)

    Go 언어 내부 소스코드(runtime/proc.go 등)에서 고루틴이 생성되는 과정을 간단히 요약하면 다음과 같습니다.

    1. 새로운 G 구조체 할당: runtime.newg() 함수 등을 통해 G(고루틴)에 대한 구조체를 생성하고 초기화합니다.

    2. 스택 할당: 최소 스택(2KB) 사이즈를 갖는 메모리를 할당합니다.

    3. 함수 진입점 설정: 실행할 함수 포인터와 스택에 대한 정보를 기록해 둡니다.

    4. 러닝 큐에 등록: 새로 생성된 G를 전역/로컬 run queue 등에 등록합니다.

    5. 스케줄러가 선택: Scheduler는 다음에 실행 가능한 G를 선택하여 실제 OS 스레드 M에서 실행하게 합니다.

    // go func() { ... } 호출 시
    func newproc(fn *funcval) {
    	// 현재 실행 중인 고루틴(G 구조체)에 대한 포인터를 반환
    	gp := getg() 
    	// 함수를 호출한 쪽(즉, newproc를 호출한 곳)의 PC(Program Counter) 주소를 가져옵니다.
    	pc := sys.GetCallerPC() 
    	systemstack(func() { // 블록 내부 코드를 시스템 스택(system stack) 위에서 실행합니다.
    		newg := newproc1(fn, gp, pc, false, waitReasonZero)
    
    		pp := getg().m.p.ptr()
    		runqput(pp, newg, true)
    
    		if mainStarted {
    			wakep() 
    			// 다른 OS 스레드(M)가 잠들어 있거나, 일할 여유가 없는 상황이라면 새로 깨워서(wake) 고루틴을 실행하게 만듭니다.
    			// mainStarted가 참이면, 이미 런타임 초기화가 끝나서 메인 고루틴이 실행 중이므로, 새 고루틴을 빠르게 스케줄링하기 위해 wakep()를 호출합니다.
    		}
    	})
    }
    
    • newg := newproc1(fn, gp, pc, false, waitReasonZero): 실제 새로운 고루틴(G 구조체)을 생성·초기화하는 핵심 함수입니다.

      • fn: 실행할 함수(포인터)

      • gp: 현재 고루틴 정보

      • pc: 호출 위치 정보

      • false: 고루틴 생성 옵션(예: 스택 복사 여부 등)

      • waitReasonZero: 대기 이유(디버깅용)

    • getg().m.p: 현재 스레드(Machine) m이 보유한 p(Processor)를 가리킵니다.

      • ptr()는 내부에서 p를 실제 포인터로 변환

    • runqput(pp, newg, true): 새로 생성된 고루틴(newg)을 해당 P의 실행 큐(run queue) 에 집어넣습니다.

    런타임 스케줄러의 동작

    스케줄러는 주기적으로(또는 I/O 대기, syscall, GC 등 주요 이벤트가 발생할 때) 고루틴 상태를 확인하고, 현재 실행 가능한 고루틴을 적절한 OS 스레드(M)에 할당하여 실행하도록 합니다. 아래는 아주 단순화된 스케줄러 루프 예시입니다.

    func schedule() {
        for {
            gp := findRunnableG()
            if gp != nil {
                executeG(gp)
            }
        }
    }
    
    func findRunnableG() *g {
        // 1) 현재 P의 run queue, 2) 전역 run queue, 3) work stealing 순으로 검색
        // ...
    }
    
    func executeG(gp *g) {
        // 현재 M이 P를 소유 중이라면 gp를 실행
        // ...
    }
    

    - The Go Programming Language
    https://go.dev/src/runtime/proc.go
    What are goroutines and how are they scheduled?
    I use goroutines all the time, but I got asked about how they are scheduled recently and I did not...
    https://dev.to/gophers/what-are-goroutines-and-how-are-they-scheduled-2nj3






    - 컬렉션 아티클