Go Web Programming: [02/07] Concurrency

Concurrency

Go언어를 21세기의 C언어라는 사람도 있습니다. Go 언어는 설계가 간단하고, 21세기 환경에서 가장 중요한 것은
멀티 스레드이기 때문 입니다. Go는 언어차원에서 멀티 스레드를 지원하고 있습니다.

goroutine

goroutine은 Go 멀티 스레드의 핵심입니다.

goroutine은 실제로 처음부터 끝까지 스레드입니다. 그러나 스레드보다 작은 수십 개의 goroutine은 로레벨에서
5,6개의 스레드를 통해서 실제 구현된 것입니다. Go 언어의 내부에서 goroutine 사이의 메모리 공유를 구현하고 있습니다.
goroutine을 실행하려면 단지 아주 작은 스택 메모리(대략 4~5KB입니다.)만 필요로 할뿐 입니다. 당연히 이러한 처리로
해당 데이터가 약간 늘어납니다만, 이런 기능으로 여러 다중 스레드 작업을 수행 할 수 있게 됩니다.

goroutine은 thread에 비해보다 사용하기 쉽고, 효과적이고 편리 합니다.

goroutine은 Go의 runtime 관리를 이용한 스레드 컨트롤러 입니다.
goroutine은 go 키워드에 의해 구현 됩니다. 사실 그냥 일반적인 함수 입니다.

go hello(a, b, c)

키워드 go를 통해 goroutine을 시작 합니다.실제 예를 들어 살펴 보겠습니다.


package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i <5; i ++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world") // Goroutines으로 실행
    say("hello")    // 일반 함수로 실행
}

위의 프로그램을 실행하면 다음과 같이 출력됩니다.

// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

go 키워드로 아주 쉽게 멀티 스레드 프로그래밍을 작성할 수 있다는 것을 알 수 있습니다.
위의 여러개의 goroutine은 동일한 프로세스에서 실행되고 있습니다. 메모리에 데이터를 공유해서 처리하고 있지만
이러한 메모리 공유를 통해서 통신을 처리하지 말고, 채널을 통해서 데이터를 공유 처리할 것을 권장 합니다.

runtime.Gosched()에서는 CPU 시간을 다른 사람에게 전달 합니다. 다음의 단계에서 계속적으로 이 goroutine을
실행 합니다. 멀티 코어 프로세서의 멀티 스레드를 실현 하려면 프로그램에서 runtime.GOMAXPROCS(n) 함수를 호출해서
호출한 곳에서 동시에 여러 프로세스를 사용하도록 통보해야 합니다. GOMAXPROCS() 함수는 동시에 실행하는
코드 시스템 프로세스의 최대 개수를 설정한 후 이전 설정을 반환 합니다.
만약 n < 1 인 경우 현재 설정은 변경되지 않습니다.

Go의 새로운 버전으로 프로세스 배치가 수정되면, 이것은 삭제 될 것입니다.
Rob Pike의 멀티 스레드의 개발은 다음 문서에서 자세한 내용을 확인하시기 바랍니다.
Rob Pike’s Multi Thread

channels

goroutine은 동일한 주소 공간에서 실행됩니다.

따라서 공유된 메모리에 대한 액세스는 반드시 동기화되어 있어야만 합니다. 문제는 goroutine 사이에서 어떻게 데이터 통신을
처리 하느냐 문제입니다. Go는 채널이라는 아주 좋은 통신 메커니즘을 제공 합니다.
채널은 Unix shell과 양방향 파이프를 만듭니다. 이를 통해서 값을 보내거나 받을 수 있게 되는 것입니다.
이 값은 특정 형태만 허용 됩니다. 바로 채널 형입니다.

channel을 정의하면 채널에 전송하는 값의 형식도 정의해야 합니다.

주의하시기 바랍니다. 반드시 make()를 사용해 channel을 만듭니다.

ci := make (chan int)
cs := make (chan string)
cf := make (chan interface {})

channel은 <- 연산자를 사용하여 데이터를 보내거나 받거나 합니다.

ch <- v        // v를 channel ch에 보냅니다. 
v := <-ch      // ch 채널에서 데이터를 받아서 v에 대입 합니다.

예제에 적용시켜 보겠습니다.

package main

import "fmt"

func sum(a [] int, c chan int) {
    total := 0
    for _ v := range a {
        total += v
    }
    
    c <- total              // 보내기   
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(a[:len(a) / 2], c)
    go sum(a[len(a) / 2:], c)
    x, y := <-c <-c       // 받기 

    fmt.Println (x, y, x + y)
}

기본적으로 channel이 주고받는 데이터는 차단되어 있습니다. 다른 하나가 준비되어 있지 않으면 Goroutines
동기화는 더 쉬워집니다. lock을 별도로 표시 할 필요가 없습니다. 이른바 블록은 value := <-ch에서 채널에서
데이터를 읽기 전까지는 차단 됩니다. 데이터를 받은 후 ch <-5로 데이터가 읽힐 때까지 블록 됩니다.
버퍼링이없는 channel은 여러 goroutine 간의 동기화를 처리하는 아주 훌륭한 도구 입니다.

Buffered Channels

위에서는 기본적으로 버퍼링이없는 channel을 설명 했습니다. 그러나 Go는 channel 버퍼링을 지정하는 것을 허락
합니다. 사용 방법은 아주 간단 합니다. 즉 channel에 여러 가지 요소를 저장할 수 있습니다.

ch := make(chan book 4)는 4개의 bool 형의 요소를 처리할 channel을 만듭니다. 이 channel에서 4가지
요소는 블록되지 않고 입력 할 수 있습니다. 다섯 번째 요소가 입력 된 경우 코드는 차단 된후, 다른 goroutine이
channel에서 요소를 꺼낼 때까지 블럭합니다.

ch := make(chan type, value)

value == 0 이면 버퍼링 없다는 의미이며 블록 처리 됩니다.
value > 0 이면, 버퍼링 처리를 의미하며, 버퍼크기를 처리할때 까지는 블록 처리 되지 않습니다.

아래의 예를 참고 하시기 바랍니다. 로컬에서 테스트 해 볼 수 있습니다.
대응하는 value 값을 변경해서 사용하시기 바랍니다.

package main

import "fmt"

func main () {
    c := make(chan int 2)  // 2를 1로 수정하면 오류가 발생 합니다. 
    c <- 1                 // 2를 3으로 수정하면 정상적으로 실행 됩니다.
    c <- 2
    fmt.Println (<- c)
    fmt.Println (<- c)
}

Range와 Close

위의 예에서는 2번 걸처 c채널에서 값을 가져와야 했습니다. 이런 방식은 그다지 유용하지 않습니다.
Go는이 이런 점을 고려해서, range 를 사용해서 slice나 map을 조작했던 것과 같은 방식으로
버퍼링 channel을 처리 할 수 있습니다. 아래의 예를 참고 하시기 바랍니다.

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 1 1
    for i := 0; i <n; i ++ {
        c <- x
        x, y = y, x + y
    }
    close(c)
}

func main() {
    c := make(chan int 10)
    go fibonacci(cap(c) c)
    for i := range c {
        fmt.Println(i)
    }
}

for i : = range c에서 이 channel이 명시적으로 닫힐때까지 연속족으로 channel의 데이터를 로드
할 수 있습니다. 위의 코드에서도 channel의 종료가 명시적으로 처리됨을 알 수 있습니다.
생산자는 close() 내장 함수에 의해 channel을 닫습니다.

channel을 닫은 후에는 어떠한 데이터도 보낼 수 없습니다.

소비자는 v, ok := <-ch과 같은 방식으로 channel이 이미 닫혀 있는지 여부를 테스트 할 수 있습니다.
만약 ok 변수에 false가 반환되면 channel에는 어떤 데이터도 없고, 닫혀 있다는 의미 입니다.

생산자에서 channel이 닫히는 점에 주의하십시오.

소비자에서 처리하지 않습니다. 이것은 panic을 유발하게 됩니다.

또한 channel은 파일과 같은 자원이 아님에 유의하십시오.

즉, 파일처럼 자주 닫을 필요가 없습니다. 어떤 데이터도 보낼 수없는 경우 또는 range 루프를 종료시키고 싶은
경우에만 사용합니다.

Select

지금까지는 channel이 한개라는 상황에서 설명했습니다. 만약 여러개의 channel이 사용된다면 어떻게 처리해야 할까요?
Go는 키워드 select를 제공해서 여러개의 채널에서 선택문을 처리할 수 있도록 합니다. select문을 통해서
channel에 어떤 데이터를 모니터링 할 수 있게 됩니다.

select은 기본적으로 차단 상태로 작동 됩니다. channel에서 교환되는 데이터를 모니터링 할 때만 실행 합니다.
여러개의 channel이 존재할때, select는 무작위로 하나를 선택한 후 실행 합니다.

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 1 1
    for {
        select {
        case c <- x:
            x, y = y, x + y
        case <- quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i ++ {
            fmt.Println(<- c)
        }
        quit <- 0
    }()
    fibonacci (c, quit)
}

select 문에서도 default문이 존재합니다. select은 사실 채널 switch라고 생각하시면 편합니다.
select문에서 default의 의미는 감시하고있는 channel이 모두 준비가되어 있지 않을 때 기본적으로 실행 됩니다.
(select는 channel을 기다리고 차단하지 않습니다.)

select {
case i := <-c:
     // use i
default :
     // c가 차단 될 때, 이부분이 실행 됩니다.
}

시간

가끔 goroutine이 차단되는 상황에 직면하게 됩니다. 이때 프로그램 전체가 차단되는 상황을 어떻게 피할 수 있을까요?
select를 사용해서 시간을 설정 할 수 있습니다. 다음과 같이 사용합니다.

func main() {
    c := make (chan int)
    o := make (chan bool)
    go func() {
        for {
            select {
            case v := <- c:
                println(v)
            case <- time.After (5 * time.Second):
                println ( "timeout")
                o <- true
                break
            }
        }
    }()
    <- o
}

runtime goroutine

runtime 패키지는 goroutine을 처리하는 몇 가지 기능이 포함되어 있습니다.

  • Goexit
    이전에 실행 된 goroutine에서 탈출합니다. 그러나 defer() 함수는 계속해서 호출 됩니다.

  • Gosched
    이전의 goroutine의 실행 권한을 전달 합니다. 디스패처에서 대기중인 다른 작업의 실행을 계획하고
    다음의 어떤 시점에서 현재 위치로 실행을 복원 합니다.

  • NumCPU
    CPU의 코어 수를 반환 합니다.

  • NumGoroutine
    현재 실행중인 수와 대기 작업의 수를 반환 합니다.

  • GOMAXPROCS
    실행할 수있는 CPU 코어 수의 최대 값을 설정하고 설정된 코어 수를 반환 합니다.