Transactional memory (TM TS)
事务性内存是一种并发同步机制,它将语句组组合在事务中,这些事务
- 原子性(要么所有语句都执行,要么都不执行)
- 隔离性(事务中的语句无法观察到其他事务执行过程中的部分写入结果,即使它们并行执行)
典型实现会在支持的硬件上使用硬件事务内存(直至变更集饱和),并回退到软件事务内存,通常通过乐观并发实现:若其他事务更新了当前事务使用的某些变量,该事务将静默重试。因此,可重试事务("原子块")只能调用事务安全函数。
请注意,在事务内和事务外访问变量且无其他外部同步时,将产生数据竞争。
如果支持功能测试,此处描述的功能由宏常量 __cpp_transactional_memory 指示,其值等于或大于 201505 。
目录 |
同步块
synchronized
复合语句
执行 复合语句 时如同处于全局锁下:程序中所有最外层的synchronized块按单一全序执行。每个synchronized块的结束都与该顺序中下一个synchronized块的开始同步。嵌套在其他synchronized块内部的synchronized块不具有特殊语义。
同步块并非事务(与下方的原子块不同),可能调用非事务安全函数。
#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>
。
原子块中的
复合语句
不允许执行任何非
transaction_safe
的表达式、语句或调用任何非
transaction_safe
函数(这将导致编译时错误)。
// 每次调用 f() 都会获取唯一的 i 值,即使在并行执行时也是如此 int f() { static int i = 0; atomic_noexcept { // 开始事务 // printf("before %d\n", i); // 错误:不能调用非事务安全函数 ++i; return i; // 提交事务 } }
除异常外的任何方式离开原子块(到达末尾、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; // 正确:非volatile变量 p = &x; // 正确:指针本身非volatile int i = *p; // 错误:通过volatile泛左值读取 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 上具有事务安全性的操作,在
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; } } } } // 工作线程每秒执行数十万次此函数的调用 // 来自其他翻译单元中同步块对 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
|
本节内容不完整
原因:需使用trunk版本检查汇编代码,同时展示调用方变更 |
注释
|
本节内容不完整
原因:需补充Wyatt论文/演讲中的实践经验总结 |
关键词
atomic_cancel , atomic_commit , atomic_noexcept , synchronized , transaction_safe , transaction_safe_dynamic
编译器支持
此技术规范自 GCC 6.1 版本起获得支持(需启用 - fgnu - tm 编译选项)。该规范的旧版本自 GCC 4.7 起 已获支持 。