CppCoreGuidelines CP.1 预先假定代码会是多线程程序的一部分
“Assume that your code will run as part of a multi-threaded program”
理由
目前没用到并发,很难确定未来某个时候会不会用到。比如:代码重用了。没用到线程的库在程序其他部分的使用了,而这些程序用到了线程。
注意,此规则对于代码库来说比较重要,对于单独的应用相对还好。不过,随着时间推移,很多代码片段也会运行在某些多线程环境。
坏例子
double cached_computation(int x) { // bad: these statics cause data races in multi-threaded usage static int cached_x = 0.0; static double cached_result = COMPUTATION_OF_ZERO; if (cached_x != x) { cached_x = x; cached_result = computation(x); } return cached_result; }
尽管 cached_computation
的计算在单线程环境下能很完美地工作。但是如果放到多线程环境,两个静态便利的结果就会产生数据竞争。于是,会有难以预料的行为结果了。
好例子
struct ComputationCache { int cached_x = 0; double cached_result = COMPUTATION_OF_ZERO; double compute(int x) { if (cached_x != x) { cached_x = x; cached_result = computation(x); } return cached_result; } };
这段代码中, chached_x
是 ComputationCache
对象的成员数据,而不是静态的共享数据。这个代码重构把多线程的处理上升到调用代码的地方。如果调用此接口的程序是单线程程序,那么依然可以使用全局的 ComputationCache
,如果是多线程程序,那么你可能需要给每一个线程分配一个 ComputationCache
实例。这个重构后的函数,不在尝试管理 cached_x
的资源分配。这里应用到了单一责任原则(Single Responsibility Principle, SRP)。
在这个典型的例子中,针对线程安全的代码重构,也同时提升了代码的可重用性。不难想象,一个单线程程序可能需要在程序的多个不同的部分用到多个
ComputationCache
实例,而不互相重置缓存的数据。
还有其他一些支持线程安全的方式:
- 不要把状态变量设置为
static
而是设置为thread_local
- 添加并发控制,比如,通过一个静态的
std::mutex
保护对静态变量的访问 - 添加编译开关,让非线程安全的代码在多线程环境无法编译或运行
- 提供两个实现方案:一个用于单线程环境,一个用于多线程环境
例外
如果确定代码永远不会在多线程环境下执行的话,就可以不用考虑此条规则。
注意:很多例子表明,一开始觉得不会在多线程环境中执行,而多年以后却需要支持多线程。这种程序通常会痛苦的发现需要很多精力避免数据竞争。所以,对于永远不会在多线程环境执行的代码,请明确进行标记,最好是添加编译或运行时保障机制,确保尽早发现可能出现的 bug。