事务性内存

来自cppreference.com
< cpp‎ | language

事务性内存是在事务中结合语句组的并发同步机制,事务为

  • 原子(要么全部语句出现,要么全部不出现)
  • 孤立(事务中的语句不会观察到另一事务写入一半,即使它们并行执行)

典型实现在受支持的平台上将硬件事务性内存使用到极致(例如,直至变更集饱和),再回落到软件事务性内存,后者常以乐观并发实现:若另一事务更新了事务所用的某些变量,则该事务会安静地重试。为此可重试事务(“原子块”)只能调用事务安全的函数。

注意事务内和事务外访问变量而无外部同步,是数据竞争。

若支持特性测试,则由大于或等于 201505 的宏常量 __cpp_transactional_memory 指示描述于此的特性。

目录

[编辑] 同步块

synchronized 复合语句

如同在全局锁下执行复合语句:程序中的所有最外层同步阻塞以单独全序执行。每个同步块的结尾在该顺序中与下个同步块的开始同步。内嵌于其他同步块的同步块没有特殊语义。

同步块不是事务(不同于后面的原子块),并可以调用事务不安全的函数。

#include <iostream>
#include <vector>
#include <thread>
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_allocstd::bad_array_new_lengthstd::bad_caststd::bad_typeidstd::bad_exceptionstd::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; // OK :非 volatile
  p = &x; // OK :指针非 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

[编辑] 注意

[编辑] 编译器支持

GCC 从版本 6.1 起支持此技术规范(要求启用 -fgnu-tm )。此规范的旧版变体从 GCC 4.7 起受到支持