[Golang] Go Routine 정리

2022. 1. 26. 17:46Dev/Golang

Go를 아십니까?

 

Go Routine

Go의 런타임에서 관리하는 가상 쓰레드로, 쉽게 말하면 함수를 병렬적으로(=비동기적으로, Async하게) 실행하게 해주는 것이다.

그러니까 써야한다. 비동기적으로 처리한다는 것은 함수의 처리를 기다리지 않고 바로 실행하는 것이기 때문에 동기적으로 처리하는 것보다 성능상에서 매우 유리하기 때문.. 다만 마치 JS진영에 async await, 혹은 Promise를 사용하는 것처럼 함수가 절차적으로 진행되어 함수의 결과값을 가지고 동기적으로 처리해야 할 경우에는 굳이 Go Routine을 사용할 필요는 없어보인다.  물론 이 경우도 Case By Case겠지만.

go 키워드를 사용하면 된다.

import "fmt"

func SomeLogic() {
	//...
}

func main() {
	fmt.Println("Go Routine 테스트!!")
    
    //go 키워드를 이용
    go SomeLogic()
    
    //혹은 익명함수로 처리할 수도 있다
    go func(s string) {
    	fmt.Println(s)
    }("Hello")
    
    fmt.Println("Done")
}

//출력결과 : Go Routine 테스트!! \n Done

보면 알겠지만, go function은 go의 응답을 기다리지 않고 병렬적으로 처리가 되었기 때문에 Hello는 출력이 되지 않고 main 함수가 끝나 버리는 것을 확인할 수 있다.

Go Channel & Wait

둘다 Go Routine의 종료를 기다리기 위한 것.

그럼 둘 중 하나만 써도 되는 것이 아니냐, 한다면 조금 차이가 있다.

Channel : Go Routine의 처리 값을 저장 가능하다.

Wait : Go Routine의 종료만 기다림

즉 Go Routine의 리턴 결과가 밖에서 필요하다면 Channel을, 그렇지 않고 Go Routine의 종료만 기다리면 된다면  wait를 사용하면 된다.

Channel의 예는 아래와 같다.

import "fmt"

func NeedReturnValue(rslt chan error){
	//...some Logic
    
    //error 타입의 channel인 rslt에 위에서 ...some Logic부문에서 처리된 결과값인 err를 송신함
    fmt.Println("고루틴 첫번째")
    rslt <- err
}

func main() {
	//채널을 만들 때는 반드시 make를 활용하여 아래와 같이 만들어줘야함
    //chan은 channel을 나타내고, 기본적으로 import 되어있음
    chanErr := make(chan error)
    
    //위에서 정의된 함수의 로직이 모두 끝나면 chanErr는 err값을 송신받게 된다.
    go NeedReturnValue(chanErr)
    
    //익명함수 예제, 위의 go routine과 아래의 go routine은 병렬적으로 작동하게 됨
    go func() {
    	//...another Logic
        
        fmt.Println("고루틴 두번째")
        chanErr <- err
    }()
    
    //chanErr는 결과적으로 2번 송신받음, 따라서 정확히 2번(더도 말고 덜도 말고) 내보내줘야 한다.
    //만약 정확히 2번 모두 내보내지 않는다면 데드락이 걸려 아래의 코드는 영원히 실행되지 않을 것이다.
    
    //한번 내보냄, 이 때 아래의 <-chanErr가 실행되기 위해서는 반드시 chanErr가 데이터를 받아야하고, 이것은 위의 go Routine에서 처리된다.
    if err := <-chanErr; err != nil {
    	fmt.Println(err)
    }
    
    //두번 내보냄
    if err := <-chanErr; err != nil {
    	fmt.Println(err)
    }
    
    //아래의 출력은 위에서 chanErr에서 데이터가 빠져나오기 전(즉, <-chanErr)까지 대기된다.
    fmt.Println("끝!")    

}

//출력 결과
//고루틴 첫번째 \n 고루틴 두번째 \n 끝!
//or 고루틴 두번째 \n 고루틴 첫번째 \n 끝!

 

Wait의 예는 아래와 같다.

import (
	"fmt"
    "sync"	//wait을 사용하기 위한 모듈 임포트
)

func NotNeedReturnValue() {
	//...some Logic
    fmt.Println("첫번째")
}

func main() {
	//wait Group 선언
	var wait sync.WaitGroup
    wait.Add(2)	//아래 2개의 고루틴을 추가함
    
    //익명 함수 이용
    go func() {
    	//해당 고루틴이 종료되면 wait Gropu에 완료되었다고 알려줌, 즉 wait Group에서 1개의 값을 뺀다
    	defer wait.Done()
        NotNeedReturnValue()
    }
    
    go func() {
    	defer wait.Done()
    	fmt.Println("두번째")
    }
    
    //Wait Group에서 기다려야 할 Go Routine이 0이 될 때까지 기다림
    wait.Wait()
    
    //wait.Wait()를 통해 모든 고루틴이 끝나면 이제부터 아래의 코드를 실행
    fmt.Println("끝!")
    
}

//출력 결과
//첫번째 \n 두번째 \n 끝!
//or 두번째 \n 첫번째 \n 끝!

 

How To Use

구글링을 하다보면 Go Routine에 관한 글이 굉장히 많다. 다만 매우 간단한 개념들 뿐이고, 실제로 처리해야 할 고루틴 및 채널이 많아졌을 때 어떻게 해결해야하는지 이슈를 겪었다.

예를 들면 분명 동일하게 작성한 것 같은데.. 어떤 api는 데드락이 걸리고 어떤 api는 정상 작동되고 하는..

물론 Go Routine 및 Channel에 대한 이해 부족으로 생긴 결과긴 했지만, 다음엔 이런 실수가 없길 바라며..

아래의 예를 보자

import "fmt"

func main() {

	c := make(chan int)
	c2:= make(chan string)
	
    go func(s string){
    	c <- 3
        c2 <- s
    }("Hello")
	
	
	x, y := <-c, <-c2

	fmt.Println(x, y)
}

출력 결과가 어떻게 될까?

3 Hello
가 출력될 것이다.

그럼 아래의 코드는?

import "fmt"

func main() {

	c := make(chan int)
	c2:= make(chan string)
	
    go func(s string){
    	c2 <- s
        c <- 3
    }("Hello")
	
	
	x, y := <-c, <-c2

	fmt.Println(x, y)
}

데드락이 걸린다.

왜 channel에서 읽는 순서와 쓰는 순서가 같아야 하는걸까?

 

주석으로 설명한다.

import "fmt"

func main() {

	c := make(chan int)
	c2:= make(chan string)
	
    go func(s string){
    	//2. c2로 s라는 데이터를 송신했다. 따라서 c2에서 데이터가 빠져나가기 전까지 go routine내에서 슬립상태
    	c2 <- s
        //3. 그러므로 <-c2가 메인에서 실행되기 전까지는 아래의 코드는 실행되지 않는다.
        c <- 3
    }("Hello")
	
	//1. c에서 데이터를 빼기 전까지, 메인에서는 슬립 상태.
    //4. 최종적으로 <-c도 기다리고, <-c2도 기다려야 하기 때문에 교착상태가 발생한다.
	x, y := <-c, <-c2

	fmt.Println(x, y)
}

따라서 해당 문제를 해결하기 위해서는

1. Channel에 버퍼를 설정하거나

2. 애초에 순서를 잘 맞추거나

3. 채널을 하나만 둬서 사용하거나

하는 방법들이 있다... 애초에 Go Routine내에서 send는 비동기적으로 전부 전송된다고 이해해서 생긴 문제였다.

버퍼가 없을 시, 채널로 전송하면 채널에서 데이터가 빠져나가기 전까지 모든 함수들이 슬립된다.

 


Reference

http://golang.site/go/article/21-Go-루틴-goroutine

 

예제로 배우는 Go 프로그래밍 - Go 루틴 (goroutine)

1. Go루틴 Go루틴(goroutine)은 Go 런타임이 관리하는 Lightweight 논리적 (혹은 가상적) 쓰레드(주1)이다. Go에서 "go" 키워드를 사용하여 함수를 호출하면, 런타임시 새로운 goroutine을 실행한다. goroutine은

golang.site

https://medium.com/wesionary-team/understanding-go-routine-and-channel-b09d7d60e575

 

Understanding GO Routine and Channel

What is a GO routine?

medium.com

https://forum.golangbridge.org/t/order-in-channels/23826

 

Order in channels

I don’t understand why I cannot read from channels in any order I would like to. I’ve already sent values to the channels. Why should I keep the same order? Thanks 🙂 func main() { ch1 := make(chan int) ch2 := make(chan int) go send(ch1, ch2) // this

forum.golangbridge.org

https://jbhs7014.tistory.com/179

 

[Go] 동시성을 위한 GoRoutine과 Channel

Go 언어의 특징중 하나는 동시성이다. 동시성이란 프로그램이 순차적인 흐름으로 수행되는 것이 아니라 동시에 수행되어 처리되는 것을 말한다. Go 언어는 이러한 동시성을 위해 고루틴(goroutine)

jbhs7014.tistory.com