从整体上看,所有线程之间共享数据的问题,都是修改数据导致的。在谈到并发时,术语竞争条件通常用来表示有问题的竞争条件,良性的竞争条件没什么意思。C++标准还定义了术语数据竞争,表示因单个对象的并发修改而产生的特定类型的竞争条件,造成可怕的未定义行为

使用C++中的互斥元

通过构造 std::mutex 的实例创建互斥元,调用成员函数 lock() 来锁定它,unlock() 来解锁。然而,直接调用成员函数是不推荐的做法,因为这意味着你必须记住在离开函数的每条代码路径上都调用 unlock,包括异常所导致的在内。作为替代,标准C++库提供了 std::lock_guard 类模板,实现了互斥元的 RAII 管用语法;它在构造时锁定所给的互斥元,在析构时将互斥元解锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list; // 全局变量,被相应的 std::mutex 全局实例保护
std::muitext some_mutex;

void add_to_list(int val)
{
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(val);
}

bool list_contains(int val)
{
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(), some_list.end(), val) != some_list.end();
}

这是只是函数级别的保护,但通过能够访问(并可能修改)该指针或引用的任意代码可以访问受保护的数据而无需锁定该互斥元。因此使用互斥元保护数据需要仔细社畜接口,以确保在有任意对受保护的数据进行访问前,互斥元已被锁定,且不留后门。

不要将对受保护的数据的指针和引用传递到锁的范围之外,无论是通过从函数中返回它们、将其存放在外部可见的内存中,还是作为参数传递给用户提供的函数。

发现接口中固有的竞争条件

1
2
3
4
5
6
7
stack<int> s;
if (!s.empty())
{
int const value = s.top(); // 多线程不安全1:和 s.empty() 之间可能删除了元素
s.pop(); // 多线程不安全2:也可能删除了元素
do_something(value);
}

双线程下,它们的运行顺序可能是以下的情况,将看到两个一样的值:

线程A 线程B
if (!s.empty())
if (!s.empty())
int const value = s.top();
int const value = s.top();
s.pop();
do_something(value); s.pop();
do_something(value);

pop() 的两次调用之间没有对 top() 的调用,这远比 empty() / top() 竞争的未定义行为更糟糕,从来没有任何明显的错误发生,同时错误造成的后果可能和诱因差距甚远,尽管他们明显取决于 so_something() 到底做什么。