Skip to content

실용주의 프로그래머 6장 - 동시성 #8

@saint6839

Description

@saint6839

동시성

동시성이란?

둘 이상의 코드 조각이 실행될 때 동시에 실행 중인 것’처럼’ 행동하는 것이다

동시성을 얻기 위해서는 파이버, 스레드, 프로세스 등을 이용해서 코드의 다른 부분으로 실행을 전환한다.

병렬성이란?

둘 이상의 코드 조각을 동시에 실행하는 것

병렬성을 얻기 위해서는 두 가지 이상의 일을 동시에 할 수 있는 하드웨어가 필요하다.

모든 일에 동시성이 있다

서버를 운영하는데 한번에 한명의 사용자만 순차적으로 접근할 수 있다면 어떨까? 매우 답답할 것이다. 이처럼 동시성은 알게 모르게 존재하고 있다.

그러나 동시성이나 병렬성을 지원하는 코드를 작성하는 것은 쉽지 않다. 그 첫 번째 이유는 우리가 프로그래밍을 순차적 시스템으로 배웠기 때문이고, 두 번째 이유는 공유상태때문이다.

이러한 어려움이 있지만, 동시성을 구현하는 것은 더 이상 선택이 아닌 필수이기 때문에 잘 이해할 필요가 있다.

시간적 결합 깨뜨리기

시간적 결합이란?

가령 예를 들면, “B라는 동작을 하기 위해서는 A라는 동작이 선행되어야 해” 라던가 “버튼 클릭을 하려면 먼저 화면이 갱신되어야 한다” 와 같은 경우를 시간적 결합이 생겼다고 한다.

그러나 이러한 접근 방법은 유연하지 않기 때문에 동시성을 확보해야 하며, 시간적 결합을 끊는 방법을 생각해 내야한다.

동시성 찾기

동시에 일어나도 되는게 무엇이고, 반드시 순서대로 일어나야 하는 것이 무엇인지 찾아내야 한다. 이를 위해서는 활동 다이어그램(Activity Diagram)을 이용해 흐름을 기록하는 것이 도움이 된다.

위와 같이 피나 콜라다를 만드는 로봇 소프트웨어를 만든다고 가정해보자. 아마 위 작업들을 하나가 끝나고 다음 작업을 진행하는 식으로 진행한다면, 쓸모없는 로봇이 될 것이다.

그렇기 때문에 위와 같이 활동 다이어그램으로 그려, 동시에 작업할 수 있는 작업들을 파악해보는 것이 좋다.

동시 작업의 기회

활동 다이어그램은 동시에 작업할 수 있는 부분을 파악할 수 있게 도움을 주는 것은 맞지만, 실제로 그것을 동시에 하는 것이 좋은지 알 수는 없다.

그렇기 때문에 설계가 필요하다. 예를 들어 8번 작업을 할 동안 1분이 걸린다면, 그 시간 동안 로봇은 10번과 11번 작업을 하면 효율적일 것이다. 이런식으로 설계할 때 코드가 아닌 곳에서 시간이 걸리는 활동을 찾아야 한다.

데이터베이스를 조회하거나 외부 서비스에 접근할 때, 사용자 입력을 기다릴 때 같이 우리 프로그램이 다른 작업이 끝나기를 기다려야하는 상황이 그 예다.

병렬 작업의 기회

병렬성은 여러 개의 프로세서가 있을 경우, 작업들을 프로세서들에게 적절히 나누어 준다면 소요 시간을 단축할 수 있을 것이다. 그리고 이렇게 나눈 작업들을 결과를 합치게 되는 방식으로 주로 사용된다.

공유 상태는 틀린 상태

만약에 카페에서 종업원1이 고객A에게 파이가 하나 남았다라고 말해 고객A가 파이를 주문하려고 하고 종업원2도 파이가 하나 남았다 라고 고객B에게 말해 고객B도 파이를 주문하려고 했다. 이 경우 결국 파이는 둘 중 한명의 고객에게만 갈 것이므로, 둘 중 한명은 파이를 먹지 못해 실망하게 될 것이다.

이러한 경우의 문제는 바로 상태가 공유되었기 때문이다. 종업원들이 서로를 고려하지 않고, 남아있는 파이만 봤기 때문이다.

이러한 문제를 프로그래밍에서는 어떻게 해결할 수 있을까?

세마포어 및 다른 상호 배제 방법

세마포어란?

단순히 한 번에 한 사람만이 가질 수 있는 무언가

예를 들어 위 카페 예제에서 세마포어를 적용해보자. 먼저 파이가 있는 진열장 위에 인형을 올려 놓는다. 종업원들은 파이를 주문 받기 전에 도깨비 인형을 챙겨야 한다. 그리고 주문을 받고 파이를 접시에 담아 고객에게 내 놓은 이후에는 인형을 원래 위치에 되돌려 놓는다.

위 이야기에서 인형이 세마포어이다. 두 명 이상의 종업원이 인형을 획득하려고 해도, 한명만이 세마포어를 획득할 수 있다. 이 경우를 lock이 되었다고 한다. 세마포어를 획득하지 못한 종업원은 세마포어를 얻을 수 있을 때 까지 기다린다(wait)

그러나 이 경우의 가장 큰 문제점은 만약 어떤 한 종업원이라도 이 규칙을 지키지 않는다면, 다시 혼돈에 빠지게 된다는 것이다.

리소스를 트랜잭션으로 관리하라

위의 설계가 허술한 이유는 책임을 종업원에게 넘겼기 때문이다. 제어를 중앙으로 집중시켜보자.

이번에는 종업원이 하나의 호출로 파이 조각 수를 확인함과 동시에, 파이 조각을 가져가게 만들어본다.

slice = display_case.get_pie_if_available()
if slice
	give_pie_to_customer()
end
def get_pie_if_available()
	if @slices.size > 0
		update_sales_data(:pie)
		return @slices.shift
	else
		return false
	end
end

위와 같이 구현 했지만, 이 메서드 자체도 여러 스레드에서 동시 호출이 가능하므로 세마포어로 보호해야한다.

def get_pie_if_available()
	@case_semaphore.lock()
	if @slices.size > 0
		update_sales_data(:pie)
		return @slices.shift
	else
		return false
	end
	@case_semaphore.unlock()
end

하지만 이 역시도 문제가 있다. 만약 update_sales_data 에서 예외가 발생하면, 영원히 unlock()은 호출되지 않고 아무도 이 자원에 접근할 수 없을 것이다.

def get_pie_if_available()
	@case_semaphore.lock()
try {
	if @slices.size > 0
		update_sales_data(:pie)
		return @slices.shift
	else
		return false
	end
}
ensure {
	@case_semaphore.unlock()
}
end

예외가 발생하더라도 unlock()의 호출이 보장 되어지도록 위와 같이 개선해 볼 수 있다.

여러 리소스와 트랜잭션

만약 파이에 아이스크림이 올라간 메뉴가 있다고 가정하자. 종업원은 이제 파이와 아이스크림이 모두 있는지 확인을 해야 한다.

slice = display_case.get_pie_if_available()
scoop = freezer.get_ice_cream_if_available()

if slice && scoop
	give_order_to_customer()
end

만약 이렇게 된다면, 파이를 꺼내고 보았더니 아이스크림이 없다면 문제가 생기게 된다. 이럴 경우를 대비해, 다시 파이를 반환하는 메서드를 추가한다. 이는 리소스를 계속 차지하고 있지 않도록 예외 처리를 추가함을 의미한다.

slice = display_case.get_pie_if_available()

if slice
	try {
		scoop = freezer.get_ice_cream_available()
		if scoop
			try {
				give_order_to_customer()
			}
			rescue {
				freezer.give_back(scoop)
			}
		end
	}
	rescue {
		display_case.give_back(slice)
	}
end

이 코드는 여전히 너무 지저분하며, 비즈니스 로직이 이런저런 관리를 위한 코드에 섞여있다. 앞의 예제에서는 고민이 되는 리소스의 클래스 안으로 코드를 옮겨서 해결했지만, 이 예제에서는 리소스가 두 가지이다. 이 경우에는 코드를 파이가 있는 쪽으로 옮겨야 할까? 아니면 아이스크림이 있는 쪽으로 옮겨야할까?

실용주의적 관점에서는 이 두 가지를 하나로 묶을 모듈을 생성하고, 모듈에서 관리하는 것이 효율적이라고 말하고 있다.

트랜잭션이 없는 갱신

공유 메모리는 동시성 문제의 원인으로 많이 꼽혀진다. 그러나 수정 가능한 리소스를 공유하는 애플리케이션 코드 어디에서나 동시성 문제는 발생할 수 있다.

병렬 처리를 하지 않을 때는 작업 디렉터리를 다시 원래 위치로 바꾸어 버리는식으로도 동작이 충분히 가능하다고 한다.

칠판

형사들이 살인 사건 조사를 해결할 때, 해결하고자 하는 문제 주제를 딱 하나 칠판에 적는다. 이 과정에서 칠판 접근 방법에는 몇 가지 특징이 있다.

  • 형사들은 칠판만 바라보며, 다른 형사들의 존재를 알 필요가 없다
  • 형사들은 저마다 독립적인 존재이다. 그렇지만 문제를 해결하고자 하는 공통 관심사가 있다.
  • 수사 과정에서 여러 형사가 들어오거나 나갈 수 있고, 임무 교대 시간도 제각기 다를 수 있다.
  • 칠판에는 제한 없이 어떤 것이든 올릴 수 있다. 사진, 증언 등등

이렇게 자유로우면서도 동시성을 띄는 방법으로, 서서히 결론에 도달할 수 있다. 이러한 방식이 최근 복잡한 인공 지능 애플리케이션에서 사용되고 있다.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions