19.3. 프로그램언어 고(Go)의 채널을 이용한 고루틴 통신

프로그램언어 고(Go)에서의 채널을 이용한 고루틴 간 메시지 전달

package main

import "fmt"

func ping(c chan string) {
    c <- "ping"
}

func pong(c chan string) {
    msg := <-c
    fmt.Println(msg)
    c <- "pong"
}

func main() {
    var c chan string = make(chan string)

    go ping(c)
    go pong(c)

    msg := <-c
    fmt.Println(msg)
}

고(Go)언어에서 채널(channel)은 고루틴간 통신을 위한 통로역할을 합니다. 채널을 통해 한 고루틴은 다른 고루틴으로 데이터를 보내거나 받을 수 있습니다.

위 예제코드에서 보듯이 make를 사용하여 채널을 생성할 수 있습니다. 이렇게 만들어진 채널에 대한 참조값을 고루틴에 전달하여 고루틴간에 데이터를 주고받을 수 있습니다.

예를들어 ping 고루틴은 문자열 "ping"을 채널 c로 보냅니다. pong 고루틴은 이 채널 c에서 데이터를 수신합니다. 이렇게 채널을 통해 고루틴간에 데이터를 안전하게 동기화하면서 전달할 수 있습니다.

채널을 사용할 때는 보내기(c <- 1)와 받기(num := <- c)의 개념을 이용합니다. <- 기호를 사용하여 채널에 데이터를 보내거나 채널에서 데이터를 수신합니다. 주의할 점은 보내기와 받기는 서로 대칭적으로 이루어져야 합니다. 즉, 보낸 만큼 받고, 받은 만큼 보내야 합니다. 이러한 대칭성이 깨지면 Deadlock이 발생할 수 있습니다.

프로그램언어 고(Go)에서의 채널을 이용한 고루틴 동기화

프로그램언어 고(Go)에서 제공하는 채널을 이용하여 고루틴들 간의 동기화를 구현하는 방법에 대해 설명드리겠습니다.

고(Go) 언어의 고루틴(goroutine)은 논리적으로 병렬로 실행되는 경량화된 스레드입니다. 여러 고루틴들이 서로 데이터를 주고받거나 동기화를 맞출 때 채널(channel)을 활용합니다.

채널은 고루틴 간에 데이터를 전달하는 통로 역할을 합니다. make 함수로 채널을 생성하면, 다른 고루틴에 그 채널을 전달하여 데이터를 교환할 수 있습니다.


package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // sum을 채널 c로 보냄
}

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

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 채널 c로부터 두 개의 값을 받음

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

위 예제코드에서 main 고루틴은 슬라이스를 절반으로 나누어 두 개의 고루틴에 전달합니다. 각 고루틴은 자신이 받은 슬라이스의 합을 계산한 뒤, 결과값을 채널 c로 보냅니다.

main 고루틴은 채널 c로부터 두 개의 결과값을 받아서 출력하는 방식으로 동작합니다. 채널 c를 통해 각 고루틴의 실행이 끝날 때까지 기다리면서 동기화가 이루어지는 것을 확인할 수 있습니다.

이처럼 고(Go)의 채널은 고루틴간 통신과 동기화를 위한 핵심적인 기능을 제공합니다. make 함수로 채널을 생성하고, 여러 고루틴에서 그 채널에 접근함으로써 데이터 교환과 동기화를 쉽게 구현할 수 있습니다.

프로그램언어 고(Go)에서의 채널 선택적 수신(Select Statement)

고(Go) 언어의 채널 선택적 수신(Select Statement)은 다수의 채널들에서 데이터를 입력 받거나 출력 할 수 있는 기능을 제공합니다.

이를 통해 한 프로그램에서 다수의 채널을 동시에 처리할 수 있으며, 이중에서 필요한 채널을 선택적으로 활성화시킬 수 있습니다.


package main

import "fmt"

func main() {

    // 2개의 채널 생성
    ch1 := make(chan int)
    ch2 := make(chan int)

    // 각 채널에 데이터 보내기 
    go func() {
        ch1 <- 10
    }()

    go func() {
        ch2 <- 20 
    }()

    // 채널 선택적 수신
    select {
    case value := <-ch1:
        fmt.Println(value)
    case value := <-ch2:
        fmt.Println(value)
    }
}

위 예제에서 2개의 채널 ch1, ch2를 생성하고 goroutine을 이용해 각각 10, 20 데이터를 전송합니다.

그리고 select문을 사용해 이 2개의 채널 중 데이터 수신 가능한 채널을 선택적으로 받아 value 변수에 할당합니다.

select문은 case 절을 통해 수신 받고자 하는 다수의 채널을 나열할 수 있습니다.

그 중 먼저 데이터 수신이 가능한 채널을 자동 탐지하여 해당 case문을 실행합니다.

위 예에서 ch1 또는 ch2 채널 중 먼저 데이터를 보내는 쪽을 수신하여 값을 출력합니다.

또한 default 문을 사용하면 채널 선택적 수신 시간 제한도 가능합니다.


select {
case <-ch1:
     // ch1 수신
case <-ch2:
     // ch2 수신 
default:
     // 일정시간 이상 대기시 실행
}

이처럼 고(Go)의 select문을 사용하면 채널들 간의 동기화 없이도 다수의 채널을 동시에 처리할 수 있어 병렬처리 코드를 간결하고 효율적으로 작성할 수 있습니다.

프로그램언어 고(Go)에서의 채널을 이용한 고루틴 취소(Signalling Shutdown)

package main

import (
    "fmt"
    "time"
)

func worker(done chan bool) {
    fmt.Println("working...")
    time.Sleep(time.Second)
    fmt.Println("done")

    done <- true
}

func main() {
    done := make(chan bool)

    go worker(done)

    <-done
    fmt.Println("job finished")
}

위의 코드는 하나의 worker 고루틴을 실행하는 간단한 예제입니다. main 함수에서 고루틴을 실행하고 done 채널을 통해 완료 신호를 받습니다.

이를 확장해서 여러 개의 worker 고루틴을 실행하고 전체 종료를 처리하는 방법은 다음과 같습니다.

package main

import (
    "fmt"
    "sync"
)

func worker(wg *sync.WaitGroup) {
    fmt.Println("working...")
    wg.Done()
}

func main() {

    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(&wg)
    }

    wg.Wait()

    fmt.Println("All jobs done!")
}

sync.WaitGroup을 사용하여 고루틴의 개수를 세고, 모든 고루틴이 끝날 때까지 기다립니다.

다음은 done 채널을 통해 각 worker로부터 신호를 받는 방법입니다.

package main

import (
    "fmt"
    "sync"
)

func worker(wg *sync.WaitGroup, done chan bool) {
    fmt.Println("working...")
    done <- true
    wg.Done()
} 

func main() {

    var wg sync.WaitGroup

    done := make(chan bool)

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(&wg, done)
    }

    for i := 1; i <= 5; i++ {
        <-done 
    }

    wg.Wait()

    close(done)

    fmt.Println("All jobs done!")
}

위 코드에서 worker 고루틴을 실행할 때 done 채널을 전달합니다. 각 worker는 작업 완료 후 이 채널로 신호를 보냅니다.

main 함수에서는 5개의 신호를 받은 후 WaitGroup의 Wait 메서드로 더 이상 대기합니다. 이렇게 하면 모든 고루틴의 완료를 확인할 수 있습니다.

마지막으로 done 채널을 닫아서 더 이상 사용되지 않도록 합니다.

채널과 WaitGroup을 조합하면 복잡한 고루틴 상황을 효과적으로 처리할 수 있습니다. 특히 고루틴의 개수가 확실치 않거나 동적인 경우 유용합니다.

프로그램언어 고(Go)에서의 채널을 이용한 고루틴 에러 핸들링

고(Go)언어에서 고루틴을 사용하다보면 에러 핸들링이 중요한 문제가 됩니다.

고루틴 내부에서 발생한 에러는 고루틴 자체가 종료되기 때문에, 에러 정보가 누락될 수 있습니다.

이를 방지하기 위해 고(Go)언어의 채널을 이용해 에러 핸들링을 할 수 있습니다.


func doWork(done chan bool) {
    // 여기서 에러가 날 수 있는 작업 수행
    ...

    if err != nil {
        // 에러 핸들링
    }

    done <- true
}

func main() {
    done := make(chan bool)

    go doWork(done)

    <-done
}

위 예제코드에서 보다시피, 고루틴이 실행을 완료했을 때 done 채널로 true 값을 보냅니다.

메인 고루틴에서는 이 done 채널로부터 값을 받으면 해당 고루틴이 에러 없이 성공적으로 완료됐다는 것을 의미합니다.

따라서 채널 통신을 이용해 고루틴의 생명주기를 관리하면서 에러 핸들링을 수행할 수 있습니다.

채널 대신 wait group을 사용하는 방법도 있지만,
채널을 사용하면 값 전달도 가능하여 더 유연합니다.

위 예제에서 done 채널을 통해 실패나 성공을 나타내는 다른 타입의 값을 보내거나,
실제 에러 객체를 전달할 수도 있습니다.

이렇게 하면 정확한 에러 원인을 파악할 수 있어 디버깅에도 유용합니다.

고루틴과 채널은 비동기 처리에 강점이 있는 고(Go)언어의 큰 특징입니다.
채널을 잘 활용하면 고루틴의 실행 생명주기를 효과적으로 관리하고 에러처리도 체계적으로 할 수 있습니다.

Leave a Comment