三.线程之间共享数据
3.1线程间共享数据的问题
问题根源:改动数据
“不变量”可以帮助分析代码,它是指一个针对某一特定数据的断言。
总结:改动线程间的共享数据,可能导致最简单的问题就是破坏不变量。
数据改动可能造成的问题:条件竞争
3.1.1条件竞争
产生条件:某个操作,由两个或多个线程争先执行自己的线程操作。执行结果取决于他们执行的相对次序。
条件竞争通常特指恶性条件竞争。
防止恶性条件竞争的方法:
1.包装数据结构,确保"不变量"被破坏时,中间状态仅对执行改动的线程可见。
2.修改数据结构及"不变量",由一连串不可拆分的改动完成数据变更,每个改动数据都维持“不变量”不被破坏,这种方法通常称为:无锁编程,但是难以正确编写
3.将修改数据结构当作事务处理。类似于数据库在一个事务完成更新
3.2用互斥保护共享数据
该方法可以避免条件竞争,防止“不变量”被破坏,其中,在c++中可以构造std::mutex()实例来创建互斥,调用成员函数lock()对其加锁,调用unlock()进行解锁。此方法有一个缺点:在函数以外,需要在每条代码路径上调用unlock(),包括由于异常导致退出的路径。通常采用std::lock_guard<>,可避免上述问题,std::lock_guard<>融合实现了RALL手法:在构造时给互斥加锁,析构时解锁,从而保证互斥总被正确解锁。
需要注意的是:向调用者返回指针或引用,它们指向受保护的共享数据,会危及数据安全。
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);
fun(data);//向使用者提供的函数传递收保护的共享数据
}
}
some_data* unpeotected;
void malicious_function(some_data& protected_data)
{
unprotected=&protected_data;
}
data_wrapper x;
void foo()
{
x.process_data(malicious_function);//传入恶意函数
unpeotected->do_something();//以无保护方式访问本应受保护的共享数据
}
针对数据结构接口中固有的条件竞争,解决办法:修改数据接口
具体方法:
1:传入引用
借用一个外部变量接收数据结构中传出的数据,将指涉它的引用通过参数传入函数中。
但是该方法有以下缺点:
1.数据量大时,不可行
2.构造函数不带参数,不可行
3.不支持赋值构造不可行
2:提供不抛出异常的拷贝构造函数,或不抛出异常的移动构造函数
优点:安全
缺点:效果不理想
3:返回指针,指向弹出的元素
优点:指向的是元素,而不是返回的值,其优点是指针可以自由的复制,不会抛出异常。
缺点:在返回的指针所指向的内存中,分配的目标千差万别,既可能是复杂的对象,也可能是简单的性别,对内存管理,构成了额外的负担。可以选择指针性别:std::shared_ptr,有效避免内存泄漏
4:结合方法1和2,或者1和3
5:类定义示例:线程安全的栈容器类
简化接口换来最大安全保证
死锁:问题和解决办法
两个线程分别只锁住了一个互斥,都等着再给另一个互斥加锁,以上情况称为死锁。
解决办法:通常按照相同顺序对两个互斥加锁
std::lock()可以同时锁住多个互斥,而没有发生死锁的风险。
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::lock_guard<std::mutex> lock_a(rhs.m,std::adopt_lock);
swap(lhs.some_detail,rhs.some_detail);
}
}
防范死锁的补充准则
即使没有牵扯锁,也可能造成死锁,比如:两个线程分别调用彼此的join();
避免上述问题,程序设计准则:
1.避免嵌套锁
2.一旦持锁,就须避免调用由用户提供的程序接口
3.依从固定程序获取锁
4.按层级加锁
5.将准则推广到锁操作以外
运用std::unique_lock<>灵活加锁,该锁放宽了“不变量”的成立条件,相对于std::lock_guard<>更灵活一些。std::unique_lock<>对象不一定始终占有与之关联的互斥。
加锁需要选择合适的粒度,两个要点:选择足够大的锁粒度,确保目标数据受到保护,二是限制范围,务求只在必要的操作过程中持锁
保护共享数据的其他工具
1.为了并发访问,共享数据仅需在初始化过程中受到保护,(创建开销不小)容易诱发恶性条件竞争
- 保护甚少更新的数据结构,
3.递归加锁