[Tucker의 Go 언어 프로그래밍] 24장 고루틴과 동시성 프로그래밍

Coen·2023년 11월 12일
1

tucker-go

목록 보기
17/18
post-thumbnail

이 글은 골든래빗 《Tucker의 Go 언어 프로그래밍》의 24장 써머리입니다.

24 고루틴과 동시성 프로그래밍

24.1 스레드란?

  • 고루틴 - 경량 스레드로 함수나 명령을 동시에 실행할 때 사용

  • main() 또한 고루틴에 의해 실행된다.

  • 프로세스

    • 메모리 공간에 로딩되어 동작하는 프로그램
    • 한 개 이상의 스레드를 가지고 있음
    • 스레드가 한개면 싱글 스레드 프로세스, 여럿이면 멀티 스레드 프로세스 라고 한다.
  • 스레드

    • 프로세스 안의 세부 작업 단위
    • 실행 흐름이라고 볼 수 있음
  • 원래 CPU 코어는 한 번에 한 명령밖에 수행할 수 없지만, CPU 코어가 스레드를 빠르게 전환해가면서 수행하면 사용자 입장에서는 마치 동시에 수행하는 것처럼 보인다.

24.1.1 컨텍스트 스위칭 비용

  • context switching - CPU 코어가 여러 스레드를 전환하면서 수행하면 더 많은 비용이 드는 것을 말함.
  • 스레드 전환에는 현재상태인 명령 포인터(instruction pointer)와 스택 메모리 등의 정보를 보관해야하는데, 이를 스레트 컨텍스트라고 한다.
  • 스레드 전환시 마다 스레트 컨텍스트를 저장하고 복원하기 때문에 전환 비용이 들게 된다.
  • Go는 CPU 코어마다 OS 스레드를 하나만 할당해 사용하기 때문에 컨텍스트 스위칭 비용이 거의 발생하지 않는다.

24.2 고루틴 사용

  • main() 또한 goroutine이기 때문에 모든 Golang 프로그램은 고루틴을 최소한 하나는 가지고 있다.

  • main()는 메인 루틴이고, 메인 루틴이 종료되면 프로그램 또한 다른 루틴이 실행중이더라도 종료하게 된다.

  • 고루틴을 사용하는 방법은 go func_name()이다.

24.2.1 서브 고루틴이 종료될 때까지 기다리기

  • 고루틴이 종료될 때까지 대기할 수 있는 방법은 sync 패키지의 WaitGroup객체를 사용하면 된다.
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")
}

24.3 고루틴의 동작 방법

  • 고루틴은 명령을 수행하는 단일 흐름으로 OS 스레드를 이용하는 경량 스레드이다.
  • Golang은 CPU 코어, OS 스레드, 고루틴을 서로 조율하여 사용해 고루틴을 효율적으로 다룬다.

24.3.1 고루틴이 하나일 때

  • main()루틴만 존재하면 OS 스레드를 하나 만들어 첫 번째 코어와 연결한다. 그리고 OS 스레드에서 고루틴을 실행한다.

24.3.2 고루틴이 두 개일 때

  • 두 번째 고루틴이 생성되고 첫 번째 코어가 첫 번째 고루틴을 실행하고 있지만 두 번째 코어가 남아 있기 때문에 두 번째 OS 스레드를 생성하여 두 번째 고루틴을 실행할 수 있다.

24.3.3 고루틴이 세 개일 때

  • 코어가 두개일 때, 한 고루틴이 남는데, 세 번째 고루틴용 스레드를 생성하지 않고, 남는 코어가 생길 때까지 대기한다. 즉 세 번째 고루틴은 남는 코어가 생길때 까지 실행되지 않고 멈춰있는다.

  • 두 번째 고루틴이 모두 실행 완료되면 코루틴2는 사라지고 코어2가 비게 된다. 그 때 대기하던 고루틴 3이 실행된다.

24.3.4 시스템 콜 호출 시

  • 시스템 콜 - 운영체제가 지원하는 서비스를 호출할 때를 말한다.

  • 시스템 콜을 호출하면 운영체제에서 해당 서비스가 완료될 때까지 대기해야 한다.

  • 대표적으로 네트워크 기능 등이 있다.

  • 대기 상태인 고루틴에 CPU코어와 OS 스레드를 할당하면 CPU 자원 낭비가 발생한다.

  • Golang은 이런 상태에 들어간 루틴을 대기 상태로 보내고, 실행을 기다리는 다른 루틴에 CPU코어와 OS스레드를 할당하여 실행할 수 있게 한다.

  • 코어가 스레드를 변경하지 않기 때문에 컨텍스트 스위칭 비용이 발생하지 않는다.

24.4 동시성 프로그래밍 주의점

  • 동일한 메모리 자원에 여러 고루틴이 접근하면 문제가 생길 수 있다.
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에 저장하는데, 다회의 고루틴이 한 번 입금한 효과밖에 나지 않아 - 잔고가 될 수 있다.

24.5 뮤텍스를 이용한 동시성 문제 해결

  • 가장 단순한 해결 방법은 한 고루틴에서 값을 변경할 때 다른 고루틴이 건들지 못하게 하는 것이다.
  • 뮤텍스는 mutual exclusion(상호 배제)의 약자이다.
  • 뮤텍스의 Lock() 메서드를 호출해 뮤텍스를 획득할 수 있다. 사용 중이던 뮤텍스는 Unlock() 메서드를 호출해 반납한다.
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")
}

24.6 뮤텍스와 데드락

  • 뮤텍스로 동시성 프로그래밍 문제를 해결할 수 있지만 또 다른 문제가 발생할 수 있다.
    1. 동시성 프로그래밍으로 얻는 성능 향상을 얻을 수 없다.
    2. 데드락 발생 가능
// 데드락 발생 예시
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()
}

24.7 또 다른 자원 관리 기법

  • 모든 문제는 같은 자원을 여러 고루틴이 접근하기 때문에 발생한다. 각 고루틴이 같은 자원에 접근하지 않으면 애당초 문제가 발생하지 않는다.
  • 각 고루틴이 서로 다른 자원에 접근하게 만드는 두 가지 방법
  1. 영역을 나눈다.
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()
}
  1. 역할을 나눈다.
    • 밑그림, 배경, 배경 스케치, 채색 등 각자 맡아 수행한다면 작업자간 간섭 없이 그림 완성이 가능하다.
profile
백엔드 프로그래머

0개의 댓글