'병렬'에 해당되는 글 2건

허니몬의 IT 이야기
병렬(Concurrent) 프로그래밍 언어 : Erlang
현재 주로 사용되는 프로그래밍 언어 C, C++, 자바 등의 프로그래밍 모델은 병렬 컴퓨팅이나 분산 시스템(Distributed
system)을 이론적으로 설명하는 수학적 모델과 상당한 격차가 있다. 이런 격차는 이론과 현실의 괴리를 가져왔고 잘
정립된 이론적 바탕을 두고도 개발자들은 뮤텍스와 컨디션 변수를 가지고 씨름하는 현실에 생기게 된 것이다.
이런 이론과 현실과의 괴리를 좁히고 병렬 프로그래밍을 전혀 다른 방식에서 접근한 언어로 프로그래밍 언어 Erlang
이 있다. Erlang은 1982년 이동통신 회사인 에릭슨(Ericsson)이 프로그래밍 언어에서 병렬 수행 지원을 가장 큰 목표로
두고 개발한 언어이다. 일련의 연구를 통해 Erlang 팀은 2가지 결론을 내렸다.
(1) 리스프(Lisp)나 프롤로그(Prolog) 같은 심볼릭(Symbolic) 언어가 훨씬 생산적이다.
(2) 초경량 프로세스(Ultra-lightweight process), 적극적 오류 회복(Activeerror recovery), 분산 메시지 전달(Distributed message passing) 같은 병렬 수행의 개념을 갖춘 언어가 병렬 프로그래밍에 훨씬 유리하다.
하지만 당시에 이런 조건을 모두 갖춘 언어가 없었으므로 새로운 언어를 디자인했는데, 그 결과로 나온 것이 바로 Erlang이다.
프로그래밍 언어를 연구하는 사람들은 함수형(Functional)언어가 병렬성 지원에 이상적이라고 말을 한다. 왜냐하면
병렬성 이론을 설명하는 데 일반적으로 수학적 함수를 많이 사용하고, 함수형 언어는 이런 수학적 함수를 직관적으로
구현하기 쉽기 때문이다. 하무형 언어는 명령형(imperative) 언어처럼 문(statement)을 실행하는 개념이 아니라 표현식
(Expression)을 계산(evaluation)하는 방식으로 프로그램을 수행한다. 덕분에 익숙하지 않은 사람은 함수형 언어가
어색하게 느껴질 수도 있지만, 이런 언어들은 매우 안정적임이 이론적으로 증명되어 있다.
Erlang은 함수형 언어로 설계되었다. 이해를 돕기 위해 피보나치 수열을 계산하는 간단한 예제를 살펴보자.

< Erlang으로 작성한 피보나치 수열>
fib(1) -> 1;
fib(2) - > 1;
fib(X) - > fib(x-1) + fib(x-2).

fib(5)를 호출하면 위에서 정의한 세가지 함수 fib(1), fib(2), fib(N) 중에 알맞은 함수를 패턴 매칭(Pattern matching)으로
찾아서 부른다. 5는 1, 2가 아니므로 fib(N)이 불린다. fib(5)는 fib(4)+fib(3)이 되고 fib(4)는 다시 fib(3)+fib(2), fib(3)은
fib(2)+fib(1)이 된다. fib(2)와 fib(1)은 1이라는 값을 가지므로 여기서 계산이 끝나게 된다. 즉 C/C++의 재귀함수(Recursive
function)가 마찬가지로 계산한다.
Erlang의 기본적인 데이터 타입은 다른 함수형 언어와 마찬가지로 튜플(tuple)과 리스트(list)이다. 더불어 아톰(atom)은
심볼, 상수(constant), 숫자, 문자열처럼 하나의 값을 말한다. 다음은 Erlang 데이터 타입의 예젱다.


atom
16
4.6

[this, is, a, ist, that, can, go, on, and, on]

{a, tuple}
{
{{word, tuple},
{definition, "a fixed length ordered collection of atoms"}},
{{world, erlang},
{definition, "a functional language optimized for concurrency"}}
}


sum([H|T]) -> sum(T) + H;

Erlang에서 리스트는 보통 패턴 매칭(Pattern matching)으로 처리한다. sum([H|T])에서 H는 머리(Head)로 리스소의
첫 번째 원소를 뜻하고 T는 꼬리(Tail)로 첫번째 원소를 제외한 리스트의 나머지를 말한다. 앞의 함수는 첫 번째
원소(H)의 값을 나머지 리스트(T의 전체합과 더하는 방식으로 전체 리스트[H|T]의 합을 계산한다. 이런 재귀 호출
방식의 계산은 함수형 언어의 전형적인 특징이다.
Erlang 언어 자체에 깊이 있는 내용은 관심있는 독자 분들은 좀 더 찾아보기를 권하며 Erlang의 병렬 프로그래밍
지원에 대한이야기로넘어가겠다. Erlang은스레드라는용어 대신에 경량프로세스(lightweight process)라는개념을
사용한다. Erlang에서 Pid=spawn(math, fact, [1234])와 같이 주면 매우 큰 팩토리얼(factorial)을 계산하는 프로세스를
생성한다.
Pid는 프로세스 간에 메시지를 주고받는 경로로 사용된다. Pid ! {hello, world}와 같이 주면 ! 연산자는 해당 Pid의
프로세스에 {hello, world} 라는 튜플을 메시지로 전달하라 의미이다. 받는 쪽에서 다음 코드처럼 receive 키워드를 통해
이 메시지를 받을 수 있다. 여기서도 함수형 언어의 전통을 따라 패턴 매칭을 사용하였다.


loop() ->
receive
{From, message} ->
io:format("Received : ~w ~n", [Message]),
loop();
stop ->
true
end.

여기서 다시 Erlang이 스레드가 아닌 프로세스라는 용어를 사용하고 있다는 점이 주목할 필요가 있다. Erlang이 말하는
경량 프로세스는 단순히 스레드를 지칭하는 또 다른 용어가 아니라 OS에서 말하는 프로세스에 가까운 특징을 가지고
있다. OS 프로세스처럼 Erlang 경량 프로세스는 서로 메모리 영역을 전혀 공유하지 않는다.
따라서 자바 멀티스레드 프로그래밍처럼 공유자원을 여러 스레드가 동시에 접근하는 일이 일어날 수 없다. 모든 데이터는
!와 receive를 통해서
명시적으로 주고받아야 한다. 덕분에 자바 프로그램에 빈번히 데드락과 레이스 컨디션 같은 상당수의 멀티스레드 버그가 Erlang에서는 이론적으로 발생할 수 없게 된다.
Erlang의 또하나의 특징은 단일배정(Single assignment)에 있다. 단일배정은 변수가 한번 초기화되고 나면 다른값으로
변화할 수 없는 수정 불가능한(immutable) 값이 됨을 의미한다. 수정 불가능한 타입은 값을 읽을 수만 있고 쓸 수는
없으므로 상당수의 병렬 수행 버그를 없앨 수 있다. 실제로 자바 멀티스레드 프로그래밍에서도 수정 불가능한 타입의
사용은 중요한 팁의 하나인데, Erlang은권고 수준이 아니라 언어 차원에서 이를 강제하고있다.
이처럼 Erlang의 병렬 수행 지원에 대해서 간략히 살펴보았는데, 여러가지 장점에도 불구하고 함수형 언어인 Erlang이
짧은 기간 안에 주류 언어로 부상하리라고 기대하지는 않는다. 다만 Erlang이 주는 교훈은 자바의 멀티스레드 방식이
아니더라도, 병렬 프로그래밍을 쉽게할 수 있는 간단한 패러다임이 있을 수 있다는 점이다. 주류 언어에 익숙한 개발자는
멀티스레드 프로그래밍이 유일한 병렬 프로그래밍이라고 생각하기 쉽다. 하지만 멀티 코어 시대에 사는 소프트웨어
개발자는 이제 좀 더 생산적인 패러다임을 생각해야 한다.
허니몬의 IT 이야기

참고로... 이글은 마소 2007년 3월에 서광열님(kwangyul.seo@gmail.com)께서 기재하신 기사를 제가 이해하는 과정입니다....

ㅡㅅ-);; 무조건 따라서 치면서 머리 속에 담는 것이지요....

-------------------------------------------------------------------

멀티 코어시대에 대비한 소프트웨어기술


병렬(parallel/concurrent) 프로그래밍

하드웨어가 변하고 있다. 듀얼코어(Dual Core) 시스템이 일반 데스크탑에 탑재되고 있고, 프로세서가 4개인

쿼드코어(Quad Core)도 서버 머신에서 사용되기 시작했다. 고사양 서버 컴퓨터뿐만 아니라, 임베디드 시스템

에도 여러 개의 코어를 사용하는 경우가 늘고 있다. 대표적인 예제가 ARM11의 MPCore 기술인데, 1-4개의

코어를 사용할 수 있다. 하드웨어 단에서 멀티코어의 등장은 개발자들에게도 변화를 요구하기 시작했다. 하부의

여러 프로세서를 좀 더 적극적으로 활용하기 위해 멀티스레드 프로그램이 더욱 중요한 요소로 주목받기 시작한

것이다. 하지만 멀티스레드 프로그램은 작성하기 어렵고 테스트와 디버깅은 더 어렵기로 악명 높다.

멀티 코어 시대에 대비하는 소프트웨어 기술은 무엇인지 살펴보자.


멀티 코어 시대의 소프트웨어

멀티 코어 시대는 멀티스레드 프로그래밍을 요구한다. 프로세스 벤더들이 과도한 열 발생 문제 때문에 CPU클럭

경쟁에 한계를 느끼고, 멀티코어 아키텍처로 전환하기 시작한 시점부터 소프트웨어 개발자들도 이 변화에 동참

하기를 요구받고 있다. 과거 프로세서의 최적화 기술인 인스트럭션 레벨 병렬화(Instruction Level Parallelism)가

소프트웨어의 변화 없이 하드웨어의 성능 향상을 추구했다면, 현재의 멀티 코어 아키텍처는 성능 향상을 위해

소프트웨어 개발자의 적극적인 참여를 필요로 한다.

즉 과거는 하드웨어가 발전하면 소프트웨어는 가만히 앉아서 성능 향상의 이득을 누렸지만, 앞으로는 소프트웨어가

적극적으로 하드웨어의 병렬성(parellelism)을 이용하지 않는 이상 추가적인 성능향상을 기대하기 어렵다는 말이다.

이런 상황에서 전문가들은 병렬 혁명(Concurrent revolution)은 하드웨어가 아닌 소프트웨어에서 나올 것이라고

말하고 있다. 실제로 기술적인 관점에서 멀티 코어 하드웨어를 만드는 일은 크게 어렵지 않다고 한다. 문제는 현재의

프로그래밍 패러다임으로는 멀티 코어를 제대로 활용하는 멀티스레드 소프트웨어를 작성하기가 쉽지 않다는 것이다.

조금만 복잡해지면 버그 투성이가 되고 마는 멀티스레드 프로그램이기 때문이다. 하지만 그럼에도 불구하고 이제

소프트웨어 개발자는 코어가 여러 개 있다는 가정 하에 명시적으로 이를 활용하는 소프트웨어를 작성해야 한다.

앞으로는 병렬 프로그래밍은 개발자들의 선택이 아닌 필수가 될 것이다.


멀티스레드 프로그래밍의 어려움

하지만 멀티스레드 프로그래밍은 개발자의 골칫거리다. 스레드 숫자가 늘어날수록 프로그램의 복잡도는 급속히

상승한다. 멀티스레드 프로그램은 디버깅도 어렵다. 멀티스레드 프로그래밍은 프로그램 수행이 불확정적(nondeter- minstic)이다. 다시 말해서, 같은 프로그램이라도 매번 실행 때마다 미묘하게 타이밍이 바뀐다. 이런

특성 때문에 멀티스레드 고유의 버그인 데드락(Deadlock), 라이브락(Livelock), 레이스컨디션(Race condition)은

디버깅하기도 테스트하기도 쉽지 않다. 테스트할 때는 멀쩡하게 잘 되다가 실제로 배포되고 나면 말썽을 일으켜서

개발자들을 괴롭힌다.

현재 멀티스레드 프로그램은 스레드를 새로 생성하고, 뮤텍스(Mutex, mutual exclusion), 컨디션 변수(Conditional

variable)등 기초적인 요소를 이용해 작성한다. 하지만 뮤텍스나 컨디션 변수는 너무 하위 레벨의 스레드 제어 도구라서

올바른 멀티스레드 프로그램을 작성하기는 무척 어렵다. 공유 자원에 접근할 때마다 매번 뮤텍스를 잡았다 풀어야 하고,

이 과정에서 한 번이라도 뮤텍스 unlock을 빠뜨리거나 순서를 잘못 정하면 데드락이 된다. 결국 현재의 패러다임으로는

올바른 멀티스레드 프로그램을 만들 수 있기를 기대하기가 어렵다.

흔히들 자바의 등장이 멀티스레드 프로그래밍을 대중화시켰다고 한다. C/C++프로그램이 pthread 같은 외부 라이브러리

에 의존해야만 멀티스레드 프로그래밍을 할 수 있는 반면에 자바는 멀티스레드 프로그래밍을 언어에 내재시켰다. 자바는

멀티스레드 프로그래밍을 언어에 내재시켰다. 자바에서는 java.lang.Thread 클래스로 쉽게 새로운 클래스를 만들 수 있고,

synchronized 같은 키워드를 통해 모니터를 잡을 수 있다. java.nio가 나오기 전에 자바는 모든 IO가 블로킹(Blocking)이

었기 때문에 스레드의 사용이 매우 보편화되기에 이른다.

하지만 여기서 생각해 보아야 할 점은 스레드를 쉽게 만들 수 있는 것과 멀티스레드 프로그램 버그 없이 쉽게 작성할 수

있는 것 사이에는 큰 차이가 있다는 점이다. 멀티스레드 프로그래밍을 제대로 이해하지 못한 일련의 자바 개발자들은

데드락과 레이스 컨디션으로 얼룩진 엉터리 멀티스레드 프로그램을 양산해내기 시작했기 때문이다. 이런 자바 프로그램은

개발자가 테스트할 때만 잘 동작한다는 특징을 가진다.

제대로된 멀티스레드 프로그램을 작성하기 어려운 이유가 무엇일까? 현재의 멀티스레드 프로그래밍 모델을 묘사한 말

중에 "바닥에 포도 주스 쏟지 마라(Don't spill grape juice on the carpet)"는 말이 있다. 풀어서 말하면, Lock, Unlock을

제대로 하고, 컨디션 변수에 제대로 wait, signal을 날려주는 것은 절대적으로 개발자의 몫이고 여기서 발생하는 실수를

방지하기 위한 어떠한 안전장치도 제공해주지 않는다는 것이다. 포도 쥬스를 쏟지말라(뮤텍스를 제대로 써라)라는 원칙만

있지 포도 쥬스가 쏟아지는 일(데드락, 레이스 컨디션의 발생)을 막을 장치는 하나도 없는 것이다.

자바 멀티스레드 프로그래밍에 대한 새로운 지평을 연 책인 "Java Concurrency in Practice" [1] 에 대한 다음과 같은

자바 코드 예제가 나온다.

<멀티스레드 환경에서 문제가 되는 코드>

public class UnsafeSequence {

private int value;

/** Returns a unique value **/

public int getNext() {

return value++;

}

}

이 프로그램을 이용해 고유한 ID(Unique ID)를 생성한다면 단일 스레드에서는 문제없이 동작하지만 멀티스레드 환경에서는

문제가 발생한다. 두 스레드가 동시에 Value의 값을 읽은 후에 value +1을 value에 저장할 경우 두 스레드가 같은 값을

읽어갈 수 있기 때문이다. 이런 버그가 생기는 이유는 value++ 이란 코드가 원자성(atoicity)을 보장해주지 않기 때문이다.

이 코드는 하나의 명령으로 보이지만 실제로는 (1)value의 메모리 값을 읽어오는 일과 (2) value + 1을 메모리에 쓰는 2가지

일로 이루어져 있고 (1)과 (2) 사이에 컨텍스트 스위치(Context Switch)가 일어나 다른 스레드가 끼어들 여지가 있다.

멀티스레드 프로그램 버그를 만드는 또 다른 원인으로는 가시성(Visibility)이 있다. 현재의 CPU는 캐시(Cache)를 가지고

있고, 멀티코어의 경우는 각각의 CPU가 캐시를 가진다. 이런 상황에서 한 스레드가 바꾼 메모리의 값을 다른 스레드가

곧바로 볼 수 있다는 보장을 하기가 힘들다. 가시성은 한 스레드 변경한 메모리의 값이 다른 스레드에 어떤 시점에 보여야

하는지를 명확히 제어하려는 시도이다.

멀티스레드 프로그램 버그를 만드는 또 다른 원인으로는 가시성(Visibility)이 있다. 현재의 CPU는 캐시(Cache)를 가지고

있고, 멀티 코어의 경우는 각각의 CPU가 캐시를 가진다. 이런 상황에서 한 스레드가 바꾼 메모리의 값을 다른 스레드가

곧바로 볼 수 있다는 보장을 하기가 힘들다. 가시성은 한 스레드 변경한 메모리의 값이 다른 스레드에 어떤 시점에 보여야

하는지를 명확히 제어하려는 시도이다.

자바에서는 원자성과 가시성을 보장하기 위해 Synchronized 키워드를 사용한다. 문제는 자바의 Synchronized 키워드는

오용하기 무척 쉽다는 점이다. 오용 사례는 다음과 같다.

(1) 동기화해야 하는 리소스에 대해 synchronized 사용하는 것을 빠뜨린 경우

(2) 잘못된 리소스에 synchronized 를 사용한 경우

(3) synchronized 를 너무 빈번히 사용하는 경우

(1), (2)번 사례의 문제점은 쉽게 알 수 있고, (3)의 경우는 성능과 관련이 있다. 모든 리소스에 락을 걸어서 사용하면

멀티스레드 버그를 피할 수는 있다. 하지만 모든 리소스에 락을 걸면 사실상 동시에 하나의 스레드만 동작할 수 있으므로

실제로는 멀티스레드가 아닌 단?嬋볜뭇 프로그램이나 마찬기자기 되는 문제가 있다. 멀티 코어의 시대가 왔을 ??도 이런

멀티스레드 프로그래밍 패러다임을 이용해서는 하부 하드웨어의 성능을 100% 끌어내기가 힘들다. 우리가 작성한

프로그램은 데드락과 레이스 컨디션으로 오작동하는 프로그램이 되거나 멀티코어를 제대로 활용 못하는 싱글스레드에

가까운 프로그램이 되는 두 운명의 갈래에서 서성일 것이다.

To be continue....

----------------------------------------------------------------------------------------------------------------

내가 생각하는 내가 나아갈 방향은... 서버 + 네트워크 + 프로그래밍... 그 중심에는 멀티스레드 프로그래밍이...

중심이 되지 않을까라는 생각을 하고 있다. 아마도... 미래의 컴퓨터들은 고클럭+다중코어 체제로 들어설 것이다.

이를 효율적으로 사용할 수 있는 능력을 키운다면... 충분히 내 위치를 확보할 수 있을 거라고 생각한다.

우선은 내가 해야할 부분은... 네트워크 쪽이 될 것으로 보인다.... ^^;;

일단은 이 바닥...(ㅡㅅ-);; 사촌형이 이바닥이라고 하지말라는데... 후후~~)을 잘 디뎌서 이 세상으로 뛰쳐나갈

준비를 하고 있습니다. ^3^ 우후후후~~ 덤벼라 세상아!!?


관련링크 : 병렬(parallel/Concurrent) 프로그래밍 2/3
   병렬(parallel/Concurrent) 프로그래밍 3/3
1
블로그 이미지

Email : ihoneymon@gmail.com 안녕하세요, 꿀괴물 입니다. ^^ 멋진 비행을 준비 하는 블로그 입니다. 만능형 인간이 되어 많은 이들에게 인정받고, 즐겁고 행복하게 살기를 간절히 원합니다!! 달콤살벌한 꿀괴물의 좌충우돌 파란만장한 여정을 지켜봐주세요!! ^^

허니몬