🚀

Goroutine — Ruby의 Thread와는 차원이 다른 경량 동시성

go 키워드 하나로 수만 개의 동시 작업을 돌린다. GIL은 없다.

Ruby에서 동시성을 다뤄본 사람은 GIL의 한계를 알 것이다. Thread를 10개 만들어도 CPU 바운드 작업은 하나만 실행된다. IO 바운드에서만 동시성이 의미 있다.

Go는 이 문제가 없다. goroutine은 진짜 병렬 실행된다.

goroutine 시작하기

go doSomething()  // 이게 전부다

Ruby에서 Thread.new { do_something } 하는 것과 비슷한데, goroutine은 스레드가 아니다. Go 런타임이 관리하는 경량 "그린 스레드"에 가깝다. 메모리 사용량이 스레드의 1/100 수준이다. goroutine 하나가 약 2KB, OS 스레드는 약 1MB.

GIL이 없다

Ruby(CRuby)의 GIL은 한 번에 하나의 스레드만 Ruby 코드를 실행하게 한다. IO 대기 중에만 다른 스레드가 실행된다. Go에는 이런 제약이 없다. goroutine이 CPU 코어 수만큼 진짜 병렬로 실행된다.

Ruby에서 Sidekiq으로 프로세스를 여러 개 띄워서 우회하는 패턴이 있다. Go에서는 그럴 필요가 없다. goroutine이 멀티코어를 자동으로 활용한다.

문제: 공유 상태

Ruby에서 Mutex를 쓰듯이 Go에서도 sync.Mutex가 있다. 하지만 Go 커뮤니티의 격언이 있다: "메모리를 공유하여 통신하지 말고, 통신하여 메모리를 공유하라." 이게 채널(Channel)이다. 다음 글에서 다룬다.

WaitGroup — Ruby의 Thread#join

Ruby에서 threads.each(&:join)으로 모든 스레드 완료를 기다리듯이, Go에서는 sync.WaitGroup을 쓴다: wg.Add(1)go func() { defer wg.Done(); ... }()wg.Wait().

Ruby에서 Go로

1

Ruby: Thread.new { } (GIL로 CPU 병렬 불가) → Go: go func() (진짜 병렬)

2

goroutine은 ~2KB, OS 스레드는 ~1MB — 수만 개 동시 실행 가능

3

Ruby: threads.each(&:join) → Go: sync.WaitGroup (Add/Done/Wait)

4

Ruby: Mutex.new → Go: sync.Mutex (있지만 Channel 사용 권장)

장점

  • GIL 없이 진짜 병렬 — CPU 바운드 작업에서 Ruby 대비 압도적 성능
  • goroutine이 극도로 가벼워서 동시성 모델 설계의 자유도가 높다

단점

  • 경쟁 조건(race condition) 디버깅이 Ruby보다 어렵다 — go run -race 플래그 필수
  • goroutine 누수(leak) — 끝나지 않는 goroutine이 메모리를 잡아먹는 문제

사용 사례

Ruby Sidekiq 워커를 Go로 전환하여 프로세스 수를 줄이고 싶을 때 수천 개의 동시 HTTP 요청을 처리하는 서버가 필요할 때 (Ruby는 프로세스/스레드 제한)

참고 자료