Concurrency Programming == 동시에 병렬처리가 필요할 때
프로세스 대신 쓰레드를 쓰는 이유와 같다.
쓰레드는 Stack 을 제외한 모든 메모리상의 자원을 공유하여 Process 보다 상대적으로 가벼운 Context Switching을 할 수 있다.
고루틴은 한 발 더 나아가서, Stack 영역까지 공유하는 쓰레드 보다도 더 가벼운 실행 흐름이다.
(그래서 고루틴을 경량 스레드라 칭하기도 한다.)
하나의 쓰레드 내에서 마구잡이로 고루틴을 생성할 수 있다.
Stack Pointer, PC 만 교체하면 된다. (Context Switcing 비용 ⬇️)
굉장히 쉽다.
func 앞에 go
키워드만 붙여주면 된다.
func printCount(name string) {
for i := 0; i < 10; i++ {
println(name, i)
time.Sleep(time.Second)
}
}
func main () {
go printCount("falcon")
printCount("milkcoke")
}
//==== Result ======
// falcon 0
// milkcoke 0
// milkcoke 1
// falcon 1
// ... ~ 10
func waitAndCheckOdd(num int, channel chan string) {
for i := 0; i < 10; i++ {
println("wait for result about : ", num, i, "seconds...")
time.Sleep(time.Second)
}
if num%2 == 0 {
channel <- "false: " + strconv.Itoa(num)
} else {
channel <- "true " + strconv.Itoa(num)
}
}
func main() {
// make channel
channel := make(chan string)
go waitAndCheckOdd(5, channel)
go waitAndCheckOdd(2, channel)
// When main function watch '<-' block the code (wait the message)
// and getting a message from the channel
// <- is receiver!
fmt.Println("Received message from the channel : ", <-channel)
fmt.Println("Received message from the channel : ", <-channel)
// Go knows how many goroutines are running
// and administrate goroutine state (message queue)
}
다음과 같이 3가지 상태로 나타낼 수 있다.
thread 처럼 프로그램이 실행중이어야한다.
lifecycle 을 main function과 같이한다.
고루틴의 또다른 이름은 경량 스레드
main - goroutine 간 커뮤니케이션을 위해 channel 을 사용한다.
실행 순서를 결정하기 위해 존재한다.
고루틴은 Communicating Sequential Process(CSP) 의 일종으로 다음 특성을 가진다.
⚠️ WaitGroup 사용시 반드시
goroutine 을 호출하는 스코프 내에서 add 를 먼저해야한다.
그래야 wait() 문에서 기다린다.
각자 Stack 영역을 할당받지만, Heap 으로 승격할 수 있다.
goroutine 을 실행시킨 스코프 내에서는 해당 스코프가 stack 에서 해제되더라도
접근 가능한 상태를 유지한다. (Heap 으로 승격)
The variables accessed by the goroutine are captured and stored in a closure, so they can be accessed by the goroutine even if the variables go out of scope in the enclosing function.
func main() {
var wg = &sync.WaitGroup{}
incr := func(wg *sync.WaitGroup) {
var i int
wg.Add(1)
go func() {
defer wg.Done()
i++ // 2. `return` 시점에 i 변수를 stack -> heap 으로 승격
fmt.Printf("value of i: %v\n", i) // 3. heap 으로 승격된 i 의 값을 출력
}()
fmt.Println("return from the function")
return // 1. incr stack 메모리 해제
}
incr(wg)
wg.Wait()
fmt.Println("done..") // 4. 프로그램 종료.
}
return from the fuction
value of i: 1
done ..
incr 라는 함수 내에 선언된 i 변수에 대해 함수가 종료된 시점 (incr 이 차지한 stack 메모리가 해제된 시점) 이후에도 익명의 고루틴이 i 라는 값에 access 가능하다.
고루틴이 스케쥴러에 의해 실행 기회를 가지지 못할 경우 아예 실행되지 않고 main() 실행이 끝나버릴 수 있다.
func asyncDoSomething(wg *sync.WaitGroup) {
wg.Add(1) // ❌ 스케쥴링 상황에 따라 아예 실행되지 않을 수 있다.
defer wg.Done() // decrement dynamically
// ..
}
func main() {
waitGroup := &sync.WaitGroup{}
go asyncDoSomething(waitGroup)
waitGroup.Wait()
}
고루틴 실행 스코프 밖에서 waitGroup.Add()
를 호출한다.
이로써 고루틴이 실행됨을 보장할 수 있다.
func asyncDoSomething(wg *sync.WaitGroup) {
defer wg.Done()
// ..
}
func main() {
waitGroup := &sync.WaitGroup{}
waitGroup.Add(1) // ✅ 명시적으로 기다릴 고루틴 숫자를 1개로 미리 설정해야한다.
go asyncDoSomething(waitGroup)
waitGroup.Wait()
}
채널은 Message Queue로 FIFO 방식으로 동작한다.
<-
Operator 를 만나는 순간 메시지를 전달받을 때까지 기다린다.
메시지는 먼저 return 된 goroutine 함수 순서대로 받아온다.
멀티 프로세싱, 멀티 쓰레드에서도 그러하듯 모든 병렬처리에서는 항상 Consistency , Synchronization 이슈가 발생한다.
동일 자원을 동시에 여러곳에서 접근하기 때문이다.
흔히 사용하는 Mutex
(Mutual Exclusion) 기법을 써도 좋다.
다만 다음과 같은 단점을 고려해야 한다.
1. mutex 의 Lock 자체에 드는 소요시간
2. 성능 저하 (고루틴의 대기 상태가 많아진다.)
서로 다른 실행 흐름이 각기 다른 자원을 선점해놓고 서로 상대 작업이 끝나기를 기다리는 상태로
결과적으로 아무도 작업을 완료할 수 없는 상황이 발생할 수도 있다.
쓰레드 수가 아니라 CPU 갯수 만큼의 Processor Object 를 만들어 스레드가 아닌 프로세서 객체가 LRQ를 가지고있게 하여 효율적인 스케쥴링을 하도록 구현했다.
LRQ (Local Run Queue)
GRQ (Global Run Queue) 를 자체구현해놨다.
고루틴도 기본 10ms 의 time slice 가 정의되어 프로세서의 장시간 점유를 방지한다. => 선점 스케쥴링 기법이다.
시간이 오버될 경우 실행중이던 고루틴을 preempted 되어 GRQ로 들어간다.
자세한 설명은 Reference 참조 링크를 참고하시라
만약, goroutine func 내에서 recursive call 하면 상위 고루틴은 알아서 끝나나?
Index | Thread | Goroutine |
---|---|---|
Communication way | Shared memory | By channel |
Memory share | Heap, Data, Code except stack | All spaces memory address even stack |
Scheduling | By OS | By Go runtime |
Context Switching Cost | Relatively high | Low |
아니다. 고루틴은 Go Runtime 에 귀속된다.
고루틴은 호출 시점에 시작되고 (ex. go func()
)
고루틴 자체의 스택 메모리를 할당받는다.
다음 3가지 경우에 생명주기가 종료된다.
return
statement종료 후에 할당 받았던 스택 메모리는 해제된다.
⚠️ 종료 시점에 waitGroup 이나 가지고 있던 channel 이 있는 경우 종료 사실을 waitGroup 과 channel 에 알린다.