用互斥保护共享数据
1)不得向锁所在的作用域之外传递指针和引用,指向受保护的共享数据,无论是通过函数返回值将它们保存到对外可见的内存,还是将它们作为参数传递给使用者提供的函数,否则保护失效。
2)接口固有的条件竞争
如stack里empty()和top(),top()和pop()
解决:将top()和pop()合起来,但之所以分割是怕复制过程抛异常而造成数据丢失,所以只要解决这一点即可。
1.传入引用,存储弹出的元素。
2.提供不抛异常的拷贝构造函数,或不抛异常的移动构造函数。
3.返回指针,指向弹出的元素。使用share_ptr不必亲自管理内存分配。
结合方法1和方法2,或方法1和方法3灵活构建代码。
此外在合起来的函数内检查是否空栈。
另外,只留拷贝构造函数,删除赋值运算符,从而保证互斥的锁定会横跨整个复制过程。
另外,使用mutable修饰mutex,才能在带const的函数里锁定互斥。
坑:自定义异常出现“1个无法解析的外部命令”,是因为没有实现。
解决错误:返回一个内容即可。
/*
线程安全的栈容器类
*/
struct empty_stack :exception
{
const char* what() const throw()
{
return "stack is empty!";
}
};
template<typename T>
class threadsafe_stack
{
stack<T> data;
mutable mutex m;
public:
threadsafe_stack(){}
threadsafe_stack(const threadsafe_stack& other)
{
scoped_lock lock(other.m);
data = other.data;
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_data)
{
scoped_lock lock(m);
data.push(move(new_data));
}
void pop(T & value)
{
scoped_lock lock(m);
if (data.empty()) throw empty_stack();
value = data.top();
data.pop();
}
shared_ptr<T> pop()
{
scoped_lock lock(m);
if (data.empty()) throw empty_stack();
shared_ptr<T> const res(make_shared<T>(data.top()));
data.pop();
return res;
}
bool empty() const
{
scoped_lock lock(m);
return data.empty();
}
};
3)C++17引入了类模板参数推导,还引入了scoped_lock是增强版的lock_guard,它是可变参数模板,可以同时锁住多个互斥,从而防范死锁。
4)防范死锁准则:
1.避免嵌套锁:假如已经持有锁,就不要试图获取第二个锁。万一确有需要获取多个锁,应采用lock()函数或scoped_lock<>模板,一次获取全部锁来避免死锁。
2.一旦持锁,就须避免调用由用户提供的程序接口。
3.依从固定顺序获取锁。
4.按层级加锁:如果已经持有锁,那只能获取相对低层级的锁,从而限制代码行为。
具体做法是将层级的编号赋予对应层级的互斥,并记录各线程分别锁定了哪些互斥。
另外,层级编号使用thread_local修饰,即每个线程都有最后一次加锁操作所涉及的层级编号。
5.将准则推广到锁操作以外。
在不同作用域之间转移互斥归属权,可使用unique_lock<>。因为unique_lock实例不占有与之关联的互斥,所以互斥归属权可以在多个unique_lock实例之间转移。
另外,unique_lock<>比lock_guard<>灵活,不需要时可手动解锁。
除非绝对必要,否则不得在持锁期间进行耗时操作,如等待IO完成或获取另一个锁(即便知道不会死锁)。
/*
层级互斥
自定义的互斥型别只要实现3个成员函数,lock(),unlock(),try_lock(),就满足互斥概念所需具备的操作。
层级编号使用thread_local修饰,即每个线程都有最后一次加锁操作所涉及的层级编号。
*/
class hierarchical_mutex
{
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 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()
{
if (this_thread_hierarchy_value != hierarchy_value)
throw logic_error("mutex hierarchy violated");
this_thread_hierarchy_value = previous_hierarchy_value;
internal_mutex.unlock();
}
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);
//(使用)层级防范死锁
hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
int do_low_level_stuff() { return 1; }
int low_level_func()
{
scoped_lock lock(low_level_mutex);
return do_low_level_stuff();
}
void do_high_level_stuff(int some_param) { cout << some_param << endl; }
void high_level_func()
{
scoped_lock lock(high_level_mutex);
do_high_level_stuff(low_level_func());
}
5)实现线程安全的延迟初始化,用call_once()函数和once_flag类,而不要用互斥(会毫无必要地迫使多个线程循序运行),也不要用双重检验(可能会导致数据竞争)。
6)C++17提供新的互斥(也称读写互斥),shared_mutex和shared_timed_mutex,后者支持更多的操作,所以若无须进行额外操作,应选用shared_mutex,可能带来性能增益。
具有两种使用方式:
(更新操作)允许单独一个“写线程”进行完全排他的访问,可用lock_guard<>或unique_lock<>锁定。
(无须更新)允许多个“读线程”共享数据或并发访问,可用shared_lock<>锁定,即多个线程同时锁住同一个互斥。
/*
运用shared_mutex保护数据结构
*/
template<typename T>
class shared_vector
{
vector<T> data;
mutable shared_mutex m;
public:
shared_vector(const vector<T>& _data):data(_data){}
bool find_vector(T const& value) const
{
shared_lock lock(m);
auto it = find(data.begin(), data.end(), value);
return (it != data.end()) ? true : false;
}
void update_vector(T const& value)
{
scoped_lock lock(m);
data.push_back(value);
}
};
7)thread构造错误
1错误:出现“尝试引用已删除的函数”,是因为thread类删除了拷贝构造函数和赋值运算符。
解决:使用move。
2错误:出现“std::invoke”: 未找到匹配的重载函数”,是因为线程运行的函数有返回值,但没有处理。
解决:使用lambda传引用,保存返回值。
3错误:出现“<function-style-cast>”: 无法从“void”转换为“std::thread”,是因为线程运行的函数有参数,但构造错误。
解决:使用thread t(&shared_vector<int>::update_vector, &my_v,11);
8)递归加锁,recursive_mutex,但不推荐使用这种设计。
如:每个公有函数都要锁住互斥后才操作,此时有一公有函数需要调用另一公有函数,若用mutex重复加锁将导致未定义行为,拙劣设计直接改用recursive_mutex。
但更好的方法,根据这两个公有函数的共同部分,提取出一个新的私有函数,新函数由这两个公有函数调用,并在调用前各自对mutex成员加锁,故无须重复加锁。