[Go] Goroutine

Falcon·2021년 10월 8일
1

go

목록 보기
1/11
post-thumbnail

🎯 Goals

  • 고루틴, 언제 써야할까?
  • 고루틴, 스레드랑 무엇이 다른가?
  • 고루틴 사용 방법
  • 고루틴의 특징

When to use?

Concurrency Programming == 동시에 병렬처리가 필요할 때

Why to use goroutine?

프로세스 대신 쓰레드를 쓰는 이유와 같다.
쓰레드는 Stack 을 제외한 모든 메모리상의 자원을 공유하여 Process 보다 상대적으로 가벼운 Context Switching을 할 수 있다.
고루틴은 한 발 더 나아가서, Stack 영역까지 공유하는 쓰레드 보다도 더 가벼운 실행 흐름이다.
(그래서 고루틴을 경량 스레드라 칭하기도 한다.)
하나의 쓰레드 내에서 마구잡이로 고루틴을 생성할 수 있다.
Stack Pointer, PC 만 교체하면 된다. (Context Switcing 비용 ⬇️)

사용 방법

굉장히 쉽다.
func 앞에 go 키워드만 붙여주면 된다.

func printCount(name string) {
	for i := 0; i < 10; i++ {
		println(name, i)
		time.Sleep(time.Second)
	}
}

func main ()  {
	go printCount("falcon")
	printCount("milkcoke")
}

//==== Result ======
// falcon 0
// milkcoke 0
// milkcoke 1
// falcon 1


// ... ~ 10

with Channel

func waitAndCheckOdd(num int, channel chan string) {

      for i := 0; i < 10; i++ {
          println("wait for result about : ", num, i, "seconds...")
          time.Sleep(time.Second)
      }

      if num%2 == 0 {
          channel <- "false: " + strconv.Itoa(num)
      } else {
          channel <- "true " + strconv.Itoa(num)
      }
}

func main() {
	// make channel
	channel := make(chan string)
	go waitAndCheckOdd(5, channel)
	go waitAndCheckOdd(2, channel)
	// When main function watch '<-' block the code (wait the message)
	// and getting a message from the channel
	// <- is receiver!
	fmt.Println("Received message from the channel : ", <-channel)
	fmt.Println("Received message from the channel : ", <-channel)

	// Go knows how many goroutines are running
	// and administrate goroutine state (message queue)

}

고루틴의 상태

다음과 같이 3가지 상태로 나타낼 수 있다.


고루틴의 특징

lifecycle

thread 처럼 프로그램이 실행중이어야한다.
lifecycle 을 main function과 같이한다.

고루틴의 또다른 이름은 경량 스레드

main - goroutine 간 커뮤니케이션을 위해 channel 을 사용한다.

WaitGroup

실행 순서를 결정하기 위해 존재한다.

고루틴은 Communicating Sequential Process(CSP) 의 일종으로 다음 특성을 가진다.

  • 실행 순서를 강제할 수 있다. (런타임 스케쥴링이 가능하다)
  • 공유 메모리를 가지지 않는다.
  • 데이터를 서로 주고받을 수 있다.
  • Scale-out 할 수 있다.

⚠️ WaitGroup 사용시 반드시
goroutine 을 호출하는 스코프 내에서 add 를 먼저해야한다.
그래야 wait() 문에서 기다린다.

Scope

각자 Stack 영역을 할당받지만, Heap 으로 승격할 수 있다.
goroutine 을 실행시킨 스코프 내에서는 해당 스코프가 stack 에서 해제되더라도
접근 가능한 상태를 유지한다. (Heap 으로 승격)

The variables accessed by the goroutine are captured and stored in a closure, so they can be accessed by the goroutine even if the variables go out of scope in the enclosing function.

func main() {
	var wg = &sync.WaitGroup{}

	incr := func(wg *sync.WaitGroup) {
		var i int
		wg.Add(1)
		go func() {
			defer wg.Done()
			i++ // 2. `return` 시점에 i 변수를 stack -> heap 으로 승격 
			fmt.Printf("value of i: %v\n", i) // 3. heap 으로 승격된 i 의 값을 출력
		}()
		fmt.Println("return from the function")
		return // 1. incr stack 메모리 해제
	}

	incr(wg)
	wg.Wait() 
	fmt.Println("done..") // 4. 프로그램 종료.
}

출력 결과

return from the fuction
value of i: 1
done ..

incr 라는 함수 내에 선언된 i 변수에 대해 함수가 종료된 시점 (incr 이 차지한 stack 메모리가 해제된 시점) 이후에도 익명의 고루틴이 i 라는 값에 access 가능하다.

Wrong example

고루틴이 스케쥴러에 의해 실행 기회를 가지지 못할 경우 아예 실행되지 않고 main() 실행이 끝나버릴 수 있다.

func asyncDoSomething(wg *sync.WaitGroup) {
	wg.Add(1)       // ❌ 스케쥴링 상황에 따라 아예 실행되지 않을 수 있다.
	defer wg.Done() // decrement dynamically
    // ..
}

func main() {
  	waitGroup := &sync.WaitGroup{}
    go asyncDoSomething(waitGroup)
    waitGroup.Wait()
}

Right example

고루틴 실행 스코프 밖에서 waitGroup.Add() 를 호출한다.
이로써 고루틴이 실행됨을 보장할 수 있다.

func asyncDoSomething(wg *sync.WaitGroup) {
	defer wg.Done()
    // ..
}

func main() {
  	waitGroup := &sync.WaitGroup{}
    waitGroup.Add(1) // ✅ 명시적으로 기다릴 고루틴 숫자를 1개로 미리 설정해야한다.
    go asyncDoSomething(waitGroup)
    waitGroup.Wait()
}

channel

채널은 Message Queue로 FIFO 방식으로 동작한다.
<- Operator 를 만나는 순간 메시지를 전달받을 때까지 기다린다.

메시지는 먼저 return 된 goroutine 함수 순서대로 받아온다.

⚠️ 고루틴 사용시 주의할 점

멀티 프로세싱, 멀티 쓰레드에서도 그러하듯 모든 병렬처리에서는 항상 Consistency , Synchronization 이슈가 발생한다.
동일 자원을 동시에 여러곳에서 접근하기 때문이다.

동기화는 어떻게?

흔히 사용하는 Mutex (Mutual Exclusion) 기법을 써도 좋다.

다만 다음과 같은 단점을 고려해야 한다.
1. mutex 의 Lock 자체에 드는 소요시간
2. 성능 저하 (고루틴의 대기 상태가 많아진다.)

DeadLock

서로 다른 실행 흐름이 각기 다른 자원을 선점해놓고 서로 상대 작업이 끝나기를 기다리는 상태로
결과적으로 아무도 작업을 완료할 수 없는 상황이 발생할 수도 있다.

고루틴은 실행순서를 어떻게 Scheduling 할까?

쓰레드 수가 아니라 CPU 갯수 만큼의 Processor Object 를 만들어 스레드가 아닌 프로세서 객체가 LRQ를 가지고있게 하여 효율적인 스케쥴링을 하도록 구현했다.
LRQ (Local Run Queue)
GRQ (Global Run Queue) 를 자체구현해놨다.

고루틴도 기본 10ms 의 time slice 가 정의되어 프로세서의 장시간 점유를 방지한다. => 선점 스케쥴링 기법이다.
시간이 오버될 경우 실행중이던 고루틴을 preempted 되어 GRQ로 들어간다.

자세한 설명은 Reference 참조 링크를 참고하시라

고루틴 생명주기?

만약, goroutine func 내에서 recursive call 하면 상위 고루틴은 알아서 끝나나?

스레드 vs 고루틴

IndexThreadGoroutine
Communication wayShared memoryBy channel
Memory shareHeap, Data, Code except stackAll spaces memory address even stack
SchedulingBy OSBy Go runtime
Context Switching CostRelatively highLow

고루틴은 쓰레드처럼 부모 프로세스를 갖는가?

아니다. 고루틴은 Go Runtime 에 귀속된다.

고루틴을 재귀함수로 호출하면 어떻게되는가?

시작

고루틴은 호출 시점에 시작되고 (ex. go func())
고루틴 자체의 스택 메모리를 할당받는다.

종료

다음 3가지 경우에 생명주기가 종료된다.

  • 함수의 끝 블록 도달.
  • return statement
  • Panic 또는 Runtime Error 발생

종료 후에 할당 받았던 스택 메모리는 해제된다.

⚠️ 종료 시점에 waitGroup 이나 가지고 있던 channel 이 있는 경우 종료 사실을 waitGroup 과 channel 에 알린다.


🔗 Reference

profile
I'm still hungry

0개의 댓글