学习并发数据结构的笔记,看这篇博客主要可以看代码,代码有注释
需要注意的点:
1、保证线程持有锁的时间最短;(最小保护区域原则)
2、操作需要获取多个锁时,可能会产生死锁(死锁:不同的线程相互等待锁,各自得不到需要的锁);
3、在保证线程安全的前提下,提高并发访问的概率对数据结构;
4、调用不完全构造对象或是已经销毁对象的成员函数不可取;
存在的问题:
1、死锁问题:当持有一个锁的用户调用时,1、2、3处的拷贝构造和移动构造、拷贝赋值或者移动赋值,有造成死锁的风险;相当于说data本身的构造也是存在锁的。可如果控制不好,则能会造成死锁问题;如果data本身的构造不存在锁,则不会造成死锁问题;(注:需要考虑用户的data的构造里面有没有锁,会不会造成死锁)
2、性能问题:在等待push的过程中、在等待empty时,会造成资源浪费;
#include <exception>
#include <stack>
#include <mutex>
#include <memory>
//定义异常,可以抛出异常,但是stack数据没有改变,则是安全;
struct empty_stack: std::exception
{
const char* what() const throw()
{
return "empty stack";
}
};
template<typename T>
class threadsafe_stack
{
private:
std::stack<T> data;//栈
mutable std::mutex m;//每个线程访问时,都会共用的锁
public:
//构造和析构不是线程安全的,用户需要保证在对象创建前和销毁后都不会进行操作;
threadsafe_stack(){}
threadsafe_stack(const threadsafe_stack& other)
{
std::lock_guard<std::mutex> lock(other.m);
data=other.data;
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
//等待添加元素是没有意义的;
void push(T new_value)
{
std::lock_guard<std::mutex> lock(m);//加锁
data.push(std::move(new_value));//1
}
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
//1、make_shared共享指针,线程安全;
//2、make_shared的异常不会导致内存泄漏和修改statck数据,所以是安全的;
//3、拷贝构造或者移动时可能有异常,但是不会内存泄漏,新创建的对象也会销毁,是安全的;
std::shared_ptr<T> const res(
std::make_shared<T>(std::move(data.top())));//2
data.pop();
return res;
}
void pop(T& value)//引用返回,也是安全的
{
std::lock_guard<std::mutex> lock(m);//加锁
if(data.empty()) throw empty_stack();//在empty处重复加锁,属于良性竞争锁
//1、拷贝和移动时会可能异常,但是不会修改statck数据,是安全的;
value=std::move(data.top());//3
data.pop();
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m);//加锁
return data.empty();
}
};
参考:
1、《C+±Concurrency-In-Action-2ed》