goroutine(고루틴) 자세히 알아보기

POII·2022년 10월 23일
1

go_study

목록 보기
5/7
post-custom-banner

goroutine

  • goroutine은 가벼운 실행 쓰레드이다.
  • 함수 앞에 go를 붙여 병행적으로 실행 가능하다.
...

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

    f("direct")

    go f("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("going")

    time.Sleep(time.Second)
    fmt.Println("done")
}

goroutine의 장점

  • gorountine은 쓰레드보다도 적은 stack을 사용한다 (약2KB, 쓰레드는 MB 단위)
    → goroutine간 스위칭시 오버헤드 적음
    → 쓰레드는 기본적으로 goroutine을 처리할때 필요하면 생성하는데, 생성된 스레드에 더 이상 실행할 goroutine이 없어도 종료하지 않고 일정시간 idle상태로 관리하다가 추후 다른 goroutine을 할당해줌

  • 병행 실행에 관한 최적화된 처리를 go runtime scheduler가 처리해준다
    → 커널 단위 스위칭 적음(동기처리가 필요한 시스템 콜 등에서 일어나기 때문에 아얘 없는건 아님)

goroutine의 동작 원리

  • 기본적으로 os는 go로 짜여진 프로그램을 실행하면 다중쓰레드, 유저레벨 싱글 프로세스로 본다.
    • 그렇다면 goroutine의 관리는?
      go runtime에서 go runtime scheduler가 관리! 커널이 관리 x

  • go runtime에서는 세가지 C 구조체를 활용하여 go routine을 관리한다
    • G(goroutine)구조체: 단일 goroutine을 표현하는 구조체이다. goroutine에 필요한 스택, 상태정보, 담당하고 있는 코드의 레퍼런스를 담고있다.
    • M(machine)구조체: OS 쓰레드를 표현하는 구조체이다. global queue, local queue에 있는 실행 가능한 goroutine에 대한 포인터, 지금 실행하고 있는 고루틴에 대한 포인터, 스케쥴러의 cache와 reference를 담고 있다.
    • P(processor)구조체: 논리적인 프로세서를 표현하는 구조체이다.
      • 기본적으로 cpu 코어 갯수로 할당되는데, runtime.GOMAXPROCS로 조절도 가능하다.
      • 할당 수에 제한이 있기 때문에 고루틴이 많아도 그 갯수만큼 많은 쓰레드를 생성하지 않음
    • go scheduler는 두가지 큐를 활용하여 G를 스케쥴링한다
      • GRQ(global run queue): P에 할당되지 않은 G들을 저장
      • LRQ(local run queue): P에서 처리할 G들을 저장
    • 각각의 P 구조체에는 LRQ가 하나씩 할당된다.
    • 결국 G(goroutine)을 실행하기 위해선 G를 M,P에 할당해주어야한다.
    • 하나의 G가 블락되면 P는 LRQ의 다른 G를 M에 할당한다.
    • 만약 시스템 콜 등으로 P에 할당된 M 자체가 OS 기준으로 블락된 경우엔 P의 할당을 해제해버리고 쉬고있는 M을 P에 할당한다!!
      → go의 라이브러리에서 일반적으로 system call로 처리되어 블로킹 당할만한 것들(소켓과 파일의 I/O처리, 타이머)은 OS에서 제공하는 기능들을 이용해 I/O 멀티플렉싱을 수행하기 때문에 사실 go 에서는 블락킹 자체가 많이 일어나지 않음
      → 하나의 코어의 효율적인 쓰레딩 지원
    • P안에서 처리 우선순위는 다음과 같다
      • LRQ의 G → 다른 P의 LRQ의 G → GRQ의 G → network poll

  • go scheduler의 구현은 비 선점 방식이 때문에 function call 단위로 조심스럽게 실행시간을 할당한다.
    → 만약 function call이 없는 하드한 loop 문이 매우 길어진다면 프로그램 성능에 영향을 줌

  • 결국 이 그림처럼 구조를 표현할 수 있다.

goroutine끼리의 정보 전달

  • 기본적으로 channel을 이용
  • channel
    • 채널은 값을 전달하는 pipe라고 볼 수 있다.

    • 채널은 전달하는 값의 타입에 따라 고정된다.

    • make(chan 타입) 으로 채널을 만들 수 있다.

    • 채널 < - 값 형식으로 값을 넣을 수 있다.

    • 변수 < - 채널 형식으로 넣은 값을 가져올 수 있다.

    • 기본적으로 sender 와 receiver가 모두 준비 될 때 까지 send, receive가 블락되므로 별도의 동기화 구문 없이 다음과 같은 정보 전달이 가능하다.

      ...
      func main() {
      
          messages := make(chan string)
      
          go func() { messages <- "ping" }()
      
          msg := <-messages
          fmt.Println(msg)
      }
  • channel-buffering
    • 채널을 버퍼링 하도록 만들기 위해선 채널을 만들 때 다음과 같이 버퍼의 크기를 지정해주면 된다.

      message := make(chan string, 2)
    • 채널을 버퍼링하게 만들면 채널은 병행적으로 send와 receive를 처리하게 된다.

      ...
      
      func main() {
      
          messages := make(chan string, 2)
      
          messages <- "buffered"
          messages <- "channel"
      
          fmt.Println(<-messages)
          fmt.Println(<-messages)
      }
  • channel-synchronization
    • 만약 다음과 같이 receiver 구문을 다른 실행흐름에서 사용한다면 sender에서 값이 입력되기 전까지 실행 흐름이 block된다.

      ...
      
      func worker(done chan bool) {
          fmt.Print("working...")
          time.Sleep(time.Second)
          fmt.Println("done")
      
          done <- true
      }
      
      func main() {
      
          done := make(chan bool, 1)
          go worker(done)
      
          <-done
      }
  • channel-direction
    • 채널을 함수의 인자로 사용할 때, 방향을 지정해주어 receive only 또는 send only로 지정할 수 있다.

    • 이렇게 채널의 방향을 지정하는 것은 프로그램을 한층 더 type-safety하게 만들어 준다.

      ...
      func ping(pings chan<- string, msg string) {
      	pings <- msg
      }
      
      func pong(pings <-chan string, pongs chan<- string) {
      	msg := <-pings
      	pongs <- msg
      }
      
      func main() {
      	pings := make(chan string, 1)
      	pongs := make(chan string, 1)
      	go pong(pings, pongs)
      	go ping(pings, "passed message")
      
      	fmt.Println(<-pongs)
      }
  • select
    • 여러개의 goroutine의 완료를 기다리기위해 select를 사용할 수 있다.

    • 다음과 같이 case문을 통해 여러개의 채널 중 먼저 send가 끝난 채널을 수신하는 것이 가능하다.

      ...
          c1 := make(chan string)
          c2 := make(chan string)
      
          go func() {
              time.Sleep(1 * time.Second)
              c1 <- "one"
          }()
          go func() {
              time.Sleep(2 * time.Second)
              c2 <- "two"
          }()
      
          for i := 0; i < 2; i++ {
              select {
              case msg1 := <-c1:
                  fmt.Println("received", msg1)
              case msg2 := <-c2:
                  fmt.Println("received", msg2)
    • select 문을 통해 timeout을 쉽게 구현할 수 있다.

      ...
      func main() {
      
          c1 := make(chan string, 1)
          go func() {
              time.Sleep(2 * time.Second)
              c1 <- "result 1"
          }()
      
          select {
          case res := <-c1:
              fmt.Println(res)
          case <-time.After(1 * time.Second):
              fmt.Println("timeout 1")
          }
    • 위 코드에서 After는 인자로 받은 시간이 지나면 값을 할당받는다.

    • 기본적으로 send와 receive는 block의 형태이지만 select의 default 구문을 사용해 non-blocking 으로 구현할 수 있다.

      c1 := make(chan string)
      select {
      	case res:= <- c1:
      		fmt.Println(res)
      	default:
      		fmt.Println("no data in c1")
      } //non-blocking 형태의 receive
      select {
      	case c1<-"msg":
      		fmt.Println("msg sent")
      	default :
      		fmt.Println("no receiver ready")
      } //non-blocking 형태의 sender
  • closing-channel
    • close(chan) 함수를 이용하여 더 이상 보낼 값이 없는 채널의 입력을 닫을 수 있다.
    • 채널에서 데이터를 보낼 때 다음과 같은 구문을 이용하면 채널이 닫힌 경우 more 값에 false를 반환한다.
      value, more <- my_channel
    • 이를 응용하여 채널이 닫히기 전 까지만 값을 받는 함수를 구현할 수 있다.
      ...
      go func(){
      	value, more <- my_channel
      	if more{
      		fmt.Printf("data received %d",value)
      	} else{
      		fmt.Printf("no more data in channel")
      		return
      	}
      }
    • close를 통해 채널의 입력이 닫힌 경우 range를 이용해 해당 채널의 버퍼에 있는 값만을 읽어올 수도 있다.
      ...
      close(my_channel)
      for value:= range my_channel{
      	fmt.Println(value)	
      }
      	

references:

https://calvinfeng.gitbook.io/gonotebook/concurrency/04-01-go-routines

https://gobyexample.com/

https://ykarma1996.tistory.com/188

https://golangcn.org

profile
https://github.com/poi1649/learning
post-custom-banner

0개의 댓글