命名空間
變體
動作

事務記憶體 (TM TS)

出自 cppreference.com
< cpp‎ | language
 
 
C++ 語言
一般主題
流程控制
條件執行陳述式
if
疊代陳述式 (迴圈)
for
範圍 for (C++11)
跳躍陳述式
函式
函式宣告
Lambda 函式運算式
inline 指定符
動態例外規範 (直到 C++17*)
noexcept 指定符 (C++11)
例外
命名空間
型別
指定符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
儲存期指定符
初始化
 
 

事務記憶體是一種並行同步機制,它將一組語句組合在事務中,這些事務是:

  • 原子的(要麼所有語句都執行,要麼什麼都不執行)
  • 隔離的(事務中的語句無法觀察到另一個事務產生的、尚未完成的寫入,即使它們是並行執行的)

典型的實作會盡可能地使用硬體事務記憶體(例如在變更集飽和之前),並回退到軟體事務記憶體,通常使用樂觀併發機制實現:如果另一個事務更新了該事務使用的某些變數,它將會靜默重試。因此,可重試的事務(「原子區塊」)只能呼叫事務安全(transaction-safe)的函式。

請注意,在沒有其他外部同步的情況下,在事務內部和事務外部存取同一個變數會導致資料競爭(data race)。

如果支援功能測試,此處描述的功能由巨集常數 __cpp_transactional_memory 指示,其值大於或等於 201505

目錄

[編輯] 同步區塊

synchronized 複合語句

執行該複合語句,效果就像在全域鎖之下執行一樣:程式中所有最外層的同步區塊以單一總順序執行。每個同步區塊的結束與該順序中下一個同步區塊的開始同步。嵌套在其他同步區塊內的同步區塊沒有特殊語義。

同步區塊不是事務(與下方的原子區塊不同),並且可以呼叫非事務安全的函式。

#include <iostream>
#include <thread>
#include <vector>
 
int f()
{
    static int i = 0;
    synchronized { // begin synchronized block
        std::cout << i << " -> ";
        ++i;       // each call to f() obtains a unique value of i
        std::cout << i << '\n';
        return i;  // end synchronized block
    }
}
 
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 複合語句

1) 如果拋出例外,則呼叫 std::abort
2) 如果拋出例外,則呼叫 std::abort,除非該例外是用於事務取消的例外(見下文),在這種情況下事務會被「取消」:程式中所有由原子區塊操作的副作用所修改的記憶體位置,其值會恢復為執行原子區塊開始時的值,且例外會如往常般繼續進行堆疊回退。
3) 如果拋出例外,事務會正常提交。

用於 atomic_cancel 區塊中事務取消的例外包括 std::bad_allocstd::bad_array_new_lengthstd::bad_caststd::bad_typeidstd::bad_exceptionstd::exception 及其所有衍生自標準庫的例外,以及特殊例外型別 std::tx_exception<T>

原子區塊中的 複合語句 不允許執行任何表達式、語句或呼叫任何非 transaction_safe 的函式(這是編譯期錯誤)。

// each call to f() retrieves a unique value of i, even when done in parallel
int f()
{
    static int i = 0;
    atomic_noexcept { // begin transaction
//  printf("before %d\n", i); // error: cannot call a non transaction-safe function
        ++i;
        return i; // commit transaction
    }
}

以例外以外的任何方式離開原子區塊(到達結尾、goto、break、continue、return)會提交事務。如果使用 std::longjmp 退出原子區塊,行為未定義。

[編輯] 事務安全函式

透過在宣告中使用關鍵字 transaction_safe,可以明確地將函式宣告為事務安全的。

lambda 宣告中,它出現在捕獲列表之後,或緊接在 (關鍵字 mutable(如果使用了)) 之後。

extern volatile int * p = 0;
struct S
{
    virtual ~S();
};
int f() transaction_safe
{
    int x = 0;  // ok: not volatile
    p = &x;     // ok: the pointer is not volatile
    int i = *p; // error: read through volatile glvalue
    S s;        // error: invocation of unsafe destructor
}
int f(int x) { // implicitly transaction-safe
    if (x <= 0)
        return 0;
    return x + f(x - 1);
}

如果透過事務安全函式的參考或指標呼叫非事務安全函式,行為未定義。


事務安全函式的指標和事務安全成員函式的指標分別隱式轉換為函式指標和成員函式指標。生成的指標是否與原始指標比較相等,未作規定。

[編輯] 事務安全虛擬函式

如果 transaction_safe_dynamic 函式的最終覆寫函式(final overrider)沒有被宣告為 transaction_safe,則在原子區塊中呼叫它是未定義行為。

[編輯] 標準庫

除了引入新的例外模板 std::tx_exception 外,事務記憶體技術規範還對標準庫進行了以下更改:

  • 將以下函式明確標記為 transaction_safe
  • 將以下函式明確標記為 transaction_safe_dynamic
  • 所有支援事務取消的例外型別的每個虛擬成員函式 (參見上文 atomic_cancel)
  • 要求對 Allocator X 為事務安全的所有操作,對 X::rebind<>::other 也必須是事務安全的。

[編輯] 屬性

屬性 [[optimize_for_synchronized]] 可應用於函式宣告中的宣告子,且必須出現在該函式的第一次宣告上。

如果在一個翻譯單元中函式被宣告為 [[optimize_for_synchronized]],而在另一個翻譯單元中未帶此屬性宣告同一個函式,則程式格式錯誤;不需要診斷。

它指示函式定義應針對從 synchronized 語句呼叫進行最佳化。特別地,它避免序列化那些呼叫「對大多數呼叫是事務安全但並非所有情況」之函式的同步區塊(例如可能需要重新雜湊的雜湊表插入、可能需要請求新區塊的分配器,或極少需要記錄日誌的簡單函式)。

std::atomic<bool> rehash{false};
 
// maintenance thread runs this loop
void maintenance_thread(void*)
{
    while (!shutdown)
    {
        synchronized
        {
            if (rehash)
            {
                hash.rehash();
                rehash = false;
            }
        }
    }
}
 
// worker threads execute hundreds of thousands of calls to this function 
// every second. Calls to insert_key() from synchronized blocks in other
// translation units will cause those blocks to serialize, unless insert_key()
// is marked [[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 # Note: this is the serialization point
	movb	$1, rehash(%rip)
	mfence
.L27:
	addq	$8, %rsp
	ret

[編輯] 附註

[編輯] 關鍵字

atomic_cancel, atomic_commit, atomic_noexcept, synchronized, transaction_safe, transaction_safe_dynamic

[編輯] 編譯器支援

此技術規範從 GCC 6.1 版本開始支援(需要 -fgnu-tm 來啟用)。此規範的舊變體在 GCC 4.7 版本中即已獲得支援

English Deutsch 日本語 中文(简体) 中文(繁體)