고루틴은 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) }
주요 특징:
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 { // 대기 또는 차단 호출 없음 } }
마이크로서비스에서 주기적으로 데이터베이스에서 읽는 고루틴을 시작하지만 요청이 취소될 때 종료하는 것을 잊습니다. 결과적으로 "걸린" 고루틴이 남아 시간이 지남에 따라 모든 메모리를 소모합니다.
장점:
단점:
작업 취소 제어를 위해 context를 사용하고, 애플리케이션 종료 전에 모든 고루틴 종료 관리를 위해 WaitGroup을 사용하며, 실행자 간 데이터 전송을 위해 채널을 사용합니다.
장점:
단점: