三、在线程间共享数据

三.线程之间共享数据

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.为了并发访问,共享数据仅需在初始化过程中受到保护,(创建开销不小)容易诱发恶性条件竞争

  1. 保护甚少更新的数据结构,

3.递归加锁

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力生活的海绵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值