第3章 线程间共享数据
本章主要内容
- 共享数据带来的问题
- 使用互斥量保护数据
- 数据保护的替代方案
保护共享数据结构的最基本的方式,是使用C++标准库提供的互斥量(mutex)。
清单3.1 使用互斥量保护列表 推荐 使用 lock_guard
不推荐使用 louck()与 unlouck()这样的方式
#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); // 3
some_list.push_back(new_value);
}
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();
}
这两个函数中对数据的访问是互斥的:list_contains()不可能看到正在被add_to_list()修改的列表。
虽然某些情况下,使用全局变量没问题,但在大多数情况下,互斥量通常会与保护的数据放在同一个类中,而不是定义成全局变量。这是面向对象设计的准则:将其放在一个类中,就可让他们联系在一起,也可对类的功能进行封装,并进行数据保护。在这种情况下,函数add_to_list和list_contains可以作为这个类的成员函数。互斥量和要保护的数据,在类中都需要定义为private成员,这会让访问数据的代码变的清晰,并且容易看出在什么时候对互斥量上锁。当所有成员函数都会在调用时对数据上锁,结束时对数据解锁,那么就保证了数据访问时不变量不被破坏。
当然,也不是总是那么理想,聪明的你一定注意到了:当其中一个成员函数返回的是保护数据的指针或引用时,会破坏对数据的保护。具有访问能力的指针或引用可以访问(并可能修改)被保护的数据,而不会被互斥锁限制。互斥量保护的数据需要对接口的设计相当谨慎,要确保互斥量能锁住任何对保护数据的访问,并且不留后门。
====================================================================================================
3.2.2 精心组织代码来保护共享数据
使用互斥量来保护数据,并不是仅仅在每一个成员函数中都加入一个std::lock_guard
对象那么简单;一个迷失的指针或引用,将会让这种保护形同虚设。不过,检查迷失指针或引用是很容易的,只要没有成员函数通过返回值或者输出参数的形式向其调用者返回指向受保护数据的指针或引用,数据就是安全的。如果你还想往祖坟上刨,就没这么简单了。在确保成员函数不会传出指针或引用的同时,检查成员函数是否通过指针或引用的方式来调用也是很重要的(尤其是这个操作不在你的控制下时)。函数可能没在互斥量保护的区域内,存储着指针或者引用,这样就很危险。更危险的是:将保护数据作为一个运行时参数,如同下面清单中所示那样。
清单3.2 无意中传递了保护数据的引用
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
对数据做了很好的保护,但调用用户提供的函数func①,就意味着foo能够绕过保护机制将函数malicious_function
传递进去②,在没有锁定互斥量的情况下调用do_something()
。
这段代码的问题在于根本没有保护,只是将所有可访问的数据结构代码标记为互斥。函数foo()
中调用unprotected->do_something()
的代码未能被标记为互斥。这种情况下,C++线程库无法提供任何帮助,只能由程序员来使用正确的互斥锁来保护数据。从乐观的角度上看,还是有方法可循的:切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中去。
虽然这是在使用互斥量保护共享数据时常犯的错误,但绝不仅仅是一个潜在的陷阱而已。下一节中,你将会看到,即便是使用了互斥量对数据进行了保护,条件竞争依旧可能存在。