2016년 1월 20일 수요일

AMP 와 병렬처리.

Atomic 함수


AMP 환경 하에서 코드를 수행하는 도중, sleep() 또는 socket.read() 등의 blocking 함수 (함수 수행 도중 쓰레드 수행을 멈추는 함수) 를 호출하면 blocking 시간 동안 동기화된 모든 모듈이 locking 상태를 유지하게 된다.

이 상황이 바람직한 상황이 아니라면, blocking 함수를 호출하기 전에 모듈의 동기화 상태를 해제해야만 한다. 그러나 AMP Transaction 수행 과정 중에 locking 된 여러 모듈을 어느 범위까지 해제할 것인지 해당 모듈의 lock을 해제해도 안전한 상태인지 보증할 방법이 없다. blocking 함수 수행 후 동기화 상태를 복구해 준다고 하더라도, 동기화가 해제된 동안 데이터 변경이 이루어 졌다면, 트랜젝션 내부의 스택에 남아 있는 이전 상태에서 참조된 값들을 무효화하고 다시 계산하도록 강제할 방법이 없기 때문이다. 즉, 임의의 시점에 모듈의 동기화를 안전하게 해제할 방법은 없다.

방법은 오직 하나, blocking 함수 수행 시점에 어떠한 객체도 동기화 상태를 가질 수 없도록 강제하는 것이다.

만약 트랜젝션 수행 중에 다른 데이터들과 전혀 관련성이 없는 데이터, 즉 원자적(atomic)인 데이터만 참조한다면, 해당 트랜젝션 수행 중에는 데이터 동기화가 전혀 필요하지 않게 된다.

다른 데이터와 전혀 연관성을 가지지 않는 원자적인 데이터를 가지는 필드를 atomic 필드라 하고. 특정 함수 내에서 atomic 필드만 참조 변경하는 함수, 즉 동기화가 전혀 필요없는 함수atomic 함수라 부르기로 한다.
(atomic 필드를 volatile 필드로 변경해도 무방하다. 다만 volatile 함수나 volatile 트랜젝션이란 용어가 상당히 어색하고, volatile 보다는 atomic 이란 용어가 좀 더 본문의 의미와 상통하기에 atomic 을 사용하기로 한다.)

Atomic 함수만 수행하는 특정 트랜젝션은 임의의 시점에 중단되어도 전체 시스템 데이터의 안정성에 전혀 위험을 끼지치 않는다. 그 중 내부에서 blocking 함수의 호출을 허용하는 함수를 interruptable 함수라 구분하여 부르고, interruptable 함수만으로 이루어진 트랜젝션을 interruptable 트랜젝션이라 부르기로 한다. (모든 blocking 함수는 socket 종료나 ^C 입력에 의해 함수 수행이 종료되는 일종의 interruptable 함수이다. 역으로 모든 interruptable 함수는 수행 도중 blocking 될 수 있는 blocking 함수이기도 하다.)
이에 따라 AMP 는 아래의 같이 함수 유형을 추가적으로 구분한다. 함수의 구분
AMP 환경 내의 모든 객체의 함수는 항상 다음이 허용된다.

  1. 읽기전용 객체 및 상수값 참조.
  2. 내부에서 생성한 로컬 객체에 대한 참조/변경 및 멤버 함수 호출.
  3. 인자로 받은 객체에 대한 참조 및 멤버 함수 호출. (변경권한은 인자 타입에 따라)
  4. atomic 함수의 호출
  5. 비동기적 메시지 전달

일반 함수
일반 함수는 다음이 추가적으로 허용된다.

  1. 일반 함수의 호출
  2. 모듈 함수의 동기적 호출
  3. atomic 필드를 제외한 객체 내부 필드의 참조.


Atomic 함수 (원자적 함수)
atomic 함수는 다음이 추가적으로 허용된다.

  1. atomic 필드의 참조 및 변경.


Interruptable 함수 (중단 가능 함수)
interruptable 함수는 다음이 추가적으로 허용된다.

  1. interruptable 함수 호출 (blocking 도 interruptable 함수이다.)
  2. 명시적인 동기화 (트랜젝션 내부의 깨어진 동기화가 허용된다.)


위에 명시되지 않은 사항은 허용되지 않는다.


Interruptable Transaction
Interruptable 함수는 interruptable 함수 내부에서만 호출될 수 있다. 따라서 최초의 interruptable 함수, 즉 interruptable 트랜젝션의 시작은 반드시 비동기 메시지에 의해 시작된다. 이를 통해 자연스럽게 모든 block 가능한 함수는 다른 트랜젝션과 명확히 분리된다. 즉, 일반적인 AMP 트랜젝션 수행 중에는 Thread dead-lock 이 발생하지 않는 한 쓰레드 blocking 상태로 변경되지 않는다. (적절한 런타임 프로파일링을 통해 쓰레드 데드락 문제가 실제로 발생하기 전에 그 가능성을 경고하는 방법에 대해선 다음 글을 통해 소개하겠다.)
Blocking 함수를 호출하는 코드 부분만 제외하면 interruptable 함수는 일반 Lock-free 함수와 동일하다. 이에 interruptable 함수 내부에서 별도의 동기화없이 모듈 내부 데이터를 참조하면 Thread-safety 오류가 발생한다. 즉, interruptable 함수 내부에서 일반 필드 또는 함수를 참조하려면 아래와 같이 별도의 명시적  동기화가 필요하다. 참고로, interruptable 함수 내부라 할지라도 모듈이 동기화되어 있는 상태에서는 다른 interruptable 함수의 호출이 금지된다.
ex)
int interruptable readSocketAndReturnSomething(byte[] buff) {
    // 동기화된 영역 내부의 계산값을 그 바깥 영역에서 참조하려면 반드시 interruptable 변수로 선언해야 한다.
// blocking 함수 수행 후 그 계산값이 달라질 수 있음을 명시적으로 나타내기 위함이다. (용어는 쫌... ^^;;)
    interruptable int x;
    // 현재 모듈을 동기화한다.
    synchronized {
        int a = this.any_field; // atomic field 제외.
        int b = this.any_method(); // interruptable 함수 제외.
        x = this.getFlag(a * b);
    }
    int len = socket.read(buff);
    // 현재의 x 의 값은 blocking 함수 호출 전에 얻어진 값이다.
    // block 함수 수행 도중 모듈 내용이 변경되었다면, 해당 변수에는 이전 상태의 계산값이 들어있다.
    // 만약 현재의 값이 아닌 과거값 자체가 필요한 경우라면 명시적으로 변수 재사용을 선언하여야 한다.
    // 이를 생략하면 아래의 return 문은 컴파일 오류 또는 경고를 발생시킨다. (문법은 쫌.... ^^;;)
    use_last_varibles(x);
    return x * buff[0] + buff[1];
}

Atomic 필드

다른 데이터와 전혀 상관없이 수시로 변경되어도 무방한 원자적 data 를 가지고 있는 필드를 Atomic 필드라 한다. 해당 데이터는 별도의 동기화 즉, locking 없이 수시로 참조하고 변경하여도 시스템 안정성에 위험을 끼치지 않음을 보장하여야 한다.
Atomic 필드는 동일 클래스 내 일지라도 atomic 함수 내에서만 참조 및 변경이 가능하다. (이는 atomic 데이터의 관리를 명시적으로 소수 함수 내로 한정하기 위한 것이다)

Atomic
필드는 "atomic<데이터 타입> 필드명;" 형태로 선언한다. 일반 필드를 참조 변경할 때와 달리, 그 실제 값을 참조하려면 get() 함수를 호출해야 하고, 그 값을 변경할 때에도 함수를 이용하여야 한다. 이는 수시로 변하는 값을 좀 더 안전하게 참조하고 변경하도록 강제하기 위한 것이다.
Atomin 필드가 포인터 형인 경우에 해당 필드에 대입된 객체가 불변자료형이거나 unique_ref 가 아니라면 해당 객체의 내부 값을 참조/변경할 수 없다. 만약 이를 허용하면 해당 객체의 내부값을 모든 쓰레드가 동시에 참조/변경하는 것이 가능해지기 때문이다. AMP 이러한 오류를 사전에 방지한다.
물론, 해당 atomic 함수를 호출한 다른 일반 함수는 그 반환값으로 받은 객체를 정상적으로 사용할 수 있다. AMP가 해당 객체가 속한 모듈의 동기화를 자동으로 처리하기 때문이다.

Atomic 필드는 직접 값을 대입할 수 없고 반드시 다음의 함수 등을 통해서만 변경 가능하다.
아래의 함수는 각각의 System 레벨의 Lock-free 함수와 대응된다.
ex)
     this.atomicIntValue.add(1);
     this.atomicIntValue.compareAndExchange(v, oldV);
     this.atomicValue.unsafeSwap(v);


AMP 와 병렬 처리


Atomic 함수는 병렬처리에 사용되는 lock-free 함수와 동일한 의미를 가진다.

AMP 는 atomic 필드와 일반 필드, atomic 함수와 일반 함수를 명확히 구분함으로써 lock-free 기술의 적용을 쉽게 안전하게 적용할 수 있도록 한다. atomic 필드와 함수는 마치 별개의 내부 클래스에 속한 것처럼 논리적으로 구분된다.
Lock-free 를 적용한 Linked-List 를 예를 들어보자. AMP atomic 함수 내에선 atomic 필드만 참조 가능하므로 반드시 아래의 두 개 필드의 타입이 atomic 형이어야 한다. 

class AtomicQueue {
atomic<AtomicLinkedList> top;
Object theOtherData;
class AtomicLinkedList {
atomic<AtomicLinkedList> next;
Object someData; void someMethod() {...}
....
};
atomic AtomicLinkedList popItem() {...}
void someMethod() {...}
....
};

AMP 환경 하에서 다음과 같은 안전성이 보장된다.
  1. Atomic 함수 내에서 공유 데이터의 안전성을 깨뜨릴 수 없다. Atomic 함수는 일반 필드를 변경할 수 없다. unique_ref 객체와 로컬 객체는 쓰레드 안정성과 관계없으므로 예외적으로 참조 및 변경이 허용된다.
  2. Atomic 필드는 Atomic 함수 내에서만 참조 및 변경이 가능하다. 위 소스에서 top next 필드는 매우 조심스럽게 동기화되어야(Lock-free를 이용하여) 한다. 일반 함수는 아예 위험한 필드에 접근하지 못하도록 함으로써 그 안전성을 높이게 된다.
  3. Atomic 함수는 항상 Lock-free 함을 보장한다. Atomic 함수는 일반 함수나 interruptable 함수를 호출할 수 없다. 이를 통해 atomic 함수가 항상 lock-free 함을 보장한다.
  4. Atomic 함수가 반환한 객체의 쓰레드 안전성을 보장한다. Atomic 함수가 반환한 객체는 unique_ref 가 아닌 한 결코 Thread-safe 한 객체가 아니다. 다른 쓰레드와 공유된 객체이기 때문이다. (그렇지 않다면 Lock-free 자체가 필요치 않다.) 해당 객체를 동기화하는 것만으로는 쓰레드 안전성을 확보할 수 없다. 데이터의 안전성은 객체 단위가 아닌 연관성을 가진 모든 객체 즉 모듈 단위로 관리되어야만 한다. AMP 는 모듈의 자동 동기화를 통해 불변자료형이 아닌 객체들도 Lock-free 함수를 통해 안전하게 교환할 수 있는 방법을 제공한다.

AMP 모듈의 데이터 독립성은 OpenMP 같은 멀티 프로세서 기술의 접목을 쉽게 하고, GPU 등과의 연동도 쉽게 처리할 수 있을 것으로 기대된다. 별도의 CPU 또는 GPU 에서 각각의 모듈을 처리한다면, 해당 CPU/GPU는 완전한 독립성을 가질 수 있기 때문이다.
참고로, 아래는 AMP 모듈과 매우 유사한 개념을 가지고 있는 Partitioned Global Address Space(PGAS) 라는 병렬처리 기술에 관한 소개 문서이다.
   http://cnx.org/contents/gtg1AzdI@7/An-Introduction-to-the-Partiti
또한 AMP의 비동기 메시지 방식은 독립된 논리적 또는 물리적인 프로세스 간의 통신을 용이하게 해 줄 것이다. AMP 의 한 모듈을 실제는 다른 서버에서 실행되는 프로세스에 대한 Proxy 로 변경하는 것은 매우 용이하다. 어쩌면 Erlang 처럼 Remote 서버를 멈추지 않고 서버에서 수행되는 메인 Process, 즉 모듈을 command line 명령어 한 줄로 변경하는 수준은 아니더라도, 특정 모듈을 Runtime 에 다른 서버로 이동시키거나 분산 배치하는 것이 이론적으로 가능하다.
AMP 를 이용하여 C/C++ 수준의 병렬 처리 기술을 구현하는 것은 불가능하겠지만, AMP 를 이용하여 병렬처리를 위한 '안전한' 기반을 확보할 수는 있을 것이다. 병렬 처리 기술을 멀티쓰레드 환경에서 특정 트랜젝션 간의 데이터 충돌을 효과적으로 해소하기 위한 기술의 일종이라고 가정하면 트랜젝션 단위의 데이터 안정성을 기본적으로 제공한다면 AMP 의 활용 가치가 더욱 크다고 여겨진다.
맺는 말.

AMP 는 코드의 퍼포먼스가 아닌 코딩의 퍼포먼스를 추구한다. 
쓰레드 안정성을 쉽게 보장하고 검증할 수 있는 언어와 플랫폼을 통해, 궁극적으로 멀티쓰레드 코딩에 대해 문외한인 개발자도 매우 쉽게 안전한 멀티 쓰레드 코드를 생산할 수 있도록 하고자 한다.
적어도 버그의 재현 자체도 하지 못해 며칠씩 밤을 새야 하고, 코드 한두 줄 바꿀 때마다 온 신경을 곤두세워 다른 소스와의 충돌 가능성을 분석해야 하는 상황을 해소해 보려 한다. 멀티쓰레드 전문가들을 쓰레드 안전성 문제에서 상당 수준 해방시킴으로서, Lock-free 와 병렬처리, 분산처리 등 좀 더 위험하지만 퍼포먼스 향상 효과가 큰 문제에 집중할 수 있도록 한다면, 시스템 전체의 퍼포먼스는 더욱 향상될 수 있을 것이다.
AMP 가 그런 솔루션이 될 수 있기를 기대해 본다.


2016년 1월 14일 목요일

Atomic Module Programing

1. 멀티쓰레드 코딩의 어려움

아래와 같은 기차 건널목 제어 코드를 멀티쓰레드 상황에서 실행한다면 심각한 사고가  발생할 수 있다.
void onTrainCommimgToCrossing() {     
trafficLight.setColor(Red); waitForTrainPassingThrough();
trafficLight.setColor(Green);
}

위 코드를 안전하게 수행하려면 신호등 객체가 반드시 일정 시간 동기화된 상태 즉, 기차가 건널목을 통과하는 동안 다른 무엇도 신호등의 상태를 바꿀 수 없는 상태를 유지해야만 한다. 당장 위의 코드만을 보고 두 개의 함수가 차례로 처리되는 동안 신호등이 동기화된 상태인지 확인할 방법은 전무하다. 더욱 심각한 문제는 단순한 테스트만으로는 위와 같은 동기화 오류를 검증하는 것이 매우 어렵다는 것이다.

복잡한 멀티쓰레드 환경에서도 단일 쓰레드 환경과 마찬가지로 안전하게 수행될 수 있는 코드를 Thread-safe 한 코드라 한다면, 다음 조건의 모든 함수는 Thread-safe 하지 못하다.
  1. 한 bit 라도 공유 메모리를 변경하는 함수 또는 해당 함수를 호출하는 함수
  2. 두 번 이상 공유 메모리를 참조하는 함수 또는 해당 함수를 호출하는 함수
  3. 공유 메모리를 한 번 참조하는 함수를 두 번 이상 호출하는 함수. 공유 메모리 : 특정 프로세스 내에서 모든 쓰레드가 접근 가능한 메모리. 일반적으로 Heap 또는 Global Memory 라 부른다.
즉 위의 함수 뿐 아니라 아래의 두 함수 모두 Thread-safe 하지 못하다.

void Rect::setSize(int w, int h) { this->width = w; this->height = h; } int Rect::getArea() { return this->width * this->height; } setSize() 함수를 호출하여 width 값을 설정하고, height 값을 설정하기 직전에 쓰레드가 전환되어 다른 쓰레드에서 getArea() 함수를 호출하면 의도치 않은 엉뚱한 값을 얻게 된다.

실로 끔찍하다. 위와 같은 코드는 매우 일상적으로 사용된다. 즉, 우리가 일반적으로 사용하는 거의 모든 코드가 멀티쓰레드 환경에서는 모두 위험성을 지니고 있는 것이다. 멀티코어 시대에 접어들면서 멀티쓰레드 코딩의 중요성은 더욱 증가하고 있지만 그 난이도와 위험성은 전혀 해소되지 않고 있다. 멀티쓰레드는 한 공간(공유 메모리) 속에 벌어지는 평행우주의 사건들을 통제하는 일이다. 지구 상의 모든 인간은 근본적으로 멀티쓰레드 코딩에 친숙해지기 힘들 수 밖에 없다. 참고) Thread-Safety 에 대한 Wikipedia 의 정의.
A piece of code is thread-safe if it only manipulates shared data structures in a manner that guarantees safe execution by multiple threads at the same time

AMP.jpg

2. 동기 vs 비동기

모든 쓰레드 안전성 문제는 쓰레드 간 데이터 동기화(Synchronization)와 관련된 문제이다. 데이터 동기화는 mutex, semaphore, CriticalSection 등을 이용하여 특정 데이터를 참조 및 변경할 수 있는 배타적인 권한(lock)을 확보함으로써 이루어진다. 그 lock은 공용화장실의 자물쇠와 동일한 의미이다. 공용화장실에 들어섰는데, 안에서 문을 잠글 수 없다고 생각해 보라. 자물쇠(lock) 가 공용화장실의 안전한 이용에 얼마나 필수적인 지 구체적인 예를 들 필요는 없을 것이다. (참고로, Lock-Free라는 특별한 동기화 방법도 있다. 데이터 크기와 잠금 시간에 제한이 없는 lock 방식에 비해 Lock-free는 CPU 명령을 이용하여 sizeof(int) 또는 그 두 배 크기 이내의 데이터를 매우 짧은 시간 안에 참조 및 변경하는 기술이다. 본 글에서 사용되는 '동기화'란 용어는 '일반적인 쓰레드 동기화', 즉 locking 과 동일한 의미로 사용됨을 참고하기 바란다.)

그런데, 이러한 locking, 즉 동기화의 필요성을 아예 제거해 버릴 수 있는 방법이 있다. 
그것은 비동기적인 메시지 전달 방식이다. 
혼자만 집에 있는 상황이라면 화장실 문에 자물쇠가 없어도 아무 문제가 없다. 모든 우편물과 배달물은 내가 찾으러 갈 때까지 문밖 사서함에 쌓이기만 할 뿐이다. 배달부가 초인종을 누르긴 하지만, 단지 배달물이 있음을 알리는 것일 뿐 절대 내 집안으로 들어오지 않고 그냥 가버린다. 낮잠을 자고 싶으면 그냥 푹 자면 그만이다.
Erlang 은 함수형 언어의 특징과 Actor 라는 비동기 메시지 처리 모델링을 결합하여, 분산 서비스 및 (GPU 를 이용한) 병렬처리에도 매우 적합한 Erlang Process 라는 매우 환상적인 프로그래밍 환경을 완성한 바 있다.

그러나, 불변자료형만 사용하는 함수형 언어와 달리 OOP 환경에서 Actor 모델을 적용하는 것은 매우 위험한 일이 될 수 있다. 두 개의 Actor 가 가변자료형 객체를 공유하는 것이 가능하기 때문이다. 우리 집의 전등 스위치가 옆집과 연결되어 있어서 서로 끄고 켤 수 있다면 낭패일 수 밖에 없다. Actor 간에서 메시지를 주고 받을 때, 메시지 인자 중 가변형 객체는 반드시 그 내용물을 복제하여 각각 서로 다른 객체를 소유하도록 해줘야만 한다. 그러나, 기존의 OOP 언어 중 비동기 메시징 처리의 쓰레드 안전성을 보장하는 언어는 없다.

(참고로 Scala 를 포함해 일부 객체지향 언어들이 제공하는 불변자료형 자료구조는 진정한(pure) 불변자료형이 아닌 경우가 많다. 해당 객체만 변경이 불가능할 뿐 해당 객체 내에 담겨있는 다른 객체는 변경이 가능한 경우가 있기 떄문이다. 해당 객체의 참조는 Thread-safe 하지만, 그 객체 내에 저장된 다른 객체를 참조하고 변경하는 것은 여전히 위험하다.)

3. Transaction 과 쓰레드 안전성

쓰레드 안전성에 있어서 비동기 메시지 전달 방식이 동기 방식보다 매우 우월할 수 있지만 OOP 환경에 쉽게 적용하기 어렵다는 것이 큰 단점이다. 무엇보다 동기화된 절차적 프로그래밍은 가장 배우기 쉽고 이해하기 쉬운 프로그래밍 방식이다. 다른 외부 요소에 대해 신경쓰지 않고, 단순한 1인칭 시점에서 1차원적 흐름의 프로그램을 작성할 수 있기 때문이다.

Transaction 이란 -그 기술적 정의는 좀 더 복잡하지만- 상호연관된 데이터를 조작하는 절차적 코드의 집합이다. 모든 프로그램은 특정 트랜젝션을 정확히 수행하는 것이 그 목적이라 하겠다.
비동기적 Actor 모델은 건널목 신호등과 같이 강력한 중앙통제가 필요한 상황을 처리할 때는 전혀 적합하지 않다. 신호등의 메시지를 바꾸라고 메시지를 보냈는데, 신호등 담당자(Actor)가 잠시 낮잠을 자고 있다면 어떻게 되겠는가? 

기존의 동기화 기술로 상당히 안전한 멀티쓰레드 프로그램을 개발하는 것은 가능하다. 그러나 그 프로그램은 단지 특정 조건 하에서 잘 수행되고 있을 뿐, 조금만 조건이 변하면 곧바로 위험에 빠질 수 있다. 기존의 모든 동기화 기술은 단지 특정코드 영역 내부의 쓰레드 안전성을 확보할 수 있을 뿐, 해당 영역 밖의 외부 코드, 즉 트랜젝션 단위의 쓰레드 안전성은 전혀 보장하지 못하기 때문이다. 아래의 단순한 자바 코드를 멀티쓰레드 환경에서 안전하게 수행되는 것을 보장하려면, buffer.setLength(buffer.getLength() + 1);
아래와 같이 변경해주어야만 한다. 물론 이는 setLength() 함수도 내부적으로 동기화를 하고 있다는 가정 하에서만 안전성이 보장된다.  
synchronized (buffer) { buffer.setLength(buffer.getLength() + 1); } 그런데, 저런 작업을 모든 코드에 걸쳐 한다는 것은 그야말로 막노동일 것이다. 무엇보다 buffer 객체가 전혀 쓰레드간 공유가 발생하지 않는다면 위의 코드는 전혀 쓸데없는 과부하만 만들 뿐이다. 트랜젝션 처리를 위한 데이터 동기화를 필요에 따라 적절히 자동으로 처리할 수 있는 방법은 없을까?

4. Atomic Module Programing(AMP)
AMP 의 주된 목적은 개발자에게 의존하지 않고, 개발 언어 및 플랫폼 차원에서 코드의 쓰레드 안전성을 자동으로 보장 또는 완벽히 검증함으로써 멀티쓰레드 코딩을 매우 쉽게 할 수 있는 프로그래밍 환경을 제공하는 것이다. AMP 는 공유 메모리를 모듈이라 불리우는 논리적 단위로 나누고, 모듈 단위로 데이터 동기화 및 쓰레드 안정성 검사를 자동화함으로써, 멀티쓰레드 환경에서 동기적/비동기적인 트랜젝션 실행의 안전성을 확보한다.

AMP Module 은 비동기형 Actor 를 확장한, 상호 결합(동기화) 가능한 조립형 공간이다.
여러 배를 하나로 묶으면 배 사이를 자유로이 왕래할 수 있듯이, 필요에 따라 여러개의 Actor 를 하나의 그룹으로 묶어 동기적인 API 호출을 자유롭게 허용할 수 있다.
하나의 트랜젝션 내에서 사용되는 모든 모듈은 (그 필요한 시간만큼) 모두 하나의 시공간으로 동기화되며, 특정 쓰레드가 독점적, 배타적으로 사용할 수 있다. 즉 그 절차적 수행과정은 겉보기엔 단일 쓰레드 환경과 동일해진다.
AMP Module 의 정의

  1. 모듈은 다음의 제약 조건을 가지는 class 이다. 1) Object 또는 다른 Module class 만을 상속할 수 있다. 2) 그 내부를 다른 클래스에서 들여다 볼 수 없다. 즉, public 멤버 변수를 가질 수 없다.
  2. 모듈은 논리적으로 고유한 공유 메모리 영역을 가진다.
  3. 모듈 함수 실행시 해당 모듈은 자동으로 동기화(lock)된다.
  4. 모듈 함수의 반환값은 모듈 공유가 가능한 객체이어야 한다. (8항 참조)
  5. 쓰레드는 모듈을 상속한다. 즉 쓰레드는 항상 고유한 모듈을 가진다.
  6. 글로벌 변수의 사용이 불가능하다. (글로벌 모듈로 대체된다.)
  7. 모든 객체는 모듈에 속하며, 하나의 객체가 두 개 이상의 모듈에 속할 수 없다. 단, 아래의 특수 객체 또는  특수 상태의 객체는 모든 모듈이 공유할 수 있다. * 모듈: 모듈은 모든 객체가 자유로이 참조 가능하다. 모듈의 소속 모듈은 그 자신이다. * 불변자료형 객체: 해당 객체의 데이터 뿐 아니라 해당 객체를 통해 참조할 수 있는 모든 데이터가 항상 동일한 값을 유지하는 객체이다. * 읽기전용 객체: 불변자료형을 포함한 내부 데이터 변경이 불가능한 객체. 다음 함수로 가변객체의 읽기전용 객체를 생성할 수 있다. 해당 객체의 데이터 변경 시도시 오류가 발생한다.
    immtable Data d2 = data.cloneImmutable();
    * 로컬 객체: 객체가 생성된 후 아직 특정 모듈에 귀속되지 않은 상태의 객체를 가리킨다. (특정 모듈 또는 특정 모듈에 속한) 다른 객체의 필드에 해당 객체를 대입할 때 객체의 귀속 모듈이 결정된다.
    * 스택 모듈 및 스택 객체: 스택 내에 생성된 객체는 스택 모듈에 속한다. 함수 인자로 사용될 수 있으며 자유로이 변경 가능하나, 다른 모듈에 귀속될 수 없다 * forien 객체 : 모듈과 그 반환값의 동기화를 유지하기 위하여 시스템 내부에서 임시적으로 사용된다. 해당 객체 사용시 해당 객체가 속한 모듈을 자동으로 동기화한다. forien 객체는  일반  field 저자장할 수 없고 면 반드시 아래의 forien_ref 중 하나를 이용하여야 한다.
    * unique_ref, weak_forien_ref, strong_forien_ref 등: 귀속 모듈의 변경 또는 간접 참조를 위한 특수한 타입이다. 그 실제값을 사용할 때, 관련 모듈의 동기화가 필요하다.

비동기 메시지 전달

AMP는 모든 객체에 대해 비동기 메시지를 전달할 수 있다. 비동기 메시지의 반환값은 두 가지 방식으로 받을 수 있다. 1) 콜백 함수를 이용하거나, 2) 메시지를 시스템 큐에 전달한 직후 생성된 Future<return_type> 형태의 결과값 수신용 객체를 이용하여 임의의 시점에 값을 확인할 수 있다. 
다른 OOP 언어들 중에도 아래와 비슷한 비동기 함수 처리를 지원하는 예가 있으나 그 처리 과정의 쓰레드 안정성을 보장되지 않기 때문에 매우 제약적인 조건에서만 사용 가능하다. AMP 는 비동기 메시지 처리 과정의 쓰레드 안정성을 보장함으로써 사용의 제약조건을 없에고, Module 기반의 안전한 비동기 Actor 모델을 제공한다.

ex 1)
anyObject ! dotSomeValue() -> (x) { printf("%d", x); } Future<int> asyncRes = anyObject ! dotSomeValue();
ex2) Reactive 방식. (콜백 함수 리스트 생성) anyObject ! doSomething() -> (x) { printf("first visit %d", x);  return anyObject ! a_method_returns_integer(x + 1); } -> anyOtherObject ! a_method_with_a_integer_argument_and_returns_void
-> this.any_method_with_no_argments_and_returns_string
-> (s) {
printf("last visit %s", s);
}
비동기 메시지 전달 및 처리 과정은 아래와 같다.

먼저 메시지를 송신하는 쓰레드에서는 다음의 과정을 수행하고,
  1. 수신 객체가 다른 모듈에 속한 경우, 함수 인자 중 8항에 해당되지 않는 가변객체들은 자동으로 복제(DeepCopy)하여 그 값을 교체한다.
  2. 함수정보와 인자를 시스템 내부 메시지 큐에 저장한다.
  3. Future<return_type> 형태의 결과 수신용 객체를 호출 쓰레드에 반환한다.

이후 시스템 쓰레드 풀에 속한 임의의 쓰레드를 통해서 다음 과정이 수행한다.
  1. 메시지를 수행하기 직전에 메시지 수신 객체가 속한 모듈을 동기화하고 수행 직후 해제한다. 
  2. 메시지를 수행 결과값이 Future 형 객체인 경우, 결과값이 생성될 때 까지 메시지를 UNFINISHED 상태로 표시하여 대기 메시지 큐에 넣고 1항부터 재실행.
  3. 결과 수신용 콜백 함수 리스트가 빈 상태면 1항부터 재실행.
  4. Future 형을 포함한 메시지 수행 결과값의 준비가 완료되면, 결과 수신용 콜백 함수 리스트에서 가장 상단의 함수를 꺼내어 해당 함수와 결과값을 메시지 큐에 저장한다. 
  5. 1 항부터 재실행.

참고) Future 는 함수의 결과값을 비동기적으로 수신하기 위한 Future/Promise 패턴의 Future 를 가리키는 것이다.
콜백 함수 리스트를 이용한 Reactive 방식의 도입 등은 AMP 의 기본 개념 설명에는 필요없는 부분이나 비동기 방식 사용시 그 안정성을 보장하는 AMP의 유용성을 돋보이게 하기 위하여 추가하였다. 콜백 함수 수행 시의 오류 처리 등 좀더 구체적인 내용은 생략한다.

5. Transaction 과 쓰레드 안전성.

AMP Transaction 은 다음 조건에서 자동 생성된다.

  1. 비동기 메시지 처리 시 자동으로 Transaction 생성된다.
  2. 쓰레드 생성시 별도 Transaction 이 생성되고  비동기 메시지 대기(Wait) 상태로 변경되면 Transaction 이 종료된다.  
  3. Sub-transaction 등을 생성하여 동기화 검증 단위를 세분화할 수 있다.
AMP Transaction 수행 중에는 아래의 작업을 자동으로 처리한다. 참고로 AMP 환경 내에서 모든 코드는 Transaction 내에서 수행된다. 즉 아래의 작업은 항시적으로 이루어진다.
  1. 참조하는 모든 모듈의 자동 동기화 정확히는 모든 모듈은 해당 모듈의 함수가 호출되기 직전에 자동으로 동기화 된다. (자동 동기화 대신에 모듈 함수 사용 전에 항상 해당 모듈을 명시적으로 동기화하지 않으면 시스템 오류를 발생시키는 방법도 있다. 이 방법은 코딩을 번거롭게 하기는 하나, 동기화와 관련된 코드 부분을 명시화하는 이점이 있다. 다만, 명시적인 동기화 방식은 소스를 수정할 수 없는 외부 라이브러리를 사용하는 경우, 문제가 될 수 있다.)
  2. 모듈 간 가변객체 공유 금지. 모듈은 논리적으로 독립된 별도의 메모리 영역이다. 한 객체가 두 개의 주소값을 가질 수 없듯이 한 객체가 여러 개의 모듈에 속할 수 없다. 모든 객체는 자신이 소속된 모듈을 표시하는 내부값을 가진다. 동기적 함수 호출 시에는 임의의 객체를 다른 모듈의 함수 인자로 자유로이 전달할 수 있다. 사용중인 모든 모듈이 자동으로 locking 되어 있으므로 동기적 함수 호출 시엔 다른 쓰레드가 인자로 사용된 객체의 내부값을 변경할 수 없음을 보장하기 때문이다. 다만, 해당 인자로 받은 객체를 모듈의 내부 자료 구조에 포함시키면 시스템 오류가 발생한다. (참고로, 인자 타입을 지정하는 Syntax를 확장하여 모듈 내에 귀속시킬 수 없는 인자형을 명시함으로써 Compile 단계에서 이 문제를 해소할 수 있다.) 객체의 소속 모듈을 결정하는 간단한 의사코드는 다음과 같다. Object* AMPVM::newObject(Class* clazz) { Object* newObj = _allocateObject(clazz); newObj->module = (newObj instanceof Module) ? newObj : null; return newObj; } void AMPVM::assignField(Object* obj, FieldInfo* fi, Object* value) { if (value->module == null) { value->module = obj->module; } else if (value->module != obj->module && ! value->isPureImmutable() && ! (value instanceof Module)) { throwModuleConfilictError(); } .... }
  3. 깨어진 동기화 금지 AMP 를 적용했을 때, 서론부의 예제 코드 중 trafficLight 가 모듈이 아니라면 해당 코드는 Thread-safe 한 코드가 된다. trafficLight 는 단 하나의 모듈에 속해 있고, 해당 모듈의 함수는 항상 모듈이 동기화된 상태로 실행되기 때문이다. 단, trafficLight 가 모듈이라 가정하면 그 처리는 조금 복잡해진다. Module 함수 수행 도중 모듈은 자동 동기화되므로 trafficLight.setColor 함수 수행 직전 trafficLight 객체는 자동으로 동기화(lock)되지만, 함수 종료와 동시에 동기화 상태가 해제되므로 두 함수를 호출하는 중간에는 신호등은 여전히 위험한 상태에 놓이게 된다. AMP는 이러한 깨어진 동기화(=동일 객체를 두 번 이상 독립적으로 동기화) 오류를 감지하면 시스템 오류를 발생시킨다. 위의 예제 코드의 함수를 실행하는 과정은 아래와 같이 안전한 코드일 때만 시스템 오류없이 수행된다. (이미 동기화된 모듈에 대한 중첩된 locking 은 오류가 아니다.) synchronized (trafficLight) { trafficLight.setColor(RED); waitTrainPassthrough(); trafficLight.setColor(Green); } 또는 아래와 같이 수정할 수도 있다. synchronized (...) { // 이 영역 내에서 처음 동기화된 모듈은 영역 종료 시까지 lock 상태를 유지한다. // 동기화할 모듈을 명확히 알 수 없을 때 사용한다. } 깨어진 동기화 오류 발견 시 시스템 오류를 발생시키는 대신에 Waring Log 만 만들고, 프로그램 수행을 계속하게 할 수도 있다. 깨어진 동기화는 오류가 발생할 가능성이 있음을 뜻하는 것이지 그로 인해 오류가 발생했음을 검증하는 것은 아니다. 실 서버 운영 시엔 생성된 Waring Log 를 분석하여 실제 오류가 발생하기 전에 신속하게 안전한 코드로 변경하는 전략을 취할 수 있을 것이다.
AMP는 위의 방법으로 Thread dead lock 을 제외한 모든 쓰레드 안정성 관련 문제, 즉 단일 쓰레드 환경이라면 문제없을 코드가 멀티쓰레드 환경에서 예상치 않은 데이터 오류를 만드는 문제를 완벽하게 검증하고 예방한다. 

6. Transaction 동기화의 문제점 - Dead lock

Transaction 내부의 동기화, 즉 비교적 긴 시간의 동기화는 Thread dead lock 이란 복병을 피해가기 어렵다. AMP 는 모듈이란 매우 큰 단위로 쓰레드 동기화를 관리하므로 일반 코딩 시보다 Thread dead lock 발생 가능성은 매우 높아지게 된다.

Thread dead lock 은 여러 개의 쓰레드가 상호 종속적인 수행 경로를 가지는, Multi-thread 에 부적합한 설계의 결과물인 경우가 대부분이다. 이를 피하는 확실한 방법은 비동기 메시지 전달 방식이며, AMP는 비동기 메시징을 기본적으로 지원한다. 그러나, 비동기 메시지 전달 방식을 사용하는 순간 코드 설계 변경의 범위가 커지고, 건널목 신호등 예제 코드처럼 Transaction 단위의 데이터 동기화를 관리하는 것은 매우 어렵고 복잡하다는 것이 문제다.
Dead-lock 문제의 해결

dead-lock은 일련의 객체를 순자척으로 locking 하기 때문에 발생하는 문제이다. 해당 객체들을 동시에 locking 하면 Thread dead lock 문제를 제거할 수 있다.

첫 번째로, 문제가 되는 모듈들을 그룹으로 묶어 어느 하나가 locking 될 때, 다른 하나도 자동으로 함께 locking 되도록 하는 방법과
두 번째로 아래와 같이 여러 개의 모듈을 동시에 동기화할 수 있도록 하는 방법이 있다.
          synchronized(moduleA, moduleB, moduleC, …) {
   // do something.
}
위의 방식은 아래와 같이 좀 더 진보된 방식으로 사용이 가능하다.
reqiure_synchronized(moduleA, moduleB, moduleC, …);

reqiure_synchronized 와 synchronized 의 차이점은 synchronized 는 동기화의 시작과 끝 영역을 지정함과 달리, reqiure_synchronized 는 동기화를 시작하고 그 끝은 정하지 않는다, 각 모듈의 동기화 종료 영역은 코드 내부의 synchronized 영역에 의존함으로써 각 모듈의 lock을 조금이라도 더 짧게 유지하는 것이다.


참고로, Thread dead-lock 을 줄이기 위해 내부 동기화 과정을 read_lock 과 rw_lock으로 구분할 수 있다. 모든 동기화는 처음엔 read_lock 으로 시작하고, 최초의 데이터 변경 직전에 rw_lock으로 변경함으로써 read_lock 상태에서는 여러개의 쓰레드의 병렬수행이 가능하도록 할 수 있을 것이다.


7. 퍼포먼스 문제는 없는가?


굳이 위험을 감수하면서 멀티 쓰레드 코딩을 하는 가장 큰 이유는 퍼포먼스 때문이다.
AMP를 이용하여 쓰레드 안전성을 얻는 대가로 얼마나 퍼포먼스를 희생해야만 할까?
몇 가지 예상 문제점에 대해선 미리 답을 찾아보았다.
  1. 모듈 간의 통신이 빈번하게 발생하는 경우엔 잦은 동기화(lock/unlock)로 인한 퍼포먼스에 악영향을 끼치지 않을까?
    모듈 내부에선 동기화가 전혀 필요없어지기 때문에 퍼포먼스의 차이는 미미하거나 오히려 좋아질 수도 있을 것이다.
  2. 동기화 범위를 정교하게 최소화한 경우와 달리 Module 이라는 매우 큰 단위의 동기화 방식은 쓰레드간 동기화 충돌이 자주 발생하지 않을까?
    동기화 충돌 그 자체는 퍼포먼스와 관계가 없다. 동기화 과정 속에서 CPU idle 상태로 빠지지만 않는다면, 좁은 범위의 잦은 동기화보다는 오히려 큰 범위의 동기화가 퍼포먼스 확보에 유리하다. (Task-switching 횟수가 적을 수록 CPU Cache 의 효율은 높아진다.)
  3. 비동기 메시지 전달 방식을 사용할 때 읽기전용 객체가 아닌 모든 인자를 복제(DeepCopy)하는 과정의 오버해드가 매우 크다.
    비동기 메시지용 인자는 읽기전용 객체, 정확히 말해 다른 쓰레드에 의해 그 내부 값이 변경될 수 없는 객체만을 사용해야만 하는 것은 AMP 의 문제가 아닌 비동기 메시지 전달 방식의 필수적인 조건이다. (참고로, 메시지 전달 및 처리를 모두 단일 쓰레드에서 처리한다면 이러한 복제과정은 불필요하다.)
    AMP는 비동기 메시지 인자의 DeepCopy 를 자동으로 처리하여 안정성을 보장하는 것이 장점이다. DeepCopy 가 필요하지 않거나 그 범위가 너무 큰 경우엔 별도의 모듈로 감싸서 전달하거나 (이번 글엔 소개를 생략한) 몇 가지 확장 기법을 써서 오버헤드를 줄일 수 있을 것이다. 특정 모듈 함수에 대해서 동기적 또는 비동기적 메시지 전달만 가능하도록 문법적으로 명시함으로써 대규모 객체의 복제가 발생하지 않도록 하는 방법도 유용할 것으로 생각된다.
  4. Lock-free 기법 등 Parallel-Computing 을 적용하기에 부적합하다. 특히 실시간 응답이 필요한 시스템 개발에 적합하지 않다. AMP 는 기본적으로 Lock 기반의 Concurrent 프로그래밍 모듈이며, 속도의 향상보다는 안정성 확보를 목표로 하고 있기에 Parellel-Computing 또는 Real-time programing 과는 그 출발점부터 다르다. 다만, Parellel-computing 이나 실시간 프로그래밍은 매우 난이도가 높아 코드 일부에만 매우 제한적으로만 사용되는 경우가 많다. AMP 의 특별한 모듈 또는 함수들에 한해 자동 동기화 및 안정성 검사를 생략하는 방법으로 쉽게 해결할 수도 있으나 좀 더 근본적인 해결책은 외부 C/C++ 라이브러리와의 연동을 좀 더 쉽게 해주는 것일 수 있다. CPU를 직접적으로 다루지 않는 한 속도의 최적화는 항상 한계가 있을 것이다. (Lock-free 연산을 위해 일련의 함수를 '안전하게' 예외 처리하는 방법에 대해서는 다음 글에 소개를 하도록 하겠다.)

8. AMP의 장점과 실용성

AMP의 장점
  1. 쓰레드 안정성 검증의 자동화.
  2. 멀티쓰레드 환경에서 Transaction 단위의 데이터 정합성 확보가 가능하다
  3. 동기 및 비동기 메시지 전달 방식 모두를 지원하며, 혼합 사용이 가능하다.
  4. 모듈은 완전한 밀폐성과 교환성을 가진다.
모듈 함수가 정상 동작하는 한 모듈 내부의 코드 및 구조 변경은 완전히 자유롭다.
동일한 API 를 가진 정상적 모듈은 상호 자유로이 교체될 수 있다.


일반적인 어플리케이션 개발 시 쓰레드를 많이 사용하는 경우는 상당히 드물다. 일반 UI 프로그램의 경우, 두세 개의 모듈만 사용하고 가끔 비동기 메시징 정도를 사용하면 쓰레드간 동기화는 충분히 처리될 것이다. 이 경우엔 AMP 는 일반적인 OOP 코딩 방식과 거의 유사할 것이므로 사용의 어려움은 거의 없을 것으로 보인다. 즉 쓰레드 안전성과 관련된 시스템 오류는 거의 발생하지 않을 것이다. 대용량 실시간 게임에 적용은 어렵더라도 조금 복잡한 멀티 쓰레드 코딩을 하면서 그 안정성을 쉽게 확보하고 싶다면 AMP는 매우 좋은 대안이 될 것이라 여겨진다. 물론 이 경우엔 쓰레드 안정성 관련 시스템 오류가 무수히 발생할 수도 있을 것이다. 다만, 대부분의 잠재된 모든 오류를 개발 과정에서 제거할 수 있을 것이다. 모든 쓰레드 안정성 문제를 모듈이란 비교적 매우 소수의 특정 객체간의 문제로 치환하여 분석할 수 있으므로 소스 분석을 통해 문제를 사전 분석하기도 쉬워질 것이다. Thread dead lock 문제도 적절한 프로파일링을 통해 실제 문제가 발생하기 전에 분석하여 수정하는 것도 가능할 수 있을 것이다.

멀티쓰레드 코딩에 관심있는 개발자들의 적극적인 평가와 조언을 기대해 본다. 후기 1)
존경하는 지인께서 AMP Module 이 Partitioned Global Address Space(PGAS) 프로그래밍 모델과 유사하다고 알려 주셔서 관련 자료를 찾아 보았다. Global Memory 를 논리적인 독립 공간으로 구획하는 개념은 서로 동일하다. 그 차이점으로는 PGAS 는 이를 사용하기 위한 별도의 API 가 필요한 일종의 라이브러리인 반면에 AMP 는 그 과정이 전체 시스템 범위에서 자동화된 일종의 언어적인 기능이라는 것, PGAS 의 주된 응용 분야가 Parallel-computing 인 반면, AMP 는 비동기적 메시지 전달의 안전성 확보 및 쓰레드 동기화를 자동화하기 위한 기본 단위로 모듈을 사용하는 것이 차이점이라 하겠다. 어쩌면 AMP 모듈을 병렬컴퓨팅에도 적합한 개념으로 확대 가능할 것도 같다. 좋은 지적에 감사드리는 바이다.

후기 2)
문제점 추가. AMP를 적용한 프로그램 설계 단위로서의 모듈 개념의 모호함.

1) 모듈의 크기를 어떤 기준으로 정해야 할 지 모호하다. 현재로선 가능한 최대 크기로 모듈을 설계할 수록 사용이 더 편리해진다. 안전 영역이 더 커지고, 그 내부에선 모듈간 연동의 번거로움 없이 기존의 OOP 코딩 스타일로 코딩을 할 수 있기 때문이다. Erlang Process 달리 모듈을 매우 작은 단위까지 잘게 쪼갤 수 있는 구조로 사용하기에 적합하지 않아 보인다.


2) 특정 객체를 어떤 모듈에 넣어야 할지 결정하기 쉽지 않다. 모듈 내부의 밀폐성은 모듈 버젼의 호환성(API 만 동일하면 동일 기능 보장)을 극대화할 수 있다. 그러나, 기존의 코드를 수정하여 객체를 여러 개의 모듈로 나눌 때 객체간 종속성을 결정하는 것이 쉽지 않을 수 있다. 상호 종속성을 가지는 모듈을 조합하여 해당 모듈 간의 객체 공유를 허락하는 일정의 복합 모듈의 개념 등이 필요할 수 있다.