스레드 로컬 스토리지

컴퓨터 프로그래밍에서 스레드 로컬 스토리지(Thread-local storage, TLS)는 정적 또는 전역 메모리를 스레드에 지역적으로 사용하는 메모리 관리 방법이다. 이 개념은 별도의 스레드가 있는 시스템에서 전역처럼 보이는 데이터를 저장할 수 있도록 한다.

많은 시스템은 스레드 지역 메모리 블록의 크기에 제한을 두며, 실제로 상당히 엄격한 제한을 두기도 한다. 반면에, 시스템이 적어도 메모리 주소(포인터) 크기의 변수 스레드 지역을 제공할 수 있다면, 이는 그러한 메모리 블록을 동적으로 할당하고 해당 블록의 메모리 주소를 스레드 지역 변수에 저장함으로써 임의 크기의 메모리 블록을 스레드 지역 방식으로 사용할 수 있게 한다. RISC 머신에서는 호출 규약이 종종 이 용도를 위해 스레드 포인터 레지스터를 예약한다.

용례
전역 변수의 사용은 일반적으로 현대 프로그래밍에서 권장되지 않지만, 유닉스와 같은 일부 오래된 운영 체제는 원래 단일 프로세서 하드웨어를 위해 설계되었으며 중요한 값을 저장하는 데 전역 변수를 자주 사용한다. 한 예로 C 라이브러리의 많은 함수에서 사용되는 errno가 있다. 여러 스레드가 errno 변수를 수정할 수 있는 현대 머신에서는 한 스레드에서 시스템 함수를 호출하면 다른 스레드에서 시스템 함수를 호출하여 이전에 설정된 값이 덮어쓰여질 수 있으며, 이로 인해 다른 스레드의 후속 코드가 오류 조건을 확인하기 전에 문제가 발생할 수 있다. 해결책은 errno를 전역처럼 보이지만 물리적으로는 스레드별 메모리 풀인 스레드 로컬 스토리지에 저장되는 변수로 만드는 것이다.

두 번째 사용 사례는 여러 스레드가 정보를 전역 변수에 누적하는 경우이다. 경쟁 상태를 피하기 위해 이 전역 변수에 대한 모든 접근은 뮤텍스로 보호되어야 한다. 대신, 각 스레드는 스레드 지역 변수에 누적하여 경쟁 상태의 가능성을 제거하고 락의 필요성을 없앨 수 있다. 그러면 스레드는 자체 스레드 지역 변수에서 단일 전역 변수로 최종 누적을 동기화하기만 하면 된다.

윈도우 구현
응용 프로그래밍 인터페이스(API) 함수 TlsAlloc은 사용되지 않는 TLS 슬롯 인덱스를 얻는 데 사용될 수 있으며, TLS 슬롯 인덱스는 '사용 중'으로 간주된다.

TlsGetValue 및 TlsSetValue 함수는 TLS 슬롯 인덱스로 식별되는 스레드 지역 변수에 메모리 주소를 읽고 쓰는 데 사용된다. TlsSetValue는 현재 스레드의 변수에만 영향을 미친다. TlsFree 함수는 TLS 슬롯 인덱스를 해제하는 데 호출될 수 있다.

각 스레드에는 Win32 스레드 정보 블록이 있다. 이 블록의 항목 중 하나는 해당 스레드의 스레드 로컬 스토리지 테이블이다.
TlsAlloc을 호출할 때마다 이 테이블에 고유한 인덱스가 반환된다. 각 스레드는 TlsSetValue(index, value)를 독립적으로 사용하고 TlsGetValue(index)를 통해 지정된 값을 얻을 수 있다. 이는 스레드 자체 테이블에서 항목을 설정하고 조회하기 때문이다.

TlsXxx 함수군 외에도 Windows 실행 파일은 실행 중인 프로세스의 각 스레드에 대해 다른 페이지에 매핑되는 섹션을 정의할 수 있다. TlsXxx 값과 달리 이러한 페이지는 임의의 유효한 주소를 포함할 수 있다. 그러나 이러한 주소는 각 실행 스레드마다 다르므로 비동기 함수(다른 스레드에서 실행될 수 있음)에 전달하거나 가상 주소가 전체 프로세스 내에서 고유하다고 가정하는 코드에 전달해서는 안 된다. TLS 섹션은 메모리 페이징을 사용하여 관리되며 그 크기는 페이지 크기(x86 머신에서는 4KB)로 양자화된다.
이러한 섹션은 프로그램의 주 실행 파일 내에서만 정의될 수 있다. DLL에는 이러한 섹션을 포함해서는 안 된다. LoadLibrary로 로드할 때 올바르게 초기화되지 않기 때문이다.

POSIX 스레드 구현
POSIX 스레드 API에서 스레드에 지역적인 메모리는 스레드 특정 데이터(Thread-specific data)라는 용어로 지정된다.

pthread_key_create 및 pthread_key_delete 함수는 각각 스레드 특정 데이터에 대한 키를 생성하고 삭제하는 데 사용된다. 키의 유형은 명시적으로 불투명하게 남겨지며 pthread_key_t로 참조된다. 이 키는 모든 스레드에서 볼 수 있다. 각 스레드에서 키는 pthread_setspecific을 통해 스레드 특정 데이터와 연결될 수 있다. 데이터는 나중에 pthread_getspecific을 사용하여 검색할 수 있다.

또한 pthread_key_create는 스레드 특정 데이터가 NULL이 아닐 경우 스레드 종료 시 자동으로 호출될 소멸자 함수를 선택적으로 받을 수 있다. 소멸자는 키와 관련된 값을 매개변수로 받아 정리 작업을 수행할 수 있다(연결 닫기, 메모리 해제 등). 소멸자가 지정되더라도 프로그램은 프로세스 수준에서 스레드 특정 데이터를 해제하기 위해 pthread_key_delete를 호출해야 한다(소멸자는 스레드에 지역적인 데이터만 해제한다).

언어별 구현
프로그래머가 적절한 API 함수를 호출하는 것에 의존하는 것 외에도 스레드 로컬 스토리지(TLS)를 지원하도록 프로그래밍 언어를 확장하는 것도 가능하다.

C 및 C++
C11에서는 _Thread_local 키워드가 스레드 지역 변수를 정의하는 데 사용된다. 
 헤더는 지원되는 경우 thread_local을 해당 키워드의 동의어로 정의한다. 사용 예시:

#include 

thread_local int foo = 0;

C11에서 
는 또한 로 시작하는 이름을 사용하여 스레드 로컬 스토리지를 검색, 변경 및 파괴하는 여러 함수를 정의한다. C23에서는 thread_local 자체가 키워드가 된다.

C++11은 다음과 같은 경우에 사용할 수 있는 thread_local 키워드를 도입했다.
 네임스페이스 수준(전역) 변수
 파일 정적 변수
 함수 정적 변수
 정적 멤버 변수

그 외에도 다양한 컴파일러 구현은 스레드 지역 변수를 선언하는 특정 방법을 제공한다.
 Solaris Studio C/C++, IBM XL C/C++, GNU C, llvm-gcc, 클랭, 및 인텔 C++ 컴파일러 (리눅스 시스템)는 다음 구문을 사용한다.
 __thread int number;
 Visual C++, 인텔 C/C++ (윈도우 시스템), C++빌더, 클랭, 및 Digital Mars C++는 다음 구문을 사용한다.
 __declspec(thread) int number;
 C++빌더는 또한 다음 구문을 지원한다.
 int __thread number;

비스타 및 서버 2008 이전의 윈도우 버전에서는 __declspec(thread)가 DLL이 실행 파일에 바인딩된 경우에만 작동하며 LoadLibrary()로 로드된 DLL에서는 작동하지 않는다(보호 오류 또는 데이터 손상이 발생할 수 있음).

커먼 리스프 및 기타 방언
커먼 리스프는 동적 스코프 변수라는 기능을 제공한다.

동적 변수는 함수 호출과 해당 함수가 호출하는 모든 자식 함수에 대해 비공개인 바인딩을 가진다.

이 추상화는 자연스럽게 스레드 특정 저장소에 매핑되며, 스레드를 제공하는 리스프 구현은 이를 수행한다. 커먼 리스프에는 수많은 표준 동적 변수가 있으며, 따라서 동적 바인딩에서 이러한 변수가 스레드 지역 의미론을 갖지 않으면 언어 구현에 스레드를 합리적으로 추가할 수 없다.

예를 들어, 표준 변수 *print-base*는 정수가 인쇄되는 기본 진수를 결정한다. 이 변수가 재정의되면 모든 둘러싸는 코드는 정수를 다른 진수로 인쇄한다.

;;; 함수 foo와 그 자식 함수는
;; 16진수로 인쇄한다.
(let ((*print-base* 16)) (foo))

함수가 다른 스레드에서 동시에 실행될 수 있다면 이 바인딩은 스레드 지역이어야 한다. 그렇지 않으면 각 스레드가 전역 인쇄 진수를 제어하기 위해 서로 싸울 것이다.

D
D 버전 2에서는 모든 정적 및 전역 변수가 기본적으로 스레드 지역이며 다른 언어의 "일반" 전역 및 정적 변수와 유사한 구문으로 선언된다. 전역 변수는 shared 키워드를 사용하여 명시적으로 요청해야 한다.

int threadLocal; // 이것은 스레드 지역 변수이다.
shared int global; // 이것은 모든 스레드와 공유되는 전역 변수이다.

shared 키워드는 저장소 클래스이자 타입 한정자 역할을 한다. 공유 변수는 데이터 무결성을 정적으로 강제하는 몇 가지 제한 사항이 있다. 이러한 제한 없이 "고전적인" 전역 변수를 선언하려면 안전하지 않은 __gshared 키워드를 사용해야 한다.

__gshared int global; // 이것은 일반적인 전역 변수이다.

자바
자바에서는 스레드 지역 변수가 클래스 객체에 의해 구현된다. ThreadLocal은 T 타입의 변수를 저장하며, 이는 get/set 메서드를 통해 접근할 수 있다. 예를 들어, Integer 값을 저장하는 ThreadLocal 변수는 다음과 같다.
private static final ThreadLocal
 myThreadLocalInteger = new ThreadLocal
();

적어도 Oracle/OpenJDK의 경우, 자바 스레딩의 다른 측면에서 OS 스레드가 사용되더라도 이는 네이티브 스레드 로컬 스토리지를 사용하지 않는다. 대신, 각 Thread 객체는 ThreadLocal 객체에서 해당 값으로 매핑되는(Thread 객체에서 값으로 매핑되는 ThreadLocal을 가져와 성능 오버헤드를 발생시키는 것과 반대) (스레드 안전하지 않은) 맵을 저장한다.

.NET 언어: C# 및 기타
닷넷 프레임워크 언어(예: C 샤프)에서는 정적 필드를 ThreadStatic 속성으로 표시할 수 있다.

class FooBar
{
 [ThreadStatic]
 private static int _foo;
}

.NET Framework 4.0에서는 System.Threading.ThreadLocal
 클래스를 사용하여 스레드 지역 변수를 할당하고 지연 로딩할 수 있다.
class FooBar
{
 private static System.Threading.ThreadLocal
 _foo;
}

또한 스레드 지역 변수를 동적으로 할당하는 API도 사용할 수 있다.

오브젝트 파스칼
오브젝트 파스칼 (델파이) 또는 프리 파스칼에서는 'var' 대신 'threadvar' 예약어를 사용하여 스레드 로컬 스토리지를 사용하는 변수를 선언할 수 있다.
var
 mydata_process: integer;
threadvar
 mydata_threadlocal: integer;

오브젝티브-C
코코아, 그누스텝, 오픈스텝에서 각 NSThread 객체는 스레드의 threadDictionary 메서드를 통해 접근할 수 있는 스레드 지역 사전을 가지고 있다.
NSMutableDictionary *dict = [[NSThread currentThread] threadDictionary];
dict[@"A key"] = @"Some data";

펄
펄에서는 언어 진화 후반에 스레드가 추가되었는데, 이미 CPAN에 많은 기존 코드가 존재한 이후였다. 따라서 펄에서는 기본적으로 모든 변수에 대해 자체 지역 저장소를 가져와 스레드가 기존의 스레드 비인식 코드에 미치는 영향을 최소화한다. 펄에서 스레드 공유 변수는 속성을 사용하여 생성할 수 있다.
use threads;
use threads::shared;

my $localvar;
my $sharedvar :shared;

퓨어베이직
퓨어베이직에서 스레드 변수는 Threaded 키워드로 선언된다.

 Threaded Var

파이썬
파이썬 버전 2.4 이상에서는 threading 모듈의 local 클래스를 사용하여 스레드 로컬 스토리지를 생성할 수 있다.
import threading
mydata = threading.local()
mydata.x = 1local 클래스의 여러 인스턴스를 생성하여 서로 다른 변수 집합을 저장할 수 있다. 따라서 이는 싱글턴이 아니다.

루비
루비는 []=/[] 메서드를 사용하여 스레드 지역 변수를 생성/접근할 수 있다.

Thread.current[:user_id] = 1

러스트

러스트에서는 러스트 표준 라이브러리에서 제공하는 매크로를 사용하여 스레드 지역 변수를 생성할 수 있다.

use std::cell::RefCell;
use std::thread;

thread_local!(static FOO: RefCell
 = RefCell::new(1));

FOO.with(|f| {
 assert_eq!(*f.borrow(), 1);
 *f.borrow_mut() = 2;
});

// 이 스레드가 이미 스레드 지역 값의 복사본을 2로 변경했음에도 불구하고, 각 스레드는 초기 값인 1로 시작한다.
let t = thread::spawn(move || {
 FOO.with(|f| {
 assert_eq!(*f.borrow(), 1);
 *f.borrow_mut() = 3;
 });
});

// 스레드가 완료될 때까지 기다리고 패닉 시 종료한다.
t.join().unwrap();

// 자식 스레드가 해당 스레드의 값을 3으로 변경했음에도 불구하고, 원래 스레드는 원래 값인 2를 유지한다.
FOO.with(|f| {
 assert_eq!(*f.borrow(), 2);
});

 같이 보기 
 OpenMP — "데이터 공유 속성 절"(§절 참조)을 통해 스레드별 저장소를 지원하는 또 다른 공유 메모리 다중 처리 기능

 각주 

 참고 문헌 

 

 외부 링크 
 스레드 로컬 스토리지를 위한 ELF 처리 — C 또는 C++ 구현에 대한 문서.
 ACE_TSS< TYPE > 클래스 템플릿 참조
 RWTThreadLocal
 클래스 템플릿 문서
 더그 도덴스의 문서 "스레드 특정 데이터 전달에 스레드 로컬 스토리지 사용"
 로렌스 크롤의 "스레드 로컬 스토리지"
 월터 브라이트의 문서 "공유가 항상 좋은 것은 아니다"
 자바에서 ThreadLocal의 실제 사용법: http://www.captechconsulting.com/blogs/a-persistence-pattern-using-threadlocal-and-ejb-interceptors
 GCC ""
 러스트 ""

분류:소프트웨어 디자인 패턴
분류:변수 (컴퓨터 과학)
분류:스레드