多线程之间共享数据经常会产生竞争条件,当竞争条件破坏不变量时会导致问题的产生。比如多个线程对同一数据的修改可能会导致未定义行为,多线程中某一行为需要获取多个锁时可能会造成死锁。解决竞争条件产生的问题的解决方案通常有两个,一个是修改数据结构的设计,被称为无锁编程,另一个是使用互斥元保护共享数据。
案例1. 利用互斥元保护列表之简单实现
#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_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(fome_list.begin(), some_list.end(), value_to_find) != some_list.end();
}
案列2. 避免在使用互斥元保护数据的时候,将数据的指针和引用传递到锁的范围之外。
class some_data;
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); //有风险,传递了受保护数据到保护范围之外
}
};
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(); //对受保护数据进行了未受保护的访问
}
案列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_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()
}
};
案例4. 在竞争条件中经常会出现死锁,死锁较常出现在需要锁定两个或以上互斥元的情况中。常见的建议是:1:使用相同的顺序锁定这两个互斥元 2:避免嵌套锁 3:在持有锁时,避免调用用户提供的代码 4:使用锁层次
class some_big_object;
void swap(some_big_object& lhs, some_big_object& rhs);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd) :some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::lock(lhs.m, rhs.m); //同时对两个互斥元上锁避免了死锁的发生
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock); //不要忘了锁管理
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
};
案例5. 使用层次锁以及层次锁的定义
获取高层次锁可以获取底层次锁,而获取低层次锁则不得获取高层次锁且会抛出异常
hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
int do_low_level_stuff();
int low_level_func()
{
std::lock_guard<hierarchical_mutex> lk(low_level_mutex);
return do_low_level_stuff();
}
void high_level_stuff(int some_param);
void high_level_func()
{
std::lock_guard<hierarchical_mutex> lk(high_level_mutex);
high_level_stuff(low_level_func());
}
//线程a
void thread_a()
{
high_level_func(); //高层次锁下可以获取低层次锁
}
//线程b
hierarchical_mutex other_mutex(100);
void do_other_stuff()
{
high_level_func();
do_other_stuff();
}
void thread_b()
{
std::lock_guard<hierarchical_mutex> lk(other_mutex);
other_stuff(); //低层次锁下无法获得高层次锁
}
//层次锁的实现
class hierarchical_mutex
{
std::mutex internal_mutex;
unsigned long const hierarchy_value;
unsigned long previous_hierarchy_value;
static thread_local unsigned long this_thread_hierarchy_value; //使用静态成员变量来标识当前线程的层次值
void check_for_hierarchy_violation()
{
if(this_thread_hierarchy_value <= hierarchy_value)
{
throw std::logic_error("mutex hierarchy violated");
}
}
void update_hierarchy_value()
{
previous_hierarchy_value = this_thread_hierarchy_value;
this_thread_hierarchy_value = hierarchy_value;
}
public:
explicit hierarchical_mutex(unsigned long value):
hierarchy_value(value), previous_hierarchy_value(0)
{}
void lock()
{
check_for_hierarchy_violation();
internal_mutex.lock();
update_hierarchy_value(); //上锁的时候更新层次值
}
void unlock()
{
this_thread_hierarchy_value = previous_hierarchy_value; //解锁的时候要还原当前线程的层次值
internal_mutex.lock();
update_hierarchy_value();
}
bool try_lock()
{
check_for_hierarchy_violation();
if(!internal_mutex.try_lock())
return false;
update_hierarchy_value();
return true;
}
};
thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX); //静态成员变量需要初始化
案例6. 更灵活的RAII风格锁,std::unique_lock
std::lock_guard是比较简单的RAII风格锁,并且可以传入std::adopt_lock作为第二个参数,表示已经上锁,使用起来十分的方便。另外,还有更加灵活的锁,std::unique_lock可以主动进行上锁 lock()和解锁unlock(),也能使用std::defer_lock()表示互斥元在构造时未上锁。在拥有了灵活性的同时,损失了一些性能。主要体现在std::unique_lock实现了lock()、try_lock()和unlock(),他们会调用底层上互斥元的同名函数去做实际工作,并且其实例内部有一个标识来表示该实例是否拥有此互斥元,只有在拥有互斥元,其析构的时候才能调用unlock()。因此,其牺牲了性能以获取易用性和灵活性。
class some_big_object;
void swap(some_big_object& lhs, some_big_object& rhs);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd):some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock); //定时的时候,互斥元未上锁
std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
std::lock(lock_a, lock_b); //多两个互斥元同时上锁
swap(lhs.some_detail, rhs.some_detail);
}
};
案例7. 在作用于之间转移锁的所有权
std::unique_lock是可移动但不可复制的,如果被转移实例是右值即临时量,那么可以自动进行转移(隐式),否则需要显式地进行转移
std::unique_lock<std::mutex> get_lock()
{
extern std::mutex some_mutex;
std::unique_lock<std::mutex> lk(some_mutex);
prepare_data();
return lk;
}
void process_data()
{
std::unique_lock<std::mutex> lk(gete_lock)); //右值,可以直接进行转移所有权
do_something();
}