허니몬의 IT 이야기

트랜잭션(Transaction) 지원 프로그래밍 언어

Erlang과 같은 함수형 언어가 병렬 수행을 다루는 한 가지 축이라면 또 다른 축은 트랜잭션 지원 프로그래밍

언어가 있다. 프로그래밍 언어의 트랜잭션 지원을 이해하기 위해서 우선 낙관적인 컨커런시 제어(Optimistic

concurrency control)라는 개념을 이해해야 한다. 자바의 java.util.Random 클래스를 살펴보자.


protected int next(int bits) {

long oldseed, nextseed;

AtomicLong seed = this.seed;

do {

oldseed = seed.get();

nextseed = (oldseed * multiplier + addend) & mask;

} while (!seed.compareAndSet(oldseed, nextseed));

return (int) (nextseed>>> (48-birs));

}


java.util.Random 클래스는 임의의 숫자를 돌려주는 유틸리티 클래스이다. 위 코드를 보면 알 수 있듯이 next()

메소드는 전혀 락을 쓰지 않고 있지만, 이 메소드는 스레드 세이프하다. 즉 여러 스레드가 동시에 next() 메소드를

부르더라도 각각 다른 값을 돌려주는 것이다. 이런 일이 어떻게 가능할까?

Random 클래스의 구현이 앞서 언급한 낙천적인 컨커런시 제어라는 개념을 적용한 예제이다. 이 말이 뜻하는

바는 다음과 같다.

(1) 스레드는 락을 잡지 않고 다른 스레드가 동시에 들어오지 않을 것이라는 낙천적인 가정하에 프로그램을 수행한다.

(2) 메소드 수행이 끝나고 나면 내가 그 동안 읽은 데이터가 다른 스레드에 변경되지 않았는지 확인한다.

(3) 변경되지 않았다면 내가 수정한 값을 모두 반영한다(commit). 이는 데이터베이스에서 말하는 트랜잭션과 같은 개념이다.


메모리 트랜잭션(Memory Transaction)

여기서 말하는 트랜잭션의 개념은 데이터베이스에서 말하는 트랜잭션과 동일하다.

(1) 트랜잭션은 일련의 메모리 오퍼레이션이 모두 수행되거나(Commit) 혹은 전혀

수행되지 않는(abort) 두가지 모드로만 동작함을 보장한다(All or nothing).


(2) 트랜잭션은 원자성(Atomicity)을 보장하는데, 쉽게 말해 일련의 메모리

오퍼레이션이 하나의 단위로 수행되는 것처럼 보인다. 즉, 외부에서 보기에는

한번에 모든 메모리 오퍼레이션이 일어나는 것처럼 보인다.


(3) 트랜잭션은 독립성(isolation)을 보장한다. 시스템은 다른 스레드는 모두 멈춰있고

오직 한 가지 스레드만 수행된다고 가정하고 코드를 수행한다. 바꿔 말해서 다른

스레드에 의해서 간섭 현상이 없음을 의미한다.


(4) 또한 트랙잭션은 프로그램이 순차적으로 실행된다는 환상을 준다. 다른 스레드가

없다는 가정 하에 프로그래밍을 하면 논리적으로 훨씬 간단한 코드를 작성할 수 있다.

물론 트랜잭션이 실제로 순차적으로 코드를 실행하지는 않는다. 순차적으로 코드를

실행하면 단일스레드 프로그램과 다를 바가 없고 멀티 코어 프로세서를 전혀 활용하지

못하게 되기 때문이다. 트랜잭션 시스템은 독립성(isolation)이나 원자성(atomicity)

등을 보장하는 범위 내에서 최대한 많은 스레드를 동시에 수행하는 방식으로 효율을 높인다.


여기서 내가 읽은 값이 다른 스레드에 의해서 변경되지 않았는지 확인하는 부분은 java.util.concurrent.atomic에

있는 클래스의 compareAndSwap() 메소드를 이용해 구현해야 한다. 메모리 값을 읽고 새로운 값을 메모리를 쓰는

동작을 하나의 인스트럭션으로 하기 위해 하드웨어의 도움을 받는 것이다. 실제로 Intel과 ARM 등 여러 CPU를

보면 atomicCompareAndSwap에 해당되는 인스트럭션이 있다. 이 인스트럭션을 이용하면 OS의 도움 없이

앞선 경우와 같이 스레드 세이프한 Random 클래스를 만들 수 있다.

자바의 Random 클래스는 이 방식으로 수정한 후에 벤치마크 결과를 보며 상당한 성능 향상이 있었음을 알 수

있다. 뮤텍스를 사용하는 방식은 뮤텍스 락을 잡을 때 OS 시스템콜을 해야 하는 반면에 이 방식은 OS의 도움 없이

스레드 세이프티를 구현하였기 ??문이다. 물론 Random 클래스 자체가 애초에 스레드 세이프한 클래스였을 필요가

있는지에 대해서는 의문을 제기할 수 있지만(스레드가 다르면 별도의 객체들을 쓰면 된다) 여기서는 그 문제는

차차하기로 하자.

앞의 예제는 낙천적인 컨커런시 제어를 atomicCompareAndSwap의 도움을 받아 구현한 사례이다. 성능 향상은

분명하지만, 여기서 문제점은 atomicCompareAndSwap을 사용해 스레드 세이프한 프로그램을 구현하는 일은

뮤텍스를 사용하는 기존 멀티스레드 프로그래밍보다 오히려 더 어렵다는데 있다. 데이터베이스처럼 트랜잭션

지원이 자동으로 되는 것이 아니라 트랜잭션을 개발자가 구현해야 하기 때문이다.

현재 프로그래밍 언어 연구자들은 프로그래밍 언어 차원에서 트랜잭션을 지원하기 위한 연구를 계속하고 있다.

현재 계측으로는 3-5년 안에는 주류 언어에서 트랜잭션 지원을 볼 수 있을 것이라는 이야기가 있다. 실제로

HSPC(High-Productivity Computing System, 고생산성 컴퓨터 컴퓨터 시스템)의 일환인 썬(Sun)의 포트리스

(Fortress), IBM의 X10, 크레이(Cray)의 샤펠(Chapel)은 모두 락 대신에 트랜잭션을 지원하는 프로그래밍 언어

컨스트럭트(Construct)를 지원하고 있다.

연구자들이 주목하는 분야는 트랜잭션 메모리(Transactional memory)라는 기술이다. 사실 트랜잭션이라는

개념은 데이터베이스 커뮤니티에서는 수십 년간 사용해왔으며 유동성이 이미 검증된 기술이다. 트랜잭션

메모리를 사용하는 프로그래밍 언어는 사용하기 편하고 규모가 커지더라도 성능 저하 없이 사용할 수 있다는

장점이 있다. 프로그래밍 언어 이론 분야에서 가장 중요한 학회인 POPL(Principles of Programming Language)

의 2006년 키노트에서 에픽 게임즈(Epic games)의 팀 스위니(Tim Sweeney)가 기존 수동 동기화 방식의

문제점을 지적하며 앞으로는 트랜잭션 지원이 사실상 유일한 대안임을 역설하였다.

아직 구체적으로 상용화되어 쓰이는 언어는 없지만 다음과 같은 가상의 예를 통해 트랜잭션을 지원하는

프로그래밍 언어가 어떤 형태인지 느껴보자.

<스레드 세이프한 복합 연산의 예>

A

move(Object key) {

synchronized(mutex) {

map2.put(key.map1.remove(key));

}

}

B

move(Object key) {

atomic {

map2.put(key.map1.remove(key));

}

}


컨트런트 해시 맵(Concurrent Hash Map)

기존 자바 콜렉션 프레임워크(Collection Framework)에서 제공하는 HashMap의 경우

스레드 세이프티를 고려하지 않고 설계되었다. 따라서 멀티스레드가 동시에 HashMap의

put()이나 remove() 메소드를 부르면 문제가 생길 수 있다. 이 경우 collection 클래스의

synchronizedMap() 메소드를 통해서 동기화된 맵을 얻어 올 수 있는데, 이렇게 얻어온

맵은 한 번에 하나의 스레드만 수행할 수 있다는 단점이 있다.

자바 5에서 추가한 java.util.concurrent 패키지는 자바 동기화와 관련된 여러 가지

유틸리티 클래스를 제공하는데, 그 중 하나가 ConcurrentHashMap()으로 얻어온 동기화

맵의 한계를 극복하고 내부적으로 여러 개의 해시 테이블을 운영하여 키가 같은 해시

테이블에 겹치지 않는 경우에는 동시에 여러 연산을 수행할 수 있도록 설계되었다.


위 프로그램은컨커런트 해시 맵(자바의 ConcurrentHashMap 클래스의 객체)에서 특정키 값을 뽑은 후에는

다른 컨커런트 해시 맵으로 옮기는 연산을 수행한다. 여기서 요구사항은 키가 두 해시 맵 중 어느 한 쪽에는

반드시 들어있어야 한다는 것이다. 즉 한 맵에서는 키를 제거하고 다른 맵에 넣어주기 전에 다른 스레드가

이를 볼 수 있어서는 안된다. 프로그램 A는 전통적인 동기화 방식을 사용한 예제이고, 프로그램 B는 트랜잭션

지원 프로그래밍 언어로 atomic이라는 키워드로 트랜잭션을 묶어준다.

A 방식과 B 방식은 문법적으로 유사해 보이지만, 실제로 큰 차이가 있다. A 의 경우 map2와 map1의 연산을

통째로 synchronized(mutex)로 묶어주었기 때문에 기존의 컨커런트 해시 맵이 제공하는규모 가변성(salability)

키가 겹치지 않는 한 여러 스레드가 동시에 해시 맵에 put(), remove()할 수 있다)을 활용하지 못하게 된다. 즉

remove() 메소드는 반드시 한 번에 한 스레드만 수행할 수 있는 것이다. 반대로 atomic으로 묶어준 프로그램

B의 경우 map1에서 뽑아 와서 map2에 넣어주는 키가 다른 스레드의 키와 상충 관계가 없으면 동시에 여러

스레드가 키를 뽑아서 넣을 수 있으므로 규모 가변성 측면에서 이득이 크다. 같은 일을 A 방식으로 하려면

훨씬 더 복잡한 프로그래밍이 필요하고 정확하게 만들기도 어렵다.

간단한 예제만 살펴봤지만 트랜잭션 메모리 지원이 차세대 프로그래밍 언어의 주요 이슈임을 분명하다.

트랜잭션이라는 검증된 기술이기에 새로운 이론적 배경을 만드는 일에 비해서 훨씬 더 실용화될 가능성이

크기도 하다. 길게 잡아도 앞으로 5-6년 안에는 트랜잭션 지원 언어가 주류로 편입하는 모습을 볼 수 있을

것이다.

이번에 소개한 기술은 당장 개발 환경에서 적용할 수 있는 기술은 아니다. Erlang의 같은 함수형 언어를

도입할 수 있는 개발 환경도 많지 않고, 프로그래밍 언어의 트랜잭션 지원은 아직까지 연구만 활발히 일어나고

있는 수준이다. 하지만 멀티 코어를 중심으로 빠르게 변화하는 프로그래밍 환경에서 병렬 프로그래밍을

용이하게 하기 위한 노력은 앞으로도 지속될 것이다. 병렬 프로그래밍을 성공적으로 이끌 궁극적인 패러다임이

무엇이 될지는 아직 명확하지 않지만 변화의 바람이 조만간 개발자들을 강타할 것은 분명하다.

=======================================================================================================

새로운 쿼드 코어가 인텔에서 소개되었습니다. 이제 점점 더 멀티스레드 처리 방식을 지원하는 소프트웨어의

개발은 더욱더 절실해지고 있다. 32비트를 넘어서 64비트의 운영체제를 지원하면서 처리속도의 비약적인 발전을

거듭해 나가고 있다.

@_@);; 내 머리는... 아직은 Hyperthreading 단계인 것 같습니다...