当前位置: 首页 > news >正文

CppCon 2014 学习第4天:Transactional Language Constructs for C++ TS(未进入到标准)

事务性编程

“Transactional Language Constructs for C++ TS”指的是在C++技术规范(Technical Specification, TS)中提出的一套用于支持**事务性编程(Transactional Programming)**的语言构造。


什么是事务性编程?

  • 事务(transaction)是一组操作的原子单元,要么全部执行成功,要么全部不执行(回滚)。
  • 事务广泛用于并发编程、数据库操作和多线程数据一致性保证。

C++中事务性语言构造的目标

  • 简化并发和并行代码中对共享资源的访问控制。
  • 提供一种语言级别的机制,保证代码块的“原子性”和“一致性”。
  • 减少死锁、竞态条件等并发问题。

关键语言构造示例(设想)

transaction {// 事务性代码块shared_data.x += 1;shared_data.y += 2;
} // 事务结束,成功则提交,异常则回滚
  • transaction 代码块确保内部操作原子执行。
  • 如果代码块中出现异常或冲突,自动回滚修改。
  • 编译器或运行时负责管理事务的开始、提交和回滚。

当前状态

  • 这是C++并发扩展的实验性提案,尚未成为C++标准的一部分。
  • 相关研究和实现还在进行中,如Intel的Transactional Synchronization Extensions(TSX)硬件支持。
  • TS提出的事务语言构造,可能以关键字、库或属性的形式出现。

作用与意义

  • 大幅提升多线程程序的安全性和可维护性。
  • 简化复杂的同步代码,减少死锁等问题。
  • 结合并行算法库,提升并发性能。

如果你想,我可以帮你写一个模拟事务性代码块的简单示例,或者介绍已有的事务内存(Transactional Memory)库和用法。需要吗?

**事务内存(Transactional Memory, TM)**的几个主要优点解释如下:


事务内存的优势

  • 使用简单,像粗粒度锁一样容易

    事务内存让开发者写多线程代码像使用一个大锁(粗粒度锁)一样简单,避免了复杂的锁管理和死锁问题。

  • 扩展性好,性能接近细粒度锁

    虽然用法像粗粒度锁,但事务内存可以在内部实现细粒度的冲突检测和并发控制,性能能达到或接近细粒度锁的水平,支持更高的并发度。

  • 模块组合性(Composition)

    • 安全且可扩展地组合软件模块

      事务内存允许多个模块的事务安全地组合在一起,无需担心模块间的锁交叉和死锁问题。

    • 传统锁不具备这种组合性

      普通锁很难组合,不同模块使用不同锁很容易产生死锁,导致系统复杂且难维护。


总结

优势说明
易用性开发者不必精细管理锁,编写简单易懂的并发代码
高性能内部实现支持高并发,扩展性好,避免了大锁带来的性能瓶颈
模块组合性事务代码块可以安全组合,模块间不会因锁冲突产生死锁

事务内存是并发编程中一种兼顾简单性性能的有力工具,是未来多核并行开发的关键技术之一。

在这里插入图片描述

该图展示了播放Ogg Vorbis音频文件的流程。核心组件是libvorbisfile,它负责处理Ogg文件并生成解码流。流程如下:

  1. libogg 读取Ogg文件并提供原始流。
  2. libvorbis 处理Vorbis流并解码为音频流。
  3. 解码后的流由libvorbisfile整合,输出为最终的解码流。
  4. libalsa 接收解码流并将其播放。

图中用虚线分隔了软件处理(左侧)和硬件/音频输出(右侧)的部分,清晰地展示了从文件读取到音频播放的整个过程。

你这段内容是在说明**事务(transactions)**在并发编程的大框架(grand scheme)中的位置,以及并发领域的各种抽象和技术的关系。下面是对这段内容的整理和中文解释:


事务在并发体系中的位置

并发编程的几个重要抽象和概念:

抽象类别作用/特点举例/说明
异步代理(Asynchronous Agents)独立运行的任务,彼此通过消息通信GUI界面,后台打印,磁盘/网络访问
并发集合(Concurrent Collections)对一组数据进行操作,利用数据和算法结构的并行性树结构操作、快速排序、编译器中的并行处理
可变共享状态(Mutable Shared State)避免竞态条件和使用同步对象管理共享内存中的数据锁保护的数据(99%的情况)、无锁库(专家级)、原子操作(专家级)

关键指标和要求:

  • 响应性(Responsiveness)
  • 吞吐量(Throughput)
  • 多核可扩展性(Many-core Scalability)
  • 无竞态(Race-free)
  • 无锁(Lock-free)
  • 隔离性(Isolation)
  • 低开销(Low Overhead)
  • 可组合性(Composability)

当前的抽象实现:

  • 线程(Threads)、消息传递(Messages)
  • 线程池(Thread pools)、OpenMP
  • 锁和锁层级(Locks, Lock hierarchies)

未来的抽象方向:

  • 期望值(Futures)、活动对象(Active Objects)
  • 任务(Chores)、并行STL(Parallel STL)、PLINQ(并行LINQ)
  • 事务内存(Transactional Memory):声明式的锁支持,用于简化共享状态管理,增强并发安全性

总结

  • 事务(Transactional Memory)提供了一种高级抽象,帮助开发者写出安全、可组合且高效的并发代码。
  • 事务内存在并发编程生态中,介于低级锁机制和更高层消息传递、任务异步等模型之间,目标是让共享状态管理更简单且更安全。

这段内容演示了事务内存(Transactional Memory)如何保证事务“看起来像是原子执行的”,并举了一个经典的例子来说明事务执行的正确与错误情况。


核心点总结

  • 事务是原子的:事务中所有操作,要么全部执行成功,要么全部不执行,外界看到的执行结果好像事务是一次“整体”执行。
  • 事务可以并发执行,但结果必须等价于某种顺序执行,即串行化一致性。
  • 事务内存是乐观的并发机制:先假设事务间不冲突,运行中检测冲突,如果冲突则回滚重试。

例子详解

假设初始变量状态:

a = 1, b = 2, c = 3

有两个事务 T1 和 T2:

T1: atomic {a = 2;b = a + 1; // 3c = b + 1; // 4
}T2: atomic {r1 = a;r2 = b;r3 = c;
}

两种正确的执行顺序:

  1. T2先执行,T1后执行

    • T2 读取旧值
      r1 = 1, r2 = 2, r3 = 3
    • T1 更新变量
      a = 2, b = 3, c = 4
  2. T1先执行,T2后执行

    • T1 更新变量
      a = 2, b = 3, c = 4
    • T2 读取新值
      r1 = 2, r2 = 3, r3 = 4

错误的交叉执行(非法情况):

T1                     T2r1 = 1   (读旧值)
a = 2;                  
b = 3;                  r2 = 3   (读新值)r3 = 3   (读旧值)
c = 4;

这里:

  • T2 读取 r1 是旧的 a(1),r2 是新的 b(3),r3 是旧的 c(3),数据不一致。
  • 这种结果不可能出现在任何串行执行的顺序中,因此事务内存不允许这种结果发生。

总结

  • 事务内存保证所有事务的并发执行结果和某种顺序执行结果相同,避免“脏读”和不一致的视图。
  • 任何部分看到“中间状态”的情况都会被事务系统检测并避免。

这就是事务内存“看起来像原子执行”的含义。


你贴出的代码片段是关于 Lock Elision(锁消除)synchronized 关键字的一个伪代码示例。让我帮你解释一下。


Lock Elision(锁消除)是什么?

锁消除是一种优化技术,目的是减少对锁的使用开销,提高多线程代码的性能。其核心思想是:

  • 在某些情况下,锁其实是不必要的,或者说对锁的保护是多余的。
  • 编译器或硬件可以“消除”锁操作,直接执行代码段,前提是保证这样做不会导致竞态条件。

这项技术有时结合**硬件事务内存(HTM)**来实现,比如Intel TSX,它允许硬件尝试以事务方式执行临界区代码,若无冲突则不真正获取锁。


代码示例分析

synchronized {node.next = succ;node.prev = pred;node.pred = node;node.next = node;
}
  • synchronized 表示一个临界区,进入时会自动加锁,退出时释放锁。
  • 临界区内部是对 node 结构的几个指针成员进行赋值。

这段代码可能表示的是某种链表或双向链表节点的链接操作。


在锁消除环境下会发生什么?

  • 如果硬件事务内存(HTM)支持,并且在运行时检测到没有冲突,事务执行成功,就会跳过真正的锁操作。
  • 程序执行这段代码时,表面上看是加了锁的,但实际上硬件没有加锁,而是用事务保证操作的原子性和一致性。
  • 如果事务失败(比如检测到冲突),才会回退并退化为传统的加锁执行。

总结

  • 这段代码用 synchronized 表示加锁的临界区。
  • 锁消除优化会尽量用事务替代传统锁操作,从而加快执行速度。
  • 对于简单指针赋值这类操作,硬件事务可以很高效地实现。

为什么要用事务内存(Transactional Memory, TM)


为什么需要事务内存(TM)?

  • 事务是一个原子操作序列
    事务内存允许你将一组操作组合成一个“原子”执行的块,要么全部执行成功,要么全部不执行,避免了中间状态被其他线程看到。

  • 事务内存旨在取代常用的锁机制
    传统多线程编程中,锁(mutex)是最常见的同步手段,但锁容易导致死锁、优先级反转、性能瓶颈等问题。事务内存提供了一种更简洁、易用且安全的替代方案。

  • 构建无锁数据结构的更好方式
    目前主流的无锁编程技术,例如CAS(Compare-And-Swap)LL/SC(Load-Linked/Store-Conditional),只能保证单个内存位置的原子操作,或者最多是一块连续内存的原子操作。

  • 但是,CAS 等技术无法实现对多个不连续内存位置的原子修改
    现实中的数据结构往往涉及多个内存位置,例如链表节点的多个指针字段,或者复杂对象的多个成员变量。需要原子地同时更新它们,但单个 CAS 无法覆盖这种场景。


总结

事务内存(TM)是为了解决以下问题:

  • 希望将多个步骤组合成一个不可分割的原子操作序列。
  • 传统锁存在的易错、复杂和性能问题。
  • 传统无锁同步只能操作有限的内存区域,难以操作复杂结构。

TM通过硬件或软件支持,让程序员更简单地写出安全且高效的并发代码。


事务内存(Transactional Memory, TM)是一种并发控制机制,它借鉴了数据库中事务(transaction)的概念,用于在多线程程序中简化对共享内存的访问,并避免传统锁机制的复杂性和问题。


事务的 ACI(D) 属性

事务内存也遵循数据库事务的几个核心属性(称为 ACID,TM中通常关注 ACI):


A - Atomic(原子性)
  • 一个事务中的所有操作要么全部成功(称为“提交”commit),要么全部无效(称为“回滚”abort)。
  • 程序员不用担心中途操作被其他线程看到 —— 要么全执行,要么全不执行。

C - Consistent(一致性 / 可串行化)
  • 多个事务的执行结果,看起来就像是依次执行的某个顺序(而不是交错执行)。
  • 这确保了线程间不会因为并发而破坏逻辑一致性。

I - Isolated(隔离性)
  • 在事务执行过程中,它的中间结果对其他线程是不可见的
  • 其他线程只能看到事务“提交”之后的最终状态。

(可选) D - Durable(持久性)
  • 在数据库系统中,Durable 意味着一旦事务提交,结果就会永久保存在磁盘上
  • 在内存中的事务内存里,通常不强调 Durability,所以这个属性常被忽略。

总结一句话:

事务内存使并发编程更容易,因为它保证了一组操作的原子性、隔离性和一致性,无需显式加锁。

“讨厌事务内存”的理由解释:


1. STM 可能效率低(这是最严重的问题)

  • STM = 软件事务内存(Software Transactional Memory)
  • 运行时需要记录读写、检测冲突、可能要回滚,这些操作 开销较大
  • 这是批评 TM 的最常见理由。
  • 不过,STM 的性能 正在快速改进,有些反对意见只是 FUD(恐惧、不确定和怀疑)。
  • 标准委员会确实被要求关注和解决这个问题。

2. TM 永远不会流行起来,不如直接用函数式编程

  • 函数式语言(如 Haskell)强调不可变性无副作用,天然适合并发。
  • 这些人认为:“与其用 TM 来让共享内存安全,还不如用函数式语言从根源上避免这些问题。”
  • 但现实中,许多大型系统(尤其是 C++)已有大量面向对象、命令式代码,TM 是一种更实际的解决方案。

3. 共享内存注定失败,而 TM 正好让它更容易用,这反而是坏事

  • TM 让开发者更容易使用共享内存,从某些角度来说,这鼓励了一个有问题的模型
  • 有些人认为应该避免共享内存,比如转向 消息传递(如 actor 模型、channel 等)。

4. 并发软件没什么危机,我们现在用的(锁、原子操作)已经很好了

  • 有人觉得当前的并发技术(例如 mutex、atomic 等)已经能满足需求。
  • 但这些技术写起来复杂、容易出错、难以组合(composition)。

5. 现在用 TM 为时过早

  • 事务内存技术还不够成熟:

    • 缺乏工具链支持
    • 编译器、调试器支持有限
    • 缺少成功的工业案例
  • 开发者不熟悉 TM 模型,需要学习成本。


6. TM 并不能让你的程序自动变并行

  • TM 简化了并发控制(同步),但不能自动并行化代码
  • 你仍然要自己设计多线程结构、划分任务、管理负载。

总结:

这些观点反映了 TM 在落地过程中遇到的现实挑战(性能、生态、习惯),但它的目标是:

让多线程编程更简单、更安全、更易组合

事务内存(Transactional Memory,简称 TM)在技术发展过程中的**“使命膨胀(mission creep)”炒作周期(hype cycle)**现象,下面为你逐条解释:


1. Mission Creep(使命膨胀)是什么意思?

原意是指某项技术或项目在初期有清晰的目标,但逐渐被赋予了越来越多原本不属于它的职责。

在事务内存中的体现:
TM 最初是为了简化多线程中的同步控制,替代手动加锁。
但后来人们对它的期待不断增加,例如:

  • “TM 能解决所有并发问题”
  • “TM 能彻底简化多核编程”
  • “TM 能自动并行化程序”等等

这种 不切实际的扩大期望 会让技术难以满足全部预期。


2. Hype Cycle(炒作周期)是什么意思?

由 Gartner 提出的技术成熟度模型,包括几个阶段:

  1. 技术触发点(初现)
  2. 期望膨胀峰值(被吹得很厉害)
  3. 幻灭低谷(实际表现不如宣传)
  4. 复苏阶段(理性看待并改进)
  5. 生产力平台期(成熟应用)

事务内存当前的位置:
TM 曾在学术界/工业界被大量宣传,但现实中落地困难、效率问题突出,正逐步从幻灭低谷走向理性回归。


3. 它能帮忙降低功耗吗?

这是一种过高的期望(mission creep 的例子):

  • TM 本质上不是为节能设计的,而是为了简化并发控制
  • 有些人认为:“如果 TM 能自动并行化并减少锁等待,就可能提高能效”。
  • 但实际中,STM 的运行时开销反而可能增加功耗。

**结论:**事务内存不是专门的省电工具,不应寄望于它优化能耗。


4. 能用于嵌入式设备吗?

也是 mission creep 的一个体现:

  • 嵌入式系统通常资源(CPU、内存)有限,对效率和功耗要求更高。

  • 事务内存在嵌入式上的实现非常受限:

    • STM 太重,运行开销大
    • HTM(硬件事务内存)需要芯片支持(如 Intel TSX、IBM Power)
  • 目前 TM 在嵌入式设备中的应用仍然非常有限。


总结一句话:

“事务内存是一项有用的并发控制工具,但它不是万能药(panacea)。不要指望它能解决所有多核编程的问题,更不要过度宣传它的附加价值(节能、嵌入式适用性等)。”

为什么我们需要在 C++ 中引入事务内存(TM, Transactional Memory)语言支持,下面是详细解读:


为什么需要 TM 语言支持?

1. 事务内存不是“语法糖”,它需要语言级支持

  • TM 的核心目标是:让并发编程变得更简单、安全、自动化

  • 这不是库层(library)就能完全做到的,必须通过语言结构(例如 atomic, synchronized, transaction)来:

    • 定义事务块的边界
    • 控制变量的读写规则
    • 与异常、取消等语言机制集成
  • 类似于 try/catchasync/await,这些机制都需要编译器和运行时的支持,TM 也是如此。


2. 硬件已经准备好了

  • HTM(Hardware Transactional Memory) 已在许多处理器上提供:

    • Intel TSX
    • IBM Power8+
    • AMD ASF(未正式发布)
  • 这些硬件支持事务性并发控制,但仍然需要编译器或语言来暴露这个能力

  • 语言支持能让程序员优雅、安全地使用这些底层功能。


3. 多个项目已经在扩展 C++ 来支持 TM

  • 各大公司和研究机构已经开始在 C++ 上实现自己的 TM 扩展:

    • Intel C++ Compiler
    • Sun (Oracle)
    • IBM XL C++
  • 问题:每家实现不一样,程序无法跨平台共享。
    解决方案:制定统一的语言规范(标准)


4. 需要共同的 TM 语言扩展规范

为了让 TM 成为 C++ 的一等公民,业界启动了语言标准化进程

年份进展
2008Intel、Sun、IBM 开始讨论 C++ 中 TM 的支持
2009发布 Transactional C++ Draft v1.0
2011发布 v1.1,修复异常处理等问题
2012提案正式提交给 C++ 标准委员会的 SG1 小组(并发与并行)
并成立专门的 SG5 小组(事务内存)
2013基本完成了一个面向 C++ 技术规范(TS)的语言草案


为什么将 TM 添加到 C++ 中很难?


1. 与 C++0x 内存模型和原子操作冲突

  • C++11(曾称 C++0x)定义了详细的内存模型和原子操作(如 std::atomic),明确规定了多线程间的内存可见性和同步方式。
  • 事务内存(TM)提供了一种“乐观并发控制”,这与传统的内存同步方式可能发生冲突(如事务中的原子操作是否还能按原语语义执行?)。

2. 支持成员初始化语法(member initializer syntax)

  • C++ 类的构造函数允许通过初始化列表(member = value)来初始化成员变量。
  • 如果构造函数中用事务包装这些初始化操作,需要保证语义一致且不违反事务规则,这非常棘手。

3. 支持完整的 C++ 表达式

  • C++ 表达式可以非常复杂,包括函数调用、模板、操作符重载等。
  • TM 需要理解和追踪这些表达式中是否存在副作用,并确保它们在事务中可安全执行。

4. 与旧代码兼容(Legacy Code)

  • 大量已有的 C++ 代码并没有考虑事务语义,比如依赖于锁、使用裸指针或不安全的共享状态。
  • 让这些代码“无缝”支持事务,会引入巨大的技术挑战。

5. 支持结构化的嵌套块(Structured block nesting)

  • C++ 支持嵌套语句块(if、while、for等)。
  • 如果将某个外层块声明为事务,TM 系统必须正确管理内部所有嵌套块的事务边界,防止逻辑错误。

6. 支持事务的多入口和多出口

  • 在 C++ 中,一个函数/块可以有多个 return、异常抛出点,甚至是 goto
  • TM 需要正确处理这些“提前退出”情况,比如在事务中途中 return 时需要回滚所有事务操作。

7. 多态(Polymorphism)

  • 虚函数、多态调用在运行时才决定实际执行逻辑。
  • 如果虚函数中有事务,必须保证在不同派生类中也能正确处理事务边界和回滚逻辑。

8. 异常处理(Exceptions)

  • 异常机制本身就是控制流程的一种“非正常路径”。
  • 如果事务中抛出异常,TM 系统必须确保状态一致性(比如事务要回滚,资源要释放,捕获逻辑不能漏处理)。

总结

将事务内存(TM)引入 C++ 不是单纯加个 atomic {} 语法块,而是要兼容现有语言特性、运行时行为和旧代码,牵一发动全身。每一个挑战都是在试图回答一个问题:

“如何让事务在 C++ 中既安全、易用,又不破坏语言已有的强大能力?”

这正是 SG5(C++ TM 小组)多年努力的方向。

这是对你提到的 Transactional Memory(TM)概览内容 的详细解释:


Overview 概览


1. Use Cases:TM 最适合用在哪些场景?

事务内存最适合以下类型的并发编程问题:

  • 复杂共享数据结构(例如:并发链表、红黑树、哈希表等),其中多个线程可能同时修改不同部分。
  • 非阻塞操作,但又希望使用直观的顺序代码风格
  • 多步共享状态更新:例如你需要在多个变量间保持一致性,但这些变量可能在内存中不相邻。
  • 高争用场景:传统锁容易导致性能瓶颈或死锁,而 TM 能乐观地“冲突后重试”。

示例:

atomic {x = y + 1;y = z + x;// 这些操作要么全部成功,要么一个都不生效
}

2. Usability:TM 是否比锁更容易?

答案通常是 “是的”,原因如下:

  • 不需要手动 acquire/release 锁 → 减少死锁、优先级反转、忘记释放锁的问题。
  • 代码更像串行代码 → 更好维护、更少错误。
  • 组合性好:事务可以组合多个操作而不用暴露内部实现细节。

对比:

特性使用锁使用事务 TM
锁的顺序你必须自己决定不用手动管理锁顺序
死锁风险低(系统自动检测冲突)
组合性(Composability)差,很难组合多个锁操作好,可以组合多个事务块
开发者负担高(锁管理、边界条件)相对低(更像顺序逻辑)

3. Performance:TM 是否够快?

这依赖于实现,但现实中:

  • 现代硬件(如 Intel TSX)上的 HTM(硬件事务内存)已经很快,适合短小事务;
  • 软件事务内存(STM)仍然存在一定开销,但通过优化、编译器辅助等方式持续改善;
  • 适合读多写少的场景效果尤其好;
  • 比细粒度锁更可扩展,尤其在多核系统上。

性能权衡:

  • 低争用:TM 表现优异;
  • 高争用:TM 性能下降,事务频繁失败重试;
  • 事务中使用 I/O、系统调用:性能和行为不确定,不推荐。

总结

维度结论
用例多变量原子更新、并发数据结构、组合逻辑
可用性高:比锁更容易写、更安全
性能高:在低争用、多核系统下优于传统锁

“为什么锁不适合泛型编程(Generic Programming)” 的核心问题 —— 特别是当涉及回调函数、模板函数等 未知类型操作 时,无法明确预知或控制锁的使用顺序和范围,这会导致死锁和不安全行为。


问题背景:锁(mutex)是如何导致死锁的

基础死锁例子:

// Thread 1
m1.lock();
m2.lock();  // 如果 Thread 2 先锁了 m2,这里可能阻塞// Thread 2
m2.lock();
m1.lock();  // 如果 Thread 1 正好锁住 m1,也会阻塞

两个线程尝试以不同的顺序加锁,一旦碰上,就死锁了

解决方法:“固定锁顺序”


更复杂的例子 —— 泛型函数与不可预知的锁

template <class T> 
void f(T &x, T y) {unique_lock<mutex> _(m2);  // 明确加锁 m2x = y;                     // 问题出在这里:x = y 会干什么?
}

问题:x = y 会不会加锁?会加什么锁?

这取决于 T 的具体类型:

  • 如果 T 是简单类型(比如 int),没问题;
  • 但如果 T 是一个包含互斥锁(mutex)成员的类,比如:
struct SharedObject {std::mutex m;int data;SharedObject& operator=(const SharedObject& other) {std::lock_guard<std::mutex> lock(m);data = other.data;return *this;}
};

那么 x = y 可能内部加锁 x.m,而你之前加的是 m2(外部的锁)。

结果就像:

// Thread A
lock(m2);
x = y;       // 又去 lock(x.m)// Thread B
lock(x.m);
f();         // 又去 lock(m2)

死锁风险!


核心结论:锁与泛型编程不兼容

问题:

  • 泛型编程中你无法预测模板参数的行为
  • 也无法保证在调用泛型代码时会遵循某种加锁顺序。

TM(Transactional Memory)更合适:

  • 事务内存 不需要你显式地加锁
  • 能捕捉整个 x = y 这样的操作;
  • 更适合泛型编程、回调、lambda、模板等抽象级别高的代码风格。

总结:

特性使用锁时的问题使用 TM 的优势
泛型函数对共享状态的赋值操作隐式加锁,导致不可预知的行为自动追踪事务中的冲突
多线程之间锁顺序不一致死锁自动回滚 + 冲突重试
无法组合多个通用组件(锁不可组合)难以维护、调试事务可以自由组合、嵌套
更高层抽象如模板、回调、泛型算法不可控更友好,行为明确

这就是为何在高层泛型编程中,锁变得不切实际,而事务内存提供了更自然的替代方式。

这段内容是在讨论多线程编程中一个非常现实且棘手的问题,特别是在使用锁(mutex)保护共享数据时,尤其是结合**泛型编程(template)**的场景下。你的问题是:


x = y 会加锁哪些锁?”


详细理解:

  1. x = y 的赋值操作依赖于类型 T

    • 不同类型的赋值操作内部可能会加锁,也可能不会加锁。
    • 例如,如果 T 是简单的基本类型,比如 int,赋值操作本身不涉及锁。
    • 但如果 T 是复杂类型,比如 std::shared_ptr<TT>,赋值操作会调用拷贝构造或赋值操作符,可能会涉及对引用计数的操作,而引用计数的更新本身通常是线程安全的,内部会有锁或者原子操作。
  2. 作者编写 f() 函数时,不应该关心内部细节

    • 函数模板 f 应该是模块化的:它不应该也不知道 T 的内部实现细节。
    • 这保证了封装性和模块化设计原则,但也带来了并发安全分析的复杂性。
  3. 递归依赖锁的情况

    • 如果 Tshared_ptr<TT>TT 的析构函数可能也会涉及锁(因为析构时要减少引用计数)。
    • TT 的成员析构函数可能又调用了其他锁,甚至可能是另一个 shared_ptr<TTT>,依此类推,形成锁的嵌套和递归。
    • 你根本不可能完全知道所有涉及的锁。
  4. 这导致现实中“死锁”问题非常难避免

    • 因为你不知道所有锁的顺序和持有情况,很难保证没有循环依赖(死锁)。
    • 传统解决办法是“死锁避免策略”或“规定锁的顺序”,但在泛型代码和复杂依赖中很难实施。
    • 现实中常见的是“先写代码后测试死锁”,即通过大量测试和修复来解决死锁问题。

总结

  • 赋值语句 x = y 可能隐式地使用多个锁,具体依赖 T 的实现。
  • 泛型函数 f 的作者不应也无法知道所有这些锁,导致 锁管理变得非常复杂且容易出错
  • 这体现了为什么在复杂的多线程环境下,使用锁来保证线程安全是非常困难和易错的。
  • 这也是为什么像 事务内存(Transactional Memory, TM) 这样的新技术被提出来,尝试解决锁带来的复杂性和死锁问题。

template <class T> 
void f(T &x, T y) { 
unique_lock<mutex> _(m2); 
x = y; 
}

事务(Transactions) 如何自然契合泛型编程模型,特别是在避免死锁和保证并发安全方面的优势。具体理解如下:


核心观点

  • 事务是可组合的(Composable),不要求严格的锁获取顺序。
  • 在事务里,代码块要么完全成功提交,要么完全失败回滚,避免了锁的显式管理。
  • 这样,编写泛型代码时,不用担心锁的细节,也就避免了死锁。

代码示例解读

template <class T>
void f(T &x, T y) {transaction {x = y;}
}
  • f 是一个泛型函数模板,对任意类型 T,在一个事务块中执行赋值。
  • 事务块会自动处理并发冲突和内存一致性,开发者不需要管理锁。
  • 不管 T 的赋值操作内部会触发什么样的锁,事务保证了整个赋值的原子性。

class ImpT {ImpT& operator=(ImpT &rhs) {transaction {// handle assignment}}
};
  • ImpT 的赋值操作符实现也是在一个事务中。
  • 这样即使赋值很复杂,内部可能涉及多个成员的修改,也能保证操作的原子性和隔离性。

为什么“Impossible to deadlock”(不可能死锁)

  • 传统锁机制需要开发者自己管理锁顺序,容易因不同线程锁顺序不同产生死锁。
  • 事务内存通过乐观并发控制,允许多个事务并行执行,只有在提交时检测冲突,冲突时自动回滚重试。
  • 因此没有“锁顺序”这个问题,也就自然避免了死锁。

小结

  • 事务使泛型代码写起来更安全,且无须关心底层锁细节。
  • 事务提高了代码的可组合性和可维护性,消除了死锁等传统锁机制中的难题。
  • 这正是事务内存被提倡和设计的重要原因之一。


问题所在

  • 普遍观点:通过“强制锁的获取顺序”(locking ordering)可以避免死锁。

  • 但是:在 C++ 模板编程中,这几乎不可能做到。

    • 因为模板编程的复杂性和泛型特性,使得锁的顺序难以静态确定和维护。
  • 而且,模板编程在 C++ 中非常普遍(例如 STL、智能指针、各种泛型库)。

  • 因此,C++ 模板编程非常需要事务内存(TM)来解决死锁和并发问题


具体理解

  1. 锁顺序死锁问题
    死锁的一个经典避免方法是所有线程以同样顺序锁定资源。
    但在模板编程中,T 是任意类型,无法在编写泛型代码时知道具体会锁哪些资源。
    例如,你写一个模板函数给不同类型的对象赋值,不同类型的赋值操作可能锁不同的资源,顺序也不同,导致死锁。

  2. 模板编程的复杂性

    • 代码是泛型的、模块化的、层层嵌套的。
    • 不能预先硬编码锁顺序,因为具体调用时的类型不同,锁的数量和顺序都会变化。
    • 这使得静态分析或代码设计很难保证锁顺序,死锁风险大。
  3. 为何需要 TM(事务内存)

    • 事务内存用原子事务替代显式锁,避免死锁问题。
    • 事务自动处理并发冲突,开发者不用管锁顺序。
    • 这为模板代码提供了自然的并发安全保障。

小结

  • 传统的锁顺序策略在模板泛型编程中基本行不通。
  • 事务内存提供了对模板编程特别友好的并发控制机制。
  • 因此,C++ 标准和库设计越来越关注把事务内存纳入语言支持。

事务内存(TM)在实际编程中的常见模式和典型应用场景,总结了四大主要用例:


TM 的四大典型用例

  1. 不规则结构且冲突频率低

    • 例如:图、链表、树等数据结构,数据访问模式不规律,事务冲突发生的频率较低,事务内存可以较好地管理这些复杂结构的并发修改。
  2. 低冲突且高读共享、操作复杂的结构

    • 读操作占多数且共享频繁,写操作少且复杂,事务内存可以通过乐观并发减少锁竞争,提高性能。
  3. 读多写少的结构,且大量读操作是只读的

    • 这种结构适合利用事务内存的事务快照和版本控制特性,读操作几乎不冲突,写操作较少且可以自动回滚。
  4. 可组合的模块化结构和函数

    • 事务内存支持模块之间的事务嵌套和组合,写出既可组合又线程安全的代码,避免死锁和复杂锁管理。

总结

事务内存特别适合处理复杂且难以用传统锁机制管理的并发场景,尤其是当数据结构不规则、读多写少、冲突少且需要模块化组合时,TM 提供了自然且高效的解决方案。


事务内存在“不规则结构且冲突频率低”场景中的优势和应用。


不规则结构(Irregular Structures)

定义:
不规则结构通常指数据结构的拓扑和访问模式不规则、不可预测,比如图结构、稀疏矩阵、树等。

例子:

  • 图应用(graph applications)
  • 最小生成森林(minimum spanning forest)
  • 稀疏图(sparse graph)
  • VPR(可编程逻辑阵列相关工具)和 FPGA(现场可编程门阵列)设计中的数据结构

为什么事务内存适合这些场景?

  • 低冲突频率
    不规则结构往往使得不同线程访问不同部分的数据,事务冲突相对较少。

  • 提高并发性
    事务内存让多个线程能乐观地并行执行事务,冲突少时基本无阻碍。

  • 避免死锁
    传统锁机制往往需要复杂的锁顺序管理来避免死锁,事务内存自动处理冲突回滚,天然避免死锁问题。

  • 易于编程
    事务内存让程序员关注业务逻辑,无需操心锁管理和死锁,降低编程难度。


总结

事务内存对不规则数据结构是一种天然的、有效的并发控制手段,能够提高程序的并发效率和开发效率,同时减少死锁和错误。


在这里插入图片描述

该图解释了为什么不使用锁(Why Not Locks?)的原因,并展示了线程操作的实现方式。关键点包括:

  • 问题:如果出现冲突,细粒度锁(fine-graining locking)可能导致死锁或性能下降。
  • 实现方式:通过Thread 1和Thread 2的并行操作来避免锁。

图中:

  • Thread 1(绿色)处理图的一部分,涉及多个节点和边。
  • Thread 2(橙色)处理另一部分,涉及不同的节点和边。
  • 蓝色圆圈表示Thread 1和Thread 2共享的冲突区域,显示了潜在的竞争点。

通过避免使用锁,而是设计线程操作互不干扰的区域,可以减少死锁和性能问题。

  • 任务:需要原子性地将元素1从左树移动到右树,同时将元素4从右树移动到左树,且两个移动操作之间不能有冲突。
  • 图示
    • 左树和右树各有一个二叉树结构,节点为棕色。
    • 左树底部有元素1、2、3、4(绿色),右树底部也有相同的元素。
    • 箭头表示元素1从左树移到右树,元素4从右树移到左树。

这个挑战强调了在并发环境中,如何在不引入冲突的情况下,原子性地执行多结构更新,RCU技术可以用来解决这类问题。
在这里插入图片描述

低冲突数据结构,支持高读共享和复杂操作,如红黑树和AVL树。这些结构具有易于并行化、高并发、低缓存一致性流量和编程简单的优势。图表展示了一个树结构,进行只读遍历,其中不同节点由线程1(绿色)和线程2(橙色)更新,演示了无冲突的并发修改。
在这里插入图片描述

在这里插入图片描述

该图展示了一个事务内存(Transactional Memory, TM)的使用示例,适用于树和图等数据结构。关键点如下:

  • 适用场景:树、图等数据结构(E.g., trees, graphs)。
  • 操作:支持未同步的操作(Unsynchronized operations),包括:
    • void insert(Item &x);:插入一个元素。
    • void delete(Item &x);:删除一个元素。

图中展示了一个树形结构,包含多个节点和边,表示TM可以用来管理这些数据结构的并发操作。TM通过事务的方式确保插入和删除操作的原子性,从而避免锁的使用,减少冲突和死锁问题。

在这里插入图片描述

该图展示了一个事务内存(Transactional Memory, TM)在低冲突场景下的使用示例,具体内容如下:

  • 场景:并发操作在低冲突情况下执行,无需复杂的细粒度设计。
  • Thread 1
    • 执行操作:atomic_cancel { tree.insert(x); }
    • 功能:原子性地向树中插入元素x
    • 图中标记为“Updated by Thread 1”(绿色节点)。
  • Thread 2
    • 执行操作:atomic_noexcept { tree.delete(y); }
    • 功能:原子性地从树中删除元素y
    • 图中标记为“Updated by Thread 2”(橙色节点)。
  • 图示
    • 树结构包含多个节点,蓝色节点表示只读遍历(Read-Only Traversal)路径。
    • 绿色节点表示Thread 1插入的元素,橙色节点表示Thread 2删除的元素。
  • 特点:通过TM,插入和删除操作以原子方式执行,避免了锁的使用,同时支持只读遍历,减少冲突。

总结:TM通过原子操作确保Thread 1和Thread 2的并发更新(插入和删除)在低冲突场景下高效执行,同时允许只读遍历操作。

在这里插入图片描述

该图展示了一个事务内存(Transactional Memory, TM)使用示例,强调了组合性和异常原子性(composability and except atomicity)。具体内容如下:

  • Thread 1

    • 执行操作:atomic_cancel { tree1.delete(x); tree2.insert(x); }
    • 功能:原子性地从Tree 1删除元素x,并将其插入到Tree 2。
    • 图中:x(绿色)从Tree 1移除,插入到Tree 2(标记为Updated by Thread 1)。
  • Thread 2

    • 执行操作:atomic_cancel { tree2.delete(y); tree1.insert(y); }
    • 功能:原子性地从Tree 2删除元素y,并将其插入到Tree 1。
    • 图中:y(橙色)从Tree 2移除,插入到Tree 1(标记为Updated by Thread 2)。
  • 图示

    • Tree 1和Tree 2是两个树形结构,蓝色节点表示只读遍历(Read-Only Traversal)路径。
    • 绿色节点表示Thread 1的操作,橙色节点表示Thread 2的操作。
  • 特点

    • 组合性:多个操作(如delete和insert)组合成一个原子事务。
    • 异常原子性:使用atomic_cancel确保即使发生异常,操作也能保持原子性。

总结:TM通过原子事务实现Thread 1和Thread 2在Tree 1和Tree 2之间的元素移动,支持组合性和异常安全,同时允许只读遍历路径。

事务内存(Transactional Memory, TM)最适合的几种情况:


事务内存的最佳适用条件

  1. 低内在冲突(Low inherent conflict)

    • 事务间冲突少,TM可以充分发挥高并发能力
    • 事务执行时较少回滚,提高效率
  2. 不规则结构和操作(Irregular structures and operations)

    • 结构复杂、内存访问模式不规则,不容易用细粒度锁处理
    • TM使用起来更简单,无需设计复杂锁机制
  3. 复合操作(Composite operations)

    • 多个步骤组合成一个原子操作,TM可自动保证原子性
    • 使用锁时,维护多个锁的顺序和嵌套较难,易死锁

理解

TM尤其适合复杂且并发冲突低的场景,它简化了并发控制的复杂性,让程序员更专注于业务逻辑而非锁的管理。锁在细粒度和组合操作上的管理复杂度往往较高,TM则能自动处理这些问题。


以读为主的结构,以及事务内存在这类结构中的优势。总结如下:


Read-Mostly Structures (读多写少的结构)

  • 定义
    结构中大部分操作是读取(read-only),写操作较少。
    典型例子:各种搜索结构,如搜索树、哈希表等。

  • 优势

    • 高并发:大量读操作可以并行执行,几乎不互相干扰
    • 避免不必要的写操作:读操作不会导致写缓存,从而减少缓存一致性开销
    • 事务内存可以智能管理冲突:写操作时,TM保证原子性,读操作则可以无锁执行(或者开销很低)

理解

在读多写少的场景下,事务内存的优势尤为明显:

  • 读操作无锁或低开销,提高程序整体吞吐量
  • 写操作时自动处理冲突,保证数据一致性
  • 传统锁可能会限制读写并发,导致性能瓶颈

事务内存(TM)在组合性和模块化设计中的优势,总结和理解如下:


Composition / Modularity(组合性和模块化)

  • 背景
    在复杂系统里,代码往往由许多模块、函数组成,操作多个数据结构。
    传统锁机制经常因为锁的粒度和顺序问题导致死锁或复杂的锁管理。

  • 事务的优势
    事务内存支持原子执行一组操作,不论它们涉及多少个数据结构或模块。
    你可以把多个操作组合成一个事务,事务会保证:

    • 操作整体要么全部成功(commit),要么全部不生效(abort)
    • 不用担心手动管理多个锁的顺序和依赖

代码示例理解:

__transaction {// 在结构A中搜索某个键K对应的条目,并移除它X = remove(A, K);  if (X != NULL) {// 根据X的值,计算出对应结构BB = f(X->Value);  // 将X插入到B中insert(B, X);}
}
  • 这个事务:

    • 组合了对不同数据结构(A、B)的操作
    • 保证这些操作的原子性和一致性
    • 编写简洁,避免了传统锁中可能遇到的死锁或锁管理问题
    • 便于代码模块化和维护

总结

事务内存让多模块、多数据结构的复杂操作能够安全、简洁、模块化地进行,而不用手动编写复杂的锁策略。
这段内容介绍了事务内存在真实世界软件中的应用实例,具体理解如下:


Real-World STM Application(真实世界的STM应用)

  • 研究主题:使用事务内存(Transactional Memory,尤其是软件事务内存STM)支持多玩家游戏的高效并行化,实现可扩展性和透明化。
  • 作者团队:Daniel Lupei, Bogdan Simion, Don Pinto, Mihai Burcea, Matthew Misler, William Krick, Cristiana Amza
  • 应用案例:SynQuake——一个模拟《Quake》战斗的多人游戏系统
  • 技术:采用了软件事务内存(STM),也就是通过软件实现的事务内存机制,而不是依赖硬件事务内存
  • 会议:成果发表于EuroSys 2010,欧洲系统研究领域的重要会议

重点理解

  • 该项目通过STM实现了多人游戏中复杂并发操作的管理,使得代码可以并行执行且易于扩展,同时对程序员来说是“透明的”——不必显式管理锁,减少并发bug。
  • 这是事务内存在高性能、多线程实际应用中的一个成功案例,显示了STM在现实复杂系统中的潜力和价值。

游戏交互 Game interactions

在这里插入图片描述

该图展示了游戏交互中的游戏地图。地图上分布着多个物体,包括医疗箱(带红十字标记)和弹药箱(棕色盒子)。玩家角色通过“行动边界框”与这些物体互动,框内包含玩家、医疗箱和弹药箱,指示玩家可以与之交互的区域。箭头表示玩家的移动方向,指向行动边界框内的物体。

游戏中的碰撞检测

在这里插入图片描述

该图展示了游戏中的碰撞检测机制。游戏地图上分布着医疗箱(带红十字标记)和弹药箱(棕色盒子)。玩家角色通过“行动边界框”移动,框内包含玩家和多个物体。红圈标记了与玩家可能发生碰撞的物体,包括弹药箱和医疗箱,指示了碰撞检测的范围和目标。箭头表示玩家的移动方向。

游戏中玩家动作冲突的情景

在这里插入图片描述

该图展示了游戏中玩家动作冲突的情景。游戏地图上有两个玩家(T1和T2),他们各自的行动边界框内包含医疗箱和弹药箱。两个边界框重叠的部分(红圈标记)表示玩家T1和T2可能同时尝试与同一物体(如医疗箱)交互,导致冲突。图中标注“需要同步”,表明需要同步机制来协调玩家动作,避免冲突。

玩家在游戏中的复合动作

在这里插入图片描述

该图展示了玩家在游戏中的复合动作。玩家沿着路径(红线)移动,依次与健康包(healthpack)和弹药(ammo)交互,然后冲锋并射击(move, charge weapon, and shoot)。这些动作构成一个复合动作,图中强调需要整个游戏动作的“一致性和原子性”(consistency and atomicity),以确保动作按顺序正确执行,不被中断或冲突。

保守锁定

保守锁定(Conservative locking)策略是在游戏动作(GAME ACTION)开始时,一次性获取所有需要的锁(Lock 1, Lock 2, Lock 3),执行所有子操作(Subaction 1, 2, 3),然后一次性释放所有锁。

问题:
这种方式会导致锁的持有时间过长,即使某些子操作之间可能没有真正冲突,锁也一直被占用,造成不必要的冲突时间(conflict duration)。这降低了并发性能和系统响应能力。

简单来说,保守锁定虽然避免了死锁(因为提前拿齐所有锁),但牺牲了并行度和效率。


Conservative locking(保守锁定)在动作开始时,估计影响范围(impact range)并锁定所有相关对象。

问题2:
由于采用保守估计,往往会锁定比实际操作需要的更多对象,导致锁定对象数量过多

这带来的后果是:

  • 增加锁竞争和冲突概率
  • 降低系统整体并发度
  • 资源浪费

换句话说,为了安全起见,一开始锁了太多东西,即使某些对象根本不需要被修改或访问。


所以保守锁定的两个主要问题是:

  1. 锁持有时间过长(conflict duration长)
  2. 锁对象数量过多(锁范围过大)

这些都会限制并行性能。


在这里插入图片描述

**Fine-grained locking(细粒度锁)**的做法是:

针对每个子动作(subaction)分别加锁、解锁,锁的范围很小,锁持有时间短。

示意:

  • Lock 1,执行子动作1,Unlock 1
  • Lock 2,执行子动作2,Unlock 2
  • Lock 3,执行子动作3,Unlock 3

问题:
这样做无法保证整个动作(GAME ACTION)的原子性(atomicity)

具体来说,细粒度锁虽然减少了锁竞争,提高并发,但如果中间某个子动作失败或被中断,或者其他线程介入,整体动作的逻辑状态可能不一致,容易导致数据不一致和错误。


总结:

策略优点缺点
保守锁定保证动作原子性锁时间长,锁范围大,性能差
细粒度锁锁时间短,提升并发无法保证整体动作的原子性,易出错

另一种细粒度锁策略:

  • 在执行多个子动作时,先依次加锁(Lock 1, Lock 2, Lock 3),
  • 等所有锁都获得后再执行子动作(Subaction 1, 2, 3),
  • 最后一起释放所有锁(Unlock 1, 2, 3)。

问题:

  1. 死锁(Deadlocks)
    因为多个线程可能会尝试按不同顺序获取锁,比如线程A先锁1再锁2,线程B先锁2再锁1,容易产生循环等待导致死锁。

  2. 不一致视图(Inconsistent view)
    如果不能成功获得所有锁,就无法保证操作的原子性和一致性,可能看到中间状态,数据不一致。


总结:

  • 一起拿所有锁才能保证动作原子性,但这会带来死锁风险。
  • 逐个加锁又难以保证操作的整体原子性。

这就是为什么细粒度锁“Not possible!” —— 传统锁机制难以兼顾高并发、原子性和死锁避免。

STM(软件事务内存)的核心思想:

  • STM作为并行化的新范式
    不用显式加锁,而是把游戏中的“动作”当作一个个事务来执行。

  • 访问跟踪与冲突检测
    STM 会自动跟踪事务中对共享数据和私有数据的读写访问,检测是否发生了冲突。

  • 自动保证一致性和原子性

    • 如果事务执行过程中没冲突,事务就提交(commit),结果生效。
    • 如果发生冲突,事务会回滚(rollback),撤销所有改动,重试执行。

这种方式的优点:

  • 不用人为管理锁,避免死锁问题。
  • 提高并发度,多个事务只要不冲突就能同时进行。
  • 程序更易编写和维护。

STM(软件事务内存)中同步的过程,具体是:

  • 游戏动作(GAME ACTION)被封装为一个事务(Transaction)
    事务开始(BEGIN Transaction),执行多个子操作(Subaction 1, 2, 3),然后提交(COMMIT Transaction)。

  • 解决的问题

    • 死锁(Deadlocks):传统锁机制可能导致死锁,而STM自动检测冲突并重试,避免死锁。
    • 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败,保证一致性。
  • 自动处理
    事务的开始、冲突检测、提交或回滚都由STM系统自动管理,开发者无需显式管理锁。


简而言之,STM 让开发者用“事务”来组织并发代码,系统帮你自动搞定同步、死锁和一致性问题。比传统锁机制更直观,也更安全。

STM 同步机制中的冲突检测优化


核心思想:渐进式冲突检测(Incremental Collision Detection)

传统做法的问题:
在整个事务执行完成后再做冲突检测,可能会浪费大量计算资源(如果最后才发现冲突,需要回滚全部操作)。


优化方案:

  • 将游戏动作(action)拆分为多个子动作(subactions)

  • 每执行一个子动作,就进行一次冲突检测

    • 如果此时发生冲突,可以立刻中止事务,避免继续浪费资源。
    • 如果没有冲突,就继续执行下一个子动作。

优点:

  1. 提高效率

    • 避免在冲突不可避免时继续执行无效的后续操作。
  2. 更及时的冲突检测

    • 越早发现冲突,回滚代价越小。
  3. 适合高交互、高并发的游戏环境

    • 游戏中很多操作涉及共享资源(玩家位置、状态、物品),需要高效检测与处理并发访问。

总结: 通过将事务分解为子事务并逐步检测冲突,STM 提供了一种既高效又自动的并发控制机制,非常适合如多人游戏这样复杂而动态的场景。
在这里插入图片描述

事务内存(Transactional Memory, TM) 的几个关键优势,尤其是与传统锁机制的对比:


事务内存的优势:

1. 像粗粒度锁(coarse-grain locks)一样易于使用
  • 程序员不需要手动管理复杂的加锁/解锁逻辑。

  • 写起来简单直观:

    transaction {shared_data = new_value;
    }
    
2. 像细粒度锁(fine-grain locks)一样具备良好的扩展性(scalability)
  • TM 自动处理冲突检测和回滚,可以让多个线程并发执行而不会互相阻塞,前提是它们访问的数据不冲突。
  • 类似于手动使用细粒度锁那样可以获得较高的并发性,但无需管理复杂的锁粒度。
3. 支持模块组合(composition)
  • 安全且可扩展地组合多个模块

    • 你可以把多个操作组合在一个事务中,而不必担心死锁或组合失败。
    • 举个例子:在一个事务中,可以“从容器 A 移除元素并插入到容器 B”,保证要么两个操作都成功,要么都不做,避免状态不一致。
对比:锁不支持组合
  • 锁机制中,模块之间无法安全组合,因为锁的获取顺序和依赖关系难以协调,会导致死锁或竞态条件。
  • 模块之间使用不同的锁策略,组合在一起时容易产生不可预测的问题。

总结

事务内存结合了粗粒度锁的“易用性”与细粒度锁的“扩展性”,并且天然支持安全组合多个操作或模块,这使得它特别适合现代复杂并发程序,尤其是那些需要模块化、可重用的场景。


SG5 TM Language in a Nutshell(事务内存语言简述)

1. 关键字:transaction_safe
  • 用于函数或函数指针声明。

  • 必须是关键字,因为它影响函数的类型系统行为(例如是否可以在事务中安全调用)。

  • 作用类似于:

    transaction_safe void my_func();
    

2. 属性:[[transaction_unsafe]]
  • 用于标记函数不安全、不能在事务中使用。

  • 是一个属性(attribute),因为它主要用于静态检查和性能优化建议,不改变函数类型。

  • 例如:

    [[transaction_unsafe]] void legacy_func();
    
  • 如果你在事务中调用这个函数,编译器就会发出警告或报错。


3. 事务构造语句(Compound Statement)

支持两种事务类型的语法结构:

事务块语法(类似于语言级 try 块):
atomic_noexcept {// 事务体,不允许抛出异常
}
atomic_commit {// 事务体,保证提交行为
}
atomic_cancel {// 显式取消事务的语义块
}
另一种语法:synchronized
synchronized {// 用于将事务表达得更接近于传统同步块
}
  • 更强调语义上的“同步行为”,语法风格接近 Java 的 synchronized

小结

元素用途类型
transaction_safe标记函数可以在事务中安全调用关键字
[[transaction_unsafe]]标记函数不可在事务中使用属性
atomic_* {}不同类型的事务语句块构造语法
synchronized {}可替代事务语句块的同步构造语法糖

Transaction statement 是 C++ 中关于事务性内存(Transactional Memory, TM)语言提案的一部分,主要来自 SG5 的技术规范草案(TS),但这些目前并未正式纳入 C++ 标准。


基本概念

这些语句的目的是在 事务上下文中执行一段代码,并自动处理并发冲突、回滚或提交,避免使用传统的锁机制。


语句形式解析

语句形式含义
atomic_noexcept { ... }表示 不抛异常 的原子事务。最优化执行路径,用于确定性代码。
atomic_commit { ... }明确意图为 提交事务,可以在某些 TM 实现中手动控制事务边界。
atomic_cancel { ... }强制取消事务并回滚,表示此事务块不会生效。主要用于错误处理。
synchronized { ... }表示自动同步的事务代码块,语义类似于 Java 的 synchronized,但在 TM 中用于简洁标识一个事务。

示例对比

atomic_noexcept {x++;y = x + 5;
}
  • 如果没有冲突:代码成功提交。
  • 如果发生冲突:事务会回滚并重试(可能依赖于 TM 实现)。
  • 不能抛出异常:这块代码不能包含抛异常的逻辑。
atomic_cancel {// 逻辑中止或条件不满足时,事务无效return;
}
synchronized {x++;if (x > 10)y = x * 2;
}
  • 语义简洁,适合写事务性模块化代码,通常等价于某种 atomic_commit

理解总结

特性atomic_noexceptatomic_commitatomic_cancelsynchronized
支持异常不支持支持支持支持
常用于高性能路径显式提交逻辑显式中止逻辑通用 TM 编程
TM 语义自动回滚、重试显式事务控制手动取消事务简洁事务控制

适用场景

  • atomic_noexcept:数据结构中纯内存操作(无异常抛出)。
  • synchronized:高层结构体、模板函数中简洁的并发控制。
  • atomic_commit/cancel:更细粒度的事务控制逻辑。

注意

这些事务语法目前并非正式标准,只在 SG5 的 TM 技术规范中提出,需要特定编译器支持或实验性扩展(如 GCC 的 TM 扩展)。


理解请求关于 atomic_cancelsynchronized 的事务语义,这段内容主要阐明了两种不同事务类型在 C++ TM 模型中的行为差异。以下是逐点解析和总结,帮助你清晰理解:


1. atomic_cancel 的特点

atomic_cancel {x++;if (cond)throw 1;
}

语义要点:

  • 事务语义:原子性保证

    • 在没有冲突时,atomic_cancel 中的操作会作为一个原子事务整体执行。
    • 所有事务似乎按某个串行顺序执行(即:serializability)。
  • 可取消(可以显式回滚)

    • 事务可以失败,例如遇到异常(throw)或内部条件(如 cond 为真)。
    • 一旦取消,该事务对共享内存的修改将被全部撤销
  • 对代码内容有严格限制

    • 禁止使用不安全语句(比如:I/O、系统调用、不可追踪的资源访问)。
    • 异常(如 throw)要小心使用,因为可能触发回滚。
  • 数据竞态行为未定义

    • 如果多个事务或线程对同一内存位置进行未同步访问(即竞态),则程序行为是 undefined behavior

2. synchronized 的特点

synchronized {x++;print(x);
}

语义要点:

  • 语义更宽松,但仍有事务特性:

    • 事务是原子执行的(即:一整个事务不可中断,执行结果要么全有要么全无)。
    • atomic_cancel 不同的是,它 不能取消(no aborts/rollbacks)。
    • 更适合含有副作用的事务,如打印、日志、I/O 等。
  • 允许更自由的操作

    • 可以包含任意类型语句,包括 I/O、函数调用等。
  • 适用于模块组合和高层事务封装

    • 开发者可用于大型模块或泛型代码,不用担心事务中使用了“非纯函数”或副作用逻辑。

比较总结表

特性atomic_cancelsynchronized
原子性
可取消是(异常/冲突可回滚)
允许异常是(触发回滚)
允许 I/O、副作用语句
编程限制严格(只能纯语句)宽松
推荐用途纯内存修改、结构操作等泛型、带副作用的逻辑、模块组合

理解重点

  • atomic_cancel 是“纯事务块”,追求性能和可控性,适合 数据结构并发修改
  • synchronized 是“宽松事务块”,更适合 含副作用的模块化代码或顶层逻辑
  • 二者都保证事务性的执行(看起来像是串行发生的),但对中途取消和代码内容的约束差异很大。

“Function Call Safety” 是 C++ 中事务性内存(Transactional Memory, TM)模型中一个重要概念,它解决了一个核心问题:

“在事务(atomic block)内部调用哪些函数是安全的?”


核心:3种函数安全机制(Function Call Safety)

为了控制哪些函数可以在事务中被调用,C++ TM 模型定义了以下三种机制:


1. transaction_safe
  • 含义:该函数在事务中调用是 安全的
  • 用法:用于函数声明或定义中,说明它不会产生副作用(如 I/O)、不会引发不可控的竞争条件。
transaction_safe void safe_func();  // 明确声明这个函数是事务安全的
  • 特点

    • 可以在 atomic_cancel, atomic_noexcept, synchronized 中被调用。
    • 编译器可执行静态检查,确保函数内部不含“事务不安全”内容(如 I/O、不可追踪全局状态修改)。

🔹 2. transaction_unsafe (attribute)
  • 含义:标注函数为事务不安全,不允许在事务中调用。
  • 用法:适用于具有副作用或不符合 TM 要求的函数。
void dangerous() [[transaction_unsafe]];
  • 目的

    • 强化编译器检查:禁止该函数在 atomic_cancelatomic_noexcept 中调用。
    • 减少运行时错误,提高代码安全性。

🔹 3. 隐式安全函数(implicitly transaction_safe
  • 定义:如果一个函数未明确标注为 unsafe,同时:

    • 它只调用了其他 transaction_safe 函数,或
    • 只使用了事务安全的语言构造(无 I/O、无全局状态更改等),
    • 则编译器可以自动认为它是事务安全的
int add(int a, int b) {return a + b;  // 无副作用,隐式事务安全
}
  • 优势:简化事务安全代码的编写;无需手动为每个小函数添加 transaction_safe

为什么要这么做?

因为在事务内部:

  • 你希望保证原子性(一组操作要么全部执行,要么全部不执行);
  • 一旦事务中出现了不可回滚操作(如 print() 或写文件),就违背了事务的设计目标;
  • 所以要 明确规范哪些函数可以/不可以在事务中使用 —— 这正是这三种机制提供的功能。

实践中,组合效果

函数属性atomic_cancel 中能否调用synchronized 中能否调用
transaction_safe可以可以
transaction_unsafe不可以可以
隐式安全函数可以(如果符合条件)可以

总结理解重点

  • transaction_safe: 明确声明函数可以用于事务块中。
  • transaction_unsafe: 明确拒绝事务中使用(如带副作用的函数)。
  • 隐式事务安全:符合条件的普通函数会自动被视为安全。

截至目前(2025年),C++ 标准并未正式引入事务性内存(Transactional Memory, TM)支持,即便 SG5(Study Group 5)曾为此做出大量设计和提案。


当前标准状态(C++20 / C++23)

  • 没有在 C++20 或 C++23 中正式包含事务性内存语言特性。
  • 事务性内存曾以 技术规范(Technical Specification, TS) 的形式存在,但未进入主线标准。

事务性内存历史:SG5 & TS 计划

时间事件
2008-2011Intel、IBM、Sun 等提出 C++ 的 TM 扩展需求,初步规范形成。
2012SG5 成立,专注于 TM 的语言支持。
2013SG5 准备进入 TS(技术规范)阶段,起草关键字如 transaction_safeatomic_commit 等。
2014发布早期草案,但由于业界兴趣下降和实现复杂性等问题,TS 进展停滞
之后SG5 活动逐渐减少,未能推动 TM 成为 C++ 标准的一部分。

为什么没进入标准?

  1. 实现难度高

    • 和 C++ 的现有内存模型(尤其是 C++11 引入的原子和 memory_order)冲突。
    • 涉及异常处理、指针别名、内联、模板实例化等核心语言机制。
  2. 缺乏广泛实现支持

    • GCC 和 Clang 曾有原型,但并未成为主线。
    • Intel 一度有支持,但后续资源转向其他并行技术。
  3. 产业兴趣转移

    • GPU 编程(CUDA、SYCL)、异步编程(co_await)、并行算法库(std::execution)更受关注。
    • TM 更适合系统级开发,但主流 C++ 用户不迫切需要。

现状:可以在哪里用 TM?

  • GCC

    • 支持一个实验性扩展:__transaction_atomic__transaction_relaxed
    • 但不是标准的一部分,只在特定版本中启用。
  • Clang / MSVC

    • 暂无对语言级 TM 的正式支持。

替代方案(推荐使用的现代并发方式)

方法描述
std::mutex + std::lock_guard传统锁机制,稳定可靠
std::atomic原子类型,适合低级并发
std::async / std::future异步任务管理
并行算法(C++17起)std::for_each(std::execution::par, ...) 等并行 STL 算法
co_await / std::generator(C++20)协程与异步编程
第三方库(如 TBB)Intel TBB 提供任务并行和图并行模型

总结:

事务性内存在 C++ 标准中尚未引入,SG5 的提案停留在草案阶段。
实际开发中推荐使用现代并发工具(如 std::mutex, std::atomic, 协程等)来管理并行性和共享状态。

相关文章:

  • 麒麟v10+信创x86处理器离线搭建k8s集群完整过程
  • 【实证分析】上市公司全要素生产率+5种测算方式(1999-2024年)
  • Baklib知识中台驱动业务创新
  • 车载通信网络 --- 车载通信网络槪述
  • Codeforces Round 1024 (Div. 2)
  • AI编程的商业化方向
  • 【数据库】关系查询处理和查询优化
  • 【PhysUnits】15.3 引入P1后的取反特质(not.rs)
  • eNSP企业综合网络设计拓扑图
  • 鸿蒙next http网络请求工具类进阶版本来了
  • 长安链起链调用合约时docker ps没有容器的原因
  • (四) 本地YARN集群的部署
  • 记录一次发生的OOM异常,OutOfMemoryError: Java heap space
  • 函数指针和指针函数的核心区别
  • 会用和用好AI的区别
  • UFSH2024 程序化生成 笔记
  • Java实现命令行图书管理系统(附完整源码)
  • Dif-Fusion:第一个基于扩散模型实现的红外光与可见光图像融合的论文
  • WordPress搜索引擎优化的最佳重定向插件:进阶指南
  • 学习路之PHP--easyswoole简易增删改查入门
  • 热点 做网站和营销 我只服他/产品的推广及宣传思路
  • 手机上自己如何做网站/石家庄seo
  • 凡科建站做的网站收录慢吗/如何优化
  • 专业做传奇网站解析/个人如何做seo推广
  • 巩义网站建设方式优化/宁波seo网络推广公司排名
  • 北京行业网站建设/推广网站的公司