2 분 소요

3장 GO의 동시성 구성요소

고루틴

  • 당연히 main도 고루틴이다.
  • 고루틴은 다른 코드와 함께 동시에 실행되는 함수이다.
  • 하지만 꼭 병렬로 실행되는 것은 아니다.
  • 고루틴은 가볍다.
  • 익명 함수도 지원
sayHello := func() {
    fmt.Println("hello")
}()
go sayHello
  • 고루틴은 OS쓰레드가 아니며 코루틴이라는 더 높은 추상화이다.

코루틴은 단순히 동시에 실행되는 서브루틴으로서, 비선점적, 다시말해 인터럽트 할 수 없다. 대신 코루틴은 잠시 중단하거나 재 진입 할 수 있도록 여러개의 지점을 가지고 있다.

  • M:N의 스케줄러로써 M개의 그린 스레드를 N개의 OS스레드에 매핑한다는 의미이다.
  • fork-join이라는 동시성 모델을 따르는데, fork는 부모와 자식이 동시에 실행되며, join은 동시에 실행된게 합쳐진다. sync.waitgroup
var wg sync.waitGroup
wg.add(1)
go func() {
    defer wg.done()
    fmt.Println("hello")
}()
wg.wait()

생각해봐야될 문제

 // 아래 문제는 순차로 출력될것을 기대하지만 아니다.
// good day만 3번출력
// 이유는 go는 언제 실행될지 모르기 때문이다. 순차적으로 실행될것을 기대했으나 실제로는 good day까지 인덱스가 흘러간 후 go routine이 동작함
var wg sync.WaitGroup
for _, s := range []strings{"hello", "greetings", "good day"} {
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println(s)
    }()
}
wg.wait()

// 해결 하기 위해서는 아래와 같이 넘겨주면 된다
var wg sync.WaitGroup
for _, s := range []strings{"hello", "greetings", "good day"} {
    wg.Add(1)
    go func(s string) {
        defer wg.Done()
        fmt.Println(str)
    }(s)
}
wg.wait()

Sync 패키징

  • WaitGroup
  • Mutex, RWMutex
  • Cond
  • Once
  • pool
    • sync.Pool을 인스턴스화 할 때 호출 시 스레드로부터 안전한 New 멤버 변수를 전달한다.
    • Get에서 인스턴스를 받았을 때 , 돌려받은 객체의 상태에 대한 가정을 해서는 안된다.
    • Pool에서 꺼낸 객채로 작업을 마치면 반드시 Put을 호출한다. 그렇게 하지 않으면 Pool은 아무런 소용이 없다. 보통 이 작업은 defer로 이루어진다.
    • 풀 내에 있는 객체들은 구조가 거의 균일하여야 한다.

채널

<-chan struct{} // 읽기만
chan<- struct{} // 쓰기만
  • go는 필요할 때 양방향 채널을 묵시적으로 단 방향 채널로 변환한다.
  • 가득찬 채널에 쓰려고 하는 고루틴은 채널이 비워질때까지 기다리며, 비어있는 채널에서 읽으려는 고루틴은 적어도 하나의 항목이 있을때까지 기다린다.
  • 읽기 연산은 두번쨰 리턴 값이 가능한대 해당 값은 닫힌 여부이다.
select{
case c, ok <- streamCh:
    if !ok {
        fmt.Println("닫혔다")
    }
    switch c.type {
        ...
    }
}
  • 버퍼링되지 않은 채널은 단순히 여유용량이 0인 버퍼링된 채널과 같다.
  • 버퍼가 가득찬 채널에 쓰기 연산을 하면, 버퍼가 비워질 때까지 대기한다. 송신자가 없는 버퍼가 빈 채널에 읽기 연산을 하면, 송신이 발생할 때까지 대기한다.

채널 생성 시 참고사항

채널을 소유한 고루틴은 반드시 다음을 수행해야 한다.

  • 채널을 인스턴스화 한다.
  • 쓰기를 수행하거나 다른 고루틴으로 소유권을 넘긴다.
  • 채널을 닫는다.
  • 이 목록에 있는 앞의 세가지를 캡슐화하고, 이를 읽기 채널을 통해 노출한다.

이렇게 책임을 소유자에게 부여하면, 다음과 같은 효과가 있다.

  • 우리가 채널을 초기화하기 때문에 nil 채널에 쓰는 것으로 인한 데드락의 위험을 제거할 수 있다.
  • 우리가 채널을 초기화하기 때문에 nil 채널을 닫을 위험이 없다.
  • 우리가 채널이 닫히는 시기를 결정하기 때문에, 닫힌 채널에 쓰는 것으로 인한 패닉의 위험을 없앨 수 있다.
  • 우리가 채널이 닫히는 시점을 결정하기 때문에 채널을 두번 이상 닫는 것으로 인한 패닉의 위험을 제거할 수 있다.
  • 우리 채널에 부적절한 쓰기가 일어나는 것을 방지하기 위해 컴파일 시점에 타입 검사기를 사용한다.

채널의 소비자는 이제 두가지 사항만 신경쓰면 된다.

  • 언제 채널이 닫히는지 아는 것
  • 어떤 이유로든 대기가 발생하면 책임있게 처리하는 것

Select

  • switch 블록과는 다르게 select 블록의 case 문은 순차적으로 테스트되지 않는다.

  • 조건이 하나도 충족되지 않는다고 다음 조건으로 넘어가지도 않는다.

  • 대신 모든 채널 읽기와 쓰기를 동시에 고려한다
  • 준비된 채널이 없는 경우 select 문 전체가 중단되 대기한다. 그런 다음 채널들 중 하나가 준비되면 해당 연산이 진행되고 관련 구문들이 실행된다.

댓글남기기