본문 바로가기
Language/Go

Effective Go - Concurrency - 3

by ocwokocw 2022. 6. 2.

- 출처: https://go.dev/doc/effective_go#parallel

- Parallelization

앞의 파트(Concurrency - 2)까지 알아보았던 동시성과 관련해서 여러 개의 Core를 사용하여 계산을 병렬처리 하는 부분도 생각해볼 수 있다. 만약 계산이 독립적으로 실행될 수 있는 조각들로 분리될 수 있다면 각 조각들의 완료 여부를 channel을 통해 signal을 보내면 병렬화가 가능하다.
 
vector의 요소들이 비용이 큰 연산들로 구성되어있고 각 요소의 연산 값이 독립적인 경우를 생각해보자.
 
type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}
 
위의 함수를 CPU당 하나씩 loop에서 독립적인 연산을 수행해보자. 이렇게 시작된 연산들은 순서에 관계 없이 완료될 수 있지만 전혀 상관이 없다. 모든 goroutine이 시작된 후에 channel에서 완료 신호를 기다리면 된다.
 
const numCPU = 4 // number of CPU cores

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}
 
numCPU를 상수로 직접 정의하지 않고 어떤 값이 적절한지를 실행시점에 판단하게 할 수도 있다. runtime.NumCPU 함수는 머신의 하드웨어 CPU 코어수를 반환한다. 
 
var numCPU = runtime.NumCPU()
 
또 runtime.GOMAXPROCS 라는 함수도 있는데 Go 프로그램이 동시에 실행할 수 있는 사용자가 지정한 Core의 수를 반환한다. 기본적으로는 runtime.NumCPU와 동일한 값이지만 shell 환경 변수나 양수를 인자로하여 호출해서 설정하면 해당 값을 덮어쓸 수 있다. 0값으로 호출하면 해당 값을 질의한다. 사용자의 Resource 요청을 존중하려면 아래와 같이 호출하면 된다.
 
var numCPU = runtime.GOMAXPROCS(0)
 
독립적으로 실행가능한 component로 프로그램을 구조화하는 concurrency와 Multi CPU 에서 효율성을 위해 병렬로 계산을 실행하는 parallelism의 개념을 혼동하지 않게 조심해야 한다. 비록 Go의 concurrency 특징이 어떤 문제를 병렬로 계산하기 쉬운 구조로 만들어줄 수는 있지만 Go는 병렬성 기반이 아닌 동시성에 기반을 둔 언어이므로 병렬성 문제는 Go의 모델이나 사상에 딱 들어맞지는 않는다. 더 자세한 얘기는 https://go.dev/blog/waza-talk 를 참조하라.
 

- A leaky buffer

동시성 프로그래밍 도구를 이용하면 비 동시성 사상들도 더 쉽게 표현할 수 있다. 아래 코드는 RPC package 코드 중 일부를 추상화한 예제이다. client goroutine은 network와 같은 소스에서 데이터를 수신하는 loop 이다. buffer를 할당하거나 해제하는것을 방지하기 위해서 free list를 유지하고, 이를 나타내기 위해 buffered channel을 사용한다. 만약 channel이 비어 있으면 새로운 buffer가 할당 된다. 한번 메시지 buffer가 준비되면 serverChan을 통해 server로 전송된다.
 
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }
}
 
server loop는 client로 부터 각 message를 받아서 처리하고 해당 buffer를 여유 목록으로 반환한다.
 
func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}
 
client는 freeList로 부터 buffer를 회수하려고 시도한다. 만약 이용할 수 없다면 새로운 buffer를 할당한다. 만약 여유 목록이 가득차있지 않다면 server는 freeList에 b를 송신하여 여유 목록으로 둔다. 여유 목록이 꽉찬 경우 가비지 컬렉터에 의해 회수되도록 버려지게 내버려둔다. (select 문의 default절은 다른 case가 준비되지 않을 때 실행되는데, 이는 select가 block되지 않는 것을 의미한다.) buffered channel과 가비지 컬렉터를 이용하여 여유 목록을 가진 leaky bucket 를 단 몇줄로 구성하였다.
 
 

'Language > Go' 카테고리의 다른 글

Error handling  (0) 2022.06.12
Effective Go - Errors  (0) 2022.06.05
Effective Go - Concurrency - 2  (0) 2022.06.01
Effective Go - Concurrency - 1  (0) 2022.06.01
Effective Go - Embedding  (0) 2022.05.29

댓글