이 글은 골든래빗 《Tucker의 Go 언어 프로그래밍》의 24장 써머리입니다.
고루틴 - 경량 스레드로 함수나 명령을 동시에 실행할 때 사용
main()
또한 고루틴에 의해 실행된다.
프로세스
스레드
원래 CPU 코어는 한 번에 한 명령밖에 수행할 수 없지만, CPU 코어가 스레드를 빠르게 전환해가면서 수행하면 사용자 입장에서는 마치 동시에 수행하는 것처럼 보인다.
context switching
- CPU 코어가 여러 스레드를 전환하면서 수행하면 더 많은 비용이 드는 것을 말함.main() 또한 goroutine
이기 때문에 모든 Golang 프로그램은 고루틴을 최소한 하나는 가지고 있다.
main()는 메인 루틴이고, 메인 루틴이 종료되면 프로그램 또한 다른 루틴이 실행중이더라도 종료하게 된다.
고루틴을 사용하는 방법은 go func_name()
이다.
func Test_WaitGroup(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
t.Log("Go Routine")
time.Sleep(5 * time.Second)
}()
wg.Wait()
t.Log("Main Routine Done")
}
코어가 두개일 때, 한 고루틴이 남는데, 세 번째 고루틴용 스레드를 생성하지 않고, 남는 코어가 생길 때까지 대기한다. 즉 세 번째 고루틴은 남는 코어가 생길때 까지 실행되지 않고 멈춰있는다.
두 번째 고루틴이 모두 실행 완료되면 코루틴2는 사라지고 코어2가 비게 된다. 그 때 대기하던 고루틴 3이 실행된다.
시스템 콜 - 운영체제가 지원하는 서비스를 호출할 때를 말한다.
시스템 콜을 호출하면 운영체제에서 해당 서비스가 완료될 때까지 대기해야 한다.
대표적으로 네트워크 기능 등이 있다.
대기 상태인 고루틴에 CPU코어와 OS 스레드를 할당하면 CPU 자원 낭비가 발생한다.
Golang은 이런 상태에 들어간 루틴을 대기 상태로 보내고, 실행을 기다리는 다른 루틴에 CPU코어와 OS스레드를 할당하여 실행할 수 있게 한다.
코어가 스레드를 변경하지 않기 때문에 컨텍스트 스위칭 비용이 발생하지 않는다.
type Account struct {
Balance int
}
func DepositAndWithdraw(account *Account) {
if account.Balance < 0 {
log.Println("Balance should not be negative value")
panic(fmt.Sprintf("Balance should not be negative value: %v", account.Balance))
}
account.Balance += 1000
time.Sleep(time.Millisecond)
account.Balance -= 1000
}
func Test_concurrentTest(t *testing.T) {
account := &Account{Balance: 0}
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for {
DepositAndWithdraw(account)
}
}()
t.Log(account.Balance)
}
wg.Wait()
t.Log("Main Routine Done")
}
account.Balance += 1000
에 문제가 있는데, Balance에서 값을 읽 어 1000을 더해 Balance에 다시 저장하는 단계이다. 첫 번째 단계가 완료되기 전에 다른 고루틴이 첫 번째 단계를 수행하면 고루틴은 똑같은 값을 읽어 1000씩 더해 다시 Balance에 저장하는데, 다회의 고루틴이 한 번 입금한 효과밖에 나지 않아 - 잔고가 될 수 있다.var mutex sync.Mutex
type Account struct {
Balance int
}
func DepositAndWithdraw(account *Account) {
mutex.Lock()
defer mutex.Unlock()
if account.Balance < 0 {
log.Println("Balance should not be negative value")
panic(fmt.Sprintf("Balance should not be negative value: %v", account.Balance))
}
account.Balance += 1000
time.Sleep(time.Millisecond)
account.Balance -= 1000
}
func Test_concurrentTest(t *testing.T) {
account := &Account{Balance: 0}
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for {
DepositAndWithdraw(account)
}
}()
t.Log(account.Balance)
}
wg.Wait()
t.Log("Main Routine Done")
}
// 데드락 발생 예시
func diningProblem(name string, first, second *sync.Mutex) {
for i := 0; i < 100; i++ {
first.Lock()
second.Lock()
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
log.Println(name, "dining")
first.Unlock()
second.Unlock()
}
}
func Test_DeadLockWithMutex(t *testing.T) {
var wg sync.WaitGroup
rand.Seed(time.Now().UnixNano())
wg.Add(2)
fork := &sync.Mutex{}
spoon := &sync.Mutex{}
go diningProblem("A", fork, spoon)
go diningProblem("B", spoon, fork)
wg.Wait()
}
func Test_GoroutinePartitionByArea(t *testing.T) {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func(i int) {
defer wg.Done()
t.Log(i)
}(i)
}
wg.Wait()
}