我的C++并发编程实战之旅(二)

线程间共享数据

共享数据带来的问题

共享数据如果是只读的,那么不会带来影响。如果多个线程要修改共享数据,那就很麻烦。

双链表删除节点后,需要更新两边。

避免恶性调节竞争

并发中竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。而恶性条件竞争通常发生于完成对多于一个的数据块的修改时。

避免恶性条件竞争有三个主要方法。

1、对数据块进行加锁等数据保护机制。同一时间,只有一个线程可对数据进行修改操作。

2、实现无锁化编程。对数据结构进行无锁化设计。

3、利用数据库中事务的思想,采用和事务一样的原理进行数据修改。

使用互斥量保护共享数据

互斥量是c++中最基本数据保护的方式。使用std::mutex来声明互斥量,使用lock()和unlock()函数进行加锁和解锁操作。因为使用lock()后,必须使用unlock()来解锁(包括异常情况),所以操作繁琐。我们可以利用RAII(获取资源即初始化)来完成这一操作。c++库提供了模板类std::lack_guard其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。两者都声明在头文件。

下面就是两种方法,推荐第二种。

#include <mutex>
 
std::mutex my_mutex;
void foo1(){
    my_mutex.lock();
    do_something();
    my_mutex.unlock();
}
void foo2(){
    std::lock_guard<std::mutex> my_guard(my_mutex);
    do_something();
}

注意:要避免将保护数据的引用或者指针作为返回值传出或者参数传入,这将导致保护数据在保护域外被使用。

虽然某些情况下,使用全局变量没问题,但在大多数情况下,互斥量通常会与保护的数据放
在同一个类中,而不是定义成全局变量。这是面向对象设计的准则:将其放在一个类中,就
可让他们联系在一起,也可对类的功能进行封装,并进行数据保护。互斥量和要保护的数据,在类中都需
要定义为private成员,这会让访问数据的代码变的清晰,并且容易看出在什么时候对互斥量
上锁。当所有成员函数都会在调用时对数据上锁,结束时对数据解锁,那么就保证了数据访
问时不变量不被破坏。

typedef void (*funType) (int&);
class foo{
private:
    int a = 0;
    mutex my_mutex;
public:
    void f(funType func){
        std::lock_guard<mutex> my_guard(my_mutex);
        func(a);
    }
    void show(){
        cout << a <<endl;
    }
};
int* unprotectd_data;
void func(int & a){
    unprotectd_data = &a;    //恶意或许a的地址
}
 
int main(){
    foo a;
    a.show();
    a.f(func);
    *unprotectd_data = 1;    //a的地址不受保护,在互斥量保护外修改
    a.show();
}

foo类的本意是只能在f函数内对变量a进行操作。但是当我们恶意地用指针来接收a变量的引用时,我们就可以在f的保护域外修改a。

互斥量范围大小与接口设计

假设我们有一个获取栈顶的函数pop

stack<int> s;
void mypop(stack<int> s){
	if (! s.empty()){ // 1
		int value = s.top(); // 2
		s.pop(); // 3
		do_something(value);
	}
}

以上是单线程安全代码:对一个空栈使用top()是未定义行为。对于共享的栈对象,这样的调
用顺序就不再安全了,因为在调用empty()①和调用top()②之间,可能有来自另一个线程的
pop()调用并删除了最后一个元素。这是一个经典的条件竞争,使用互斥量对栈内部数据进行
保护,但依旧不能阻止条件竞争的发生,这就是接口固有的问题。

所以进行如下的修改:

void mypop(stack<int> s){
    mutex my_mutex;
	lock_guard<mutex> my_guard(my_mutex);//
	if(s.empty()) 
		throw empty_stack();
	else{
		int value = s.top(); // 2
		s.pop(); // 3
		do_something(value);
	}
}

死锁

死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

死锁的四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

  • 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  • 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

  • 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系

比如下面的例子,当foo1,foo2同时运行时,foo1和foo2会分别对m1,m2加锁。之后他们就会陷入foo1等待m2解锁,而foo2等待m1解锁的状态。这就是死锁状态。当我们要对2个以上的互斥量进行加锁的时候,要避免这种死锁状态

mutex m1;
mutex m2;
void foo1(){
    lock_guard<mutex> guard1(m1);
    do_something();
    lock_guard<mutex> guard2(m2);
}
void foo2(){
    lock_guard<mutex> guard2(m2);
    do_something();
    lock_guard<mutex> guard1(m1);
}

很幸运,C++标准库有办法解决这个问题, std::lock ——可以一次性锁住多个(两个以上)的
互斥量,并且没有副作用(死锁风险)。下面的程序清单中,就来看一下怎么在一个简单的交换
操作中使用 std::lock 。

mutex m1;
mutex m2;
void foo1(){
    lock(m1,m2)
    lock_guard<mutex> guard1(m1, adopt_lock);
    lock_guard<mutex> guard2(m2, adopt_lock);
    do_something();
    
}
void foo2(){
    lock(m1,m2)
    lock_guard<mutex> guard1(m1, adopt_lock);
    lock_guard<mutex> guard2(m2, adopt_lock);
    do_something();
 
}

lock()先对m1上锁,之后对m2上锁,当发现m2已锁时,将会抛出异常,同时对m1解锁。所以lock的作用是,要么2个一起锁,要么都不锁。std::adopt_lock表示对象已经上锁。

使用层次避免死锁

没学完,不学啦,哈哈哈

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值