Cpp Concurrency In Action(读书笔记6)——无锁并发数据结构设计

定义和意义

  使用互斥量、条件变量,以及“期望”来同步“阻塞”(blocking)数据的算法和数据结构。使用原子操作的“内存序”特性,并使用这个特性来构建无锁数据结构。
  不使用阻塞库的数据结构和算法,被称为“无阻塞”(nonblocking)结构。不过,“无阻塞”的数据
结构并非都是无锁的(lock-free)。

非阻塞数据结构

  使用std::atomic_flag 实现了一个简单的自旋锁(读书笔记4):

class spinlock_mutex
{
    std::atomic_flag flag;
public:
    spinlock_mutex() :
        flag(ATOMIC_FLAG_INIT)
    {}
    void lock()
    {
        while (flag.test_and_set(std::memory_order_acquire));
    }
    void unlock()
    {
        flag.clear(std::memory_order_release);
    }
};

  用此来保护数据:有锁的非阻塞。

无锁数据结构

  无锁数据结构:线程可以并发的访问。具有“比较/交换”操作的数据结构,通常在“比较/交换”实现中都有一个循环。使用“比较/交换”操作的原因:当有其他线程同时对指定数据的修改时,代码将尝试恢复数据。
  无锁算法中的循环会让一些线程处于“饥饿”状态。如有线程在“错误”时间执行,那么第一个线
程将会不停得尝试自己所要完成的操作(其他程序继续执行)。“无锁-无等待”数据结构,就为了
避免这种问题存在的。

无等待数据结构

  无等待数据结构:首先,是无锁数据结构;并且,每个线程都能在有限的步数内完成操
作,暂且不管其他线程是如何工作的。

无锁数据结构的利与弊

  使用原因:将并发最大化(主要);鲁棒性(次要)。
  活锁(live locks):两个线程同时尝试修改数据结构,但每个线程所做的修改操作都会让另一个线程重启,所以两个线程就会陷入循环,多次的尝试完成自己的操作。
  根据定义,无等待的代码不会被活锁所困扰,因其操作执行步骤是有上限的。换个角度,无等待的算法要比等待算法的复杂度高,且即使没有其他线程访问数据结构,也可能需要更多步骤来完成对应操作。
  “无锁-无等待”代码的缺点:虽然提高了并发访问的能力,减少了单个线程的等待时间,但是其可能会将整体性能拉低。首先,原子操作的无锁代码要慢于无原子操作的代码,原子操作就相当于无锁数据结构中的锁。不仅如此,硬件必须通过同一个原子变量对线程间的数据进行同步。

无锁数据结构的例子

  无锁结构依赖于原子操作和内存序及相关保证,以确保多线程以正确的顺序访问数据结构。

写一个无锁的线程安全栈

  不用锁实现push():

template<typename T>
class lock_free_stack
{
private:
    struct node
    {
        T data;
        node* next;
        node(T const& data_) : // 1
            data(data_)
        {}
    };
    std::atomic<node*> head;
public:
    void push(T const& data)
    {
        node* const new_node = new node(data); // 2
        new_node->next = head.load(); // 3
        while (!head.compare_exchange_weak(new_node->next, new_node)); 
        /*当new_node->next与head相等时,head=new_node,
        *返回true(可能直接由于平台原因失败,而非strong版本的反复尝试);
        *不等时,new_node->next=head(完美修改了新节点指针域)
        *返回false,再次进行尝试。
        */
    }
};

  为了更好的理解compare_exchange_weak()函数及“比较/交换”,查阅了相关资料,Understand std::atomic::compare_exchange_weak() in C++11
  compare-and-swap (CAS) :In short, it loads the value from memory, compares it with an expected value, and if equal, store a predefined desired value to that memory location. The important thing is that all these are performed in an atomic way, in the sense that if the value has been changed in the meanwhile by another thread, CAS will fail.
  the weak version will return false even if the value of the object is equal to expected, which won’t be synced with the value in memory in this case.
  pop()雏形:

template<typename T>
class lock_free_stack
{
public:
    void pop(T& result)
    {
        node* old_head = head.load();
        while (!head.compare_exchange_weak(old_head, old_head->next));
        result = old_head->data;
    }
};

  问题:1、当头指针为空的时候,程序将解引用空指针,这是未定义行为;2、异常安全(最后复制栈的数据时,若抛出异常,栈数据已经丢失,可以使用shared_ptr<>解决)。
  带有节点泄露的无锁栈(无锁——等待):

template<typename T>
class lock_free_stack
{
private:
    struct node
    {
        std::shared_ptr<T> data; // 1 指针获取数据
        node* next;
        node(T const& data_) :
            data(std::make_shared<T>(data_)) 
            // 2 让std::shared_ptr指向新分配出来的T
        {}
    };
    std::atomic<node*> head;
public:
    void push(T const& data)
    {
        node* const new_node = new node(data);
        new_node->next = head.load();
        while (!head.compare_exchange_weak(new_node->next, new_node));
    }
    std::shared_ptr<T> pop()
    {
        node* old_head = head.load();
        while (old_head && // 3 在解引用前检查old_head是否为空指针
            !head.compare_exchange_weak(old_head, old_head->next));
        return old_head ? old_head->data : std::shared_ptr<T>(); // 4
    }
};

  ##停止内存泄露:使用无锁数据结构管理内存
  没有线程通过pop()访问节点时,就对节点进行回收:

template<typename T>
class lock_free_stack
{
private:
    std::atomic<unsigned> threads_in_pop; 
    // 1 原子变量,记录有多少线程试图弹出栈中的元素
    void try_reclaim(node* old_head);
    //计数器减一,当这个函数被节点调用时,说明这个节点已经被删除
public:
    std::shared_ptr<T> pop()
    {
        ++threads_in_pop; // 2 在做事之前,计数值加1
        node* old_head = head.load();
        while (old_head &&
            !head.compare_exchange_weak(old_head, old_head->next));
        //check空指针,阻止内存泄漏
        std::shared_ptr<T> res;
        if (old_head)
        {
            res.swap(old_head->data); // 3 从节点中直接提取数据,而非拷贝指针
        }
        try_reclaim(old_head); // 4 回收删除的节点
        return res;
    }
};

  采用引用计数的回收机制(实现try_reclaim()):

template<typename T>
class lock_free_stack
{
private:
    std::atomic<node*> to_be_deleted;
    static void delete_nodes(node* nodes)
    {
        while (nodes)
        {
            node* next = nodes->next;
            delete nodes;
            nodes = next;
        }
    }
    void try_reclaim(node* old_head)//节点调用即删除(于原链表)
    {
        if (threads_in_pop == 1) // 1 当前线程正在对pop进行访问
        {
            node* nodes_to_delete = to_be_deleted.exchange(nullptr);
            // 2 声明“可删除”列表,该函数返回:The contained value before the call.
            //下面进行检查,决定删除等待链表,还是还原to_be_delted
            if (!--threads_in_pop) // 3 是否只有一个线程调用pop()?
            {
                delete_nodes(nodes_to_delete); // 4 迭代删除等待链表
            }
            else if (nodes_to_delete) // 5 存在
            {
                chain_pending_nodes(nodes_to_delete); 
                // 6 nodes还原to_be_delted=nodes_to_delete
            }
            delete old_head; // 7 安全删除
        }
        else
        {
            chain_pending_node(old_head); // 8 向等待列表中继续添加节点node
            //此时to_be_deleted为old_head
            --threads_in_pop;
        }
    }
    void chain_pending_nodes(node* nodes)//nodes
    {
        node* last = nodes;
        while (node* const next = last->next) // 9 让next指针指向链表的末尾
        {
            last = next;
        }
        chain_pending_nodes(nodes, last);
    }
    void chain_pending_nodes(node* first, node* last)
    {
        last->next = to_be_deleted; // 10
        while (!to_be_deleted.compare_exchange_weak(
            // 11 用循环来保证last->next的正确性,存储第一个节点
            last->
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值