[GO] #2-6. 고랭 기본문법 (고루틴, 채널)

Study·2021년 5월 19일
0

고랭

목록 보기
7/18
post-thumbnail

고루틴

gorutine 은 GO 런타임에 의해 관리되는 경량 쓰레드 이다.

go f(x, y, z)

새로운 gorutine 을 시작한다.

f(x, y, z)

fx, y, z 의 평가는 현재의 gorutine 에서 일어나고, f 의 실행은 새로운 gorutine 에서 일어난다.

gorutine 은 같은 주소의 공간에서 실행되고, 공유된 메모리는 synchronous(동기적) 해야한다.

GO 에서 다른 기본형들이 존재하는 것처럼 sync 관련 기능이 필요 없더라도 sync 패키지는 유용한 기본형을 제공한다.

이제 고루틴를 실행해보자.

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world")
	say("hello")
}

채널

Channel 은 채널 연산자인 <- 을 통해 값을 주고 받을 수 있는 하나의 분리된 통로이다.

ch <- v    // 채널 ch에 v를 전송한다.
v := <-ch  // ch로 부터 값을 받고,
           // 값을 v에 대입한다.
           
// 데이터는 화살표 방향대로 흐른다.

channelmapslice 처럼 사용하기 전에 생성되어야만 한다.

ch := make(chan int)

기본적으로 전송/수신은 다른 한 쪽이 준비될 때까지 block 상태이다.

이는 명시적인 lock 이나 조건 변수 없이 고루틴이 synchronous 하게 작업될 수 있도록 한다.

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c

	fmt.Println(x, y, x+y)
}

위 예제는 두 고루틴을 분산시키면서 slice 의 숫자들을 더한다.

두 고루틴이 연산을 완료하면, 최종 결과를 계산한다.

버퍼가 있는 채널

채널은 버퍼를 가질 수 있다. (buffered channel)

buffered channel 을 초기화하기 위해 make 에 두 번째 인자로 버퍼 길이를 제공하자.

ch := make(chan int, 100)

buffered channel 로의 전송은 그 버퍼 의 사이즈가 꽉 찼을 때에만 블록 된다.

버퍼 로부터의 수신은 그 버퍼 가 비어있을 때 블록 된다.

버퍼가 초과되도록 예제를 수정해보고 어떻게 발생하는지 확인해보자.

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

// 결과 1 \n 2

ch <- 3 을 추가하면 다음과 같은 결과가 나타난다.

fatal error: all goroutines are asleep - deadlock!

Range 와 Close

전송자는 더 이상 보낼 데이터가 없다는 것을 암시하기 위해 channel 을 close 할 수 있다.

수신자는 수신에 대한 표현에 두 번째 매개변수를 할당함으로써 채널이 닫혔는지 테스트할 수 있다.

v, ok := <- ch

만약 더 수신할 값이 없고, channel 이 닫혀 있다면 okfalse 이다.

for i := range c 반복문은 channel 이 닫힐 때까지 반복하여 수신한다.

주의할 점으로는 수신자가 아닌 전송자만이 channel 을 닫아야한다.
그리고 닫힌 chennel 에 전송하는 것은 panic 을 야기한다.

추가적으로 channel 은 파일과 다르며 file 과 달리 보통 channel 은 닫을 필요가 없다.

channel 을 닫는 것은 range 반복문을 종료시키는 것과 같이 수신자가 더 이상 들어올 값이 없다는 것을 알 경우에만 필요하다.

Select

select 는 고루틴이 다중 커뮤니케이션 연산에서 대기할 수 있게 한다.

selectcase 들 중 하나가 실행될 때까지 블록된다.
그리고 select 문은 해당하는 case 를 수행한다.

만약 다수의 case 가 준비되는 경우엔 select 가 무작위로 하나를 선택한다.

Default Selection

select 에서의 default case 는 다른 case 들이 모두 준비되지 않았을 때 실행된다.

블록 없이 전송이나 수신을 수행하도록 default case 를 사용해보자.

func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}

실행 결과

    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
BOOM!

뮤텍스

채널이 고루틴 간에 커뮤니케이션에서 얼마나 훌륭한지 알아보았다.

근데 커뮤니케이션이 필요없다면 어떨까?

충돌을 파하기 위해 단순히 하나의 고루틴만이 어느 순간에 어떤 변수에 접근할 수 있도록 하고 싶다면 어떻게 할까?

이러한 개념은 mutual exclusion 이라 불리고, 자료 구조에서 그것의 관습적인 이름은 mutex 이다.

GO 의 표준 라이브러리는 sync.Mutex 와 두 가지 method 를 통해 mutual exclusion 을 제공한다.

Lock
Unlock

다음 예제를 살펴보자.

// SafeCounter 는 동시성에서 안전함
type SafeCounter struct {
	mu sync.Mutex
	v  map[string]int
}

// Inc 는 주어진 키로 카운터를 증가시킴
func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()
	// 하나의 고루틴만 map c.v 에 접근하도록 잠금
	c.v[key]++
	c.mu.Unlock()
}

// Valye 는 주어진 키로 카운터 값을 반환
func (c *SafeCounter) Value(key string) int {
	c.mu.Lock()
	// 하나의 고루틴만 map c.v 에 접근하도록 잠금
	defer c.mu.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}

위 예제에서 Inc 함수에서 보여지듯 코드 블럭을 LockUnlock 호출로 감쌈으로써 mutual exclusion 속에서 수행될 코드 블럭을 정의할 수 있다.

추가적으로 Value 함수에서 mutex 가 unlocked 될 것을 확실히 하기 위해 defer 을 사용할 수도 있다.

profile
Study

0개의 댓글