事務記憶體 (TM TS)
事務記憶體是一種並行同步機制,它將一組語句組合在事務中,這些事務是:
- 原子的(要麼所有語句都執行,要麼什麼都不執行)
- 隔離的(事務中的語句無法觀察到另一個事務產生的、尚未完成的寫入,即使它們是並行執行的)
典型的實作會盡可能地使用硬體事務記憶體(例如在變更集飽和之前),並回退到軟體事務記憶體,通常使用樂觀併發機制實現:如果另一個事務更新了該事務使用的某些變數,它將會靜默重試。因此,可重試的事務(「原子區塊」)只能呼叫事務安全(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 複合語句
用於 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>。
原子區塊中的 複合語句 不允許執行任何表達式、語句或呼叫任何非 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:
- 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 為事務安全的所有操作,對
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
| 本節尚不完整 原因:檢查 trunk 組譯代碼,並顯示呼叫端的變更 |
[編輯] 附註
| 本節尚不完整 原因:來自 Wyatt 論文/演講的經驗筆記 |
[編輯] 關鍵字
atomic_cancel, atomic_commit, atomic_noexcept, synchronized, transaction_safe, transaction_safe_dynamic
[編輯] 編譯器支援
此技術規範從 GCC 6.1 版本開始支援(需要 -fgnu-tm 來啟用)。此規範的舊變體在 GCC 4.7 版本中即已獲得支援。