【C++并发编程实战】互斥与死锁

std::mutex

一般不推荐直接像下面这用使用std::mutex。需要自己手动上锁和解锁。

void use_lock()
{
	while (true)
	{
		mtx.lock();
		shared_data++;
		std::cout << "shared_data = " << shared_data << std::endl;
		std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
		mtx.unlock();
		std::this_thread::sleep_for(std::chrono::microseconds(10));
	}
}

void test_lock()
{
	std::thread t1(use_lock);
	std::thread t2([]() {
		while (true)
		{
			mtx.lock();
			shared_data++;
			std::cout << "shared_data = " << shared_data << std::endl;
			std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
			mtx.unlock();
			std::this_thread::sleep_for(std::chrono::microseconds(10));
		}
	});

	t1.join();
	t2.join();
}

lock_guard

使用类模板std::lock_guard<>,实现了RAII手法:创建对象构造时自动上锁,析构时自动解锁。

void use_lock()
{
	while (true)
	{
		{
			std::lock_guard<std::mutex> lock(mtx);
			shared_data++;
			std::cout << "shared_data = " << shared_data << std::endl;
			std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
		} // 这个作用域使得sleep时已经释放锁
		std::this_thread::sleep_for(std::chrono::microseconds(10));
	}
}

不得向锁所在的作用域之外传递指针和引用,指向受保护的共享数据。

线程安全的栈

有时候我们可以将对共享数据的访问和修改聚合到一个函数,在函数内加锁保证数据的安全性。但是对于读取类型的操作,即使读取函数是线程安全的,但是返回值抛给外边使用,存在不安全性。比如一个栈对象,在某线程调用empty()size()时,返回值可能是正确的。然而,函数一但返回,其它线程立马就可以访问栈容器,比如push或pop一个元素,刚才调用empty()size()返回的结果就没意义了。

下面是一个线程不安全的stack。两个线程先后交替访问栈,都发现不为空都进行pop,pop两次发生崩溃。

template<typename T>
class threadunsafe_stack
{
private:
	std::stack<T> data;
	mutable std::mutex m; // 保证可以对一个const对象加锁
public:
	threadunsafe_stack() {}
	threadunsafe_stack(const threadunsafe_stack& other)
	{
		std::lock_guard<std::mutex> lock(other.m);
		m = other.m;
	}
	threadunsafe_stack& operator=(const threadunsafe_stack&) = delete;
	
	void push(T t)
	{
		std::lock_guard<std::mutex> lock(m);
		data.push(t);
	}

	// 问题代码
	T pop()
	{
		std::lock_guard<std::mutex> lock(m);
		auto element = data.top();
		data.pop();
		return element;
	}

	bool empty() const
	{
		std::lock_guard<std::mutex> lock(m);
		return data.empty();
	}
};

当进行如下调用时,就会发生对空栈进行pop的错误。

void test_threadunsafe_unstack()
{
	threadunsafe_stack<int> stk;
	stk.push(1);

	std::thread t1([&stk]() {
		if(!stk.empty())
		{
			std::this_thread::sleep_for(std::chrono::seconds(1)); // 让现象更明显
			stk.pop();
		}
	});

	std::thread t2([&stk]() {
		if(!stk.empty())
		{
			std::this_thread::sleep_for(std::chrono::seconds(1)); // 让现象更明显
			stk.pop();
		}
	});

	t1.join();
	t2.join();
}

可以使用抛出异常的方式,在pop函数中调用一次data.empty()判断栈是否为空,如果空则抛出异常,在外层使用的时候就需要捕获异常。
但是这个pop函数仍然存在问题,比如T是一个vector<int>类型,那么在pop函数内部element就是vector<int>类型,开始element存储了一些int值,程序没问题,函数执行pop操作, 假设此时程序内存暴增,导致当程序使用的内存足够大时,可用的有效空间不够, 函数返回element时,就会就会存在vector做拷贝赋值时造成失败。即使我们捕获异常,释放部分空间但也会导致栈元素已经出栈,数据丢失了。这其实是内存管理不当造成的,但是C++ 并发编程一书中给出了优化方案。

// 实现两个版本的pop
std::shared_ptr<T> pop() // 返回一个shared_ptr指向栈顶弹出的元素
{
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) throw empty_stack();
    std::shared_ptr<T> res = std::make_shared<T>(data.top());
    data.pop();
    return res;
}

void pop(T& t)  // 传入引用
{
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) throw empty_stack();
    t = data.top();
    data.pop();
}

第一个pop函数也可以不抛出异常,如果栈为空直接返回nullptr

std::shared_ptr<T> pop()
{
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) return nullptr;
    std::shared_ptr<T> res = std::make_shared<T>(data.top());
    data.pop();
    return res;
}

死锁

加锁顺序相反导致互相等待。避免死锁的一个方式就是将加锁和解锁的功能封装成独立的函数。这样能保证在独立的函数中执行完操作就解锁,不会导致一个函数里使用多个锁的情况。(这个代码比较好写,不贴了)。

同时加锁

但是我们无法避免在一个函数内部使用两个互斥量,我们可以采取同时加锁的方式。

比如当我们对两个对象执行交换操作的函数时,为了保证互换正确完成,我们在函数内需要给这两个对象进行上锁,如果两个线程都运行这个函数,而且调用的参数相反,就可能会造成死锁。可以采用同时加锁的方式解决这个问题。

std::lock + std::lock_guard<>
class some_big_object 
{
private:
	int data;
public:
	friend void swap(some_big_object& lhs, some_big_object& rhs)
	{
		// ...
	}
};

class X  // 这是一个结构,包含了一个复杂的成员对象和一个互斥量
{
private:
	std::mutex m;
	some_big_object some_detail;
public:
	X(const some_big_object& 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> lock1(lhs.m, std::adopt_lock);
		std::lock_guard<std::mutex> lock2(rhs.m, std::adopt_lock);
		swap(lhs.some_detail, rhs.some_detail);
	}
};
  • 要先判断交换的两个对象是否是同一个对象,如果是则要立即返回,因为对同一个std:mutex对象连续上锁两次是未定义行为
  • std:lock在其内部对lhs.mrhs.m加锁时可能就会抛出异常,如果std::lock在其中一个互斥上成功获取了锁,但在另一个互斥上获取锁时抛出异常,那么第一个锁就会被释放。即“全员共同成败”:或全部锁定成功,或没获取任何锁并抛出异常。
  • std::lock_guard的构造参数中std::adopt_lock表明这个互斥量已经被锁上,std::lock_guard对象构造时不需要在构造函数中再次加锁。(领养锁)
C++17 std::scoped_lock<>

C++17中的std::scoped_lock<>能对多个互斥量同时加锁,并且自动释放。(RAII)

friend void swap(X& lhs, X& rhs)
{
    if(&lhs == &rhs)
        return ;
    std::scoped_lock guard(lhs.m, rhs.m);  // 还使用了C++17的类模板参数推导
    swap(lhs.some_detail, rhs.some_detail);
}

层级锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值