Transactional memory (TM TS)
트랜잭셔널 메모리는 명령문 그룹을 트랜잭션으로 결합하는 동시성 동기화 메커니즘으로, 이들은
- atomic (모든 명령문이 실행되거나, 아무것도 실행되지 않음)
- isolated (트랜잭션 내 명령문들은 다른 트랜잭션에서 수행된 부분 쓰기 작업을 관찰할 수 없음, 병렬로 실행되는 경우에도)
일반적인 구현에서는 지원되는 경우 하드웨어 트랜잭셔널 메모리를 사용 가능한 범위까지 사용하고(예: 변경 세트가 포화될 때까지), 소프트웨어 트랜잭셔널 메모리로 폴백합니다. 이는 일반적으로 낙관적 동시성을 통해 구현됩니다: 다른 트랜잭션이 해당 트랜잭션에서 사용하는 일부 변수를 업데이트한 경우, 트랜잭션은 자동으로 재시도됩니다. 이러한 이유로 재시도 가능한 트랜잭션("원자적 블록")은 트랜잭션-안전 함수만 호출할 수 있습니다.
변수를 트랜잭션 내부와 외부에서 다른 외부 동기화 없이 접근하는 것은 데이터 경쟁(data race)입니다.
기능 테스트가 지원되는 경우, 여기서 설명하는 기능들은 매크로 상수 __cpp_transactional_memory 가 201505 이상의 값을 가질 때 나타납니다.
목차 |
동기화된 블록
synchronized
복합문
프로그램의 모든 최상위 동기화 블록이 단일 전체 순서로 실행되는 것처럼, 전역 락 아래에서 복합문 을 실행합니다: 각 동기화 블록의 끝은 해당 순서에서 다음 동기화 블록의 시작과 동기화됩니다. 다른 동기화 블록 내에 중첩된 동기화 블록은 특별한 의미론을 갖지 않습니다.
동기화된 블록은 트랜잭션이 아닙니다(아래의 atomic 블록과 다름). 트랜잭션-안전하지 않은 함수들을 호출할 수 있습니다.
#include <iostream> #include <thread> #include <vector> int f() { static int i = 0; synchronized { // 동기화 블록 시작 std::cout << i << " -> "; ++i; // f()의 각 호출은 i의 고유한 값을 얻음 std::cout << i << '\n'; return i; // 동기화 블록 종료 } } int main() { std::vector<std::thread> v(10); for (auto& t : v) t = std::thread([] { for (int n = 0; n < 10; ++n) f(); }); for (auto& t : v) t.join(); }
출력:
0 -> 1 1 -> 2 2 -> 3 ... 99 -> 100
동기화된 블록을 어떤 방식으로든 벗어날 때(블록 끝에 도달하거나, goto, break, continue, return을 실행하거나, 예외를 던지는 경우) 해당 블록을 종료하고, 종료된 블록이 외부 블록이었다면 단일 전체 순서에서 다음 블록과 동기화됩니다. std::longjmp 를 사용하여 동기화된 블록을 벗어나는 경우의 동작은 정의되지 않습니다.
goto 또는 switch로 동기화 블록에 진입하는 것은 허용되지 않습니다.
동기화된 블록은 전역 락 하에서 실행되는 것처럼 동작하지만, 구현체들은 각 블록 내의 코드를 검사하고 트랜잭션 안전 코드에 대해서는 낙관적 동시성(가능한 경우 하드웨어 트랜잭션 메모리로 지원)을 사용하고, 트랜잭션 안전하지 않은 코드에 대해서는 최소한의 락킹을 사용할 것으로 기대됩니다. 동기화된 블록이 인라인되지 않은 함수를 호출할 때, 함수가
transaction_safe
(아래 참조)로 선언되지 않거나
[[optimize_for_synchronized]]
속성(아래 참조)이 사용되지 않는 한, 컴파일러는 추측 실행을 중단하고 전체 호출 주변에 락을 유지해야 할 수 있습니다.
원자적 블록
| 이 섹션은 불완전합니다 |
atomic_noexcept
복합문
atomic_cancel
복합문
atomic_commit
복합문
atomic_cancel
블록에서 트랜잭션 취소에 사용되는 예외는
std::bad_alloc
,
std::bad_array_new_length
,
std::bad_cast
,
std::bad_typeid
,
std::bad_exception
,
std::exception
그리고 이것으로부터 파생된 모든 표준 라이브러리 예외, 그리고 특수 예외 타입인
std::tx_exception<T>
입니다.
원자적 블록 내의
compound-statement
은
transaction_safe
가 아닌 어떤 표현식이나 문장을 실행하거나 함수를 호출하는 것을 허용하지 않습니다(이는 컴파일 시간 오류입니다).
// f()의 각 호출은 병렬로 수행될 때도 i의 고유한 값을 검색합니다 int f() { static int i = 0; atomic_noexcept { // 트랜잭션 시작 // printf("before %d\n", i); // 오류: 트랜잭션 안전 함수가 아닌 함수를 호출할 수 없음 ++i; return i; // 트랜잭션 커밋 } }
예외 이외의 방법(끝까지 도달, goto, break, continue, return)으로 atomic 블록을 벗어나면 트랜잭션이 커밋됩니다. std::longjmp 를 사용하여 atomic 블록을 벗어나는 경우 동작은 정의되지 않습니다.
트랜잭션 안전 함수
| 이 섹션은 불완전합니다 |
함수는 선언부에서 transaction_safe 키워드를 사용하여 명시적으로 트랜잭션 안전으로 선언될 수 있습니다.
| 이 섹션은 불완전합니다 |
람다 선언에서, 이것은 캡처 목록 바로 뒤에 나타나거나, (키워드
mutable
이 사용된 경우) 그 키워드 바로 뒤에 나타납니다.
| 이 섹션은 불완전합니다 |
extern volatile int * p = 0; struct S { virtual ~S(); }; int f() transaction_safe { int x = 0; // 정상: volatile이 아님 p = &x; // 정상: 포인터가 volatile이 아님 int i = *p; // 오류: volatile glvalue를 통한 읽기 S s; // 오류: 안전하지 않은 소멸자 호출 }
int f(int x) { // 암시적으로 트랜잭션-안전 if (x <= 0) return 0; return x + f(x - 1); }
트랜잭션 안전 함수에 대한 참조 또는 포인터를 통해 트랜잭션 안전하지 않은 함수가 호출되면, 그 동작은 정의되지 않습니다.
트랜잭션 안전 함수에 대한 포인터와 트랜잭션 안전 멤버 함수에 대한 포인터는 각각 함수 포인터와 멤버 함수 포인터로 암시적으로 변환 가능합니다. 결과 포인터가 원본과 동일하게 비교되는지 여부는 명시되지 않습니다.
트랜잭션 안전 가상 함수
| 이 섹션은 불완전합니다 |
transaction_safe_dynamic
함수의 최종 오버라이더가
transaction_safe
로 선언되지 않은 경우,
원자 블록 내에서 해당 함수를 호출하는 것은 정의되지 않은 동작입니다.
표준 라이브러리
새로운 예외 템플릿 std::tx_exception 을 도입하는 것 외에도, 트랜잭셔널 메모리 기술 사양은 표준 라이브러리에 다음과 같은 변경 사항을 만듭니다:
-
다음 함수들을 명시적으로
transaction_safe로 만듭니다:
-
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, 전역 기본
operator new
, 전역 기본
operator delete
,
std::allocator::construct
(호출된 생성자가 트랜잭션 안전한 경우),
std::allocator::destroy
(호출된 소멸자가 트랜잭션 안전한 경우),
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, 트랜잭션 취소를 지원하는 모든 예외 타입의 각 비가상 멤버 함수 (위의
atomic_cancel참조)이 섹션은 불완전합니다
이유: 더 많은 내용이 있습니다
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, 전역 기본
operator new
, 전역 기본
operator delete
,
std::allocator::construct
(호출된 생성자가 트랜잭션 안전한 경우),
std::allocator::destroy
(호출된 소멸자가 트랜잭션 안전한 경우),
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, 트랜잭션 취소를 지원하는 모든 예외 타입의 각 비가상 멤버 함수 (위의
-
다음 함수들을 명시적으로
transaction_safe_dynamic으로 만듭니다
-
-
트랜잭션 취소를 지원하는 모든 예외 타입의 각 가상 멤버 함수(위의
atomic_cancel참조)
-
트랜잭션 취소를 지원하는 모든 예외 타입의 각 가상 멤버 함수(위의
-
Allocator
X에서 트랜잭션 안전(transaction-safe)한 모든 연산들이
X::rebind<>::other에서도 트랜잭션 안전해야 함을 요구합니다
속성
속성
[[
optimize_for_synchronized
]]
은 함수 선언의 선언자에 적용될 수 있으며 함수의 첫 번째 선언에 반드시 나타나야 합니다.
함수가 하나의 번역 단위에서
[[optimize_for_synchronized]]
로 선언되고 동일한 함수가 다른 번역 단위에서
[[optimize_for_synchronized]]
없이 선언된 경우, 프로그램의 형식이 잘못되었습니다; 진단은 필요하지 않습니다.
함수 정의가 synchronized 문에서의 호출에 최적화되어야 함을 나타냅니다. 특히, 대부분의 호출에 대해 트랜잭션 안전하지만 모든 호출에 대해 안전하지는 않은 함수 호출을 수행하는 동기화된 블록의 직렬화를 방지합니다 (예: 재해시가 필요할 수 있는 해시 테이블 삽입, 새로운 블록을 요청해야 할 수 있는 할당자, 가끔 로깅을 수행할 수 있는 간단한 함수).
std::atomic<bool> rehash{false}; // 유지보수 스레드가 이 루프를 실행합니다 void maintenance_thread(void*) { while (!shutdown) { synchronized { if (rehash) { hash.rehash(); rehash = false; } } } } // 작업자 스레드들은 매초 수십만 번 이 함수를 호출합니다 // 다른 번역 단위의 synchronized 블록에서 insert_key()를 호출하면 // 해당 블록들이 직렬화됩니다. 단, insert_key()가 [[optimize_for_synchronized]]로 // 표시된 경우는 예외입니다 [[optimize_for_synchronized]] void insert_key(char* key, char* value) { bool concern = hash.insert(key, value); if (concern) rehash = true; }
GCC 어셈블리에서 속성이 없을 경우: 전체 함수가 직렬화됨
insert_key(char*, char*): subq $8, %rsp movq %rsi, %rdx movq %rdi, %rsi movl $hash, %edi call Hash::insert(char*, char*) testb %al, %al je .L20 movb $1, rehash(%rip) mfence .L20: addq $8, %rsp ret
GCC 어셈블리와 속성:
transaction clone for insert_key(char*, char*): subq $8, %rsp movq %rsi, %rdx movq %rdi, %rsi movl $hash, %edi call transaction clone for Hash::insert(char*, char*) testb %al, %al je .L27 xorl %edi, %edi call _ITM_changeTransactionMode # 참고: 이 부분이 직렬화 지점입니다 movb $1, rehash(%rip) mfence .L27: addq $8, %rsp ret
|
이 섹션은 불완전합니다
이유: 트렁크로 어셈블리 확인, 호출자 측 변경 사항도 표시 |
참고 사항
|
이 섹션은 불완전합니다
이유: Wyatt 논문/발표 경험 노트 |
키워드
atomic_cancel , atomic_commit , atomic_noexcept , synchronized , transaction_safe , transaction_safe_dynamic
컴파일러 지원
이 기술 사양은 GCC 버전 6.1부터 지원됩니다(활성화하려면 - fgnu - tm 이 필요함). 이 사양의 이전 변형은 GCC 4.7부터 지원되었습니다 .