GoSuda

Goroutine基础

By hamori
views ...

Goroutine

Gopher들에게 golang의 장점을 이야기 해달라하면 자주 등장하는 동시성(Concurrency) 관련 글이 있습니다. 그 내용의 기반은 가볍고 간단하게 처리할 수 있는 **고루틴(goroutine)**입니다. 이에 대하여 간략하게 작성해보았습니다.

동시성(Concurrency) vs 병렬성(Parallelism)

고루틴을 이해하기 전에, 자주 혼동되는 두 가지 개념을 먼저 짚고 넘어가려 합니다.

  • 동시성: 동시성은 많은 일을 한 번에 처리하는 것에 관한 것입니다. 꼭 실제로 동시에 실행된다는 의미가 아니라, 여러 작업을 작은 단위로 나누고 번갈아 가며 실행함으로써 사용자가 보기에는 동시에 여러 작업이 처리되는 것처럼 보이게 하는 구조적, 논리적 개념입니다. 싱글 코어에서도 동시성은 가능합니다.
  • 병렬성: 병렬성은 “여러개의 코어에서 여러개의 일을 동시에 처리하는것” 입니다. 말 그대로 병렬적으로 일을 진행하는 것이며, 다른 작업들을 동시에 실행합니다.

고루틴은 Go 런타임 스케줄러를 통해 동시성을 쉽게 구현하게 해주며, GOMAXPROCS 설정을 통해 병렬성까지 자연스럽게 활용합니다。

흔히 이용률이 높은 자바의 멀티쓰레드(Multi thread)는 병렬성의 대표 개념입니다.

고루틴은 왜 좋을까?

가볍다(lightweight)

생성비용이 다른 언어에 비해서 매우 낮습니다. 여기서 왜 golang은 적게 사용할까요? 라는 의문이 드는데 생성 위치가 Go런타임 내부에서 관리하기 때문입니다. 왜냐하면 위의 경량 논리 스레드 이기 때문입니다 OS쓰레드 단위보다 작고, 초기스택은 2KB정도의 크기를 필요로 하며 사용자의 구현에 따라 스택을 추가하여 동적으로 가변하기 때문입니다.

스택 단위로 관리하여 생성,제거가 매우 빠르고 저렴하게 처리가 가능하여 수백만개의 고루틴을 돌려도 부담스럽지 않은 처리가 가능합니다. 이로 인해 Goroutine은 런타임 스케쥴러 덕분에 OS커널 개입을 최소화 할 수 있습니다.

성능이 좋다(performance)

우선 Goroutine은 위의 설명처럼 OS커널 개입이 적어 사용자 수준(User-Level)에서 컨텍스트 스위칭을 할때 OS스레드 단위보다 비용이 저렴하여 빠르게 작업을 전환할 수 있습니다。

외에도 M:N모델을 이용하여 OS스레드에 할당하여 관리합니다. OS 쓰레드 풀을 만들어 많은 쓰레드가 필요없이 적은 쓰레드로도 처리가 가능합니다. 예를 들어 시스템 호출 과 같은 대기상태에 빠지면 Go런타임은 OS쓰레드에서 다른 고루틴을 실행하여 OS쓰레드는 쉬지 않고 효율적으로 CPU를 활용하여 빠른 처리가 가능합니다.

이로 인하여 Golang이 특히 I/O작업에서 다른 언어에 비해 높은 성능을 낼 수 있습니다.

간결하다(concise)

동시성이 필요한 경우 go 키워드 하나로 함수를 쉽게 처리할 수 있는것도 큰 장점입니다.

Mutex , Semaphore 등 복잡한 Lock을 이용해야 하며, Lock을 이용하면 필수적으로 고려야할 데드락(DeadLock) 상태를 고려할 수 밖에 없어 개발이전 설계단계에서 부터 복잡한 단계가 필요해집니다.

Goroutine은 "메모리를 공유하여 통신하지 말고, 통신하여 메모리를 공유하라"는 철학에 따라 채널(Channel)을 통한 데이터 전달을 권장하며 SELECT 는 채널(Channel)과 결합하여 데이터가 준비된 채널부터 처리할 수 있게 해주는 기능까지 지원합니다. 또한, sync.WaitGroup을 이용하면 여러 고루틴이 모두 끝날 때까지 간단하게 기다릴 수 있어 작업 흐름을 쉽게 관리할 수 있습니다. 이러한 도구들 덕분에 쓰레드 간의 데이터 경쟁 문제를 방지하고 보다 안전하게 동시성 처리가 가능합니다.

또한, 컨텍스트(context)를 이용하여 이를 사용자 수준(User-Level)에서 생명주기, 취소, 타임아웃, 데드라인, 요청범위를 제어 할 수 있어 어느정도의 안정성을 보장할 수 있습니다.

Goroutine의 병렬 작업(GOMAXPROCS)

goroutine의 동시성이 좋은점을 말했지만 병렬은 지원하지 않나? 라는 의문이 드실겁니다. 최근 CPU의 코어의수는 과거와 다르게 두자리 수가 넘어가며, 가정용 PC또한 코어가 적지않은 수가 들어가 있기 때문입니다.

하지만 Goroutine은 병렬작업까지 진행합니다 그것이 GOMAXPROCS입니다。

GOMAXPROCS 를 설정하지 않으면 버전별로 다르게 설정됩니다.

  1. 1.5 이전: 기본값 1, 1 이상 필요시 runtime.GOMAXPOCS(runtime.NumCPU()) 와 같은 방식으로 설정이 필수

  2. 1.5 ~ 1.24: 使用所有可用的逻辑核心数。从那时起,除非开发者有特殊限制需求,否则无需进行设置。

  3. 1.25: 作为容器环境中流行的语言,它会检查 Linux 上的 cGroup,以确认容器的 CPU限制 设置。

    那么,如果逻辑核心数为10个,CPU限制值为5,则 GOMAXPROCS 将设置为较低的5。

1.25 的修改带来了巨大的变化。这主要是因为容器环境下的语言使用率得到了提升。通过减少不必要的线程创建和上下文切换,可以避免 CPU 节流 (throttling)。

 1package main
 2
 3import (
 4	"fmt"
 5	"math/rand"
 6	"runtime"
 7	"time"
 8)
 9
10func exe(name int, wg *sync.WaitGroup) {
11	defer wg.Done()
12
13	fmt.Printf("Goroutine %d: 开始\n", name) // Goroutine %d: Start
14	time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 // Delay for task simulation
15	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Start
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 // Use only 2 CPU cores
20	wg := sync.WaitGroup();
21  goroutineCount := 10
22	wg.Add(goroutineCount)
23
24	for i := 0; i < goroutineCount; i++ {
25		go exe(i, &wg)
26	}
27
28	fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Waiting for all goroutines to finish...
29	wg.Wait()
30	fmt.Println("모든 작업이 완료되었습니다.") // All tasks completed.
31
32}
33

Goroutine의 스케쥴러 (M:N모델)

前面提到M:N模型用于将OS线程分配并管理,更具体来说,存在goroutine GMP模型。

  • G (Goroutine): Go中执行的最小工作单元
  • M (Machine): OS线程(实际工作位置)
  • P (Processor): Go运行时管理的逻辑处理器

P额外拥有一个本地运行队列(Local Run Queue),并充当调度器,将分配的G分配给M。简单来说,goroutine

GMP的运作过程如下:

  1. G (Goroutine) 创建后,会被分配到 P (Processor) 的本地运行队列中。
  2. P (Processor) 将本地运行队列中的 G (Goroutine) 分配给 M (Machine)。
  3. M (Machine) 返回 G (Goroutine) 的状态,包括 block、complete 和 preempted。
  4. Work-Stealing(工作窃取):如果 P 的本地运行队列为空,则其他 P 会检查全局队列。如果全局队列中也没有 G (Goroutine),则会窃取其他本地 P (Processor) 的工作,以确保所有 M 持续运行。
  5. 系统调用处理 (Blocking):当 G (Goroutine) 在执行过程中发生 Block 时,M (Machine) 将进入等待状态。此时,P (Processor) 会将 Block 的 M (Machine) 分离,并与另一个 M (Machine) 结合,以执行下一个 G (Goroutine)。这样,即使在 I/O 操作的等待时间内,CPU 也不会浪费。
  6. 如果一个 G (Goroutine) 长期占用(preempted),它将把执行机会让给其他 G (Goroutine)。

Golang的GC (Garbage Collector) 也在Goroutine上运行,能够以最小的应用中断 (STW) 并行清理内存,从而高效利用系统资源。

最后,Golang是语言的强大优势之一,除此之外还有很多,希望广大开发者都能享受Golang的乐趣。

谢谢。