Namespaces
Variants

Transactional memory (TM TS)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

事务性内存是一种并发同步机制,它将语句组组合在事务中,这些事务

  • 原子性(要么所有语句都执行,要么都不执行)
  • 隔离性(事务中的语句无法观察到其他事务执行过程中的部分写入结果,即使它们并行执行)

典型实现会在支持的硬件上使用硬件事务内存(直至变更集饱和),并回退到软件事务内存,通常通过乐观并发实现:若其他事务更新了当前事务使用的某些变量,该事务将静默重试。因此,可重试事务("原子块")只能调用事务安全函数。

请注意,在事务内和事务外访问变量且无其他外部同步时,将产生数据竞争。

如果支持功能测试,此处描述的功能由宏常量 __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 复合语句

1) 若抛出异常,将调用 std:: abort
2) 若抛出异常,将调用 std:: abort ,除非该异常属于用于事务取消的异常类型(见下文)。在此情况下,事务将被 取消 :程序中所有因原子块操作副作用而被修改的内存位置值,都将恢复至开始执行原子块时的初始值,且异常会照常继续栈展开过程。
3) 如果抛出异常,事务将正常提交。

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
  • 将以下函数显式声明为 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

注释

关键词

atomic_cancel atomic_commit atomic_noexcept synchronized transaction_safe transaction_safe_dynamic

编译器支持

此技术规范自 GCC 6.1 版本起获得支持(需启用 - fgnu - tm 编译选项)。该规范的旧版本自 GCC 4.7 起 已获支持