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 가 그런 솔루션이 될 수 있기를 기대해 본다.


댓글 없음:

댓글 쓰기