ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • goroutine 스케줄링
    Programming Language/golang 2024. 5. 1. 15:56

    goroutine 스케줄링이 어떻게 동작하는지 정리한다.


    0. OS 스레드의 단점

     

    - 생성/소거 비용이 비싸다.
    기본적으로 약 1MB 의 메모리 크기다.

     

    - 스레드의 숫자가 많으면 많을수록 컨텍스트 스위칭 비용이 비싸다.
    코어는 한정되어 있기 때문에 여러 스레드가 돌아가면서 사용할 때마다 컨텍스트 스위칭 발생.


    1. goroutine 특징

    - Goruntime이 관리하는 goroutine(고루틴)이라는 자체 동시성 모델을 구성하여 사용한다.
    OS 스레드가 1MB의 스택을 갖는 반면 고루틴은 이보다 훨씬 작은 약 2KB의 스택을 가짐.
    레지스터도 스택포인터(SP), 프로그램 카운터(PC), 리턴 포인터 정도가 전부이다.

    type g struct {
        stack       stack   // 스택
        stackguard0 uintptr 
        stackguard1 uintptr 
        m         *m        // m (고루틴이 실행될 쓰레드)
        sched     gobuf
        ...
    }
    type gobuf struct {
        sp   uintptr  // 스택 포인터
        pc   uintptr  // 프로그램 카운터
        g    guintptr // 고루틴 번호
        ctxt unsafe.Pointer
        ret  uintptr  // 리턴 포인터
        lr   uintptr  // 리턴 레지스터 (고루틴 종료 후 돌아갈 장소)
        bp   uintptr
    }



    - 최대한 논리 프로세서(코어)의 개수 만큼만 OS 스레드를 활용하여 컨텍스트 스위칭 비용을 줄인다.
    go는 1.5버전 이후부터는 디폴트로 시스템에서 사용가능한 모든 코어를 활용하여 병렬처리를 하고(Parallel), 각 코어에서는 여러 고루틴을 시분할하여 동시 처리합니다. (Concurrent)

    type p struct {
        id uint32
        ...
        runghead uint32
        rungtail uint32
        rung     [256]guintptr // 로컬 고루틴 큐
        ...    
        gcw gcWork
    }
    
    type m struct {
    	g0      *g     // goroutine with scheduling stack
    	...
    	p             puintptr // attached p for executing go code (nil if not executing go code)
    	nextp         puintptr
    	oldp          puintptr // the p that was attached before executing a syscall
    	id            int64
    	...
    }


    Goruntime에서 P (논리 프로세서에 해당) 와 M(OS 스레드에 해당)을 구성하여 관리한다.
    P는 런타임 초기화 과정에서 논리 프로세서의 수 만큼 생성된다.
    M은 하나의 P와 연관되어 있어야 하므로 P에 대응하는 만큼의 M도 런타임 초기화 과정에 생성된다.

    2. goroutine 스케줄링 방식

     

    GMP model

    1. P는 각각 내부에 LRQ (Local Run Queue)를 가지고 있는데, 큐에는 실행 가능한 고루틴들이 저장되어 있다. M은 P의 작업 큐를 가져와서 순차적으로 실행한다.
    2. 고루틴은 10ms 정도 실행되는데 (Fairness), 10ms 이내에 완료되지 않은 고루틴은 GRQ로 들어가게 되고, LRQ를 다 소진한 P는 GRQ로부터 고루틴을 가져와 실행하게 된다.

     

    3. 최적화 방법

    • Blocking System Call

      Blocking I/O의 Syscall이 발생하면 LRQ의 다른 고루틴에도 영향을 끼치기 때문에 syscall이 발생한 고루틴을 P와 연관되어 있지 않은 (별도로 생성하거나 하여...) 다른 M에게 넘겨 독립적으로 처리하도록 한다.
      syscall 처리가 끝난 고루틴은 원래의 P를 다시 찾아와서 붙거나 GRQ로 들어가게 된다.
    • Work Stealing 

      P가 모든 작업을 마치고 GRQ로부터 고루틴을 가져오는데, GRQ 에서도 대기 중인 고루틴이 없다면 다른 P의 LRQ로부터 고루틴을 가져온다.

    • Net Poller

      Network I/O 와 같은(select, poll, epoll) async system call가 발생하면 독립된 쓰레드인 Net Poller에서 해당 고루틴들을 관리한다. Net Poller는 비동기 시스템 콜이 끝났다는 이벤트를 수신하면 해당 고루틴을 다시 LRQ에 넣어서 실행될 수 있도록 한다.

    정리
    Go는 자체 동시성 모델인 고루틴을 사용하여 스레드를 경량화 하고 
    OS 스레드의 수를 최대한 코어 수에 가깝게 하여 컨텍스트 스위칭의 낭비를 줄여 간편하고 성능 좋은 동시성을 제공한다.

     

     

    참고 자료

    https://medium.com/curg/%EA%B3%A0%EB%A3%A8%ED%8B%B4-go-%EC%96%B8%EC%96%B4%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AA%A8%EB%8D%B8-1045986cc001

    'Programming Language > golang' 카테고리의 다른 글

    goquery 이슈  (0) 2021.12.28
Designed by Tistory.