[Go 동시성 프로그래밍] 읽기 - 1장 동시성 소개

smpl published on
4 min, 749 words

Go 동시성 프로그래밍 책의 1장 동시성 소개 챕터를 읽고, 읽은 내용을 정리해봅니다.

무어의 법칙, 웹 스케일, 그리고 혼란

암달의 법칙 : 문제의 해결책을 병렬로 구성하면 어느 정도 성능이 향상될지 모델링하는 방법을 설명. 전체 수행 시간의 개선 효과는 병렬화가 불가능한 작업들의 비중에 크게 영향을 받게 된다.

지나치게 병렬적(embarrassingly parallel)인 문제는, 수평적(horizontally)으로 확장하여 실행시간을 향상시킬 수 있다.

클라우드 컴퓨팅의 도전과제 : 자원을 프로비저닝(provisioning)하고, 장비 인스턴스 간에 통신하고, 결과를 집계하고, 저장하는 문제를 모두 해결해야 함.

동시성이 어려운 이유

레이스 컨디션

둘 이상의 작업이 순서가 유지되는 것이 보장되지 않을 때. 이를 디버깅하기 위해서는, 언제나 논리의 정확성을 추구해야 한다.

원자성

컨텍스트 내에서 나누어지거나 중단되지 않음을 의미.

연산의 원자성은 현재 정의된 범위(scope)에 따라 달라질 수 있다.

연산이 원자적인 것으로 간주해야 할 컨텍스트 또는 범위를 정의하는 것이 중요하다. (연산을 원자적으로 만드는 것은 사용자가 어떤 컨텍스트에서 원자성을 얻고자 하는지에 달려있다.)

메모리 접근 동기화

임계 영역(critical section) : 메모리 영역 혹은 공유 리소스에 독점적으로 접근해야 하는 영역

임계 영역을 다루기 위해서는 개발자가 규칙(convention)을 따라야 한다.

임계 영역으로 접근을 동기화해도 데이터레이스나 논리적인 정확성은 해결하지 못 한다.

메모리 접근을 동기화하는 것은 성능에도 영향을 미친다.

예를 들어, Lock 호출 관련해서는, "임계 영역에 빈번하게 들어갔다 나오는가?", "임계 영역은 어느 정도 크기여야 하는가?" 등의 질문이 있을 수 있고, 이에 대한 정답은 없으며 art의 영역에 가깝다.

데드락

데드락 상태가 되면 외부 개입 없이는 결코 프로그램을 복구할 수 없다.

역설 : 논리적으로 완벽한 데드락에는 정확한 동기화가 필요하다.

에드가 코프먼(Edgar Coffman)의 데드락이 발생하기 위한 조건 4가지 (1971)

  • 상호 배제 (Mutual Exclusion) : 동시에 실행되는 프로세스가 어떤 임의의 시점에 하나의 리소스에 대한 배타적 권리를 보유한다.
  • 대기 조건 (Wait For Condition) : 동시에 실행되는 프로세스는 하나의 리소스를 보유하고 있는 동시에 또다른 추가 리소스를 기다리고 있다.
  • 비선점 (No Preemption) : 동시에 실행되는 프로세스 중 하나를 보유하고 있는 리소스는 해당 프로세스에서만 사용 해제(release) 될 수 있으므로 이 조건도 만족한다.
  • 순환 대기 (Circular Wait) : 동시에 실행되는 프로세스 중 하나(P1)가 다른 동시 프로세스(P2)로 이어지는 체인에서 기다려야 하며, P2는 최종적으로 P1을 기다린다.

이 조건들 중 하나라도 참이 아니라면 데드락이 발생하는 것을 막을 수 있다.

라이브락

프로그램들이 활동적으로 동시에 연산을 수행하고는 있지만 이 연산들이 실제로 프로그램의 상태를 진행시키는 데 아무런 영향을 주지 못하는 의미없는 연산 상태.

라이브락은 기아 상태(starvation)라는 더 큰 문제의 부분집합이다.

동시에 실행되는 모든 프로세스가 동일하게 리소스를 얻지 못하여 아무 것도 진행되지 않는 상황. (기아 상태와의 차이점..)

기아 상태

어떤 동시 프로세스가 작업을 수행하는데 필요한 모든 리소스를 얻을 수 없는 모든 상황.

기아 상태를 확인하고 해결하기 위해서는 계측(metric) - 기록과 표본 계측(sampling metric)이 필요하다. 로깅을 통해 작업 속도가 충분한지 판단하는 것이다.

기아 상태가 Go 프로세스의 외부에서 초래되는 경우 - CPU, 메모리, 파일 핸들, 데이터베이스 연결 등으로부터 영향을 받는 경우 - 도 고려해야 한다.

성능 균형 : 애플리케이션의 성능을 조정해야 할 경우, 우선 임계영역에서만 메모리 접근을 동기화하도록 제한할 것을 강력하게 추천한다. 동기화가 성능에 문제를 일으키는 경우 언제든 범위를 확장할 수 있다. 하지만 반대 방향으로 이동하는 것은 훨씬 어렵다.

동시실행 안전 판단

모든 문제의 근간은 바로 사람이다.

문제 공간을 합리적으로 모델링하고 동시성을 함께 고려해서 알맞은 수준의 추상화 지점을 찾는 것이 필요하다.

호출자들에게 동시성을 어떤 식으로 노출할 것인가? 쉽게 사용하고 수정할 수 있는 솔루션을 만들기 위해 어떤 기법들을 사용하는가? 이 문제에서 적절한 동시성 수준은 무엇인가? 이 질문들은 예술적인 기교의 영역이다.

// 예시 함수 시그니처의 변화

// #1
func CalculatePi(begin, end int64, pi *Pi)

// #2
// CalculatePi 함수는 시작과 끝 사이의 파이 자릿수를 계산한다.
// 내부적으로, CalculatePi는 CalculatePi를 재귀호출하는 FLOOR((end-begin)/2)개의 동시 프로세스를 생성할 것이다.
// pi 변수에 쓰는 작업에 대한 동기화는 Pi 구조체 내부에서 처리한다.
func CalculatePi(begin, end int64, pi *Pi)

// #3
func CalculatePi(begin, end int64) []uint

// #4
func CalculatePi(begin, end int64) <-chan uint

#1의 함수 시그너처를 볼 경우, 다음과 같은 의문이 들게 되고,

  • 이 함수를 사용해 어떻게 동시에 수행할 것인가?
  • 이 함수를 동시에 여러번 호출하는 것은 누가 담당하는가?
  • 함수의 모든 인스턴스가 내가 전달한 주소를 가진 Pi 인스턴스에서 직접 동작할 것이다. 해당 메모리에 대한 접근을 동기화할 책임이 나에게 있는가, 아니면 Pi 타입이 이를 처리하는가?

이를 #2처럼 주석으로 해결할 수 있다. 이 주석은 다음과 같은 부분들에 대한 해답을 준다.

  • 누가 동시성을 책임지는가?
  • 문제 공간은 동시성 기본 요소에 어떻게 맵핑되는가?
  • 동기화는 누가 담당하는가?

그러나, 함수 시그니처 자체가 모호함을 갖고 있는 부분이 잘못된 모델링이라고 보고, functional approach를 도입해서 side effect를 없애고 (#3), 동시성에 대한 부분을 인터페이스로 드러냄으로써 (#4) 모호함을 없앨 수 있다.

복잡성 속의 단순함

go 런타임은 다음과 같은 부분들로 동시성의 기반을 제공하고, 편의를 제공한다.

  • 가비지 컬렉터
  • 동시 연산을 OS의 쓰레드로 multiplexing하는 일의 자동화.(고루틴) 쓰레드를 시작하고 관리하며 프로그램의 논리를 사용 가능한 쓰레드에 균등하게 맵핑하는 세부사항을 처리하는 대신, 동시성 문제를 concurrect construct에 직접 맵핑할 수 있다.
  • 프로세스 사이에서 구성 가능하면서 동시 실행에 안전한(concurrent-safe) 방식 제공. (채널)

// fin.