프로그래밍백엔드 개발자

고루틴(goroutines)과 Go 스케줄러는 어떻게 작동하며, 동시성 작업의 올바른 관리는 왜 중요한가?

Hintsage AI 어시스턴트로 면접 통과

답변.

고루틴은 Go 아키텍처에 내장된 경량 실행 스레드로, 효율적인 동시성을 달성하기 위해 첫 번째 버전부터 존재했습니다. 역사적으로 경량 스레드의 개념은 시스템 스레드의 높은 비용을 피하고 확장 가능한 서버 애플리케이션에 대한 높은 수요로 인해 나타났습니다. Go는 본래 서버 및 네트워크 시스템을 위한 언어로 설계되었으며, 수백만 개의 작업이 동시에 처리되어야 합니다.

문제: 동시성은 race condition, 데드락, 메모리 소비 증가로 빠르게 이어질 수 있으므로 고루틴의 생명주기를 관리하고 스케줄링을 고려하며 종료 관리를 하지 않으면 됩니다.

해결책: 고루틴은 go 키워드로 시작됩니다. 고루틴의 작업은 Go 스케줄러에 의해 계획되며, 스케줄러는 M:N 모델(M OS 스레드가 N Go 고루틴을 처리)로 작동합니다. 생명주기 관리를 위해 채널, WaitGroup, context 및 채널 종료 관리를 사용합니다.

코드 예시:

package main import ("fmt"; "time") func worker(id int) { fmt.Printf("작업자 %d 시작됨 ", id) time.Sleep(time.Second) fmt.Printf("작업자 %d 완료됨 ", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }

주요 특징:

  • 고루틴의 즉각적이고 저렴한 생성(OS 스레드보다 수만 배 저렴).
  • 채널을 통한 직접적인 상호작용, 동기화 및 데이터 교환 보장.
  • 작업 완료를 수동으로 관리해야 함(어떤 고루틴이 완료되고, 누가 중단하며, 중지 신호는 어떻게 전달되는지).

함정 질문.

main에서 고루틴을 명시적으로 기다리지 않으면 항상 실행되는가?

아니요, main의 실행이 완료되면 프로세스는 자식 고루틴의 상태와 상관없이 종료되며, 모든 작업이 완료되지 않을 수 있습니다.

go func(...)를 루프에서 실행하는 것이 각 고루틴이 루프 변수의 고유 값을 받는 것을 보장하는가?

아니요, 루프 변수 캡처 문제가 발생할 수 있으며, 고루틴이 동일한 슬라이스/변수의 값을 사용할 수 있습니다. 변수를 복사하여 전달해야 합니다:

for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }

하나의 고루틴이 Go 스케줄러를 차단하고 다른 고루틴이 실행되지 않게 할 수 있는가?

네, 무한 루프나 매우 무거운 루프가 전환 지점(예: 시간 함수 호출이나 yield 없이) 없이 실행되면 OS 스레드를 계속 잡을 수 있습니다. 이는 '협력적 다중 작업'에 대한 Go의 이념에 반합니다. 예를 들어, 차단이 없는 무거운 함수:

func busy() { for { // 대기 또는 차단 호출 없음 } }

일반적인 오류 및 안티 패턴

  • 종료 관리 없이 고루틴 시작
  • 익명 함수로 내부에 전달하지 않고 루프 변수 캡처
  • "leaky goroutines"(종료되지 않는 고루틴으로 인한 시스템 과부하)
  • 채널을 통한 데이터 교환 시 동기화 오류 무시

실생활 사례

부정적인 사례

마이크로서비스에서 주기적으로 데이터베이스에서 읽는 고루틴을 시작하지만 요청이 취소될 때 종료하는 것을 잊습니다. 결과적으로 "걸린" 고루틴이 남아 시간이 지남에 따라 모든 메모리를 소모합니다.

장점:

  • 높은 시작 속도
  • 확장성의 용이함

단점:

  • 메모리 누수
  • 응답 시간 증가
  • 예측할 수 없는 종료

긍정적인 사례

작업 취소 제어를 위해 context를 사용하고, 애플리케이션 종료 전에 모든 고루틴 종료 관리를 위해 WaitGroup을 사용하며, 실행자 간 데이터 전송을 위해 채널을 사용합니다.

장점:

  • 예측 가능한 생명주기
  • 종료 관리
  • 쉽게 확장 가능

단점:

  • 명시적으로 취소 및 동기화 로직을 작성해야 함
  • 프로그램 아키텍처가 약간 더 복잡함