C++11:感觉像是门新语言 内存模型 auto 和 decltype constexpr lambda unique_ptr 并发支持 泛型编程 静态类型 标准库组件 多核、缓存、推测执行、指令乱

C++11:感觉像是门新语言

C++11 [Becker 2011] 发布后,其实现相对来说很快就出现了。这导致了极大的热情,增加了使用,有大量新人涌入 C++ 世界,并进行了大量的实验。C++11 的三个完整或几乎完整的实现在 2013 年面世。我当时的评论被广泛认为是准确的——C++11 感觉像是一门新的语言 [Stroustrup 2014d]。为什么 C++11 在帮助程序员方面做得如此出色?又是如何做到的?

C++11之所以感觉像是一门新的语言并在帮助程序员方面做得如此出色,是因为它带来了大量引人注目的新特性,这些特性不仅改进了C++语言的效率和安全性,也大大简化了代码编写和维护的复杂性。具体来说,C++11的出色之处体现在以下几个方面:

首先,C++11引入了右值引用和移动语义,这是一个非常重要的新特性。通过利用右值引用,程序员可以更高效地处理临时对象和资源的转移,从而避免了不必要的拷贝操作,提升了程序的性能。

其次,C++11还引入了完美转发和可变参数模板,这些特性使得编写泛型代码更加灵活和方便。完美转发可以确保函数模板在转发参数时保持参数的原始值类别,而可变参数模板则允许程序员编写可以接受任意数量和类型参数的函数。

此外,C++11还扩展了auto和decltype关键字的使用范围,使得类型推导更加简单和直观。程序员不再需要显式地指定变量的类型,编译器可以根据变量的初始值自动推导其类型。这大大减少了代码中的冗余和错误,提高了编程效率。

另外,C++11还改进了STL容器,提供了统一的列表初始化语法,使得初始化容器和对象更加简洁和一致。同时,新的包装器和线程库等特性也进一步增强了C++的功能和易用性。

最后,C++11还改进了语法和错误处理机制,使得代码更加易于阅读和理解。例如,新的范围for循环语法可以简化对容器或数组的遍历操作,而异常处理机制的改进则使得程序在出现错误时能够更优雅地处理。

综上所述,C++11通过引入这些新特性和改进现有特性,使得C++语言在语法、性能、安全性和易用性等方面都得到了显著提升。因此,它感觉像是一门新的语言,并在帮助程序员方面做得如此出色。

C++11 引入了大量令人眼花缭乱的语言特性,包括:

  • 内存模型——一个高效的为现代硬件设计的底层抽象,作为描述并发的基础(§4.1.1
  • autodecltype——避免类型名称的不必要重复(§4.2.1
  • 范围 for——对范围的简单顺序遍历(§4.2.2
  • 移动语义和右值引用——减少数据拷贝(§4.2.3
  • 统一初始化—— 对所有类型都(几乎)完全一致的初始化语法和语义(§4.2.5
  • nullptr——给空指针一个名字(§4.2.6
  • constexpr 函数——在编译期进行求值的函数(§4.2.7
  • 用户定义字面量——为用户自定义类型提供字面量支持(§4.2.8
  • 原始字符串字面量——不需要转义字符的字面量,主要用在正则表达式中(§4.2.9
  • 属性——将任意信息同一个名字关联(§4.2.10
  • lambda 表达式——匿名函数对象(§4.3.1
  • 变参模板——可以处理任意个任意类型的参数的模板(§4.3.2
  • 模板别名——能够重命名模板并为新名称绑定一些模板参数(§4.3.3
  • noexcept——确保函数不会抛出异常的方法(§4.5.3
  • overridefinal——用于管理大型类层次结构的明确语法
  • static_assert——编译期断言
  • long long——更长的整数类型
  • 默认成员初始化器——给数据成员一个默认值,这个默认值可以被构造函数中的初始化所取代
  • enum class——枚举值带有作用域的强类型枚举

以下是主要的标准库组件列表(§4.6):

  • unique_ptrshared_ptr——依赖 RAII(§2.2.1)的资源管理指针(§4.2.4
  • 内存模型和 atomic 变量(§4.1.1
  • threadmutexcondition_variable 等——为基本的系统层级的并发提供了类型安全、可移植的支持(§4.1.2
  • futurepromisepackaged_task,等——稍稍更高级的并发(§4.1.3
  • tuple——匿名的简单复合类型(§4.3.4
  • 类型特征(type trait)——类型的可测试属性,用于元编程(§4.5.1
  • 正则表达式匹配(§4.6
  • 随机数——带有许多生成器(引擎)和多种分布(§4.6
  • 时间——time_pointduration§4.6
  • unordered_map 等——哈希表
  • forward_list——单向链表
  • array——具有固定常量大小的数组,并且会记住自己的大小
  • emplace 运算——在容器内直接构建对象,避免拷贝
  • exception_ptr——允许在线程之间传递异常

还有很多,但这些是最重要的变化。所有这些都在 [Stroustrup 2013] 中进行了描述,许多信息可以在网上获得(例如 [Cppreference 2011–2020])。

这些表面上互不相干的扩展怎么能组成一个连贯的整体?这怎么可能真正地改变我们写代码的方式,使之变得更好呢?C++11 确实做到了这一点。在相对较短的时间里(算 5 年吧),大量的 C++ 代码被升级到 C++11(并进一步升级到 C++14 和 C++17),而且 C++ 在会议和博客上的呈现也完全改变了。

这种在语言的“感觉”和使用风格上的巨大变化,并不是由某位大师级工匠指导的传统的精心设计过程的结果,而是海量建议经由一大批不断变化的个人层层决策过滤后的结果。

在我的 HOPL3 论文 [Stroustrup 2007] 中,我正确地描述了 C++11 语言的许多特性。值得注意的例外是“概念”,我会在(§6)中进行讨论。我将不再赘述细节,而是根据它们所解决的程序员需求来描述功能的“主题”分类。我认为这种看待提案的方式是 C++11 成功的根源:

  • §4.1:支持并发
  • §4.2:简化使用
  • §4.3:改进对泛型编程的支持
  • §4.4:提高静态类型安全
  • §4.5:支持对库的开发
  • §4.6:标准库组件

这些“主题”并不是不相干的。事实上,我猜想 C++11 之所以成功,是因为它相互关联的功能彼此加成,形成了一张精细的网络,可以处理真正的需求。每一个主题里都有我喜欢的特性。我怀疑,我在写作(例如 [Stroustrup 1993, 1994, 2007])和演讲中明确表述了 C++ 的目标,也帮助设计保持了合理的重点。对我来说,衡量每个新特性的一个关键指标是它是否使 C++ 更接近它的理想,例如,是否通过引入该特性能让对内建类型和用户定义类型的支持更加相似(§2.1)。

纵观 C++11,我们可以看到有些改进建议在 2002 年左右就被提出,有不少库也出现得很早,经常是作为 Boost 的一部分 [Boost 1998–2020]。然而,直到 2013 年才有完整的 C++11 实现。在 2020 年,一些组织仍在为升级到 C++11 而苦恼,因为代码库巨大,程序员不思进取,教学方式陈旧,以及编译器严重过时(尤其是在嵌入式系统领域)。不过 C++17 的采用速度明显快于 C++98 和 C++11;并且,早在 2018 年,C++20 的一些主要特性就已经投入生产使用。

直到 2018 年,我仍能看到 C++98 前的编译器被用于教学。我认为这是对学生的虐待,剥夺了他们接触学习我们 20 多年的进展的机会。

对标准委员会、主要编译器厂商以及大多数 C++ 的积极支持者来说已是遥远的过去的东西,对许多人来说仍然是现在,甚至是未来。其结果是,人们对 C++ 到底是什么仍然感到困惑。只要 C++ 继续演化,这种困惑就会持续下去。

在C++社区中确实普遍存在,并且这也是任何不断发展的编程语言都会面临的问题。C++由于其复杂性和广泛的应用领域,这个问题尤为突出。从您的描述中,我们可以看到几个关键点:

C++11的实现与采用延迟:尽管C++11引入了大量受欢迎的特性,但直到2013年才出现完整的实现。这主要是因为实现这些特性需要编译器厂商投入大量资源,并且还需要确保与现有代码的兼容性。此外,许多组织由于各种原因(如代码库规模、程序员技能水平、教学方式和编译器版本)在升级到C++11时面临挑战,这进一步延缓了C++11的普及。

教育中的滞后:您提到的使用过时编译器进行教学的情况确实令人担忧。这剥夺了学生接触和学习最新C++特性的机会,也让他们在未来的职业生涯中可能面临技能上的挑战。教育机构和教师应该积极更新他们的教学工具和内容,以确保学生能够学习到最新、最有用的知识。

C++的持续演化与困惑:C++作为一个持续演化的语言,不断引入新的特性和改进。然而,这也导致了人们对C++到底是什么感到困惑。这种困惑不仅存在于初学者中,甚至在经验丰富的开发者中也很常见。为了解决这个问题,社区需要更好地宣传和推广新特性,并提供清晰的迁移指南和最佳实践。

针对这些问题,我们可以采取以下措施:

鼓励编译器厂商加快实现新标准:通过提供激励和支持,鼓励编译器厂商尽快实现新标准,并改进与现有代码的兼容性。
推动组织升级C++版本:通过提供迁移工具和最佳实践指南,帮助组织克服升级到新版本的挑战,加快C++新特性的普及。
更新教育内容和工具:教育机构和教师应该密切关注C++的最新发展,并及时更新他们的教学内容和工具,以确保学生能够接触到最新的知识和技术。
加强社区宣传和推广:通过举办讲座、研讨会和在线活动等方式,加强社区对新特性的宣传和推广,帮助开发者更好地理解和应用这些特性。
总之,尽管C++的演化带来了挑战和困惑,但这也为C++社区提供了持续进步和发展的机会。通过采取上述措施,我们可以更好地应对这些挑战,推动C++的不断进步和普及。

4.1 C++11:

并发支持

C++11 必须支持并发。这既是显而易见的,也是所有主要用户和平台供应商的共同需求。C++ 一直在大多数软件工业的基础中被重度使用,而在二十一世纪的头十年,并发性变得很普遍。利用好硬件并发至关重要。和 C 一样,C++ 当然一直支持各种形式的并发,但这种支持那时没有标准化,并且一般都很底层。机器架构正在使用越来越精巧的内存架构,编译器编写者也在应用越来越激进的优化技术,这让底层软件编写者的工作极为困难。机器架构师和优化器编写者之间亟需一个协定。只有有了明确的内存模型,基础库的编写者才能有一个稳定的基础和一定程度的可移植性。

C++11对并发的支持确实是一个重大的进步,它不仅满足了用户和平台供应商的需求,也为开发者提供了一个更加稳定和可移植的基础。在二十一世纪的头十年,随着多核处理器的普及和云计算的兴起,并发性变得越来越重要。因此,C++11的并发支持不仅是显而易见的,也是必不可少的。

在C++11之前,虽然C++支持各种形式的并发,但这些支持并没有标准化,通常都很底层,依赖于特定的平台和编译器。这导致了并发编程的复杂性和不可移植性。C++11通过引入一系列新的特性和工具,极大地简化了并发编程,并提高了其可移植性。

其中,最重要的特性之一是引入了线程库(),它提供了创建和管理线程的高级抽象。这使得开发者能够更轻松地编写多线程程序,而无需深入了解底层平台的细节。此外,C++11还提供了互斥量(mutexes)、条件变量(condition variables)和其他同步原语,以帮助开发者在多个线程之间协调操作,避免数据竞争和其他并发问题。

另一个重要的改进是引入了原子操作(atomic operations)。原子操作是不可中断的操作,即在执行完毕之前不会被其他线程打断。这对于实现无锁数据结构和其他高级并发模式至关重要。C++11提供了对原子类型的内置支持,使得开发者能够更容易地编写线程安全的代码。

此外,C++11还明确了内存模型,定义了数据如何在多个线程之间共享和交互。这有助于解决由于硬件架构和编译器优化导致的内存可见性问题。通过遵循C++11的内存模型,开发者可以编写出更加可靠和可移植的并发代码。

总的来说,C++11对并发的支持为开发者提供了一个更加稳定、高效和可移植的基础。它简化了并发编程的复杂性,降低了出错的可能性,并使得利用硬件并发变得更加容易。随着多核处理器和分布式系统的普及,C++11的这些并发特性将继续发挥重要作用,推动软件工业的发展。

并发方面的工作从 EWG 中分离出来,成为由 Hans-J. Boehm(惠普,后加入谷歌)领导的专家成员组成的并发组。它有三项职责:

此外,并行算法(§8.5)、网络(§8.8.1)和协程(§9.3.2)是单独分组处理的,并且(正如预期)还没法用于 C++11。

4.1.1 内存模型

内存模型是并发编程中的一个核心概念,尤其在多核、缓存、推测执行和指令乱序的复杂环境中,精确地规定访问内存的规则变得至关重要。C++11的内存模型为开发者提供了一个清晰的框架,以理解和控制并发程序中的内存访问行为。

C++11的内存模型定义了变量、数组和对象等如何在多线程环境中被访问和修改。它规定了在并发执行时,不同线程对共享数据的访问顺序和可见性。这有助于避免数据竞争和条件竞争等问题,确保程序的正确性和稳定性。

在C++11中,内存模型还涉及到一些重要的概念,如原子操作、内存顺序和内存一致性等。原子操作是不可中断的操作,即在执行过程中不会被其他线程打断,这有助于实现线程安全的共享数据访问。内存顺序则定义了多个内存访问操作之间的相对顺序,不同的内存顺序会对程序的并发性能和正确性产生影响。内存一致性则是指多个线程对共享数据的访问和修改是否保持一致。

值得注意的是,C++11的内存模型与C11的内存模型存在不兼容之处。这主要是由于在C标准付诸表决前的最后一刻,C委员会引入了与C++11不兼容的写法。这种不兼容性给C和C++的实现者和用户带来了额外的挑战和痛苦,需要他们更加小心地处理跨语言的并发编程问题。

为了应对这些挑战,开发者需要深入理解C++11和C11的内存模型,并仔细考虑如何在并发程序中正确地使用它们。此外,他们还需要关注编译器和硬件平台对内存模型的支持情况,以确保程序的正确性和性能。

总的来说,内存模型是并发编程中的一个关键概念,它有助于开发者理解和控制多线程程序中的内存访问行为。虽然C++11和C11在内存模型上存在一些不兼容之处,但通过深入理解和谨慎使用,开发者仍然可以编写出高效且稳定的并发程序。

最紧迫的问题之一,是在一个有着多核、缓存、推测执行、指令乱序等的世界里精确地规定访问内存的规则。来自 IBM 的 Paul McKenney 在内存保证方面的课题上非常活跃。来自剑桥大学的 Mark Batty 的研究 [Batty et al. 2013, 2012, 2010, 2011] 帮助我们将这一课题形式化,见 P. McKenney、M. Batty、C. Nelson、H. Boehm、A. Williams、S. Owens、S. Sarkar、P. Sewell、T. Weber、M. Wong、L. Crowl 和 B. Kosnik 合作的论文 [McKenney et al. 2010]。它是 C++11 的一个庞大而至关重要的部分。

在 C11 中,C 采用了 C++ 的内存模型。然而,就在 C 标准付诸表决前的最后一刻,C 委员会引入了不兼容的写法,而此时 C++11 标准修改的最后一次机会已经过去。这成了 C 和 C++ 实现者和用户的痛苦。

内存模型很大程度上是由 Linux 和 Windows 内核的需求驱动的。目前它不只是用于内核,而且得到了更加广泛的使用。内存模型被广泛低估了,因为大多数程序员都看不到它。从一阶近似来看,它只是让代码按照任何人都会期望的方式正常工作而已。

最开始,我想大多数委员都小瞧了这个问题。我们知道 Java 有一个很好的内存模型 [Pugh 2004],并曾希望采用它。令我感到好笑的是,来自英特尔和 IBM 的代表坚定地否决了这一想法,他们指出,如果在 C++ 中采用 Java 的内存模型,那么我们将使所有 Java 虚拟机的速度减慢至少两倍。因此,为了保持 Java 的性能,我们不得不为 C++ 采用一个复杂得多的模型。可以想见而且讽刺的是,C++ 此后因为有一个比 Java 更复杂的内存模型而受到批评。

基本上,C++11 模型基于先行发生(happens-before)关系 [Lamport 1978],并且既支持宽松的内存模型,也支持顺序一致 [Lamport 1979] 的模型。在这些之上,C++11 还提供了对原子类型和无锁编程的支持,并且与之集成。这些细节远远超出了本文的范围(例如,参见 [Williams 2018])。

不出所料,并发组的内存模型讨论有时变得有点激烈。这关系到硬件制造商和编译器供应商的重大利益。最困难的决定之一是同时接受英特尔的 x86 原语(某种全存储顺序,Total Store Order(TSO)模型 [TSO Wikipedia 2020] 加上一些原子操作)和 IBM 的 PowerPC 原语(弱一致性加上内存屏障)用于最底层的同步。从逻辑上讲,只需要一套原语,但 Paul McKenney 让我相信,对于 IBM,有太多深藏在复杂算法中的代码使用了屏障,他们不可能采用类似英特尔的模型。有一天,我真的在一个大房间的两个角落之间做了穿梭外交。最后,我提出必须支持这两种方式,这就是 C++11 采用的方式。当后来人们发现内存屏障和原子操作可以一起使用,创造出比单单使用其中之一更好的解决方案时,我和其他人都感到非常高兴。

稍后,我们增加了对基于数据依赖关系的一致性支持,通过属性(§4.2.10)在源代码中表示,比如 [[carries_dependency]]

C++11 引入了 atomic 类型,上面的简单操作都是原子的:

atomic<int> x;
void increment()
{
   
    x++;  // 不是 x = x + 1
}

显然,这些都是广泛有用的。例如,使用原子类型使出名棘手的双重检查锁定优化变得极为简单:

mutex mutex_x;
atomic<bool> init_x;  // 初始为 false
int x;

if (!init_x) {
   
    lock_guard<mutex> lck(mutex_x);
    if (!init_x) x = 42;
    init_x = true ;
}  // 在此隐式释放 mutex_x(RAII)

// ... 使用 x ...

双重检查锁定的要点是使用相对开销低的 atomic 保护开销大得多的 mutex 的使用。

lock_guard 是一种 RAII 类型(§2.2.1),它确保会解锁它所控制的 mutex

Hans-J. Boehm 将原子类型描述为“令人惊讶地流行”,但我不能说我感到惊讶。我没 Hans 那么专业,对简化更为欣赏。C++11 还引入了用于无锁编程的关键运算,例如比较和交换:

template<typename T>
class stack {
   
    std::atomic<node<T>*> head;
public:
    void push(const T& data)
    {
   
        node<T>* new_node = new node<T>(data);
        new_node->next = head.load(std::memory_order_relaxed);
        while(!head.compare_exchange_weak(new_node->next, new_node,
              std::memory_order_release, std::memory_order_relaxed)) ;
    }
    // ...
};

即使有了 C++11 的支持,我仍然认为无锁编程是专家级的工作。

4.1.2 线程和锁

在内存模型之上,我们还提供了“线程和锁”(threads and locks)的并发模型。我认为线程和锁级别的并发是应用程序使用并发最差的模型,但是对于 C++ 这样的语言来说,它仍然必不可少。不管它还是别的什么,C++ 一直是一种能够与操作系统直接交互的系统编程语言,可用于内核代码和设备驱动程序。因此,它必须支持系统最底层支持的东西。在此基础上,我们可以建立各种更适合特定应用的并发模型。就我个人而言,我特别喜欢基于消息的系统,因为它们可以消除数据竞争,而数据竞争可能产生极为隐晦的并发错误。

线程和锁确实是C++11中引入的重要并发模型,尽管在某些情况下可能不是最优的并发解决方案,但它们对于C++这样的通用编程语言来说是不可或缺的。线程允许程序在多个执行单元上并行运行,从而利用多核处理器或分布式系统的计算能力。而锁(如互斥量)则用于同步线程之间的访问,以防止数据竞争和不一致。

正如您所说,C++作为一种系统编程语言,需要与操作系统和底层硬件进行交互。因此,它必须提供对系统底层并发原语的支持,包括线程和锁。这些底层原语为开发者提供了足够的灵活性,以构建各种更高级别的并发模型。

虽然线程和锁在某些情况下可能不是最优雅的并发解决方案,但它们仍然是许多应用程序中实际使用的模型。通过谨慎地使用线程和锁,开发者可以编写出高效且正确的并发程序。当然,随着编程语言和库的发展,我们也在不断探索更高级别的并发抽象和模型,以简化并发编程的复杂性。

您提到的基于消息的系统是另一种有趣的并发模型。这种模型通过消息传递来协调线程或进程之间的操作,从而避免了直接共享数据所带来的潜在问题。基于消息的系统确实可以消除数据竞争,使得并发错误更容易被发现和修复。然而,这种模型也有其自身的挑战和限制,例如需要处理消息的传递和同步等问题。

总的来说,线程和锁是C++中不可或缺的并发模型,尽管它们可能不是所有情况下的最优选择。通过结合不同的并发模型和技术,我们可以根据具体的应用需求选择最适合的解决方案。

C++ 对线程和锁级别编程的支持是 POSIX 和 Windows 所提供的线程和锁的类型安全变体。在 [Stroustrup 2013] 有所描述,在 Anthony Williams 的书 [Williams 2012, 2018] 中有更为深入的探讨:

  • thread——系统的执行线程,支持 join()detach()
  • mutex——系统的互斥锁,支持 lock()unlock() 和保证 unlock() 的 RAII 方式
  • condition_variable——系统中线程间进行事件通信的条件变量
  • thread_local——线程本地存储

与 C 版本相比,类型安全使代码更简洁,例如,不再有 void** 和宏。考虑一个简单的例子,让一个函数在不同的线程上执行并返回结果:

class F {
     // 传统函数对象
public:
    F(const vector<double>& vv, double* p) : v{
   vv}, res{
   p} {
    }
    void operator()();        // 将结果放入 *res
private:
    const vector<double>& v;  // 输入源
    double* res;              // 输出目标
};

double f(const vector<double>& v);  // 传统函数

void g(const vector<double>& v, double* res); // 将结果放入 *res

int comp(vector<double>& vec1, vector<double>& vec2, vector<double>& vec3)
{
   
    double res1;
    double res2;
    double res3;
    // ...
    thread t1 {
   F{
   vec1,&res1}};           // 函数对象
    thread t2 {
   [&](){
   res2=f(vec2);}};    // lambda 表达式
    thread t3 {
   g,ref(vec3),&res3};       // 普通函数

    t1.join();
    t2.join();
    t3.join();

    cout << res1 << ' ' << res2 << ' ' << res3 << '\n';
}

类型安全库支持的设计非常依赖变参模板(§4.3.2)。例如,std::thread 的构造函数就是变参模板。它可以区分不同的可执行的第一个参数,并检查它们后面是否跟有正确数量正确类型的参数。

类似地,lambda 表达式(§4.3.1)使 <thread> 库的许多使用变得更加简单。例如,t2 的参数是访问周围局部作用域的一段代码(lambda 表达式)。

在发布标准的同时,让新特性在标准库中被接受和使用是很困难的。有人提出这样做过于激进,可能会导致长期问题。引入新的语言特性并同时使用它们无疑是有风险的,但它通过以下方式大大增加了标准的质量:

  • 给用户一个更好的标准库
  • 给用户一个很好的使用语言特性的例子
  • 省去了用户实现底层功能的麻烦
  • 迫使语言特性的设计者应对现实世界的困难应用

线程和锁模型需要使用某种形式的同步来避免竞争条件。C++11 为此提供了标准的 mutex(互斥锁):

mutex m;  // 控制用的互斥锁
int sh;   // 共享的数据

void access ()
{
   
    unique_lock<mutex> lck {
   m};   // 得到互斥锁
    sh += 7;                      // 操作共享数据
} // 隐式释放互斥锁

unique_lock 是一个 RAII 对象,确保用户不会忘记在这个 mutex 上调用 unlock()

这些锁对象还提供了一种防止最常见形式的死锁的方法:

void f()
{
   
    // ...
    unique_lock<mutex> lck1 {
   m1,defer_lock};  // 还未得到 m1
    unique_lock<mutex> lck2 {
   m2,defer_lock};
    unique_lock<mutex> lck3 {
   m3,defer_lock};
    // ...
    lock(lck1,lck2,lck3);  // 获取所有三个互斥锁
    // ... 操作共享数据 ...
}   // 隐式释放所有互斥锁

这里,lock() 函数“同时”获取所有 mutex 并隐式释放所有互斥锁(RAII(§2.2.1))。C++17 有一个更优雅的解决方案(§8.4)。

线程库是由 Pete Becker(Dinkumware)在 2004 年首次为 C++0x 提出的 [Becker 2004],它基于 Dinkumware 对 Boost.Thread [Boost 1998–2020] 所提供的接口的实现。在同一次会议上(华盛顿州 Redmond 市,2004 年 9 月)提出了第一个关于内存模型的提案 [Alexandrescu et al. 2004],这可能不是巧合。

最大的争议是关于取消操作,即阻止线程运行完成的能力。基本上,委员会中的每个 C++ 程序员都希望以某种形式实现这一点。然而,C 委员会在给 WG21 的正式通知 [WG14 2007] 中反对线程取消,这是唯一由 WG14(ISO C 标准委员会)发给 WG21 的正式通知。我指出,“但是 C 语言没有用于系统资源管理和清理的析构函数和 RAII”。管理 POSIX 的 Austin Group 派出了代表,他们 100% 反对任何形式的这种想法,坚称取消既没有必要,也不可能安全进行。事实上 Windows 和其他操作系统提供了这种想法的变体,并且 C++ 不是 C,然而 POSIX 人员对这两点都无动于衷。在我看来,恐怕他们是在捍卫自己的业务和 C 语言的世界观,而不是试图为 C++ 提出最好的解决方案。缺乏标准的线程取消一直是一个问题。例如,在并行搜索(§8.5)中,第一个找到答案的线程最好可以触发其他此类线程的取消(不管是叫取消或别的名字)。C++20 提供了停止令牌机制来支持这个用例(§9.4)。

4.1.3 期值(future)

一个类型安全的、标准的、类似 POSIX/Windows 的线程库是对正在使用的不兼容的 C 风格库的重大改进,但这仍然是 1980 年代风格的底层编程。一些成员,特别是我,认为 C++ 迫切需要更现代、更高层次的东西。举例来说,Matt Austern(谷歌,之前代表 SGI)和我主张消息队列(“通道”)和线程池。这些意见没有什么进展,因为有反对意见说没有时间来做这些事情。我恳求并指出,如果委员会中的专家不提供这样的功能,他们最终将不得不使用“由我的学生匆匆炮制的”功能。委员会当然可以做得比这好得多。“如果你不愿意这样做,请给我一种方法,就一种方法,在没有显式同步的情况下在线程之间传递信息!”

委员会成员分为两派,一派基本上想要在类型系统上有改进的 POSIX(尤其是 P.J. Plauger),另一派指出 POSIX 基本上是 1970 年代的设计,“每个人”都已经在使用更高层次的功能。在 2007 年的 Kona 会议上,我们达成了一个妥协:C++0x(当时仍期望会是 C++09)将提供 promisefuture,以及异步任务的启动器 async(),允许但不需要线程池。和大多数折中方案一样,“Kona 妥协”没有让任何人满意,还导致了一些技术问题。然而,许多用户认为它是成功的——大多数人不知道这当时是一种妥协——并且这些年来,已经出现了一些改进。

最后,C++11 提供了:

  • future——一个句柄,通过它你可以从一个共享的单对象缓冲区中 get() 一个值,可能需要等待某个 promise 将该值放入缓冲区。
  • promise——一个句柄,通过它你可以将一个值 put() 到一个共享的单对象缓冲区,可能会唤醒某个等待 futurethread
  • packaged_task——一个类,它使得设置一个函数在线程上异步执行变得容易,由 future 来接受 promise 返回的结果。
  • async()——一个函数,可以启动一个任务并在另一个 thread 上执行。

使用这一切的最简单方法是使用 async()。给定一个普通函数作为参数,async() 在一个 thread 上运行它,处理线程启动和通信的所有细节:

double comp4(vector<double>& v)
// 如果 v 足够大则会产生多个任务
{
   
    if (v.size()<10000)    // 值得用并发机制吗?
        return accum(v.begin(),v.end(),0.0);
    auto v0 = 
  • 49
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EwenWanW

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值