1 创建一个互斥量std::mutex; mutex::lock(),mutex::unlock()互斥量的加锁解锁,try_lock未加锁时加锁否则不加锁返回false。但是不建议直接使用lock和unlock,因为这样需要在各种情形下保证lock和unlock的匹配,即使是抛出异常。标准库中采用RAII手法封装了mutex的类std::lock_guard在构造时lock,析构时unlock。lock_guard是个模板类。
#include<list>
#include<mutex>
#include<algorithm>
#include<thread>
#include<iostream>
#include<unistd.h>
using namespace std;
list<int> myList;
mutex listMutex;
void insert(){
{
lock_guard<mutex> guard(listMutex);
myList.push_front(12);
cout<<"insert()"<<endl;
sleep(2);//占有锁
}
}
void traverse(){
sleep(1);//让insert先持有锁
{
lock_guard<mutex> guard(listMutex);
for(list<int>::iterator it=myList.begin();it!=myList.end();it++)
cout<<*it<<" ";
cout<<endl;
}
cout<<"traverse()"<<endl;
}
int main(){
myList.push_back(1);
myList.push_back(2);
thread firstThread(insert);
thread secondThread(traverse);
firstThread.join();
secondThread.join();
return 0;
}
2 通常的做法是:将mutex和需要保护的数据封装在一个类里面,以避免使用全局变量,但是若这个类的某个成员函数返回了保护数据的引用或指针会是不安全的;同理成员函数在没有使用mutex前提下保存了数据的引用或指针,而后使用数据时将会导致安全问题。
#include<mutex>
#include<iostream>
using namespace std;
class some_data{
public:
void do_something(){
cout<<"there will use data do_something"<<endl;
}
private:
int a;
};
class data_wrapper{//保护数据的类
public:
template<typename Functor>
void process_data(Functor func){
lock_guard<mutex> guard(m);
func(data);//类将保护数据泄露了
}
private:
some_data data;
mutex m;
};
some_data* unproected;
void malicious_function(some_data& protected_data){//保护数据的类的成员函数通过引用泄露了保护数据
unproected=&protected_data;
}
data_wrapper x;
int main(){
x.process_data(malicious_function);
unproected->do_something();//用户函数在没有任何保护措施下使用了数据
return 0;
}
3 接口引起的race condition
stack<int> s;
if(!s.empty()) //
{
int const value=s.top();
s.pop(); //
do_something(value);
}
若stack内部有相应的mutex保护,但是stack本身的接口是非线程安全的。如上述,线程1在判断了empty()后挂起,线程2删除了stack中所有元素,当线程1唤醒执行pop操作时会出错,最好的情况都怕是core dump。单个接口是线程安全的,但是接口的组合不一定线程安全,同理top和pop也是非线程安全的,见下面。
假设stack内部采用mutex保护在任意一个时刻只允许一个成员函数使用。
if(!s.empty())//thread 1 //thread 2
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和pop两个操作通过mutex强行原子化,安全吗?no,若stack的元素在copy constructor时抛出异常了即value=s.top()抛出异常,那么又需要其它异常机制来处理了。比如,stack<vector<string> >这种栈元素是动态数组的情形拷贝时在系统负载重的情况下极易内存分配失败。这里不能怪stack将两个操作分开,因为stack在设计时考虑到了pop可能出现copy constructor失败时元素也还在stack上。
解决办法1:重载stack::pop(vector<string> &)操作,使其在删除元素的时候通过函数引用参数返回被删除的元素。此时使用者必须先声明一个栈元素,而使用者可能不知道元素的构造参数之类的,并且要求栈元素类型必须具备可赋值assignable语义,但是很多类型具备move和copy construction却不具备assignable语义。
解决办法2:pop返回栈元素值时可能抛出异常,可以采用C++11的move语义引用右值,通常move constructor不会抛出异常,并且std::is_nothrow_copy_constructible and std::is_nothrow_move_constructible在编译时可以检查拷贝构造和移动构造是否会抛出异常。但是许多用户定义的类都是拷贝构造会抛出异常而且没有移动构造。
解决办法3:top返回指针,优先选择shared_ptr,那么栈元素类型就是shared_ptr。
将top和pop操作合并为pop操作的代码,实现了获取栈顶元素和删除栈顶元素这个组合操作是线程安全的:
struct empty_stack: std::exception
{
const char* what() const throw()//本函数不抛出任何异常
{
return "empty stack";
}
};
template<typename T>
class threadsafe_stack
{
private:
std::stack<T> data;
mutable std::mutex m;//声明为mutable可以在const成员函数中也可以加锁,否则报错
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()//返回shared_ptr
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
data.pop();
return res;
}
void pop(T& value)//通过函数参数返回
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
value=data.top();
data.pop();
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};