논리적 가상 스레드
Golang은 Goroutine
을 통해 스레드보다 훨씬 가벼운 비동기 동시 처리를 지원한다. 각각의 일에 대해 스레드와 1대 1로 대응하지 않고, 훨씬 적은 스레드를 사용한다.
메모리 측면에서, 스레드가 1MB의 스택을 갖을 때, 고루틴은 훨씬 작은 KB 단위의 스택을 갖고 필요시에 동적으로 증가한다.
또한 Gochannel
을 이용해 Goroutine간의 통신도 용이하게 할 수 있다.
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go printRandom(i)
}
fmt.Scanln()
}
func printRandom(idx int) {
randomSleepTime := rand.Intn(5)
time.Sleep(time.Duration(randomSleepTime * 1000))
fmt.Println(idx)
}
10개의 Goroutine을 띄워서 비동기적으로 index를 출력하는 모습을 확인해보자.
그런데 실행하면 아무것도 출력하지 않고 프로그램이 종료된다.
main
에서 단순히 Goroutine
을 호출시키기만 하고 기다리지 않고 종료되기 때문이다. (마치 JavaScript에서 async를 await하지 않는 것과 동일?)
그래서 일단은 억지로 main이 종료되지 않도록 해보자
``` go
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go printRandom(i)
}
fmt.Scanln() // 사용자 입력이 들어올 때 까지 block
}
func printRandom(idx int) {
randomSleepTime := rand.Intn(5)
time.Sleep(time.Duration(randomSleepTime * 1000))
fmt.Println(idx)
}
그러면 다음과 같이 비동기적으로 출력됨을 확인할 수 있다.
0
5
7
3
...
근데 딱봐도 fmt.Scanln
을 사용해서 억지로 block하는건 아닌거 같다.
다른 방법으로 해결하자.
sync
패키지의 WaitGroup
을 사용해서 Goroutine이 완료될 때 까지 기다릴 수 있다.
package sync
import (
"internal/race"
"sync/atomic"
"unsafe"
)
type WaitGroup struct {
noCopy noCopy
state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
sema uint32
}
func (wg *WaitGroup) Add(delta int) {...}
func (wg *WaitGroup) Done() {...}
func (wg *WaitGroup) Wait() {...}
Goroutine 추가
Goroutine 제거
모든 Goroutine이 제거될 때 까지 기다리기
사용법을 보면 바로 이해가 된다.
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
wait := new(sync.WaitGroup)
for i := 0; i < 10; i++ {
wait.Add(1) // 기다릴 Goroutine 개수++
go printRandom(i, wait) // waitGroup을 전달
}
wait.Wait() // 기다릴 Goroutine 개수가 0이 될 때 까지 block
}
func printRandom(idx int, wait *sync.WaitGroup) {
defer wait.Done() // 함수가 종료되면 == Goroutine이 작업을 완료하면 기다릴 Goroutine 개수--
randomSleepTime := rand.Intn(5)
time.Sleep(time.Duration(randomSleepTime) * time.Second)
fmt.Println(idx)
}
WaitGroup, Goroutine과 클로저의 관계는 Channel
에서 다루겠다.
runtime
PackageGoroutine을 사용해서 간단하게 함수를 비동기로 호출할 수 있다.
그리고 runtime
패키지를 사용하면 다중 CPU를 이용한 병렬처리를 할 수 있다.
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
sum := 0
wait := new(sync.WaitGroup)
fmt.Println(runtime.NumCPU()) // 실행 환경의 CPU 개수
runtime.GOMAXPROCS(200) // 사용할 CPU 개수를 명시적으로 지정 가능
fmt.Println(runtime.NumCPU()) // 실행 환경의 물리적 CPU 최대 개수로 설정된다.
for i := 1; i <= 100; i++ {
wait.Add(1)
go add(&sum, i, wait)
}
wait.Wait()
fmt.Print("sum: ", sum)
}
func add(sum *int, i int, wait *sync.WaitGroup) {
defer wait.Done()
*sum += i
}
Goroutine을 사용해서 1부터 100까지의 합을 구하면 항상 5050이 나오지 않는다.
멀티 코어 CPU가 Goroutine들을 효과적으로 처리하기 때문에, sum에 대해 race condition이 존재하기 때문이다.
runtime.GOMAXPROCS(1) // 단일 CPU 환경에서는 예상대로 race condition이 사라진다.
단일 CPU 환경에서는 예상대로 5050을 확인할 수 있다.
1.5 이전 버전에는, 따로 runtime.GOMAXPROCS
를 설정하지 않으면 기본적으로 1개의 CPU 코어만 사용한다.