《C++编程规范:101条规则、准则与最佳实践》——2.8懂得何时和如何进行并发性编程

2.8懂得何时和如何进行并发性编程

摘要
安线全程地[4]:如果应用程序使用了多个线程或者进程,应该知道如何尽量减少共享对象(见第10条),以及如何安全地共享必须共享的对象。

讨论
线程处理是一个大课题。之所以撰写本条,是因为这个课题很重要,需要明确地予以阐述,但是单凭一个条款显然无法做出公允的评价,所以我们只简单地概述几个要点。更多的细节和具体技术,参阅本条的参考文献。其中最重要的问题是避免死锁、活锁(livelock)[5]和恶性的竞争条件(包括加锁不足导致的崩溃)。

C++标准关于线程未置一词。然而,C++经常而且广泛地用于编写可靠的多线程代码。如果应用程序需要跨线程共享数据,请如下安全行事。

参考目标平台的文档,了解该平台的同步化原语。典型的原语包括从轻量级的原子整数操作到内存障栅(memory barrier)[6],再到进程内和跨进程的互斥体。

最好将平台的原语用自己设计的抽象包装起来。在需要跨平台移植性的时候,这样做尤其有益。或者,也可以使用程序库(比如pthreads [Butenhof 97])为我们代劳。

确保正在使用的类型在多线程程序中使用是安全的。说得具体一些,就是类型必须至少做到以下两个方面。

保证非共享的对象独立。两个线程能够自由地使用不同的对象,无需调用者的任何特殊操作。

记载调用者在不同线程中使用该类型的同一个对象需要做什么。许多类型要求对这种共享对象进行串行访问,但是有些类型却不要求这样。后者通常要么从设计中去掉加锁需求,要么自己进行内部加锁,无论哪种情况,仍然需要留意内部加锁粒度的局限。

请注意,无论类型是字符串类型,还是STL容器比如vector,或者任何其他类型,上面的原则都适用。(我们留意到有些书的作者曾经给出建议,暗示标准容器有特殊性。其实并非如此,容器也只不过是一种对象而已。)说得具体一些,如果要在多线程程序中使用标准库组件(例如string,容器),如前所述,应该参考标准库实现的文档,了解是否支持多线程。

在自己编写可用于多线程程序的类型时,也必须完成两项任务。首先,必须保证不同线程能够不加锁地使用该类型的不同对象(注意:具有可修改的静态数据的类型通常不能保证这一点)。其次,必须在文档中说明使用者在不同线程中使用该类型的同一个对象需要做什么,基本的设计问题是如何在类及其客户之间分配正确执行(即无竞争和无死锁地执行)的职责。主要的选择有下列几个方面。

外部加锁:调用者负责加锁。在这种选择下,由使用对象的代码负责了解是否跨线程共享了对象,如果是,还要负责串行化所有对该对象的使用。例如,字符串类型通常使用外部加锁(或者不变性,见第三种选择)。

内部加锁:每个对象将所有对自己的访问串行化,通常采用为每个公用成员函数加锁的方法来实现,这样调用者就可以不用串行化对象的使用了。例如,生产者/消费者队列通常使用内部加锁,因为它们存在的目的就是被跨线程共享,而且它们的接口就是为了在单独的成员函数调用(Push, Pop)期间能够进行适当的层次加锁而设计的。更一般的情况下,需要注意,只有在知道了以下两件事情之后这个选项才适用。
第一,必须事先知道该类型的对象几乎总是要被跨线程共享的,否则到头来只不过进行了无效加锁。请注意大多数类型都不会遇到这种情况,即使是在多线程处理分量很重的程序中,大多数对象也不会被跨线程共享(这是好现象,见第10条)。
第二,必须事先知道成员函数级加锁的粒度是合适的,而且能满足大多数调用者的需要。具体而言,类型接口的设计应该有利于粗粒度的、自给自足的操作。如果调用者总是需要对多个而不是一个操作加锁,那么就不能满足需要了,只能通过增加更多的(外部)锁,将单独加锁的函数组装成一个更大规模的已加锁工作单位。例如一个容器类型,如果它返回一个迭代器,则迭代器可能在用到之前就失效了;如果它提供find之类的能返回正确答案的成员算法,那么答案可能在用到之前就出错了;如果它的用户想要编写这样的代码:if( c.empty() ) c.push_back(x);,同样会出现问题。(更多的例子,参阅 [Sutter02]。)在这些情况下,调用者需要进行外部加锁,以获得生存期能够跨越多个单独成员函数调用的锁,这样一来每个成员函数的内部加锁就毫无用武之地了。因此,内部加锁是绑定于类型的公用接口的:在类型的各个单独操作本身都完整时,内部加锁才适用;换句话说,类型的抽象级别不仅提升了,而且表达和封装得更加精确了(比如,以生产者-消费者队列的形式,而不是普通的vector)。将多个原语操作结合起来,形成粒度更粗的公开操作,不仅可以确保函数调用有意义,而且可以确保调用简单。如果原语的结合是不能确定的,而且也无法将合理的使用场景集合集中到一个命名操作中,那么有两种选择:一是使用基于回调的模型(即让调用者调用一个单独的成员函数,但是以一个命令或者函数对象的形式传入它们想要执行的任务,见第87条到第89条);二是在接口中以某种方式暴露加锁。

不加锁的设计,包括不变性(只读对象):无需加锁。将类型设计得根本无需加锁是可能的(参阅本条的参考文献)。常见的例子是不变对象,它无需加锁,因为它从不发生变化。例如,对于一个不变的字符串类型而言,字符串对象一旦创建就不会改变,每个字符串操作都会创建新的字符串。

请注意,调用代码应该不需要知道你的类型的实现细节(见第11条)。如果类型使用了底层数据共享技术[如写时复制(copy-on-write)],那么你就不需要为所有可能的线程安全性问题负责了,但是必须负责恢复“恰到好处的”线程安全,以确保调用代码在履行其通常职责时仍是正确的:类型必须能够尽可能地安全使用,如果它没有使用隐蔽的实现共享(见[Sutter04c])。前面已经提到,所有正确编写的类型都必须允许在不同线程中无需同步便可操作不同的可见对象。

如果编写的是一个将要广泛使用的程序库,那么尤其要考虑保证对象能够在前面叙述的多线程程序中安全使用,而且又不会增加单线程程序的开销。例如,如果你正在编写的程序库包含一个使用了写时复制的类型,并且因而必须至少进行某种内部加锁,那么最好安排加锁在程序库的单线程编译版本中消失[#ifdef和空操作(no-op)实现是常见的策略]。

在获取多个锁时,通过安排所有获取同样的锁的代码以相同的顺序获取锁,可以避免死锁情况的发生。(释放锁则可以按照任意顺序进行。)解决方案之一,是按内存地址的升序获取锁,地址恰好提供了一个方便、唯一而且是应用程序范围的排序。

参考文献
[Alexandrescu02a] ● [Alexandrescu04] ● [Butenhof97] ● [Henney00] ● [Henney01] ● [Meyers04] ● [Schmidt01] ● [Stroustrup00] §14.9 ● [Sutter02] §16 ● [Sutter04c]

时间: 2024-08-13 20:51:09

《C++编程规范:101条规则、准则与最佳实践》——2.8懂得何时和如何进行并发性编程的相关文章

《C++编程规范:101条规则、准则与最佳实践》——导读

前言 C++编程规范:101条规则.准则与最佳实践尽早进入正轨:以同样的方式实施同样的过程.不断积累惯用法.将其标准化.如此,你与莎士比亚之间的唯一区别将只是掌握惯用法的多少,而非词汇的多少. --Alan Perlis[1]} 标准最大的优点在于,它提供了如此多样的选择. --出处尚无定论 我们之所以编写本书,作为各开发团队编程规范的基础,有下面两个主要原因. 编程规范应该反映业界最久经考验的经验.它应该包含凝聚了经验和对语言的深刻理解的公认的惯用法.具体而言,编程规范应该牢固地建立在大量丰富

《C++编程规范:101条规则、准则与最佳实践》——第2章设计风格设计风格 C++编程规范:101条规则、准则与最佳实践 复杂性啊,愚人对你视而不见,实干家受你所累。 有些人避而远之。惟智者能够善加消除。 ——Alan Perlis 我知道,但是却又忘记了Hoare的至理名言:不成熟的优化是程

第2章设计风格 C++编程规范:101条规则.准则与最佳实践 复杂性啊,愚人对你视而不见,实干家受你所累. 有些人避而远之.惟智者能够善加消除. --Alan Perlis 我知道,但是却又忘记了Hoare的至理名言:不成熟的优化是程序设计中的万恶之源. --Donald Knuth[1] The Errors of TeX[Knuth89] 完全区分设计风格与编码风格是非常困难的.我们将一般在实际编写代码时才用得到的条款留到下一部分介绍. 本部分集中讨论适用面比一个特定的类或者函数更广的原则和

《C++编程规范:101条规则、准则与最佳实践》——第一章组织和策略问题1.1不要拘泥于小节 (又名:了解哪些东西不应该标准化)

第一章组织和策略问题 C++编程规范:101条规则.准则与最佳实践如果人们按照程序员编程的方式修建房屋,那么一只啄木鸟就能毁灭整个文明. --Gerald Weinberg[1] 为了遵从C和C++的伟大传统,我们从0开始编号.首要的指导原则,也就是第0条,阐明了我们认为对编程规范而言最为基本的建议. 接下来,这个导论性部分的其他条款将主要讲述几个精心选择的基本问题,这些问题大多数与代码本身并没有直接关系,它们讨论的是编写坚实代码所必需的工具和技术. 本部分中我们选出的最有价值条款是第0条:"不

了解Java 8功能如何让并发性编程变得更容易

在期待已久的 Java 8 版本中,并发性方面已实现了许多改进,其中包括在java.util.concurrent 层级中增加新的类和强大的新并行流 功能.设计流的目的是与lambda 表达式 共同使用,Java 8 的这项增强也使得日常编程的其他很多方面变得更加简便.(参见介绍 Java 8 语言的 指南文章,了解对于 lambda 表达式的介绍及相关 interface改动.) 本文首先介绍了新的 CompletableFuture 类如何更好地协调异步操作.接下来,我将介绍如何使用并行流(

机器学习规则:ML工程最佳实践----rules_of_ml section 1【翻译】

作者:黄永刚 机器学习规则:ML工程最佳实践 本文旨在指引具有机器学习基础知识的工程师等人,更好的从机器学习的实践中收益.介绍一些应用机器学习需要遵循的规则,类似于Google C++ 风格指南等流行的编程指南.如果你已经上过机器学习相关课程或者正在从事相关的工作,那你已经满足阅读本文所需的背景知识了. Before Machine Learning Rule: #1: 不要害怕开发没有应用机器学习技术的产品 Rule: #2: 设计评价指标并设立优先级 Rule: #3: 先使用复杂的启发式规

Delphi面向对象编程的20条规则之一

问题描述 前言 大多数Delphi程序员都像使用VisualBasic那样使用他们手头上开发工具,而丝毫没有意识到Delphi的强大功能,更谈不上使用这些功能了.(写到这里,编辑惶恐的举起了手,怎么可能呢?)Delphi和VisualBasic不同,Delphi完全建立在面向对象结构上,这不仅影响到VCL的结构,而且影响到使用Delphi开发的每一个程序. 在本文中,我不想涉及到面向对象编程(OOP)的所有理论,只是提出一些简单的经验规则.希望这些规则能够帮助改善你的程序结构.无论你开发的是何种

《PostgreSQL服务器编程》一一1.8 程序设计最佳实践

1.8 程序设计最佳实践 开发应用程序软件是复杂的.一些有助于管理复杂性的方法非常流行,以至于它们被赋予容易记忆的首字母缩略词.接下来,我们就会介绍一些这样的规则,并介绍如何在服务器程序设计时更好地遵守这些规则.1.8.1 KISS--尽量简单(keep it simple stupid)成功的程序设计的一个重要技术就是编写简单的代码.也就是,你编写的代码3年以后仍然可以很容易理解,并且其他人也可以理解.这种方式并不一定总是行得通,但是尽可能用最简单的方法编写代码总是有意义的.由于各种原因,比如

【译】11条Java异常处理的最佳实践

本文翻译自Top 11 Java Exception Best Practices 在之前关于Java异常的文章中,已经探讨过suppressed exceptions和Java Exceptions Tutorial两个方面的内容.要想在实际项目中正确处理Java异常,你应该熟练掌握一些Java异常处理的最佳实践. Java 异常处理的最佳实践 不要 在catch语句块中压制异常 public class ExceptionExample { public FileInputStream te

《C++编程规范:101条规则、准则与最佳实践》——2.3编程中应知道何时和如何考虑可伸缩性

2.3编程中应知道何时和如何考虑可伸缩性 摘要小心数据的爆炸性增长:不要进行不成熟的优化,但是要密切关注渐近复杂性.处理用户数据的算法应该能够预测所处理的数据量耗费的时间,最好不差于线性关系.如果能够证明优化必要而且非常重要,尤其在数据量逐渐增长的情况下,那么应该集中精力改善算法的O(N)复杂性,而不是进行小型的优化,比如节省一个多余的加法运算. 讨论本条款阐述了第8条"不要进行不成熟的优化"和第9条"不要进行不成熟的劣化"之间的一个重要的平衡点.所以,这个条款非常