C++线程编程-设计基于锁的并发数据结构

序列化

多个线程轮流存取互斥元保护的数据,它们必须线性的而非并发的存取数据。
高并发就意味着:更小的保护区域,更少的序列化,更高的并发潜能。

设计基于锁的并发数据结构关键是要确保存取数据时要锁住正确的互斥元,并且要确保将锁的时间最小化.只用一个互斥元保护一个数据结构是很困难的.你需要确保此数据在互斥锁保护区域之外不会被存取,并且不会发生接口所固有的竞争现象.如果使用独立的互斥元来保护数据结构的独立部分,问题会变得更复杂,并且如果数据结构上的操作需要多个互斥元被锁住就有可能产生死锁.因此,设计有多个互斥元的数据结构时,需要比设计只有一个互斥元的数据结构考虑得更细致.

使用锁的线程安全栈

// 线程安全栈的类定义
// 线程安全栈的类定义
#include <exception>
#include <stack>
#include <mutex>
#include <utility>
#include <memory>
struct empty_stack : std::exception
{
    const char *what() const throw();
};

template<typename T>
class threadsafe_stack
{
private:
    std::stack<T> data;
    mutable std::mutex mut;
public:
    threadsafe_stack& operator=(const threadsafe_stack &) = delete;
    threadsafe_stack(){}
    threadsafe_stack(const threadsafe_stack &other)
    {
        std::lock_guard<std::mutex> lk(mut); // 上锁
        this->data = other.data;
    }
    void push(T new_data)
    {
        std::lock_guard<std::mutex> lk(mut);
        this->data.push(std::move(new_data));   // 对临时变量move没有问题
    }
    std::shared_ptr<T> pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data.empty())
        {
            throw empty_stack();
        }
        const std::shared_ptr<T> res(std::make_shared<T>(std::move(data.top())));
        data.pop();	// 把对数据的修改在对后面进行,防止前面异常导致对数据的修改
        return res;
    }
    void pop(T &value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data.empty())
        {
            throw empty_stack();
        }
        value = std::move(data.top());  // 先保存,防止失败
        data.pop();
    }
    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data.empty();
    }

};

如例子,基本的线程安全是用互斥锁mut保护成员函数来实现的。
但是这个例子有可能会发生死锁,因为这个例子调用了用户代码:拷贝构造函数或移动构造函数,或者用户自定义的new运算符,因为这是模板类,用户在这个模板里放什么成员是不确定,如果放了是某一种数据结构成员,而调用的那些成员函数里面又有锁操作,就会出现一种情况:在持有锁的情况下,又请求了另一把锁。这是导致死锁的原因之一。
所以有一个明智的选择:你不能理所当然地在 没有复制数据项或为之分配内存的情况下,将它加入栈或者从栈中移走它.

尽管对多线程来说,因为使用了锁,每次只有一个线程对栈数据结构进行操作,所以同时调用成员函数是安全的.但是当堆栈上存在显著的竞争时,线程序列化可能会限制应用的性能.当一个线程在等待锁的时候,它就做不了任何有用的工作.并且,栈没有为等待数据项被插入的线程提供任何方式的准备,因此如果一个线程需要等待,它就会反复地调用空()、或者只是调用POP()、并且捕捉空堆栈异常。如果这种情况发生的话,这种栈实现就成为一个比较糟糕的选择,因为一个等待中的线程要么消耗宝贵的资源在检查数据上,要么用户必须写外部的等待和通知代码(比如,使用条件变量),而这就有可能让内部锁变得无效并且造成浪费.

用条件变量设计在数据结构中包含等待的方法:

// 使用条件变量的线程安全队列的完整类定义
template <typename T>
class threadsafe_queue
{
private:
    std::queue<T> data_queue;
    mutable std::mutex mut;
    std::condition_variable cond;
public:
    threadsafe_queue(){}
    threadsafe_queue(const threadsafe_queue &q) = delete;
    threadsafe_queue& operator=(const threadsafe_queue &q) = delete;
    void push(T new_value)
    {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(std::move(new_value));
        cond.notify_one();  //  尽量不要使用all,会有惊群效应
    }

    void wait_and_pop(T &value)
    {
        std::unique_lock<std::mutex> lk(mut);
        cond.wait(lk,[this](){return !data_queue.empty();});    // 等待非空
        value = std::move(data_queue.front());
        data_queue.pop();
        // lk.unlock(); 析构自动解锁
    }

    std::shared_ptr<T> wait_and_pop()
    {
        std::unique_lock<std::mutex> lk(mut);
        // 阻塞当前线程,直到被唤醒
        cond.wait(lk,[this](){return !data_queue.empty();});
        std::shared_ptr<T> res(std::make_shared<T>(std::move(data_queue.front())));
        data_queue.pop();
        return res;
    }

    bool try_pop(T &value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty())
        {
            return false;
        }
        value = std::move(data_queue.front());
        data_queue.pop();
        return true;
    }

    std::shared_ptr<T> try_pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty())
        {
            return std::make_shared<T>();
        }
        std::shared_ptr<T> res(std::make_shared<T>(std::move(data_queue.front())));
        data_queue.pop();
        return res;
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

以上例子解决了等待的问题,可以实现内部等待。

但是仍然有一个问题,就是在wait_and_pop的时候,如果ntify_one唤醒一个等待之后,这个线程在wait_and_pop函数出现了异常(比如智能指针的构造),那么造成的结果可能就是没有线程被唤醒,这显然不是我们想要的结果。
解决方法:1.用notify_all代替,但是有惊群效应。2.引发异常的时候,让wait_and_pop调用notify_one,重新唤醒一个。3.就是确定函数没有异常。就是将shared_ptr的初始化移动到push()调用,并且存储shared_ptr<>实例,而不是直接存储值。将内部的queue<>复制到shared_ptr<>不会引发异常,因为wait_and_pop是安全的。

感觉第三种很麻烦

// 包含shared_prt<>实例的线程安全队列
template <typename T>
class threadsafe_queue
{
private:
    std::queue<std::shared_ptr<T>> data_queue;
    mutable std::mutex mut;
    std::condition_variable cond;
public:
    threadsafe_queue(){}
    threadsafe_queue(const threadsafe_queue &q) = delete;
    threadsafe_queue& operator=(const threadsafe_queue &q) = delete;

    void push(T new_value)
    {
        std::shared_ptr<T> data(std::make_shared<T>(std::move(new_value)));
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(data);
        cond.notify_one();  //  尽量不要使用all,会有惊群效应
    }

    void wait_and_pop(T &value)
    {
        std::unique_lock<std::mutex> lk(mut);
        // 阻塞等待
        cond.wait(lk,[this](){return !data_queue.empty();});    // 等待非空
        value = std::move(*data_queue.front());
        data_queue.pop();
        // lk.unlock(); 析构自动解锁
    }

    std::shared_ptr<T> wait_and_pop()
    {
        std::unique_lock<std::mutex> lk(mut);
        cond.wait(lk,[this](){return !data_queue.empty();});
        std::shared_ptr<T> res = data_queue.front();
        data_queue.pop();
        return res;
    }

    bool try_pop(T &value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty())
        {
            return false;
        }
        value = std::move(*data_queue.front());
        data_queue.pop();
        return true;
    }

    std::shared_ptr<T> try_pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if(data_queue.empty())
        {
            return std::make_shared<T>();
        }
        std::shared_ptr<T> res = data_queue.front();
        data_queue.pop();
        return res;
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

这是因为,智能指针模板类的构造可能会失败,比如因为内存不足,但是拷贝构造,移动构造,移动赋值,赋值是不可能失败的。而且,push操作可以智能指针的构造放在锁的外面,但是实际的push仍然在锁的里面,这样可以减少性能的损耗,如果拿着锁去申请内存,消耗是比较大的。

使用细粒度锁和条件变量的线程安全队列

// 一种简单的单线程队列实现
#include <memory>
#include <mutex>
#include <utility>
template <typename T>
class queue
{
private:
    struct node
    {
        T data;
        std::unique_ptr<node> next;
        node(T _data):data(std::move(_data)){}
    };
    std::unique_ptr<node> head;
    struct node* tail;

public:
    queue(){}
    queue(const queue &other) = delete;
    queue& operator=(const queue &other) = delete;

    // 弹出head指向的值,并且head后移动
    std::shared_ptr<T> try_pop()
    {
        if(!head)   // 如果head == null;
        {
            return std::shared_ptr<T>();    // 返回空值
        }
        const std::shared_ptr<T> res(std::make_shared<T>(std::move(head->data)));
        // 有一说一,这样不危险吗?
        // head = head->next; free(old_head);
        // 这样的话,好吗?head->next这块空间变成未定义空间
        // 非也,因为用的是unique_ptr,所以会自动析构内存。
        head = std::move(head->next);
        return res;
    }

    
    void push(T new_value)
    {
        // new一个,可以理解
        std::unique_ptr<node> p(new node(std::move(new_value)));
        const node* new_tail = p.get();
        if(tail)
        {
            tail->next = std::move(p);
        }
        else    // tail == null, 说明tail和head指向同一个,说明是空链表
        {
            head = std::move(p);
        }
        // new_tail会析构,但是new_tail指向的这块内存不会析构,所以这样没问题
        // 但是先get再移动,没有问题吗?
        // move并不一定真正会移动内存。
        tail = new_tail;    
    }
};

这个例子在单线程下可以很好的工作,但是多线程会游数据竞争。因为push和pop可以修改head和tail这两个指针,所以应该要加锁。但是只使用锁,并没有更好,可以使用增加傀儡节点的方式,进行简单的改进:

// 使用傀儡结点的简单队列
// 通过使用分离数据允许并发
// 傀儡结点意味着,try_pop和push不会在同一个结点上操作
#include <memory>
#include <utility>

template <typename T>
class queue
{
private:
    struct node
    {
        std::shared_ptr<T> data;
        std::unique_ptr<node> next;    
    };
    std::unique_ptr<node> head;
    node *tail;

public:
    queue(const queue &q) = delete;
    queue& operator=(const queue &q) = delete;
    queue():head(new node), tail(head.get()){}

    // 只要在最初的比较中需要tail,其他地方不需要tail,这是一个很短暂的锁
    std::shared_ptr<T> try_pop()
    {
        // 队列为空
        if(head.get() == tail)
        {
            return std::shared_ptr<T>();
        }
        const std::shared_ptr<T> res(head->data);
        std::unique_ptr<node> old_head = std::move(head);
        head = std::move(old_head->next);
        return res;
    }
    // push现在只访问tail,不访问head,这是一个很大的改进
    void push(T new_value)
    {
        const std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));
        std::unique_ptr<node> p(new node);
        tail->data = new_data;
        const node *new_tail = p.get();
        tail->next = std::move(p);
        tail = new_tail;
    }

};

最终代码:使用细粒度锁的线程安全队列

// 使用细粒度锁的线程安全队列
#include <mutex>
#include <memory>
#include <utility>

template <typename T>
class threadsafe_queue
{
private:
    struct node
    {
        std::shared_ptr<T> data;
        std::unique_ptr<node> next;
    };
    std::mutex head_mutex;
    std::unique_ptr<node> head;
    std::mutex tail_mutex;
    node *tail;

    node* get_tail()
    {
        std::lock_guard<std::mutex> tail_lock(tail_mutex);
        return tail;
    }

    std::unique_ptr<node> pop_head()
    {
        // 只有锁是可能会产生异常的,所以是异常安全的
        std::lock_guard<std::mutex> head_lock(head_mutex);
        if(head.get() == get_tail())
        {
            return nullptr;
        }
        std::unique_ptr<node> old_head = std::move(head);
        head = std::move(old_head->next);
        return old_head;
    }

public:
    threadsafe_queue():head(new node), tail(head.get()){}
    threadsafe_queue(const threadsafe_queue &other) = delete;
    threadsafe_queue& operator=(const threadsafe_queue &other) = delete;

    std::shared_ptr<T> try_pop()
    {
        std::unique_ptr<node> old_head = pop_head();
        return old_head?old_head->data : std::shared_ptr<T>();
    }

    void push(T new_value)
    {
        std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));
        std::unique_ptr<node> p(new node);
        const node *new_tail = p.get();
        // 可能发生异常的语句在锁的前面,所以这个函数是异常安全的
        std::lock_guard<std::mutex> tail_lock(tail_mutex);
        tail->data = new_data;
        tail->next = std::move(p);
        tail = tail->next;
    }
};

这个代码锁的粒度更细,因为很多操作是在锁的外部完成的,比如说申请节点内存,多个线程是可以并行申请的,只要在插入的时候一个一个插入就可以。

设计一个细粒度锁的map数据结构

使用哈希表实现,因为二叉树需要频繁对根节点上锁,而使用数组需要对整个数组上锁,锁的粒度太大。

// 线程安全查找表
// 基于哈系表实现
#include <functional>
#include <shared_mutex>
#include <algorithm>
#include <list>
#include <vector>
#include <memory>
template <typename Key, typename Value, typename Hash = std::hash<Key>>
class threadsafe_lookup_table
{
private:
    class bucket_type
    {
    private:
        typedef std::pair<Key, Value> bucket_value;
        typedef std::list<bucket_value> bucket_data;
        typedef typename bucket_data::iterator bucket_iterator;

        bucket_data data;   // 声明一个链表容器
        // 共享互斥元
        mutable std::shared_mutex mutex;
        // 根据值,返回迭代器
        bucket_iterator find_entry_for(const Key &key) const
        {
            return std::find_if(data.begi8n(), data.end(), [&](const bucket_value &item)
                                { return item.first == key; });
        }

    public:
        // 链表中是否有key这个健,如果有,返回对应的值,如果没有,返回默认值
        Value value_for(const Key &key, const Value &default_value) const
        {
            std::shared_lock<std::shared_mutex> lk(mutex); // 上读锁
            const bucket_iterator found_entry = find_entry_for(key);
            if (found_entry == data.end())
            {
                return default_value;
            }
            else
            {
                return found_entry->second;
            }
        }

        void add_or_update_mapping(const Key &key, const Value &value)
        {
            std::unique_lock<std::shared_mutex> lk(mutex); // 上写锁
            const bucket_iterator found_entry = find_entry_for(key);
            if (found_entry == data.end())
            {
                data.push_back(bucket_value(key, value));
            }
            else
            {
                found_entry->second = value;
            }
        }

        void remove_mapping(const Key &key)
        {
            std::unique_lock<std::shared_mutex> lk(mutex); // 上写锁
            const bucket_iterator found_entry = find_entry_for(key);
            if (found_entry != data.end())
            {
                data.erase(found_entry);
            }
        }
    };

    std::vector<std::unique_ptr<bucket_type>> buckets;
    Hash hasher;
    // 返回一个具体的bucket
    bucket_type& get_bucket(const Key &key) const
    {
        const size_t bucket_index = hasher(key)%buckets.size();
        return *buckets.at(bucket_index);
    }
public:
    threadsafe_lookup_table(const threadsafe_lookup_table &other) = delete;
    threadsafe_lookup_table& operator=(const threadsafe_lookup_table &other) = delete;
    typedef Key key_type;
    typedef Value mapped_type;
    typedef Hash hash_type;

    threadsafe_lookup_table(unsigned num_buckets=19,const Hash &hasher_=Hash()):buckets(num_buckets),hasher(hasher_)
    {
        for (unsigned i = 0; i < num_buckets; ++i)
        {
            buckets[i].reset(new bucket_type);
        }
    }

    Value value_for(const Key &key, const Value &default_value = Value()) const
    {
        return get_bucket(key).value_for(key,default_value);
    }

    void add_or_update_mapping(const Key &key, const Value &value)
    {
        get_bucket(key).add_or_update_mapping(key,value);
    }

    void remove_mapping(const Key &key)
    {
        get_bucket(key).remove_mapping(key);
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

橙子砰砰枪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值