2014년 3월 10일 월요일

격리된 쓰레드 메모리 모델 (1)

- 들어가는 말 : Thread-Safety 와 Transaction

Thread-Safety 란 상호 연관된 두 개 이상 데이터를 참조하거나 변경하는 일련의 코드를 다중 쓰레드 환경에서 수행할 때, 관련 데이터간의 연관성을 정확히 유지할 수 있는가를 가리킨다.


한 쓰레드가 일련의 데이터 변경을 마치기 전에 다른 쓰레드가 일부 데이터를 변경한다면 데이터간 연관성을 유지할 수 없다. 이런 문제는 특정 함수를 통해서만 해당 데이터들의 변경이 가능하도록 강제하고, 해당 함수 내에서 적절한 동기화(synchronization) 처리를 함으로써 쉽게 해결할 수 있다.


그러나 데이터 참조 시에는 위와 같이 쉽게 해결되지 않는다. 데이터에 대한 참조는 읽어오는 과정만이 아니라 그 사용이 끝나야 종료되기 때문이다. 이로 인해 큰 크기의 Vector 형 객체나 Tree 형 객체의 내부 데이터를 안전하게 사용하기 위해선 긴 시간의 동기화가 필요하다. 긴 시간의 동기화는 퍼포먼스 감소 및 내부의 다른 동기화 과정과 얽혀 쓰레드 데드락이 발생하는 원인이 되기도 한다. 이런 경우 비동기 처리방식을 주로 사용하게 되는데, 도중에 데이터 변경이 발생하더라도 처리의 정합성을 보장하는 것은 까다로운 작업에 속한다.


이렇듯 Thread-Safety 문제는 데이터를 읽고 쓰는 과정만이 아니라, 연관성을 가진 일련의 데이터를 처리하는 전체 과정으로 확대된다. 입력된 데이터와 그 결과물의 정합성이 필요한 데이터 처리 단위를 Transaction 이라 한다면, Thread-Safety 란 다중 쓰레드 환경에서 특정 Transaction 수행의 안정성을 의미하게 된다.


아래에 소개하고자 하는 Isolated Thread Memory Model (ITM : 격리된 쓰레드 메모리 모델) 은 위에 언급된 Transaction 단위의 Thread-Safety 를 용이하게 확보하는 방법에 대한 개인적 아이디어를 정리해 본 것이다.




- Isolated Thread Memory Model (ITM)

ITM 은 아래의 규칙을 가진 개발 방법론과 그 적용 방법에 관한 것이다. 
  1. Thread-Safe 함수는 그 내부에서 Thread-Safe Access 만이 허용된다.
  2. Thread-Safe Access 는 아래와 같으며, Thread-Safety 를 보장한다. - 다른 Thread 에 의해 변경될 수 없는 메모리의 참조 및 변경 - Thread-Safe 함수 호출.
  3. thread_managed 함수는 규칙 1의 제약을 받지 않는 예외적인 Thread-Safe 함수이다. - 즉, Thread-Safe 함수에 의해 호출될 수 있다. - 내부 구현시 Thread-Safety 문제를 스스로 해결하여야 한다.
  4. 트랜젝션 관리 클래스 내부에 한해 thread_managed 함수를 추가할 수 있다. 
  5. 트랜젝션 관리 클래스의 객체를 Thread-Safe 함수의 인자로 전달할 수 있다.
위의 규칙을 이용하면 Thread-Safe 한 코드 영역과 그렇지 않은 영역을 명확하게 나눌수 있다. Thread-safety 와 관련된 문제를 트랜젝션 관리 클래스 내로 한정하여 집중시킴으로써 분석 및 처리의 효율성을 높히게 된다. 아울러 트랜젝션 단위의 Thread Safety 처리 또한 트랜젝션 해당 클래스 내부로 한정해서 처리하는 것이 가능해진다.
그러나, 일반적인 언어로 코딩 시엔  위의 규칙 2항을 개발자가 직관적으로 판단하여 준수하는 것이 불가능하다. 이에, 위의 방법론을 적용하기 위해선 특정 함수가 위의 규칙을 만족하는지 즉, Thread-Safety 를 보장하는지 여부를 증빙할 수 있는 자동화된 도구가 필수적으로 필요하다.
아래에 C++, Java 등의 OOP 언어에 몇 가지 문법적 요소와 특별한 클래스를 추가함으로써 Thread-Safety 여부를 컴파일 단계에서 명확히 확인할 수 있는 방법을 제시해 보았다.  
아래 내용은 초안이며, 사용된 용어나 정의들은 이후 계속 수정될 수 있음을 미리 밝혀둔다. 아래보다 더 좋은 규격안을 제시하거나 잘못되거나 부족한 점에 대한 지적은 기쁘게 환영하겠다.

- Special classes

아래 나열된 클래스들의 하위 클래스들은 아래와 같이 특별 처리한다.
  • StackLocal class - 해당 객체는 다른 쓰레드와 공유될 수 없다.
    - 해당 객체는 스택 내부에 생성된다. - 해당 객체는 항상 Thread-Safe 객체 상태를 갖는다. - StackLocal 객체 만이 StackLocal 객체를 필드로 가질 수 있다.
  • SynchronizedAtomic class - 해당 객체가 동기화되어 있는 동안 외부 쓰레드가 데이터를 변경할 수 없다.
    - 해당 객체가 동기화되어 있는 동안 Thread-Safe 객체 상태를 갖는다.
    - java 의 SynchronizedCollection 이 이에 해당.
  • Immutable class - 해당 객체는 생성이 완료된 후, 내부 데이터 변경이 불가능하다. - java 의 String, Enum 이 이에 해당.

  • ITMTransaction class - thread_managed 함수를 가질 수 있다. - StackLocal 클래스의 하위 클래스이다. 즉 스택 내부에만 생성된다. - 중요한 특수 함수. protected: void cancelTransaction(Exception e) thread_managed; // Exception 에 의해 비정상 종료시 자동 호출된다. public: thread_safe<T> getSnapshot(T*) thread_managed; // 주어진 객체의 복사본을 반환한다. protected: thread_safe<T> guaranteeThreadSafe(T*) thread_managed; // 주어진 포인터를 thread_safe 포인터로 변환한다. protected: thread_safe<T> lock(SynchronizedAtomic*) thread_managed; // 주어진 객체를 동기화 상태로 만들고 thread_safe 포인터를 반환한다. protected: void lock(SynchronizedAtomic*) thread_managed; // 주어진 객체를 동기화 상태를 해제한다.


- Thread-Safe 객체 상태 변경과 ITMTransaction

특정 객체가 외부 쓰레드에 의한 데이터 변경이 불가능한 상태에 있는 경우 해당 객체를 Thread-Safe 객체라 부른다.
특정 객체의 포인터 값을 다른 쓰레드와 공유된 메모리에 기록하는 순간 해당 객체는 Thread-Safe 상태를 잃게 된다.  
Thread-Safe 하지 않은 객체를 Thread-Safe 상태로 변경하는 가장 바람직한 방법은 getSnapshot() 함수를 이용해 해당 객체의 복사본을 얻는 것이다. 새로운 복사본의 변경 사항은 트랜젝션이 성공적으로 종료한 경우에 한해서 실제 메모리에 반영되므로 Thread-Safety 문제를 발생시키지 않는다. (이에 대해서는 다음 글에 설명).
특정 객체가 동기화(synchronized)된 상태일 때 그 데이터 변경이 불가능하다면, 해당 상태 동안 Thread-Safe 객체로 취급되며, 명시적 변환없이 해당 포인터를 thread_safe 포인터로 사용할 수 있다. 이를 사용하기 위해선 Java 의 synchronized 키워드처럼 동기화 영역을 명확히 설정할 수 있는 방법이 다른 언어에도 필요하다.
단, Thread-Safe 함수 내에서는 동기화 처리가 금지된다. 해당 함수를 잠깐 수행하는 동안의 동기화 처리는 문제되지 않더라도, 전체 트랙젝션 단위에서 볼 때는 Thread-Safe 하지 않을 수 있기 때문이다. 트랜젝션 단위의 긴 동기화는 lock(), unlock() 함수를 통해 처리한다. 참고로, Exception 발생시 unlock 처리는 자동으로 이루어진다.
또한, 특정 객체에 대한 포인터가 다른 쓰레드와 공유된 상태이기는 하나, 해당 객체에 대한 독점적인 변경 권한을 가지고 있으며, 내부 데이터 변경시 동기화가 필요없는 것을 확증할 수 있을 때는 해당 객체를 Thread-Safe 상태로 변경하여도 무방하다. 이런 경우 guaranteeThreadSafe() 함수를 사용한다.  
(이러한 강제 변환 시에는 Snapshot 이 원본 객체와 동일하게 생성된다.만약 미리
만들어진 Snapshot 이 존재하는 경우에는 오류가 발생한다)


- Thread-Safety Type 선언

변수, 필드, 함수 선언시 아래와 같이 Thread-Safety 유형을 선언한다.
  • thread_safe 포인터 ex) thread_safe <Foo> foo; - Thread-Safe 객체만을 가리키는 포인터이다. - 필드 선언 시에는 StackLocal 유형 클래스 내에서만 사용 가능하다.
  • thread_unsafe 포인터) - Thread-Safety 유형이 명시되지 않은 일반 포인터를 따로 구분하기 위하여 thread_unsafe 포인터라 칭한다.
    - 해당 포인터가 가르키는 객체의 Thread-Safe 상태를 확인할 수 없음을 의미한다.
  • local 함수 - ex) Foo* getValue() local; - Thread-Safe 객체 상태인 경우에 한해, Thread-Safety 가 보장되는 함수이다. - Thread-Safe-Access 외에 추가로 내부 데이터의 참조/변경이 허용된다.
  • thread_safe 함수 - ex) int getType() thread_safe; - Thread Safe 객체 상태와 관계없이 Thread-Safety 를 보장하는 함수이다. - StackLocal 객체 외에는 내부 데이터의 참조/변경이 허용되지 않는다. 단, 변경 불가능한 데이터 참조는 허용된다.
  • local thread_safe 함수 - ex) void setValue(String* value) local thread_safe; - Thread-Safe 객체 상태에서만 호출 가능한 local 함수이다. - 내부 데이터 참조/변경 시에도 Thread-Safety가 보장된다.
  • thread_unsafe 함수 - ex) void setParent(Node* node, Node* parent) thread_unsafe; - thread_unsafe 를 명시하기 위해 사용된다.
  • thread_managed 함수 - ex) void setParent(Node* node, Node* parent) thread_managed; - Thread-Safety 문제 처리를 담당하는 함수이다. - ITMTransaction 및 그 하위 클래스 내에서만 선언가능하다.
  • 함수의 Thread Safety 기본값 - Immutable 유형 클래스 내 함수 : thread_safe - ITMTransaction 유형 클래스 내 함수 : thread_safe - 그 외 : thread_unsafe
  • 사용 예 class Buffer { char* data; int length; Buffer(int length) { data = new char[length]; this->length = length; } char* getBuffer() local; int getLength() local; int getType() thread_safe; void init(thread_safe<char> data, int len) local thread_safe; };

- thread_safe 포인터 속성 변환

  • StackLocal 객체에 대한 포인터는 명시적 선언이 없어도 항상 thread_safe 포인터로 취급된다.

  • thread_unsafe 포인터는 ITMTransaction 내부 함수를 통해서만 thread_safe 포인터로 전환될 수 있다.
  • thread_safe 포인터는 thread_unsafe 포인터로 변환될 수 있다.
  • new operator, clone 등 객체 생성용 함수는 thread_safe 포인터를 반환한다.



- 해설과 문제점.

이로써 기존 언어에 추가되어야 하는 문법적 요소와 규칙에 대한 정의는 마무리되었다.  
위 문법적 요소는 임의의 thread_safe 포인터가 가리키는 객체의 Thread-Safe 상태가 항상 유지됨을 보장하기 위한 것으로 요약될 수 있다.
위 문법은 소스 분석만으로 Thread-Safety를 증빙하기 위해선 필수적으로 필요하긴 하나 그로 인해 코드 개발에 있어 큰 제약을 가지게 되어 실용성에 문제를 안고 있다.
StackLocal 유형을 제외한 모든 클래스 필드는 thread_safe 포인터를 필드로 가질 수 없다. 이 때문에 거의 모든 함수가 thread_unsafe 포인터를 반환하게 된다. 실제로 특정 Thread-Safe 함수 내에서 ITMTransaction 객체를 사용하지 않았다면, 해당 함수는 -그 복잡성 여부를 떠나- 함수형 언어로 변환이 가능할 정도로 Thread-Safe 함수의 제약 조건은 가혹하다.
Thread-Safe 함수 내에서 공유된 객체의 Sanpshot 생성을 자동으로 처리해주면 Thread-Safe 함수의 구현이 상당히 용이해질 수 있다. 이러한 자동처리는 기존의 Transactional Memory Model 과 개념상 동등한 Transaction 단위의 메모리 관리 기술이 필요하게 된다. 다음 글에서는 이와 관련된 메모리 버젼 관리 시스템과 기존의 표준 라이브러리 활용 방안에 대해서도 다뤄보고자 한다.
그에 앞서 이번 글은 위의 방법론과 문법적 요소에 대해 객관적 검증의 기회를 얻고자 쓰여진 것이다. 이 글은 최소의 문법적 요소를 추가함으로써 특정 함수의 Thread-Safety를  검증하는 것이 가능하다는 것을 증명하는 것이 주목적이다. 이에 내부의 메모리 관리 방식 등 좀 복잡한 내용은 다음로 미루고자 한다.
이 글의 논리적 오류에 대한 구체적인 지적과 조언을 기다린다.
끝으로 이해를 돕고자 아래에 간단한 Java 예제 코드를 추가하였다.



- Java 코드 예제


class Result { ... } interface Inspector { void inspect(String content) thread_safe; }; class ConcurrentTransaction extends ITMTransaction { Vector targetUrls; thread_safe<Inspector> inspector;  thread_safe<Vector> results ;
ConcurrentTransaction(Vector searchUrls, Inspector inspector) { this.targetUrls = searchUrls; this.inspector = inspector.clone(); this.results = new Vector; } public thread_safe<Vector> runTask() local throws Exception { while (true) { String url = popData(); if (url == null) { return; } try { Result res = doTask(url); if (res != null) { results.add(res); } } catch (Exception e) { logDebugInfo(e); } } return results; } Result doTask(String url) thread_safe { // 특정 Web-Page 열어, 내용을 다운로드 받아 문자열 생성. return inspector.inspect(webContents); } String popData() thread_managed { synchronized (targetUrls) { int cntRemainTask = targetUrls.size(); if (cntRemainTask == 0) { return null; } String url = targetUrls.lastElement(); targetUrls.setSize(cntRemainTask - 1); return url; } } void addResult(Result res) thread_managed { results.add(res); } private void commitResultToDBMS() thread_managed { // 결과를 DB 에 기록. } // Exception 에 의한 비정상 종료 시 자동 호출된다. protected void cancelTransaction(Exception e) thread_managed { logDebugInfo(e); } private void logDebugInfo(Exception e) thread_managed { // 로그 기록 } }

댓글 1개:

  1. 위 내용은 자체 검토 결과 이론적 오류는 없는 듯 하나, 실용성에 매우 문제가 많음이 밝혀졌습니다.
    함수의 인자나 필드 수준까지 thread_safe 속성을 확장할 수 있어야만 ITMTransaction 클래스에 대한 의존도를 대폭 줄일 수 있는데, 그것이 가능하지 않은 것으로 판단됩니다.
    현 상태에서는 대부분의 코드가 ITMTransaction 클래스로 모이게 되어, 코딩 편이성 및 활용성 면에서 의미가 없는 것 같습니다.
    이에 자동화된 코드 분석을 통해 객체가 사용되는 위치에 따라 인자나 field 레벨의 thread_safety 를 미리 분석하는 시스템에 대해 따로 고민을 해보고자 합니다. 자바의 경우, 어플 실행에 필요한 모든 코드를 전수검사하여 특정 클래스가 아닌 각각의 객체의 단위로 Thread-Safety 를 분석하는 것이 가능하다고 판단됩니다만, 그 가능성 여부 확인 작업에 상당히 시일이 걸릴 것 같습니다.

    답글삭제