Go: Goroutine, 비동기, 병렬 처리

dev_314·2023년 4월 22일
0

Golang - Trial and Error

목록 보기
2/5

Goroutine

논리적 가상 스레드

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

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() {...}

Add

Goroutine 추가

Done

Goroutine 제거

Wait

모든 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에서 다루겠다.

다중 CPU 환경: runtime Package

Goroutine을 사용해서 간단하게 함수를 비동기로 호출할 수 있다.
그리고 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 코어만 사용한다.

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글