2024.04.23
go
키워드를 둠으로써 시작한다. 다른 함수 들과 같이 상태를 초기화하기 위해 파라미터를 전달할 수도 있다. 하지만 함수에서 반환되는 모든 값은 무시한다.func process(val int) int {
// val 변수로 뭔가를 처리
}
func runThingConcurrently( in <-chan int, out chan<- int) {
go func() {
for val := range in {
result := process(val)
out <- result
}
}()
}
make
함수를 사용하여 생성할 수 있는 내장 타입이다.ch := make(chan int)
<-
연산자를 사용하여 채널과 상호작용한다. 채널 변수의 왼쪽에 <-
연산자를 두어 채널로부터 데이터를 읽고 오른쪽에 두어 채널에 데이터를 쓴다.a := <-ch // ch에서 값을 읽어 a에 할당한다.
ch <- b // b의 값을 ch에 쓴다.
chan
키워드(ch ←chan int
) 앞에 화살표 연산을 사용하자.chan
키워드(ch chan<- int
)뒤에 화살표를 사용하자.ch := make(chan int, 10)
채널을 생성할 때, 버퍼의 수용력을 지정할 수 있다.for v := range ch {
fmt.Println(v)
}
for-range
루프와 다르게 채널을 위해 선언된 값을 가지는 단일 변수만 있다.close
내장 함수를 이용해 채널을 닫을 수 있다.close(ch)
v, ok := <-ch
// ok가 true면 채널은 열린 것, false라면 닫힌 것
버퍼가 없고, 열림 | 버퍼가 없고, 닫힘 | 버퍼가 있고, 열림 | 버퍼가 있고, 닫힘 | |
---|---|---|---|---|
읽기 | 뭔가 써질 때까지 일시 중지 | 제로 값 반환 (닫힘 확인 위해 콤마 OK 관용구 사용) | 버퍼가 빌 때까지 일시 중지 | 버퍼에 남은 값을 반환 버퍼가 비어 있다면 제로 값을 반환 (닫힘 확인 위해 콤마 OK 관용구 사용) |
쓰기 | 뭔가 읽을 때까지 일시 중지 | 패닉 | 버퍼가 가득 찰 때까지 일시 중지 | 패닉 |
닫기 | 동작함 | 패닉 | 동작함, 남은 값이 있을 수 있음 | 패닉 |
select
문은 동시성 모델을 구분하는 다른 하나이다. Go에서 동시성을 위한 제어 구조이고, 일반적인 문제를 훌륭하게 해결한다.select
키워드는 여러 채널의 세트 중 하나에 읽기를 하거나 쓰기를 할 수 있는 고루틴을 허용한다.select {
case v := <-ch:
fmt.Println(v)
case v := <-ch2:
fmt.Println(v)
case ch3 <- x:
fmt.Println("wrote", x)
case <-ch4:
fmt.Println("got value on ch4, but ignored it")
}
switch
문과 비슷하게 select
내에 case
는 자신만의 블록을 생성한다.select
알고리즘은 단순하다. 진행이 가능한 여러 case
중 하나를 임의로 선택한다. 순서는 중요하지 않다.case
보다 선호되는 것이 없고 모두 동시에 확인되기 때문에 기아 문제를 깔끔하게 해결한다.기아(starvation)는 자원을 제대로 분배하지 못해 발생하는 문제이다.
위에서는 모든 case 문이 동일한 우선 순위로 확인이 되기 때문에, 특정 case만 계속 처리되는 경우는 발생하지 않는다는 의미이다.
select
임의 선택의 다른 장점은 교착 상태의 가장 일반적인 원인 중 하나인 일관성 없는 순서로 잠금 획득 방지이다. 동일한 두 개의 채널을 접근하는 두 개의 고루틴이 있다면, 두 고루틴내에서 같은 순서로 반드시 접근해야 교착 상태(deadlock)에 빠지지 않는다.func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
v := 1
ch1 <- v
v2 := <-ch2
fmt.Println(v, v2)
}()
v := 2
ch2 <- v
v2 := <-ch1
fmt.Println(v, v2)
}
// 위 코드를 실행하면, 다음과 같은 메시지를 볼 수 있다.
// fatal error: all goroutines are asleep - deadlock!
// 고루틴은 ch1을 읽을 때까지 계속 진행할 수 없고
// main 고루틴은 ch2를 읽을 때까지 진행할 수 없다.
select
로 채널을 접근하도록 하여 교착 상태를 피한 고루틴 예제func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
v := 1
ch1 <- v
v2 := <-ch2
fmt.Println(v, v2)
}()
v := 2
var v2 int
select {
case ch2 <- v:
case v2 = <-ch1:
}
fmt.Println(v, v2)
}
// 아래와 같이 출력됨
// 2 1
// select가 진행할 수 있는 case를 확인하기 때문에, 교착 상태를 피할 수 있다.
// ch1에 1의 값을 쓰는 것을 고루틴에서 진행했고
// main 고루틴에서 ch1을 읽어 v2 변수에 넣는 것도 정상적으로 처리됨.
switch
문과 마찬가지로 select
문은 default
절을 가질 수 있다. switch
와 마찬가지로 default
는 읽고 쓰기를 할 수 있는 채널이 어떤 case
에도 없는 경우에 선택된다.default
와 함께 select
를 사용하자.select {
case v:= <-ch:
fmt.Println("read from ch:", v)
default
fmt.Println("no value written to ch")
}
‘메모리를 공유하여 통신하지 말고, 통신을 통해 메모리를 공유하자’
// 예제로 멀티플레이어 게임을 위한 인메모리 득점판이 있다고 가정할 때
func scoreboardManager(in <-chan func(map[string]int), done <-chan struct{}) {
scoreboard := map[string]int{}
for {
select {
case <-done:
return
case f := <-int:
f(scoreboard)
}
}
}
// 득점판으로부터 현재 값을 읽는 것은 번거롭고 한 번에 하나의 읽기만 허용한다.
func (csm ChannelScoreboardManager) Read(name string) (int, bool) {
var out int
var ok bool
done := make(chan sturct{})
csm <- func(m map[string]int) {
out, ok = m[name]
close(done)
}
<-done
return out, ok
}
// 더 나은 접근은 뮤텍스를 사용하는 것이다.
// 표준 라이브러리의 sync 패키지의 RWMutex를 사용한 예시
// 읽기 잠금은 공유되어, 한 번에 임계 영역 내에 여러 독자가 있을 수 있다.
func (msm *MutexScoreboardManager) Read(name string) (int, bool) {
msm.l.RLock()
defer msm.l.RUnlock()
val, ok := msm.scoreboard[name]
return val, ok
}
sync/atomic
패키지는 단일 레지스터에 맞는 값을 추가, 교환, 로드, 저장 혹은 비교 및 교환(Compare and Swap)하기 위한 최신 CPU에 내장된 원자 변수(atomic variable) 연산에 대한 접근을 제공한다.