https://youtu.be/MYtUOOizITs?si=bUId7ieorNpUOvJD
Jesús Espino: mattermost (slack의 오픈소스 버전) 소속 Software Engineer
goroutine을 설명하려면, go의 scheduler 이해가 필요하다.
go scheduler를 구성하는 컴포넌트부터 간단히 소개하자면
Processor (alias P): Representation of Virtual CPU
- goMaxProcs: number of Ps that scheduler have.
- Status 정보 가지고 있음 - Idle, Running, Syscall, gcStop
- Associated the Current 'M'
- 개별 P는 본인이 실행할 GoRoutine정보를 관리할 Queue가 있음.
- List of Free GoRoutines
- other metadatas..
Machine (alias M): OS의 Thread. 코드를 실행하는 CPU 구역.
- M에서 실행중인 Current Goroutine정보 포함.
- M에 연결되어 있는 Processor P 정보
- 연결된 P 정보가 null인 경우도 있다.
- other metadatas..
Scheduler (alias Sched)
- Idle Ms / Idle Ps 정보 가지고 있음.
- List of Global runnable goroutines
- 아직 아무 process에도 할당되지 않은 goroutines들
- List of Global Free goroutines
Goroutine (alias G)
- Stack (2048 bytes = 2KB chunk of memory)
- program counter (thread의 program counter와 비슷한 역할)
- status: 현재 goroutine의 상태 (Running, Waiting, Runnable...)
- Current M: goroutine이 실행되는 machine정보
- Wait Reason: waiting상태일 때 reason정보가 기록되는 곳
- other metadatas...
구성요소들을 가지고 Scheduler의 모습을 설명하자면 위와 같다.
- Scheduler는 List of Free goroutines / runnable goroutines 정보와 '어떤 goroutine이 어느 P, M에서 실행되는지를 알고 있다.
- Global Runtime 변수로 List of Ms / Ps / Gs 가지고 있음.
The Birth of a goroutine
goroutine 생성 방법은 크게 두 가지.
- create it from scratch.
- reuse an old goroutine thia is no longer working.
goroutine 실행이 끝나면 Status는 Dead가 된다. Free goroutines는 전부 Dead 상태임.
- free goroutine이 있다면, dead상태인 걸 reuse
- free goroutine이 없다면 새 goroutine 생성 -> kill -> dead상태인 것 사용함.
새로운 goroutine 실행이 필요하다?
- pick one of the free goroutine in Free List
- raise up from the dead
- put into the queue of runnable goroutines of Processor.
- call the scheduler -> scheduler Execute that goroutine.
만약 해당 processor에 free goroutine이 없다면?
- global goroutine queue에서 free인 것 가져온다. (다른 processor에 할당되지 않은 상태인 것)
- raise up from the dead -> add to queue
global goroutine list에서도 free인 게 없다면?
- 내부에서 신규 생성 -> kill -> raise up dead & add queue
The life of a goroutine
Runnable to Running: scheduler가 신규 goroutine을 활성화할 때.
- scheduler가 실행가능한 goroutine을 찾는다.
- local process 내부에서 찾기
- global runnable queue에서 찾기
- 여기에도 없으면 net poll로 간다.
- net poll: system that allows go to do IO work, in a efficient way. IO작업 끝나면 get goroutine runnable again.
- goroutine을 찾아서 machine (OS Thread)에 할당하면 Running으로 변경
- 이후 코드 실행.
Runnable to Waiting: Go Concurrency의 핵심.
- 예컨대 channel is not buffered / have to wait for something...
- goroutine 스스로 자신의 state를 wait로 변경, wait reason에 이유 작성
- detach from Machine
- run the scheduler (to schedule a New goroutine)
go 소스코드에서 확인할 수 있는 Wait Reason의 전체 목록. 요약하면 아래와 같다.
- GC
- Mutex
- Semaphore
- Channel
- Sleep
- IO
Running to Syscall / to runnable
- syscall: OS에 명령어 실행 요청하는 것.
- 요청할 경우 detach from Processor.
- 실행요청 시 state를 syscall로 변경.
- syscall 요청이 끝나면 runnable로 변경 + queue에 전달.
Running to CopyStack / back
- 실행과정에서 stack크기를 키워야 할 경우 state가 변경됨.
- inMemory stack 크기를 2배 증설 -> 할당된 데이터들 copy
- 복사 완료되면 다시 running으로 변경
Waiting to Runnable
- 다른 goroutine이 goready를 호출할 때.
- 예컨대 channel에서 메시지 받으려고 대기중인 goroutine이 있고 다른 goroutine이 channel에 메시지를 보낸다면, 메시지 보낸 뒤 wake up하는 식
- added to queue in processor
- try to get processor to execute
GC 끝나서 다시 goroutine 실행할 경우
- Reactivate a list of goroutines
- runnable로 상태 변경
- queue에 추가 + 필요한 processor에 할당
wait가 필요한 로직이라서 waiting으로 변경했는데, 확인해보니 그럴 필요가 없는 경우
- i'm going to wait for X, but X is already fulfilled.
goroutine 찾으려고 netpoll까지 가는 경우
Running to preemmpt, waiting and runnable
- go는 preemptive GC / preemptive runtime이 있다.
- 예컨대 특정 goroutine이 너무 오랜 시간 실행되고 있으면, scheduler가 goroutine 실행되는 OS thread에 signal 보낸댜.
- 그러면 goroutine은 preempt로 변경됨.
- preempt 이후 waiting으로 바뀌며, GC 등 필요한 프로세스가 끝난 뒤 다시 runnable로 변경됨.
Example
예컨대 channel이 하나 있고, goroutine에서 데이터를 넘겨주면 다른 goroutine에서 데이터를 받아 실행하는 구조를 생각해보자. channel은 buffer가 없는 상태.
- channel struct에 정의된 list of goroutine에 자기 자신을 밀어넣고 waiting 상태로 진입.
- channel에서 데이터를 읽으려는 다른 goroutine이 진입
- read the data directly from the memory of the other goroutine.
- 필요한 작업이 끝나면, waiting 상태에 있는 goroutine에 goready 호출.
- data를 전달해준 goroutine은 goready 받았으므로 runnable로 변경, data를 받은 쪽은 프로세스 마저 실행.
WaitGroup으로 3을 정의할 경우
- waitgroup 내에서 goroutine이 실행되는 동안, 메인 프로세스는 waiting
- waitgroup 개수만큼 있던 goroutine이 전부 done이 되면, waiting중인 프로세스에 getReady 명령어를 보낸다.
- getReady 받은 메인 프로세스는 waiting -> runnable로 변경됨.
the Death of a goroutine
- 기본적으로는 '작업이 끝나면' 죽는다.
- state를 dead로 바꾸고
- goroutine을 Machine에서 할당 해제하고
- free list of P에 goroutine 정보 추가
- call scheduler
Summary
- scheduler의 동작과정에서 뻬놓을 수 없는 게 go의 GC인데, 발표가 너무 길어져서 뺐다.
- netpoll도 언급만 하고 자세한 내용은 소개하지 않았음.
- cgo도 goroutine 활용해서 구현됐는데 소개하지 않음
- mark assist: gc중에 수행되는 보조 역할 / goroutine이 하는 일 중 하나.
- sysmon (system monitor)...
'학습일지 > Language' 카테고리의 다른 글
EuroPython 2022 - From Pip to Poetry: Python ways of packaging and publishing (0) | 2023.06.11 |
---|---|
Writing Beautiful Package in Go (0) | 2022.05.29 |
Go - Context 정리 (0) | 2022.05.26 |
JIT Compiler (0) | 2021.03.11 |
[Design Pattern] Facade (0) | 2020.12.28 |