[Go] Go 루틴 & Go 채널

배채윤·2020년 11월 2일
3
post-thumbnail

Go Routine이란?

go 루틴은 Go 런타임이 관리하는 Lightweight 논리적( or 가상적) 쓰레드이다. 쉽게 말해, Go 프로그램에서 동시에 독립적으로 실행되는 흐름의 단위다.

go routine은 비동기적으로 함수 루틴을 실행하므로 여러 코드를 동시에 실행하는데 사용된다.
goroutine은 OS 쓰레드보다 훨씬 가볍게 비동기 Cocurrent 처리를 구현하기 위해 만든 것으로, 기본적으로 GO 런타임이 자체 관리한다. OS가 프로세스의 스레드를 관리해주는 멀티스레딩과는 다른 개념이다. Go 런타임 상에서 관리되는 작업단위인 여러 goroutine들은 종종 하나의 OS 쓰레드 1개로도 실행되곤 한다. 즉, Go루틴들은 OS 쓰레드와 1 대 1로 대응되지 않고, Multiplexing으로 훨씬 적은 OS 쓰레드를 사용한다. 고루틴은 수 키로바이트 정도의 아주 적은 리소스에서 동작하므로 한 프로세스에서 수천, 수만 개의 고루틴을 동작시킬 수 있다.

또한, 멀티스레딩은 프로세스 메모리 공간에서 heap, data 영역을 공유하고 있기 때문에 자원 공유문제. 즉, 동기화 문제가 발생한다. 그러나 go routine은 routine 끼리 정보를 공유하는 방식이 아니라 메시지를 주고받는 방식이라서 lock으로 공유 메모리를 관리할 필요가 없고 구현도 어렵지 않다.

사용 예시

코드

package main
 
import (
    "fmt"
    "time"
)
 
func say(s string) {
    for i := 0; i < 6; i++ {
        fmt.Println(s, "***", i)
    }
}
 
func main() {
    // 함수를 동기적으로 실행
    say("Sync")
 
    // 함수를 비동기적으로 실행
    go say("Async1")
    go say("Async2")
    go say("Async3")
 
    // 3초 대기
    time.Sleep(time.Second * 3)
}

출력

Sync *** 0
Sync *** 1
Sync *** 2
Sync *** 3
Sync *** 4
Sync *** 5
Async3 *** 0
Async3 *** 1
Async3 *** 2
Async3 *** 3
Async3 *** 4
Async3 *** 5
Async1 *** 0
Async1 *** 1
Async2 *** 0
Async2 *** 1
Async2 *** 2
Async2 *** 3
Async1 *** 2
Async1 *** 3
Async1 *** 4
Async1 *** 5
Async2 *** 4
Async2 *** 5

Go Channel이란?

goroutine에서 데이터를 주고 받는 통로. 상대편이 준비될 때까지 채널에서 대기함으로써 별도의 lock을 걸지 않고 데이터를 동기화하는 데 사용된다. go 채널은 수신자와 송신자가 서로를 기다리는 속성이 있어, 다음 예제와 같이 Go Routine이 끝날 때까지 대기하는 기능을 구현할 수 있다.

채널에서 데이터를 꺼내, 특정 변수에 담는 것을 수신이라 하고 채널로 데이터를 넣는 것을 송신이라 한다.

채널로 송신/수신이 되는 순간 해당 goroutine은 정지하고, 다시 송수신을 받으면 다시 lock이 풀리고 다음 작업을 수행한다.

done := make(chan bool)
go func() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
	done <- true	// 채널로 true를 보낸다
}()
<-done				// 채널로부터 123을 받는다
fmt.Println("done")

위의 예제는 Unbuffered Channel일 때의 예시다.

채널 연산자

채널 연산자설명
채널<-값- 채널에 값을 송신한다.
- Unbuffered 채널이라면 수신자가 받을 때까지 대기한다.
- buffered 채널이라면 값 복사 비용만큼만 대기한다.
값 = <- 채널- 채널에서 값을 수신한다.
- 채널 수신 연산자 호출 대상은 데이터가 들어올 때까지 대기한다.

Unbuffered Channel

이름과 같이 저장할 공간이 없기 때문에 하나의 수신자가 데이터를 받을 때까지 송신자가 데이터를 보내는 채널에 묶여 있게 된다.

c := make(chan int)
c <- 1 // 수신 루틴이 없으므로 데드락
fmt.Println(<-c) // 코멘트 해도 데드락(별도의 Go루틴이 없어서)

에러 메시지

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /Users/baechaeyun/cheche/testgo/cheche.go:11 +0x59

Buffered Channel

수신자가 받을 준비가 되어 있지 않을지라도 지정된 버퍼만큼 데이터를 보내고 계속 다른 일을 수행한다. 선언된 저장공간만큼 송신자는 데이터를 보내고 다른 작업을 수행한(비동기)다.

ch := make(chan int, 1) // 1 : 파라미터에 사용할 버퍼 개수

// 수신자가 없어도 보낼 수 있다.
ch <- 101
fmt.Println(<-ch) // 101

채널 파라미터 사용법, 채널 닫기

  • 일반적으로는 송수신을 모두 하는 채널을 전달하지만, 특별히 해당 채널로 송신만 할 건지, 수신만할 것인지를 지정할 수도 있다.
    - chan<- : 송신 채널 파라미터
    - <-chan : 수신 채널 파라미터
  • 채널을 닫으면 더이상 송신은 불가능하고 수신만 가능해진다.
func main() {
	ch := make(chan string, 2)
	sendChan(ch)    // 송신 전용 채널
	receiveChan(ch) // 수신 전용 채널
	bothChan(ch)    // 일반 채널
    
    close(ch)		// 채널 닫음
    
    fmt.Println(<-ch) // "Data"
    fmt.Println(<-ch) // "Both"
    
}

func sendChan(ch chan<- string) {
	ch <- "Data"
	// 송신 채널에서 수신하면 에러 발생
	// x := <-ch // invalid operation: <-ch (receive from send-only type chan<- string)
}

func receiveChan(ch <-chan string) {
	x := ch
	fmt.Println(x)
}

func bothChan(ch chan string) {
	ch <- "Both"
	y := ch
	fmt.Println(y)

}

채널 select 문

select문은 복수 채널들을 기다리면서 준비된 채널을 실행하는 기능을 제공한다.
아래처럼 case문으로 각각 다른 채널을 기다리다가 준비된(데이터 송/수신을 한) 채널 case를 실행한다.

func main() {
	done1 := make(chan bool)
	done2 := make(chan bool)

	go run1(done1)
	go run2(done2)

EXIT:
	for {
		select { // goroutine이 실행되길 기다리는 중(chan에 송수신 신호가 오길 기다리는 중)
		case <-done1:
			println("run1 완료")	// 1초 뒤 실행

		case <-done2:
			println("run2 완료")	// 3초 뒤 실행
			break EXIT	// 탈출
		}
	}
	fmt.Println("test") // EXIT 되고 실행 됨
}

func run1(done chan bool) {
	time.Sleep(1 * time.Second)
	done <- true
}

func run2(done chan bool) {
	time.Sleep(2 * time.Second)
	done <- true
}

활용

아래처럼 코드를 쓰면 select 안에서 어떤 채널이 리턴하기를 기다리는데, 아무도 리턴하지 않아서 계속 기다리고 있는 상태가 된다. 즉, 무한 루프 상태로 만들 수 있다. 무한루프로 하루에 한 번씩 함수가 돌게끔 짤 수 있을듯하다.

select{}

매일 오전 9시마다 .txt 파일 parsing해서 MongoDB로 데이터 update 해주는 것을 해볼 예정이다.(secom 출퇴근 데이터 MongoDB로 보내기)
-> 만들어진 라이브러리 이용하기로 함
https://github.com/robfig/cron

c := cron.New()
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("@hourly",      func() { fmt.Println("Every hour") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
c.Start()

Reference

profile
새로운 기술을 테스트하고 적용해보는 걸 좋아하는 서버 개발자

1개의 댓글

comment-user-thumbnail
2020년 11월 2일

데드락 : 프로세스가 자원을 얻지 못해 다음 처리를 하지 못하는 상태로, ‘교착 상태’라고도 하며 시스템적으로 한정된 자원을 여러 곳에서 사용하려고 할 때 발생. 서로 원하는 리소스가 상대방에게 있어, 프로세스가 무한정 대기하는 상태

답글 달기