gorutine
은 GO 런타임에 의해 관리되는 경량 쓰레드 이다.
go f(x, y, z)
새로운 gorutine
을 시작한다.
f(x, y, z)
f
와 x
, 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에 대입한다.
// 데이터는 화살표 방향대로 흐른다.
channel
은 map
과 slice
처럼 사용하기 전에 생성되어야만 한다.
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!
전송자는 더 이상 보낼 데이터가 없다는 것을 암시하기 위해 channel 을 close 할 수 있다.
수신자는 수신에 대한 표현에 두 번째 매개변수를 할당함으로써 채널이 닫혔는지 테스트할 수 있다.
v, ok := <- ch
만약 더 수신할 값이 없고, channel 이 닫혀 있다면 ok
는 false
이다.
for i := range c
반복문은 channel 이 닫힐 때까지 반복하여 수신한다.
주의할 점으로는 수신자가 아닌 전송자만이 channel 을 닫아야한다.
그리고 닫힌 chennel 에 전송하는 것은 panic
을 야기한다.
추가적으로 channel 은 파일과 다르며 file
과 달리 보통 channel 은 닫을 필요가 없다.
channel 을 닫는 것은 range
반복문을 종료시키는 것과 같이 수신자가 더 이상 들어올 값이 없다는 것을 알 경우에만 필요하다.
select
는 고루틴이 다중 커뮤니케이션 연산에서 대기할 수 있게 한다.
select
는 case
들 중 하나가 실행될 때까지 블록된다.
그리고 select 문은 해당하는 case 를 수행한다.
만약 다수의 case 가 준비되는 경우엔 select 가 무작위로 하나를 선택한다.
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
함수에서 보여지듯 코드 블럭을 Lock
과 Unlock
호출로 감쌈으로써 mutual exclusion 속에서 수행될 코드 블럭을 정의할 수 있다.
추가적으로 Value
함수에서 mutex
가 unlocked 될 것을 확실히 하기 위해 defer
을 사용할 수도 있다.