기존에 커널이 처리하던 스케줄링을 Goroutine은 컴파일러와 Go 런타임이 처리.
대다수의 커널은 Preemptive Scheduling(선점 스케줄링)을 택하고 있으므로 프로세스나 스레드는 커널에 의해 원치 않게 CPU를 회수당할 수 있음.(자원 독점을 방지하기 위해)
Go런타임은 Cooperative Scheduling(협조 스케줄링)을 택하여 Goroutine이 자발적으로 실행을 종료하거나 대기상태로 전환되길 기다림.
스레드 스위칭에서는 Stack Pointer, Program Counter와 같은 Thread Control Block을 저장 후 로드해야 함.(PCB보단 오버헤드가 훨씬 적음)
Goroutine은 well-defined safe-point function call에서만 프로세서를 양보하며, 이러한 point들은 blocking에 관한 operation들이므로 구동에 필요한 레지스터 이외에는 사용하지 않음.
OS 대신 Go 런타임이 Goroutine에 스택 할당.
생성 및 삭제 시 Goroutine은 System Call 없이 유저 스페이스에서 동작해 훨씬 빠름.
커널 스레드는 비싸기 때문에 되도록 작은 수를 사용.
많은 수의 goroutine을 실행하여 높은 concurrency[1]를 유지.
N 코어 머신에서 N개의 goroutine을 Parallel[1]하게 동작시킴.
G: Goroutine을 의미.
M: Machine, 즉 OS의 스레드를 의미. 표준 POSIX 스레드를 따름.
P: Processor, 스케줄링에 대한 context를 지니고 있음.
실행 가능한 상태의 Goroutine들을 보관하는, 힙에 할당된 리소스.(즉 대기열)
LRQ에 할당되지 못한 대부분의 Goroutine들이 모여 있는 Run queue.
Goroutine이 생성되었을 때 해당 Processor의 LRQ가 가득찬 경우 GRQ에 저장됨.
GRQ는 Heap 영역의 공동 리소스이고, 접근할 때마다 Lock이 필요함.
각 Processor마다 존재하는 Run queue.
Processor는 자신이 소유하는 LRQ로부터 Goroutine을 하나씩 가져와 구동시킴.
GRQ에서 발생하는 Race Condition을 줄임.
FIFO와 Size 1짜리 LIFO로 이루어짐.
Create threads when needed; Keep them around for reuse.
Runtime Scheduler는 goroutine이 필요할 때 스레드를 생성.
스레드에 더 이상 실행할 goroutine이 없다면 스레드를 종료하는 대신 idle 상태로 둠.
Goroutine이 끊임 없이 생성되더라도 생성할 수 있는 스레드 수가 Processor 수를 넘지 않음.
Use N runqueues on an N-core machine.
스레드가 새로 만들어졌거나 작업이 너무 빨리 끝날 경우 run queue에 할당된 goroutine이 없을 수 있음.
이를 해결하기 위해, 다음 순서대로 goroutine을 가져옴.(Work Stealing)
1. 다른 LRQ에 goroutine이 있는지 보고, 있을 경우 절반의 goroutine을 가져옴.
2. 1에 goroutine이 없을 경우 GRQ에 goroutine이 있는지 보고, 있을 경우 goroutine을 가져옴.
3. 2에도 goroutine이 없을 경우 Net poller에 goroutine이 있는지 보고, 있을 경우 goroutine을 가져옴.
단, Goroutine의 Locality를 위해 새로 생성된 goroutine은 3ms동안 stealing 되지 않도록 제한함.
모든 Goroutine은 되도록 공평하게 실행되어야 함.
Thread: Goroutine이 이용중인 스레드를 반환하지 않고 계속 이용할 수 있으므로 goroutine이 10ms 이상 실행되는 경우 해당 goroutine을 timeout시키고 선점하여 강제로 GRQ로 보냄.
LRQ: 2개의 Goroutine이 LRQ에 번갈아가면서 저장되고 실행될 경우 LRQ의 LIFO 부분에 의해 FIFO 부분의 goroutine이 실행되지 않을 수 있음. 이를 막기 위해 LIFO 부분의 goroutine은 10ms timeout이 초기화되지 않도록 하여, LRQ의 LIFO 부분을 하나의 10ms 이상 점유하지 못하게 함.
GRQ: LRQ에 존재하는 goroutinea만을 실행하면 GRQ에 존재하는 Goroutine이 실행되지 않을 수 있으므로 '61번' Goroutine Scheduling을 수행할 때 LRQ보다 GRQ에 있는 Goroutine을 먼저 확인하고 실행하게 함.
Network Poller: 독립된 Background thread로, OS Scheduler에 따라 동작을 보장 받음.
I/O와 같은 sync system call이 발생할 경우 Blocking이 발생함.
스레드를 새로 만들고, block된 goroutine을 제외한 Processor를 통째로 새로운 스레드로 넘겨서(Hand off) 다음 goroutine 실행.
Sync system call이 완료되었다면 다시 LRQ에 들어가고, 남은 스레드는 idle 상태로 진입.
Async system call을 호출한 goroutine은 Network Poller에 들어가서 완료 event 대기.
Network poller가 완료 event를 수신하고, 해당 goroutine을 다시 LRQ로 보냄.
Concurrenty is a property of the program and parallel execution is a property of the machine.
Concurrency는 프로그램의 성질이고 Parallel execution은 기계의 성질이다.
어떤 프로그램이나 알고리즘이 순서에 상관 없이 동시에 수행될 수 있음.
프로그램 혹은 알고리즘의 개념이며, 싱글코어와 같이 병렬성을 제공하지 않는 머신에서도 동작 가능함.
Concurrency bug: 멀티 스레드 프로그램에서 벌어지는 각종 버그.
어떤 작업을 물리적으로 동시에 처리할 수 있음.
OpenMP, MPI, CUDA 등은 물리적으로 제공되는 다양한 병렬 하드웨어를 활용하므로 병렬 프로그래밍.