相互排斥量介绍
相互排斥量能够保护某些代码仅仅能有一个线程运行这些代码。假设有个线程使用相互排斥量运行某些代码,其它线程訪问是会被堵塞。直到这个线程运行完这些代码,其它线程才干够运行。
一个线程在訪问共享数据前。给相互排斥量上锁,这时其它线程再给相互排斥量上锁会堵塞直到这个线程给相互排斥量解锁。
相互排斥量是C++中最经常使用的数据保护机制,可是它也不万能的。
在编写代码时,合理的组织代码来避免资源竞争很重要。使用相互排斥量可能会带来其它问题,比方死锁。
在C++中使用相互排斥量
创建相互排斥量使用mutex,给相互排斥量上锁使用函数lock(),给相互排斥量解锁使用unlock()函数。在实际应用中,不建议直接使用上锁、解锁函数,由于你必须记得每次上锁后要解锁,否则会造成死锁。在标准库中提供了lock_guard类模板,它使用了RAII(资源申请即初始化)。在构造函数中上锁,在析构函数中解锁。
以下一个样例。给链表加入结点和查找链表中是否包括某个元素,在操作链表前都须要包括链表。
#include<mutex>//包括相互排斥量头文件 #include<list> #include<algorithm> #include<thread> #include<iostream> #include<Windows.h> //两个全局变量。用于线程间共享 std::list<int> some_list; std::mutex some_mutex; void add_to_list(int new_value) { std::lock_guard<std::mutex> guard(some_mutex);//guard为局部变量,分配在栈上,超出作用域即调用析构函数 some_list.push_back(new_value); Sleep(100);//100ms } bool list_cotains(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(); } void fun1() { for (int i = 0; i <50; ++i) { add_to_list(i); } } void fun2() { for (int i = 50; i < 100; ++i) { add_to_list(i); } } int main() { std::thread t1(fun1); std::thread t2(fun2); t1.join(); t2.join(); for (std::list<int>::iterator it = some_list.begin(); it != some_list.end(); ++it) std::cout << *it << std::endl; system("pause"); }
合理的编码来保护共享数据
保护共享数据。不是简单的在每一个函数里加上lock_guard()对象。指针或引用使用不当回破坏保护的数据。查找迷途指针或引用比較easy,仅仅要在成员函数里不返回共享数据的指针或引用就能够。假设在进一步看,没那么简单。也可能会向你不能控制的函数传递共享数据的指针或引用,这些函数可能会把指针或引用存储起来。后面在用。这种话,相互排斥量就没法保护共享数据了。
#include<mutex>//包括相互排斥量头文件 #include<thread> #include<iostream> class some_data { int a; public: some_data():a(10){} void do_something() { std::cout << a << std::endl; } }; 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);//使用相互排斥量保护data func(data);//这个函数是可能是引用传递參数,把共享数据传到外面,危急 } }; some_data *unprotected; //把模板函数定义为引用传递 void maliciout_function(some_data& protected_data) { unprotected = &protected_data;//共享数据被传出来了,不再受相互排斥量保护 } data_wrapper x; int main() { x.process_data(maliciout_function); unprotected->do_something();//这里在没有相互排斥量保护下使用共享数据了 system("pause"); }
在保护共享数据的作用域内。不要返回共享数据的指针、引用,也不要把共享数据作为參数传递给其它人提供的函数。
查找接口中的资源竞争
一些接口,对于单线程来说是没有资源竞争的。可是多个线程使用时就未必了。比如:
std::stack<int> S; void Func() { int len = S.size(); for (int i = 0; i < len; ++i) { S.pop(); } }
在多线程环境下,先推断栈S大小。再处理。
假设再处理过程中。其它线程给栈加入或删除元素。上面代码就会有问题。
单个接口是安全的,可是接口组合使用就未必了。如果栈的接口函数收保护,在某一时刻仅仅能有一个执行。
那么以下代码:
if (!S.empty())// 线程1 if (!S.empty())// 线程2 int const value = S.top(); int const value = S.top(); s.pop(); s.pop(); do_something(value); do_something(value);
这是线程1和线程2将去到同样的数据。可是两个线程都pop()。将会造成一个数据未被使用。
解决问题的一个办法是把top()和pop()用相互排斥量保护起来。
可是Tom Cargill指出,假设对象的复制构造函数在栈上且抛出异常,上面做法就会有问题。
如果把top和pop做成了原子操作。如果在top时。开辟空间失败,这是对象不能返回。而后面又运行了pop,这时元素在没有使用情况下被清除了。
所以把top和pop做成两个接口还是有道理的。
有以下集中解决的方法:
1、在pop时传递一个引用參数
//void pop(int &value);在删除时。把值赋给value int k; S.pop(k)
2、使用无异常的复制构造函数
在C++11中,右值引用使得move construtor不抛出异常(即使复制构造函数抛出异常)。一个有效的办法是:限制栈中元素的类型,仅使用不抛出异常的数据类型。
这样做是安全的,可是不是理想的。即使在编译阶段就能够借助std::is_nothrow_copy_constructible和std::is_nothrow_move_constructible类型来检測复制构造函数或move constructor是否会抛出异常,可是很多用户自己定义类型有复制构造函数,却没有move constructor。这种话,这种类型就不能存储到线程安全的栈。
3、返回pop对象的指针
返回指针而不是返回值,由于指针能够安全的复制,不会有异常。缺点是返回指针要开辟内存、保证内存不会泄露。只是能够借助shared_ptr来解决。
4、使用1和2或者1和3的组合。
以下是一个线程安全的栈
#include<exception> #include<memory> #include<mutex> #include<stack> struct empty_stack :std::exception { const char* what() const throw(); }; template<typename T> class threadsafe_stack { private: std::stack<T> data; mutable std::mutex m; public: threadsafe_stack(){} threadsafe_stack(const threadsafe_stack& other) { std::lock_guard<std::mutex> lock(other.m); data = other.data; } //不同意使用= threadsafe_stack& operator=(const threadsafe_stack& ) = delete; void push(T new_value) { std::lock_guard<std::mutex> lock(m); data.push(new_value); } std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); //先检查是否为空 if (data.empty()) throw empty(); std::shared_ptr<T> const res(std::make_shared<T>(data.top())); data.top(); return res; } void pop(T& value) { std::lock_guard<std::mutex> lock(m); //先检查是否为空 if (data.empty()) throw empty(); value = data.top(); data.top(); } bool empty()const { std::lock_guard<std::mutex> lock(m); return data.emplace(); } };