C++中通过实例化 std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行 解锁。不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在 每个函数出口都要去调用unlock(),也包括异常的情况。C++标准库为互斥量提供了一个RAII 语法的模板类 std::lock_guard ,其会在构造的时候提供已锁的互斥量,并在析构的时候进行 解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。下面的程序清单中,展示了如何 在多线程程序中,使用 std::mutex 构造的 std::lock_guard 实例,对一个列表进行访问保 护。 std::mutex 和 std::lock_guard 都在 头文件中声明。
一、互斥量保护列表
# 清单3.1
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; // 1
std::mutex some_mutex; // 2
void add_to_list(int new_value){
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(new_value);// 3
}
bool list_contains(int value_to_find){
std::lock_guard<std::mutex> guard(some_mutex);// 4
return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
}
清单1.1中有一个全局变量1,这个全局变量被一个全局的互斥量保护2。add_to_list()3和 list_contains()4函数中使用 std::lock_guard ,使得这两个函数中对数据的访问 是互斥的:list_contains()不可能看到正在被add_to_list()修改的列表。
虽然某些情况下,使用全局变量没问题,但在大多数情况下,互斥量通常会与保护的数据放 在同一个类中,而不是定义成全局变量。这是面向对象设计的准则:将其放在一个类中,就 可让他们联系在一起,也可对类的功能进行封装,并进行数据保护。在这种情况下,函数 add_to_list和list_contains可以作为这个类的成员函数。互斥量和要保护的数据,在类中都需 要定义为private成员,这会让访问数据的代码变的清晰,并且容易看出在什么时候对互斥量 上锁。当所有成员函数都会在调用时对数据上锁,结束时对数据解锁,那么就保证了数据访 问时不变量不被破坏。
当然,也不是总是那么理想,聪明的你一定注意到了:当其中一个成员函数返回的是保护数 据的指针或引用时,会破坏对数据的保护。具有访问能力的指针或引用可以访问(并可能修改) 被保护的数据,而不会被互斥锁限制。互斥量保护的数据需要对接口的设计相当谨慎,要确 保互斥量能锁住任何对保护数据的访问,并且不留后门。
二、共享数据保护
使用互斥量来保护数据,并不是仅仅在每一个成员函数中都加入一个 std::lock_guard 对象那 么简单;一个迷失的指针或引用,将会让这种保护形同虚设。不过,检查迷失指针或引用是 很容易的,只要没有成员函数通过返回值或者输出参数的形式向其调用者返回指向受保护数 据的指针或引用,数据就是安全的。如果你还想往祖坟上刨,就没这么简单了。在确保成员 函数不会传出指针或引用的同时,检查成员函数是否通过指针或引用的方式来调用也是很重 要的(尤其是这个操作不在你的控制下时)。函数可能没在互斥量保护的区域内,存储着指针或 者引用,这样就很危险。更危险的是:将保护数据作为一个运行时参数,如同下面清单中所 示那样。
class some_data{
int a;
std::string b;
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> l(m);
func(data); // 1 传递“保护”数据给用户函数
}
};
some_data* unprotected;
void malicious_function(some_data& protected_data){
unprotected=&protected_data;
}
data_wrapper x;
void foo(){
x.process_data(malicious_function);// 2 传递一个恶意函数
unprotected->do_something(); // 3 在无保护的情况下访问保护数据
}
例子中process_data看起来没有任何问题, std::lock_guard 对数据做了很好的保护,但调用 用户提供的函数func1,就意味着foo能够绕过保护机制将函数 malicious_function 传递进去 2,在没有锁定互斥量的情况下调用 do_something() 。
这段代码的问题在于根本没有保护,只是将所有可访问的数据结构代码标记为互斥。函
数 foo() 中调用 unprotected->do_something() 的代码未能被标记为互斥。这种情况下, C++线程库无法提供任何帮助,只能由程序员来使用正确的互斥锁来保护数据。从乐观的角度 上看,还是有方法可循的:切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论 是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中 去。
虽然这是在使用互斥量保护共享数据时常犯的错误,但绝不仅仅是一个潜在的陷阱而已。
来源:并行编程