본문 바로가기

4.개발 및 운영 환경

쓰레드별 전역변수 사용하기

쓰레드별 전역변수 사용하기? 무슨말?

각각 쓰레드에서 같은 변수를 액세스하지만, 쓰레드 별로 서로 다른 값을 사용하는 형태입니다. 말이 정말 어렵습니다.

즉, 전역 변수로 total이 있고 쓰레드A와 쓰레드 B에서 같이 사용한다면 문제가 발생합니다. 각 쓰레드 내에서는 total만 가져오는 쓰레드 내에서만 계산된 값을 사용하려고합니다. 그렇게 한다면 전체 로직이 쓰레드 별 total를 구분(thread_a_total, thread_b_total)할 필요 없이 하나만 사용하기 때문에 로직도 깔끔해집니다. 그리고 쓰레드 간에 충돌도 없어지게 됩니다. 이렇게 해도 이해하기 어렵군요.

아무든 전역 변수가 프로세스 하나에서 공용으로 사용하지만, 쓰레드별 변수는 해당 쓰레드 내에서만 공용으로 사용하고 다른 쓰레드에서는 사용할 수 없다는 말입니다.

여기서는 win32와 pthread를 비교하여 같이 설명합니다. 설명 내용은 그다지 없습니다. 자세한 설명은 아래 참조를 보면 되겠지요 ^^ 그럼 시작하겠습니다.

말이 조금 짧게 끝나니 양해바랍니다. ^^;

작성: http://ospace.tistory.com/,2010.09.01 (ospace114@empal.com)

TLS(Thread Local Storage)

Thread-Specific Data라고도 함. Thread-Local Storage와 Thread-Specific Data는 특정 쓰레드 범위 내에서만 또는 특정 쓰레드에 족송된 저장 공간(데이터)를 의미한다.

아래에서는 pthread와 win32로 비교했는데... linux와 windows와 같은 분류이다. 걍 생각나는데로 분류했다. 어차피 각각 기본 제공되는 부분이 분리되었지만...

좀더 이야기 하면 윈도우용 pthread 라이브러리는 있다. 그렇게 하면 linux와 windows의 분류는 무의미할 수 있다.

초기화와 해제

TLS에 대한 초기화와 해제는 한번만 해주면 된다. 중복해서 초기화하면 안 된다. 그리고 초기화하면 해제도 같이해준다.

주의 할 것은 win32와 pthread는 반환 값이다.

  • win32에서 반환 값이 0이면 실패, 0이 아니면 성공이지만,
  • pthread에서 반환 값이 0이면 성공이고 0이 아니면 실패이다.
// win32
DWORD WINAPI TlsAlloc(void);
BOOL WINAPI TlsFree(
  __in          DWORD dwTlsIndex
);
// pthread
int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *));
int pthread_key_delete(pthread_key_t key);

초기화

먼저 초기화하는 부분을 보도록 하자. 초기화부분에서 인자가 linux와 win32에서 사용하는 방식이 약간 다르다.

Win32

반환값이 TLS_OUT_OF_INDEXES이면 무조건 에러처리 하면 된다. 그 외는 정상 값으로 처리하면 된다.

DWORD aKey= TLS_OUT_OF_INDEXES; // 전역변수로 선언
// ... (중략)
if( TLS_OUT_OF_INDEXES == aKey ) {
  aVal = TlsAlloc ();
    if( TLS_OUT_OF_INDEXES == aKey) {
      // 생성에러
    }
}

pthread

pthread_key_create에서 두 번째 인자는 키가 제거될 때에 호출되는 콜백함수 있다. 콜백함수 인자에는 나중에 설정된 값이 넘겨지게 된다.

메모리 할당이나 다른 곳에서 리소스를 할당 받은 경우 제거 콜백함수를 등록하여 사용하면 된다.

pthread_key_t aKey;
if( 0 != pthread_key_create (&aKey, NULL) ) {
// 에러
}
pthread_key_t aKey;
if( 0 != pthread_key_create (&aKey, free) ) {
    // free는 malloc에 대응되는 것으로 자동으로 메모리 해제해준다.
    // 에러
}

해제

해제는 단순하다. 단순히 키를 넘겨주면 된다. win32와 pthread는 동일하다.

Win32

if( TLS_OUT_OF_INDEXES != aKey) {
    TlsFree (aKey);
    aVal = TLS_OUT_OF_INDEXES;
}

pthread

pthread_key_delete (aKey);

값 저장 및 획득

TLS에 값을 저장하고 획득하는 방식은 win32와 pthread는 동일하다. 단 주의할 것은 앞의 초기화와 해제는 쓰레드 실행 전에 외부에서 쓰레드 초기화할 때 같이 하면 된다. 그러나 값을 저장하고 획득하는 부분은 쓰레드가 동작하는 몸체에서 실행해야한다. 정말 주의가 필요하다.

예를 들어 쓰레드 시작 전에 특정 키로 해서 값을 저장해서 쓰레드 실행 로직에서 해당 값을 가져왔는데 엉뚱한 값이 나온다. 아마 0일 것이다. 당연히 쓰레드 별 저장 공간을 만들어서 해당 쓰레드 내에서만 접근하려는 구조인데, 다른 쓰레드에서 설정한 값을 가져오려고 하기 때문에 원하는 값을 가져올 수 없다.

// win32

BOOL WINAPI TlsSetValue(
  __in          DWORD dwTlsIndex,
  __in          LPVOID lpTlsValue
);

LPVOID WINAPI TlsGetValue(
  __in          DWORD dwTlsIndex
);

// pthread
int pthread_setspecific(pthread_key_t key, const void *pointer);
void * pthread_getspecific(pthread_key_t key);

저장

아래 예제는 malloc로 동적 메모리를 할당하여 사용는 예이다. 반드시 동적할당 필요 없이 값을 넣어서 사용할 수도 있다.

반환 값에 의한 성공이 win32와 pthread가 다르기 때문에 주의를 요한다.

Win32

win32에서 값 설정은 간단하다

char *buf= (char*) malloc(100*sizeof(char)); // 100개 문자열
if( 0 == TlsSetValue (aKey, (LPVOID) buf) ) {
    // 에러
}

pthread

pthread도 값 설정은 간단하다.

char *buf= (char*) malloc(100*sizeof(char)); // 100개 문자열
if( 0 != pthread_setspecific (aKey, (const void*) buf) ) {
    // 에러
}

획득

값을 가져오게 되는 부분이다. 별도 쓰레드 로직 몸체마가 서로 다른 결과를 가져오게 된다. win32와 linux가 구조는 같다. 반환 값이 획득되는 값이다. 보통 획득 값이 0이면 값이 없는 것이라고 보면 된다. 그렇게에 0에 의미있는 값으로 처리하는 형태라면 문제가 발생할 가능성이 있다. 이부분을 고려해서 사용하면 될듯하다.

Win32

값을 가져오고 원래 원하는 데이터 형으로 변환해서 사용하면 된다.

char *buf = (char*) TlsGetValue (aKey);

pthread

win32와 같다.

char *buf = (char*) pthread_getspecific (aKey);

응용

사실 TLS를 사용할 경우는 많지는 않다. 대부분 쓰레드 내에서 사용할 객체를 생성해서 이 객체를 참조하면 된다. 그러나 리눅스의 errno와 같은 형태라면 그렇게 하기 불가능하다. 즉, 어느 곳에서나 errno를 사용하면 thread-safe하게 동작해야하기 때문이다. 이럴 경우 errno는 어느 쓰레드에서 사용하는지에 대한 정보을 넘겨주지는 않는다. 알아서 판단해야 한다.

사실 errno는 이미 thread-safe하게 구성되어 있다. errno를 사용할 경우가 없기에 이런거 몰라도 된다고 생각하면 건너뛰어도 된다.

필자인 경우 직접 프로그램을 제작한 결과 errno와 비슷한 에러 처리 구조를 사용하여 구성하던 중에 쓰레드 관련 문제가 발생한 것이다. 처음에는 원래 해당 에러가 나오면 안되지만, 자꾸 엉뚱한 에러 메시지가 나온다. 이는 다른 쓰레드 처리 로직이 에러 값이 저장하고 그 에러 값을 같이 참조하기 때문에 발생하기 때문이다.

errno를 사용하게 된 이유는 멀티 플랫폼을 지원해야 하면, 기존 errno에 사용자 정의 에러값를 쉽게 정의할 수 없다는 문제점때문에 독립적인 에러 처리 구조를 구성하기 때문이다.

이럴 경우 간단하게 쓰레드에 종속적인 errno 값을 가져오게 하면 모든 쓰레드 관련되 에러처리는 끝나게 된다. 실제 코드는 넣지 않고 간단한 흐름만 설명 하겠다.

우리는 쓰레드를 관리할 관리 정보에 에러 값을 저장해 놓고, errno를 가져올 때에 현재 쓰레드를 확인하고, 해당 쓰레드에 대한 관리 정보를 획득하고 그 안에 에러 값을 참조하도록 하는 구조이다.

  1. 먼저 쓰레드 관리하는 정보 구조체에 에러 값을 선언한다.
  2. TLS 키를 전역 변수로 선언하고 초기화 한다.(키이름은 적당히 설정 - thread_self_key)
  3. 쓰레드를 실행하고 쓰레드 몸체에서 해당 키(thead_self_key)에 쓰레드 관리정보 참조를 저장한다.
  4. 에러 참조가 되면 현재 쓰레드 관리 정보를 지정된 키(thread_self_key)를 통해서 가져온다.
  5. 쓰레드 관리 정보 획득히 성공하면, 해당 관리 정보에 있는 에러 값을 참조하고, 실패하면 기존 에러값을 사용한다.
  6. 사용이 끝나면 쓰레드 해제(?)할 때에 초기화한 키(thread_self_key)를 제거해준다.

이상이다. 보너스로 본인이 사용했던 에러 처리 로직을 보면 다음과 같다.

int32_t* _jerror()
{
    jthread_t *thd = NULL;
    int32_t   *err = NULL;
    // jthread_get_self()은 thread_get_self()비슷한 목적이지만, 원하는 쓰레드 관리정보를 얻기위한 것이다.
    if( NULL == (thd = jthread_get_self ()) ) {
        err = &__jerrno;
    } else {
        err = &thd->err;
    }
    return err;
};

더 간단하게 사용하기

앞의 구현 방식은 가급적 이식성을 높이기 위한 방법이다. 그러나 그런거에 상관이 없다. 나는 특정 컴파일러만 사용한다. 절대 변경할 경우는 없다라고할 경우 더 간단한 방법이 있다.

이 방법은 컴파일러 종속적인 방법이기 때문에 주의를 요한다.

Solaris Studio, IBM XL, GCC, Intel Compiler인 경우는 아래 처럼 사용이 가능하다.

__thread int errno;

Visual C++, Intel (for Windows), C++ Builder, Digital Mars Compiler인 경우는 아래와 같다.

__declspec(thread) int errno;

C++ Builder인 경우 아래 처럼 다른 표기도 가능하다.

int __thread errno;

참고로 java인 경우는 다름 처럼 사용이 가능하다.

private static ThreadLocal<Integer> errno = new ThreadLocal<Integer>();

위의 방법은 컴파일러에 종속적인 방법이기에 이식성은 급격히 떨어지기에 적당히 취사 선택하면 될 것이다.

결론

TLS를 응용한 방법은 매우 많다. 그러나 사실 위의 코드(에러처리) 처럼 쓰레드 관리정보를 얻어온다면, 그 안에 값을 넣어서 참조하는 것도 쉽게 확장할 수 있는 방법일 것이다. 실제 errno를 thread-safe하게 처리하는 것을 보았을 때에는 운영체제에 종속적인 코드가 대부분이어서 서로간에 쉽게 포팅할 수 있는 구조로 만들기는 어렵다. 리눅스인 경우는 실제 쓰레드 제어 블럭을 접근하는 어셈블러를 사용한 것도 있으며, pthread의 고유 pthread_self()라는 함수를 사용하기도 했다.

운영체제 내의 쓰레드 구조는 서로 다르며, 같다고 해도 사용자 원하는 값을 추가해서 사용한다는 것은 매우 어렵다. 위의 win32와 pthread의 함수 모양이 거의 비슷하기 때문에 호환되는 코드를 작성하는 것은 그다지 어렵지 않다. 그래서 pthread의 thread_self() 흉내내어 직접 작성한 쓰레드 제어 정보를 이용하여 원하는 값을 사용할 수 있게된다.

정말 에러 처리에서 thread-safe한 부분을 구현하는게 쉽게 떠오르지 않았다. 해쉬 테이블이나 배열을 두어서 쓰레드 ID를 가져오고 이를 바탕으로 원하는 쓰레드 정보를 획득하여 사용하는 구조도 생각해 보았다. 그러나 이렇게 했을 경우 코드 복잡도가 올라가서 작업량이 커지게 된다.

그래서 찾은 것이 TLS이다. 정말 만세다!!!

간단하게 리눅스와 윈도우 호환 코드를 작성하고, 에러 처리도 완벽하게 되었다.

호환 코드에 상관이 없다라고 하면 간단한 사용법을 적용하면 된다. 정말 간단하게 적용이 된다. ㅡ.,ㅡ;;;

내용이 도움이 되었기를... 모두 즐프하세요...

참조

[1] pthread_key_create, MAN, http://man.kldp.net/wiki/ManPage/pthread_key_create.3

[2] pthread_key_create, http://publib.boulder.ibm.com/iseries/v5r2/ic2924/index.htm?info/apis/users_35.htm

[3] Thread Local Storage, MSDN, http://msdn.microsoft.com/en-us/library/ms686749(VS.85).aspx

[4] Using Thread Local Storage, MSDN, http://msdn.microsoft.com/en-us/library/ms686991(v=VS.85).aspx

[5] Thread-local storage, WIKIPEDIA, http://en.wikipedia.org/wiki/Thread-local_storage

반응형

'4.개발 및 운영 환경' 카테고리의 다른 글

log4cxx 및 apr 설치 및 빌드 환경 구성  (0) 2011.07.20
C++멤버함수 포인터 크기 확인  (0) 2011.01.05
Xwindow에서 한글 폰트  (0) 2010.03.26
D-Bus란  (0) 2010.03.18
Syntaxhightlighter 2.x 사용하기  (0) 2009.11.13