동시성 사용 시점
- 동시성
- 단일 프로세스를 독립적인 컴포넌트로 분리하고, 해당 컴포넌트가 안전하게 데이터를 공유하는 방법을 지정하는 컴퓨터 과학 용어
- 언제 동시성을 사용해야 하는가?
- 더 많은 동시성이 더 빠르게 만들어 주는 것은 아니다.
- 동시성은 병렬성이 아니다. → 동시성은 해결할 문제를 더 나은 구조로 만들기 위한 도구이다.
- 동시성: 동시에 실행되는것처럼 보이는 것
- 병렬성 : 실제로 동시에 여러 작업이 처리되는것
- 동시에 실행되는 것이 시간이 얼마 걸리지 않을 때도, 오히려 동시성 사용 시의 오버헤드가 더 커질 수 있다.
⇒ 독립적으로 수행할 수 있는 여러 처리로부터 데이터를 결합시키길 원한다면 동시성을 사용하자.
고루틴
- 프로세스
- 컴퓨터 운영체제에서 수행 중인 프로그램의 인스턴스
- 운영체제는 프로세스와 메모리를 연결시키고, 다른 프로세스에서 접근할 수 없도록 보장한다.
- 스레드
- 운영체제가 주어진 시간동안 수행되는 실행의 단위
- 스레드는 자원들의 접근을 통해 공유된다. CPU 코어 수에 따라 스레드 명령어를 동시에 실행할 수 있다.
- 고루틴
- Go 런타임에서 관리되는 가벼운 스레드이다.
- 프로그램이 실행되면, 단일 고루틴을 시작하고, Go 런타임 스케줄러가 자동으로 스레드들을 할당한다.
- 운영체제 레벨 자원을 생성하지 않으므로, 스레드 생성보다 빠르다.
- 고루틴의 스택 크기가 스레드의 스택 크기보다 작다. 메모리를 더 효율적으로 사용할 수 있다.
- 고루틴간의 전환은 프로세스 내에서 일어난다. 시스템 콜을 회피하여, 전환이 더 빠르다.
go func() {} ()
형태로 이루어진다.- 상태를 초기화하기 위해 파라미터를 전달할 수 있다.
- 하지만 함수에서 반환되는 모든 값은 무시된다.
- 고루틴 정리
- Go 런타임은 고루틴이 다시 사용되지 않는지 검출할 수 없다. 고루틴이 제때 종료되지 않으면, 고루틴 누수가 발생하여 프로그램이 느려질것이다.
Done
채널 패턴- 채널을 사용하여 처리를 종료해야 하는 시점을 고루틴에게 알리는 방법
package main
import (
f "fmt"
"time"
)
func main() {
f.Println("synchronization")
done := make(chan bool, 1)
go worker(done)
<-done
}
func worker(done chan bool) {
f.Println("working...")
time.Sleep(time.Second)
f.Println("done")
done <- true
}
채널
- 고루틴끼리 메세지를 전달할 수 있는 메세지큐를 채널이라 한다.
- 고루틴은 채널을 통해 통신한다.
- 초기화
ch := make(chan int)
(제로 값은 nil이다.)
- 변수에 채널 값 할당
a := <- ch
- 채널에 변수 값 할당
ch <- b
- 읽기 채널과 쓰기 채널 - 함수 인자에도 읽기 채널과 쓰기 채널로 구분할 수 있다.
- 읽기 채널 (
<-chan
) ch <-chain int
- 쓰기 채널 (
chan<-
) ch chan<- int
- 기본적으로 채널은 버퍼가 없다.
- 보내는 측과 받는 측이 동기화되어야 하며, 채널에 값을 보내면 해당 채널에서 값을 받을 때까지 블록된다.
- 적어도 두 개의 수행중인 고루틴 없이는 버퍼가 없는 채널로 읽기나 쓰기를 할 수 없다.
- 버퍼가 없는 열린 채널에 쓰기를 할 때마다 다른 고루틴에서 같은 채널을 읽을 때까지 고루틴은 일시 중지된다.
- 버퍼가 없는 열린 채널에 읽기를 하면 다른 고루틴에서 같은 채널에 쓰기를 할 때까지 고루틴은 일시 중지된다.
- 아래 예제에선
consuming
고루틴은console
채널에 값이 채워지길 기다린다. production
고루틴이 시작해 name값을 console 채널에 넘겨주면,consuming
고루틴 내부 로직이 실행된다.
var scheduler chan string
func consuming(prompt string) {
fmt.Println("consuming 호출됨")
select {
case scheduler <- prompt:
fmt.Println("이름을 입력받았습니다 : ", <-scheduler)
case <-time.After(5 * time.Second):
fmt.Println("시간이 지났습니다.")
}
}
func producing(console chan string) {
var name string
fmt.Print("이름:")
fmt.Scanln(&name)
console <- name
}
func main() {
console := make(chan string, 1)
scheduler = make(chan string, 1)
go func() {
consuming(<-console)
}()
go producing(console)
time.Sleep(100 * time.Second)
}
이름:tom
consuming 호출됨
이름을 입력받았습니다 : tom
- 메인루틴에서 1을 보내며 수신자를 기다리고 있는데, 수신자 고루틴이 없기 때문.
package main
import "fmt"
func main() {
c := make(chan int)
c <- 1
fmt.Println(<-c)
}
// fatal error: all goroutines are asleep - deadlock!
- 다음과 같이 버퍼가 있는 채널을 만들 수 있다.
ch := make(int, 10)
- 버퍼가 있는 채널은 블로킹 없이 제한된 쓰기의 버퍼를 가진다.
- 버퍼가 다 채워지면, 채널이 읽어질때까지 쓰기 고루틴은 일시 중지된다.
- 대부분의 경우에는 버퍼가 없는 채널을 사용한다.
- 얼마나 많은 고루틴이 실행될 지 알고있을 때, 실행, 대기 중인 고루틴의 개수를 제한하려는 경우 사용한다.
채널 for-range
- for-range 루프를 이용해 채널의 값을 읽을 수 있다.
- 채널이 닫히거나, break, return문에 도달할 때까지 루프가 지속된다.
for v := range ch {
fmt.Println(v)
}
채널 close
- 채널에 쓰기를 완료했을 때
close(chan)
를 통해 채널을 닫을 수 있다. - 이미 닫은 채널에 쓰기를 시도하거나
close()
를 시도하면 패닉이 발생된다. - 하지만 읽기를 시도하는 것은 언제나 성공한다. 채널의 제로 값을 반환한다.
- 콤마 ok 구문으로 채널의 닫힘 유무를 확인할 수 있다.
v, ok := <-ch
- 채널을 닫는 책임은 채널에 쓰기를 하는 고루틴에 있다.
- 채널 닫기는 해당 채널이 닫혀지기를 기다리는 고루틴이 있는 경우에만 필요하다.
- Go 런타임이 해당 채널이 더 이상 사용하지 않다고 확인하면, GC로 정리한다.
select 문
- select문은 동시성 모델을 구분하는데 주로 사용된다.
- 여러 채널들 중 하나에 읽기를 하거나 쓰기를 할 수 있는 고루틴을 허용한다.
peerChan := make(chan *model.Peer)
copyErrChan := make(chan error)
createErrChan := make(chan error)
fileChan := make(chan string)
// 로직을 수행하과 결과를 channel에 전달
go createPeer(fileChan, createErrChan, copyErrChan)
// select문으로 여러 채널들 중의 결과를 수행할 수 있다.
select {
case tempFileName := <-fileChan:
newPeer := <-peerChan
checkPeerDeployment(peerUser, request, tempFileName, newPeer, peerRepo, nodePortRepo, tokenUser)
return
case copyErr := <-copyErrChan:
log.Errorf("Error copying certificates: %s", copyErr)
case createErr := <-createErrChan:
log.Errorf("Error creating peer: %s", createErr.Error())
case <-time.After(time.Minute * 2):
log.Error("Error due to timeout for creating peer")
}
- select문은 case에 걸리는 채널이 여러개라면, 그 중 채널을
임의로
선택한다. - 임의로 선택하기 때문에, 기아 상태와 교착 상태에 빠지지 않는다.
for-select 루프
- select을 for 루프에 임베딩시키는것, 여러 채널을 처리하는 일반적인 방법이다.
- 반드시 루프를 빠져나가는 방법을 포함시켜야 한다.
Chan1 := make(chan string)
Chan2 := make(chan string)
Chanerr := make(chan error)
go doSomeThing(Chan1,Chan2, Chanerr)
// for {
select {
case Chan1Result := <-Chan1:
// do something with result of chan1
case Chan2Result := <-Chan2:
// do something with result of chan2
case caseErr := <-Chanerr:
// err handling
case <-time.After(time.Minute * 2):
// timeout err return
}
// }
WaitGroup
- 고루틴이 실행되는 동안 다른 고루틴이 모두 종료될때까지 기다리는데 사용되는 동기화 기능
- 여러 고루틴의 작업 완료를 기다려야 하는 경우 사용한다.
- 단일 고루틴을 기달리 때는
Done 채널
패턴을 사용한다.
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
wg := new(sync.WaitGroup)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
fmt.Println(n)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("the end")
}
sync.WaitGroup
func (wg *WaitGroup) Add(delta int)
- 대기 그룹에 고루틴 개수 추가, 대기 카운터를 인자만큼 증가시킨다.
- 고루틴 생성 시
Add
함수로 고루틴 개수를 추가해준다. func (wg *WaitGroup) Done()
- 고루틴이 끝났다는 것을 알려줄 때 사용, 대기 카운터를 1 감소시킨다.
Add(5)
라면,Done()
이 5번 호출되어야 한다. 그렇지 않으면 패닉func (wg *WaitGroup) Wait()
- 모든 고루틴이 끝날 때까지 기다림
뮤텍스를 사용하는 경우
- 뮤텍스는 공유 데이터의 접근이나 어떤 코드의 동시 실행을 제한한다.
sync.Mutex
를 사용한다.Mutex.Lock()
Mutex.Unlock()
- 다음 예시를 보자
var wg sync.WaitGroup
var mu sync.Mutex
func main() {
var sharedValue int
for i := 0; i < 5; i++ {
wg.Add(1)
go increment(&sharedValue)
}
wg.Wait()
fmt.Println("Final value:", sharedValue)
}
func increment(value *int) {
defer wg.Done()
newValue := *value + 1
time.Sleep(time.Second) // 작업 시뮬레이션
*value = newValue
}
Final value: 1
func increment(value *int) {
defer wg.Done()
fmt.Println("Lock의 범위가 아닌 곳은 병렬처리")
mu.Lock()
defer mu.Unlock()
newValue := *value + 1
fmt.Println("여기는 순차적인 직렬처리")
time.Sleep(time.Second) // 작업 시뮬레이션
*value = newValue
}
Lock의 범위가 아닌 곳은 병렬처리
여기는 순차적인 직렬처리
Lock의 범위가 아닌 곳은 병렬처리
Lock의 범위가 아닌 곳은 병렬처리
Lock의 범위가 아닌 곳은 병렬처리
Lock의 범위가 아닌 곳은 병렬처리
여기는 순차적인 직렬처리
여기는 순차적인 직렬처리
여기는 순차적인 직렬처리
여기는 순차적인 직렬처리
Final value: 5
Share article