개발일기

Go - 채널을 사용한 고루틴의 동시성 활용 본문

프로그래밍 언어/Go

Go - 채널을 사용한 고루틴의 동시성 활용

Flashback 2022. 9. 9. 21:06
728x90
반응형

다른 고루틴들과 데이터를 주고 받거나 코드의 흐름을 조절하는 등의 제어를 하기 위해 채널을 사용한다. 고루틴 안에 생성된 채널은 해당 채널에 데이터가 전달될 때 까지 데이터를 받는 채널은 대기하게 된다. 이러한 동시적인 특징으로 인해 채널은 고루틴의 데드락을 방지하며 함수의 흐름을 제어할 수 있다.

 

1. 채널 생성법

// 채널명 := make(chan 자료형)
c := make(chan int)

채널을 생성할 때는 make 함수를 사용하여 생성해야 한다. make 함수로 채널의 주소 공간을 할당한 후, make() 파리미터 안에 채널을 나타내는 chan을 써주고, 채널에 대입될 값의 자료형을 추가하면 된다.

 

1-1. 채널에 데이터 전송

// 채널 생성
c := make(chan int)

// 데이터 전달
c<-3 // int형이기에 정수형의 데이터 전달

채널에 데이터를 전송하기 위해서는 <-를 사용하여 해당 채널에 데이터를 전송한다.

 

1-2. 채널에 데이터 수신

// 채널 생성
c := make(chan int)

// 데이터 수신
<-c

데이터를 전송할 때와는 달리 <-의 위치를 달리하여 데이터를 수신한다.

 

  • 데이터 전송 : c<-3   -   <- 연산자를 채널명 뒤에 사용한다. 연산자 뒤에 데이터를 추가한다.
  • 데이터 수신 : <-c     -   <- 연산자를 채널명 앞에 사용한다.

 

1-3. 채널 대기 상태

채널에 데이터를 보내게 되면 데이터가 수신 될 때까지 대기하는 상태가 된다.

package main
import "fmt"

func main() {

    c := make(chan int)
    go func() {
        c<-3
        fmt.Println("test")
    }()
}

/*
    실행 결과 : 
    
*/

위의 코드를 실행하면 test라는 문구가 콘솔에 찍혀야 하지만 아무것도 나오지 않는다. 그 이유는, 채널 데이터를 수신할 대 까지, 대기 상태가 되었기에 실행이 멈춘 것이다. 데이터 수신 부분을 추가하면 위의 코드는 정상 작동하게 된다.

 

1-4. 채널 데이터 송, 수신 사용법

package main
import "fmt"

func main() {
    c := make(chan int) // 채널 생성
    go func() {
        c<-3 // 데이터 전송
    }()

    fmt.Println(<-c) // 데이터 수신 후, 값 출력
    fmt.Println(c) // 채널의 주소값. 실행 결과는 매번 달라진다.
}

/*
    실행 결과 : 
    3
    0xc000080060
*/

c라는 채널을 생성한 후, 고루틴 익명함수 안에서 데이터를 전송한 후, main() 함수에서 c채널의 데이터 값을 출력한다.

 

2. 채널과 큐

채널에 데이터를 하나만 넣을 수 있는게 아니라 여러개 추가할 수 있다.

package main
import "fmt"

func main() {
    c := make(chan int)

    go func() {
        c<-3
        c<-5
    }()

    fmt.Println(<-c)
    fmt.Println(<-c)
}

/*
    실행 결과 : 
    3
    5
*/

채널에 데이터를 전송하면 전송된 순서에 따라 데이터가 쌓이게 된다. 반대로 데이터를 수신하여 출력하는 경우는 가장 처음에 전송된 데이터를 출력한다. c 채널에 3과 5값을 순서대로 전송하였다. main 함수에서 수신하여 출력할 때는 3이 먼저 출력되고 5가 출력된다. 즉, Go의 채널은 자료구조의 큐(Queue)와 동작하는 원리가 같다.

 

3. 버퍼 채널

채널을 생성할 때, 사이즈를 부여하여 보관할 수 있는 데이터의 수를 제한 할 수 있다. 해당 채널이 보관할 수 있는 데이터 수를 초과할 경우, 채널의 데이터 값들을 출력한다.

 

3-1. 버퍼 채널 생성

// 버퍼 채널 생성
c := make(chan int, 5) // 버퍼 사이즈를 추가한다.

채널을 생성할 때와 동일하지만 2번째 파라미터에 버퍼 사이즈를 추가한다. 해당 개수 만큼의 데이터를 채널에 저장하여 보관할 수 있다.

 

2-2. 버퍼 채널에 데이터 추가

package main
import _"fmt"

func main() {

    c := make(chan int, 5)
    c<-1
    c<-2
    c<-3
    c<-4
    c<-5
}

c라는 채널의 버퍼 사이즈는 5이기 때문에 5개의 데이터를 저장하였다. 만약의 c<-6을 통하여 데이터를 하나 추가하여 저장하려면 에러가 발생한다. 보관할 수 있는 개수를 넘어섰기 때문에 다음과 같은 에러가 발생한다.

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /home/runner/znq0op0uxf/main.go:12 +0xdb
exit status 2

 

3-3. 버퍼 채널의 버퍼 사이즈

package main
import _"fmt"

func main() {

    c := make(chan int, 5)
    c<-1
    c<-2
    c<-3
    c<-4
    c<-5
    <-c
    c<-6
}

위의 코드는 채널에 데이터를 6개를 전송하기에 버퍼 사이즈를 초과하여 에러가 발생한다고 생각할 수 있다. 하지만 <-c를 통하여 채널의 데이터를 수신하여 빼냈기 때문에 에러는 발생하지 않는다. 즉, 채널의 데이터를 하나 수신할 경우, 버퍼 채널에 보관할 수 있는 자리가 하나 발생하게 된다.

 

4. 닫힌 채널

채널에 일정한 개수의 데이터를 전송하고 더이상의 데이터 전송을 없거나 불필요할 경우 close 함수를 통해 채널을 닫을 수 있다. 닫힌 채널에 데이터를 전송할 수 없으며, 데이터 수신 또한 불가능하다.

 

4-1. 채널 닫는법

c := make(chan int) // 채널 생성
close(c) // 채널 닫기

close 함수의 파라미터에 채널명을 입력하면 해당 채널은 닫히게 된다.

 

4-2. 닫힌 채널의 데이터 전송 및 수신

package main
import "fmt"

func main() {
    c := make(chan int)

    go func() {
        c<-3
        c<-5
    }()

    fmt.Println(<-c)
    close(c) // 채널 닫기
    fmt.Println(<-c)
}

/*
    실행 결과 : 
    3
    0
*/

채널이 닫히기 전에는 채널의 데이터가 잘 수신되었지만, 닫힌 후에는 0이 출력된다. 채널이 닫히게 되면 해당 채널에 데이터를 전송하거나 수신이 이루어지지 않는다.

 

4-3. 채널의 닫힘 유무 확인

package main
import "fmt"

func main() {
    c := make(chan int)

    go func() {
        c<-3
        c<-5
    }()

    close(c)
    cValue, ok := <-c
    fmt.Println(cValue, ok)
}

/*
    실행 결과 : 
    3 true
*/

채널의 닫힘 유무는 해당 채널의 수신할 값을 변수에 반환할 때, 두번째 반환값에 채널 닫힘의 유무가 저장된다.

채널이 열려 있을 경우 true, 닫혀있을 경우 false 값이 반환된다.

 

4. 채널과 range

채널에 여러 데이터가 전송됬을 때, for - range를 통해 해당 채널의 값을 순서대로 출력할 수 있다.

package main
import "fmt"
import "sync"

func main() {
    wg := new(sync.WaitGroup) // 대기 그룹 생성
    c := make(chan int) // 채널 생성
    go func() {
        c<-1
        c<-2
        c<-3
        close(c) // 채널 닫기
    }()

    wg.Add(1) // 대기 그룹 추가
    go func() {
        for a := range c { // range를 통해 채널 c의값 출력
            fmt.Println(a)
        }
        defer wg.Done() // 함수가 종료되면 대기 그룹 종료
    }()

    wg.Wait() // 대기 그룹이 종료 될 때 까지 기다린다.
}

/*
    실행 결과 : 
    3
    5
    7
*/

 

5. 전송 및 수신 전용 채널

위의 코드들은 채널을 생성했을 때, 생성된 채널을 통해 데이터를 전송하거나 수신의 작업을 모두 행할 수 있었다. 특정한 채널에 데이터를 전송만 할 수 있도록 하거나, 데이터를 수신만 할 수 있도록 변경할 수 있다.

package main

import "fmt"
import "sync"

func receiver(ch chan<- int) { // 전송 채널
    ch<-5
    ch<-7

    close(ch)
}

func sender(ch <-chan int, wg *sync.WaitGroup) { // 수신 채널
    fmt.Println(<-ch)
    fmt.Println(<-ch)

    wg.Done()
}

func main() {
    wg := new(sync.WaitGroup)
    ch := make(chan int)

    wg.Add(1)
    go receiver(ch)
    go sender(ch, wg)

    wg.Wait()
}

 

  • 데이터 전송 전용 : 파라미터의 chan 뒤에 <-   chan<-
  • 데이터 수신 전용 : 파라미터의 chan 앞에 <-   <-chan

위와 같이 각 함수에 채널의 데이터를 전송, 수신만 할 수 있도록 설정하여 사용할 수 있다.

만약 receiver 함수에 채널 데이터를 수신하는 구문(ex) <-ch)을 추가하면 에러가 발생한다. 또한 반대로, sender 함수에 채널 데이터를 전송하는 구문(ex ch<-9)을 추가하면 에러가 발생한다.

 

// receiver 함수에 데이터 수신 구문 추가했을 경우
invalid operation: <-ch (receive from send-only type chan<- int)

// sender 함수에 데이터 전송 구문 추가했을 경우
invalid operation: ch <- 9 (send to receive-only type <-chan int)

 

6. 함수에서 채널 변수 반환

package main

import "fmt"

func fruitReceiver(ch chan string) <-chan string {
    // 반환값을 채널로 지정 <-chan
    go func() {
        ch<-"mango"
        ch<-"orange"
        ch<-"kiwi"

        close(ch)
    }()

    return ch
}

func main() {
    fruitCh := make(chan string)
    fruits := fruitReceiver(fruitCh)

    for fruit := range fruits {
        fmt.Println(fruit)
    }
}

/*
    실행 결과 :
    mango
    orange
    kiwi
*/

fruitReceiver라는 함수를 통해 채널에 값을 전송 한 후, 반환값을 채널로 지정하였다. 반환된 채널 변수를 fruits라는 변수에 담아 for range로 출력하면 채널에 담긴 데이터가 출력되는 것을 확인할 수 있다.

 

7. 채널 선택 select

하나의 채널을 사용하는 것이 아니라 여러개의 채널을 사용하는 상황에서 각 채널에 데이터가 전송되었을 때, 실행할 코드가 달라지도록 구현을 하고 할 경우, select를 사용하여 채널에 따라 실행 결과를 달리 할 수 있다.

 

7-1. select 분기

/*
    select {
        case <-채널명 :
            [실행할 코드]
    }
*/

// ex)
select {
    case <- fruits :
        fmt.Println("과일 채널!")
    case <- games :
        fmt.Println("게임 채널!")
    default : // 충족되는 조건이 없을 경우 실행된다.
        fmt.Println("기본 채널!")
}

다른 언어의 select / case를 쓸때와 사용법이 비슷하다.

 

7-2. select 사용법

package main

import "fmt"
import "sync"

func main() {
    wg := new(sync.WaitGroup)
    fruits := make(chan string)
    games := make(chan string)

    wg.Add(1)
    go func() {
        fruits <- "mango"
        
        close(fruits)
        close(games)
        wg.Done()
    }()

    select {
        case fruit := <-fruits : 
            fmt.Println(fruit, "과일 채널!")
        case game := <- games :
            fmt.Println(game, "게임 채널!")
        default :
            fmt.Println("기본 채널!")
    }

    wg.Wait()
}

/*
    실행 결과 : 
    mango 과일 채널!
*/

고루틴 익명함수에 fruits 채널에 mango라는 데이터를 전송하였다. 이로 인해 select의 첫번째 case의 조건이 충족되어 과일 채널! 이라는 문구가 출력된다.

 

package main

import "fmt"
import "sync"

func main() {
    wg := new(sync.WaitGroup)
    fruits := make(chan string)
    games := make(chan string)

    wg.Add(1)
    go func() {
        games <- "cod"
      
        close(fruits)
        close(games)
        wg.Done()
    }()

    select {
        case fruit := <-fruits : 
            fmt.Println(fruit, "과일 채널!")
        case game := <- games :
            fmt.Println(game, "게임 채널!")
        default :
            fmt.Println("기본 채널!")
    }

    wg.Wait()
}

/*
    실행 결과 :
    cod 게임 채널!
*/

과일 채널과는 반대로 고루틴 익명 함수에 games 채널에 cod라는 데이터를 전송하면 select의 두번째 case의 조건이 충족되어 게임 채널! 이라는 문구가 출력된다.

 


참고 사이트 :

https://go.dev/tour/concurrency/2

 

A Tour of Go

 

go.dev

 

https://www.sohamkamani.com/golang/channels/

 

An Introduction to Channels in Go (Golang)

An introduction on channels in Go, and how to visualize them

www.sohamkamani.com

 

https://www.bogotobogo.com/GoLang/GoLang_Channel_with_Select.php

 

GoLang Tutorial - Channels ("<-") with Select - 2020

Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization

www.bogotobogo.com

 

https://stackoverflow.com/questions/45020481/range-a-channel-finishes-with-deadlock

 

Range a channel finishes with deadlock

The following code ends with a fatal error: all goroutines are asleep - deadlock! // Package letter returns the frequency of letters in texts using parallel computation. package letter import "fm...

stackoverflow.com

 

https://medium.com/golangspec/range-over-channels-in-go-3d45dd825d2a

 

Range over channels in Go

It’s common to see range clause with array, slice, string or map as an expression’s type:

medium.com

 

https://www.velotio.com/engineering-blog/understanding-golang-channels

 

Getting Started With Golang Channels! Here’s Everything You Need to Know

Channels in Golang are a powerful way to achieve concurrency in application and build incredible high performance apps. Learn more about channels here.

www.velotio.com

 

https://stackoverflow.com/questions/13596186/whats-the-point-of-one-way-channels-in-go

 

What's the point of one-way channels in Go?

I'm learning Go and so far very impressed with it. I've read all the online docs at golang.org and am halfway through Chrisnall's "The Go Programming Language Phrasebook". I get the concept of ch...

stackoverflow.com

 

https://gobyexample.com/channel-directions

 

Go by Example: Channel Directions

func main() { pings := make(chan string, 1) pongs := make(chan string, 1) ping(pings, "passed message") pong(pings, pongs) fmt.Println(<-pongs) }

gobyexample.com

 

https://phsun102.tistory.com/118

 

Go - goroutine 사용법

Go는 스레드와 비슷한 기능을 가진 고루틴(goroutine)을 제공한다. 고루틴은 함수를 동시에 여러개 실행시킬 수 있는 기능으로써 스레드 보다 언어 문법이 더 간단하며 OS의 리소스를 덜 사용하는

phsun102.tistory.com

 

728x90
반응형

'프로그래밍 언어 > Go' 카테고리의 다른 글

Go - goroutine 사용법  (0) 2022.09.03
Go - 인터페이스 선언 및 사용법  (0) 2022.08.28
Go - 구조체 선언 및 사용법  (0) 2022.08.27
Go - 포인터란 무엇인가  (0) 2022.08.14
Go - panic() / recover() 예외처리  (0) 2022.08.09
Comments