多线程编程中,如果需要在多个线程之间共享数据,一个必须要面对的问题是数据竞争。《C++并发编程实战》中提到有三种方法处理竞争。

  1. 在封装数据结构式提供保护机制,确保只有修改数据的线程在不变量损坏的地方读取数据。C++标准库提供了这样的机制,也是本文的重点。
  2. 使用无锁编程,通过修改数据结构,包括其不变量的设计来完成。
  3. 将对数据结构的更新作为一个事务(transaction),把对数据的读写存储在事务日志中,通过该日志完成各个操作。这个方法C++还没有相关支持。

使用互斥元共享数据

C++提供了互斥元(mutex)对共享数据进行保护。通过互斥元,将所有访问该数据结构的代码块标记为互斥的,即所有试图同时访问该数据结构的线程中只有一个线程能够成功,其他所有线程必须等待该线程访问完成。在访问共享数据之前,锁定(lock)与该数据相关的互斥元,其他试图锁定相同互斥元的线程必须等待;访问完成后,解锁(unlock)该互斥元,以供其他线程锁定互斥元并访问数据。

API

std::mutex即是一个互斥元类,成员函数lock()unlock()负责锁定和解锁它。当然,直接调用其成员函数不是推荐的做法,更好的方法是利用RAII实现一个类,在其构造和析构时自动锁定和解锁互斥元。C++标准库提供了std::lock_guard完成了对互斥元的RAII。

#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list;
std::mutex some_mutex;

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

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

该例中,some_listsome_mutex保护的对象,两个函数则是互斥的代码块,意味着你不能在为some_list添加元素的同时访问它。
当然,该例中使用全局变量的做法并不值得推荐。更好的做法是将需要被保护的数据和互斥元封装到一个类中并将它们声明为privatelist_containadd_to_list将会成为类的成员函数。所有类的成员函数在访问受保护的数据时都应锁定互斥元,并在访问完成后解锁。
对于这种做法,聪明的你会发现,如果某个成员函数返回受保护数据的指针或引用时,会造成严重的隐患:无需锁定互斥元,即可通过指针或引用访问访问受保护的数据。因此,需要精心设计函数接口,以免在不经意间把受保护的对象泄露出去。

为保护共享数据精心组织代码

你已经发现,成员函数中,一个不经意的输出指针或引用,就会造成被保护数据的泄露。然而,如果我们检查所有成员函数没有输出被保护对象的指针或引用,就能保证数据安全了吗?并不是的,你仍有可能向某些不在你掌控之下的函数传入危险的指针或引用。考虑下面这个例子:

class some_data
{
    int a;
    std::string s;
public:
    void do_something();
};

class data_wrapper
{
private:
    some_data data;
    std::mutex m;
public:
    template<typename Function>
    void process_data(Function func)
    {
        std::lock_guard<std::mutex> guard(m);
        func(data);
    }
};

some_data* unprotected;
void malicious_function(some_data& protected_data)
{
    unprotected = &protected_data;
}

data_wrapper x;

void foo()
{
    x.process_data(malicious_function);
    unprotected->do_something();
}

尽管看起来成员函数process_data提供了对数据的保护,但是被保护的代码块,即malicious_function是通过成员函数参数在运行时提供的,它捕获了了受保护对象的指针,这意味着可以绕过互斥元,实现对some_data::do_something()的调用。本质上,该段代码的问题仍在于,对于所有访问受保护数据的代码块,没有标记其为互斥的,忽略了unprotected->do_something()中对数据的访问。C++并不会给你指出这样的错误,需要作为程序员的你思考并发现潜在的问题,但是它给我们指明了一个基本的原则:不要将受保护数据的指针和引用传递到互斥元可保护范围之外。