03 重修C++之并发实战7

03重修C++之并发实战7

上一篇:03 重修C++之并发实战6
【设计无锁的并发数据结构】

7.1定义和结果

使用互斥元、条件变量以及 future 来同步数据的算法和数据结构被称为阻塞(bloking)的算法和数据结构。调用库函数的应用会中断一个线程的执行,直到另一个线程执行一个动作。这种库函数调用被称为阻塞调用,因为直到阻塞被释放时线程才能继续执行下去。通常,操作系统会完全阻塞一个线程(并且将这个线程的时间片分给另一个线程)直到另一个线程执行了适当的动作将其解锁,可以是解锁互斥元、通知条件变量或者使得 future 就绪。

不使用阻塞库函数的数据结构和算法被称为**非阻塞(nonblocking)的。但是,并不是所有这样的数据结构都是无锁(lock-free)**的。

7.1.1 非阻塞数据结构的类型

第五章中,实现了一种使用 std::atmic_flag 作为自旋锁的基本互斥元。代码如下

#include <iostream>
#include <atomic>
#include <thread>
#include <mutex>

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);
    }    

};

这段代码不调用任何阻塞函数,lock()一致循环直到对test_and_set()的调用返回false。这就是自旋锁(spin lock)名字的由来。无论如何,这里没有阻塞调用,因此使用任何互斥元来保护共享数据的代码因而都是非阻塞的。然而,它并非无锁的。它仍然是一个胡次元,并且一次仍然只能被一个线程锁定。

7.1.2 无锁数据结构

对于有资格称为无锁的数据结构,就必须能够让多于一个线程可以并发地访问此数据结构。这些线程不需要做相同地操作,无锁地队列允许一个线程push地同时另一个线程pop,但是如果两个线程同时试图插入push新数据项地时候,就会打破无锁队列。不仅如此,如果一个访问数据结构地线程在操作中途被调度器挂起地话,别的线程必须仍然能够完成操作而无需等待挂起地线程。

在数据结构上使用比较、交换操作地算法经常在其中包含循环。使用比较、交换操作时因为可能另一个线程正在同时修改数据,这种情况下,代码就需要在试图重新比较/交换前重做部分操作。如果比较/交换操作最终在其他线程都被中断地情况下成功,那么这种代码仍然是无锁地。如果没有,最起码要使用自旋锁,是非阻塞的而不是无锁的。

具有这种循环的无锁算法可能导致一个线程承受饥饿。如果另一个线程在“错误的”时间执行操作,那么当第一个线程持续重试其操作时,别的线程则可以继续前进。能避免此类问题的数据结构时无等待的也是无锁的。

7.1.3 无等待的数据结构

无等待的数据结构时一种无锁的数据结构,并有着额外的特性,每个访问数据结构的线程都可以在有限数量的步骤内完成它的操作,而不用管别的线程的行为。因为其它线程的冲突而可能卷入无限次重试的算法不是无等待的。

正确地编写无等待地数据结构时及其困难地。为了保证每个线程都能够在有限步骤内完成它的操作,就必须保障每个操作都可以在一个操作周期内执行,并且一个线程执行地操作不会导致另一个线程上操作的失败。这会使得各种操作的整体算法变得相当复杂。

正确的设计无锁或无等待的数据结构时非常困难的,你需要一些很好的理由来支撑这一点,你需要确信收益大于代价。

7.1.4 无锁数据结构的有点与缺点

  • 优点

第一点:实现了最大程度的并发。对于基于锁的容器,总有可能一个线程必须阻塞,并在等待另一个线程完成其操作后继续前进。这是一种很希望得到但是却很难得到的特性。

第二点:健壮性。当一个线程在持有锁的时候终止,那个数据结构就永远被破坏了。但是如果一个线程在在操作无锁数据结构时终止了,就不会丢失任何数据,除了此线程的数据之外,其它线程可以继续正常执行。

  • 缺点

第一点:如果不能排除线程访问数据结构,那么就必须确保持有不变量或选择可依此有的替代的不变量。并且,必须注意你在操作上的顺序限制。为了避免与数据竞争有关的未定义行为,就必须在修改时使用原子操作。仅仅如此还是不够的,还必须保证这个改变以正确的顺序对其他线程时可见的。所有这些都以为着设计线程安全数据结构时,不使用锁比使用锁要困难的多。

第二点:因为不使用任何锁,因此无锁的数据结构时不会发生死锁的,尽管可能存在活锁。当两个线程都试图修改数据结构,但对于每个线程来说,另一个线程所作的修改都会要求此线程的操作被重新执行,因此两个线程都会一直循环和不断尝试,在这种情况下就会发生活锁。除非某个线程先到达(通过协议,通过更快,或者靠运气),不然此循环会一直继续下去。在这个简单例子中,活锁通常是短暂的,因为它取决于线程的精确调度。因此活锁会降低性能而不会导致长期问题,但是也是需要注意,根据定义无等待的代码无法忍受活锁,因为它的执行操作的步骤通常是有上限的。另外,这种算法比别的算法更复杂,并且即使没有线程春去数据结构的时候也需要执行更多的步骤。

第三点:尽管这种设计增加了在数据结构上操作的并发能力,并且减少了线程等待的时间,但是它的缺点是可能减低整体性能。首先无锁代码使用的原子操作可能比非原子操作慢的多。并且与基于锁的数据结构的互斥元锁代码相比,无锁数据结构中需要更多的原子操作。不仅如此,硬件必须在存取同样的原子变量的线程间同步数据。

7.2 无锁数据结构的例子

依赖于使用原子操作的无锁数据结构,以及与之相关联的内存顺序保证是为了确保数据结构以正确的顺序对其他线程可见。起初,我们为所有原子操作都使用默认的memory_order_seq_cst内存顺序,因为这是最简单的(记住所有的memory_order_seq_cst操作构成了全局顺序)但在后来的例子中我们考虑降低一些排序约束到memory_order_acquire memory_order_release ,甚至 memory_order_relaxed中。尽管这些例子中都未直接使用互斥锁,但是需要记住的是,只有std::stomic_flag保证在实现中是不使用锁的。在一些平台上,有些看上去是无锁的代码,实际上却可能使用了C++标准库的内部锁。在这些平台上,一个简单的基于锁的数据结构可能更合适。但是还有比这个更重要的是,在选择一种实现的时候,必须先确定你的需求,然后考虑那些选择可以满足此要求。

因此,可以追溯到一种最简单的数据结构:栈。

7.2.1 编写不用锁的线程安全栈

栈的基本假设是相当简单的,按照添加结点的逆序来检索结点——后进先出(LIFO)。因此,确保一次只添加一个值到栈中是很重要的。另一个线程可以立刻检索结点,并确保只有一个线程返回给定的数值是很重要的。最简单的栈是一个链表,head指针指向第一个节点(这个结点将被第一个检索),并且每个节点都按顺序指向下一个结点。在这种原则下,添加一个节点相对比较简单。

  1. 创建一个新结点。

  2. 将他的next指针指向当前的head结点。

  3. 将head指针指向此新结点。

如果两个线程同时添加结点,那么第2步和第3步间就会存在竞争条件。当你的线程在第2步读取头节点和第3步更新头节点之间,第二个线程可能会修改head的值。这就会导致另一个线程所作的修改无效或有更坏的影响。在考虑解决这一竞争条件之前,请注意一点head被更新指向你创建的新节点别的线程就可以读取该节点,因此,在head指向新节点之前将新节点完全准备好也是至关重要的。

竞争条件解决:在第3步中使用一个原子比较、交换操作来保证从第2步中读取它的开始,head就未被修改过。如果head被修改了,那么可以循环和再次尝试。

实现

#include <iostream>
#include <atomic>
#include <thread>
#include <unistd.h>

template<typename T>
class lock_free_stack
{
private:
    struct node 
    {
        T data;
        node* next;
        node(T const& data_):data(data_) {} //【1】
    };
    std::atomic<node*> head;
public:
    lock_free_stack() {}
    lock_free_stack(lock_free_stack &&) = default;
    lock_free_stack(const lock_free_stack &) = default;
    lock_free_stack &operator=(lock_free_stack &&) = default;
    lock_free_stack &operator=(const lock_free_stack &) = default;
    ~lock_free_stack() {}

    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));//【4】
    }

    void print() {
        for (auto p = head.load(); p != nullptr; p = p->next) 
        {
            std::cout << p->data << std::endl;
        }
    }

};


int main(int argc, char const *argv[])
{
    /* code */

    lock_free_stack<int> stack;

    std::thread t1([&]{
        for(int i = 0; i < 200000; i += 2) 
        {
            stack.push(i);
        }
    });
    std::thread t2([&]{
        for(int i = 1; i < 200000; i += 2) 
        {
            stack.push(i);
        }
    });
    t1.join();
    t2.join();
    sleep(2);
    stack.print();
    return 0;
}
/************************运行结果******************************/
[wangs7@localhost 7th_chapter]$ ./a | wc -l
200000

统计输出200000条数据,证明全部插入,从结果看单数和双数分别各自严格遵守大小排列,说明插入有效。这段代码恰好符合上面提到的三点计划:1.【2】创建一个新结点。2.【3】将他的next指针指向当前的head结点。3.【4】将head指针指向此新结点。通过在node构造函数【1】中填充数据,保证结点创建即可用。另外compare_exchange_weak()在某些架构下比compare_exchange_strong()能够产生更优化的代码。

下面需要添加移除栈中数据的方法。从表面上看,这要简单一些。

  1. 读取head当前值。
  2. 读取head->next。
  3. 将head->next设置为head。
  4. 返回检索到的结点的值。
  5. 删除检索到的节点。

然而,在存在多个线程的情况下,这个问题就不这么简单了。如果同时有两个线程从栈中移除元素,他们可能在第1步中读取了相同的head值。如果一个线程在其他线程执行第2步前顺利执行到了第5步,那么第二个线程将被解引用悬挂指针。这是写无锁代码中最大的问题之一。因此从现在开始,先忽略第5步并且先不删除结点。

但是,这也没有解决掉所有的问题。这里存在着另一个问题,如果两个线程读取同一个head值,那么它们将会返回同一个结点。这就违背了栈数据结构的目的,因此必须避免发生这种情况。你可以用push()中使用的方法来解决竞争,使用比较/交换来更新head。如果比较/交换失败了,要么是因为在栈中插入了一个新节点,要么是因为另一个线程从栈中移除了你打算移除的结点。无论是哪一种情况,都需要返回第一步(尽管比较/交换调用可以重新读取head)。一旦比较/交换调用成功,那么这就是唯一的从栈中移除指定结点的线程,因此可以安全的执行第4步。

代码如下

#include <iostream>
#include <atomic>
#include <thread>
#include <unistd.h>

template<typename T>
class lock_free_stack
{
private:
    struct node 
    {
        T data;
        node* next;
        node(T const& data_):data(data_)
        {

        }
    };
    std::atomic<node*> head;
public:
    lock_free_stack()
    {

    }
    lock_free_stack(lock_free_stack &&) = default;
    lock_free_stack(const lock_free_stack &) = default;
    lock_free_stack &operator=(lock_free_stack &&) = default;
    lock_free_stack &operator=(const lock_free_stack &) = default;
    ~lock_free_stack()
    {

    }

    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));
    }
    void pop(T& result) {
        node* old_head = head.load();
        while (!head.compare_exchange_weak(old_head, old_head->next));
        result = old_head->data;
    }

    void print() {
        for (auto p = head.load(); p != nullptr; p = p->next) {
            std::cout << p->data << std::endl;
        }
    }

};


int main(int argc, char const *argv[])
{
    /* code */

    lock_free_stack<int> stack;

    for (int i = 0; i < 20000; i++) 
    {
        stack.push(i);
    }

    std::thread t1([&]{
        int n;
        for(int i = 0; i < 20000; i += 2) 
        {   
            stack.pop(n);
            std::cout << n << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });
    std::thread t2([&]{
        int n;
        for(int i = 0; i < 20000; i += 2) 
        {   
            stack.pop(n);
            std::cout << n << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });
    t1.join();
    t2.join();

    return 0;
}
/************************运行结果******************************/
[wangs7@localhost 7th_chapter]$ ./a | wc -l
20000

功能正常,尽管用这种方法很好很简明,但是除了未删除节点外还有一些特别的问题。首先,当链表为空时他就行不通了,如果head是空指针,那么当它试图读取next指针时就会导致未定义的行为。这也很容易通过在while循环中检查空指针来解决。可以同时在空栈上引发一个异常或者返回一个bool来表明成功或失败。当然由于没有删除结点必然会导致内存泄漏的问题,这个暂时不做处理。

第二个问题就是异常安全问题。只通过值返回对象会留下一个异常安全问题,当复制返回值的时候,如果引发了异常,那么此值就会被丢失。在这种情况下,传递对值的引用是一种解决办法。因为如果抛出异常的话,这就可以确保栈不会被修改。可惜这里不能使用这种方法。一旦你知道这是唯一一个返回结点的线程,你就可以安全的复制数据了。这就意味着这个结点被移出队列了。因此,通过引用传递返回值的对象就不再时一个优势了,当然也可能只是返回值。如果想安全地返回,就必须返回一个指向数据值地(智能)指针。

如果返回智能指针,那么就可以只返回空指针来表明没有返回值,但是这就要要求数据是在堆上被分配地。如果将对分配作为pop()地一部分,那么这个设计还不是更好地,因为堆地分配也可能引发异常。反而,当把数据push()进栈时,可以为此数据分配内存(反正都要为新节点分配内存)。返回std::shared_ptr<>不会引发异常,因此pop()是安全的。代码如下。

#include <iostream>
#include <atomic>
#include <thread>
#include <memory>
#include <chrono>
#include <unistd.h>

template<typename T>
class lock_free_stack
{
private:
    struct node 
    {
        std::shared_ptr<T> data; //替换成智能指针
        node* next;
        node(T const& data_)
        {
            data = std::make_shared<T>(data_); //创建指针
        }
    };
    std::atomic<node*> head;
public:
    lock_free_stack() {}
    lock_free_stack(lock_free_stack &&) = default;
    lock_free_stack(const lock_free_stack &) = default;
    lock_free_stack &operator=(lock_free_stack &&) = default;
    lock_free_stack &operator=(const lock_free_stack &) = default;
    ~lock_free_stack() {}

    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();
        //在解引用之前检查old_head不是一个空指针
        while (old_head && !head.compare_exchange_weak(old_head, old_head->next));
        return old_head ? old_head->data : std::shared_ptr<T>();
    }

    void print() {
        for (auto p = head.load(); p != nullptr; p = p->next) {
            std::cout << *(p->data) << std::endl;
        }
    }

};


int main(int argc, char const *argv[])
{
    /* code */

    lock_free_stack<int> stack;

    for (int i = 0; i < 20000; i++) 
    {
        stack.push(i);
    }

    std::thread t1([&]{
        int n;
        for(int i = 0; i < 20000; i += 2) 
        {   
            
            std::cout << *(stack.pop()) << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });
    std::thread t2([&]{
        int n;
        for(int i = 0; i < 20000; i += 2) 
        {   
            std::cout << *(stack.pop()) << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });
    t1.join();
    t2.join();

    return 0;
}
/************************运行结果******************************/
[wangs7@localhost 7th_chapter]$ ./a | wc -l
20000

数据现在由智能指针持有,因此需要在结点构造函数中在堆上分配数据。在compare_exchange_weak()循环解引用old_head前,必须检查空指针。最后,如果存在与该节点相关的值,那就返回该节点,否者返回一个空指针。注意尽管这是无锁的,但是它不是无等待的,因为push()和pop()的while循环中,如果compare_exchange_weak()一直失败的话,理论上可以一直循环下去。

7.2.2 停止恼人的泄漏:在无锁数据结构中管理内存

前面提到过不删除pop的结点会导致内存泄漏的问题,最基本的问题就是,你想释放一个结点,但是直到你确保没有别的线程持有指针指向此结点的时候才能释放此节点。如果只有一个线程曾经在一个特定的栈实例上调用pop,那么可以自由释放此节点。一旦结点被添加到栈中,push就不会再操作此节点了。因此调用pop的线程就一定是唯一一个操作此节点的线程,并且可以安全地删除此节点。

如果没有线程调用pop,那么可以删除目前等待删除地所有结点。因此当你获得数据是。如果将此节点添加到“将被删除”的列表中,那么当没有线程调用pop时就可以删除它。如何知道有没有别的线程在调用pop呢?有个简单的方法——数清数目。进入时计数器加一,离开时计数器减一。那么当计数器为零时,就可以安全删除“将被删除”列表中的结点。当然计数器必须为原子计数器,从而可以安全地被多个线程存取。

#include <iostream>
#include <atomic>
#include <thread>
#include <memory>
#include <chrono>
#include <unistd.h>

template<typename T>
class lock_free_stack
{
private:
    struct node 
    {
        std::shared_ptr<T> data;
        node* next;
        node(T const& data_)
        {
            data = std::make_shared<T>(data_);
        }
    };
    std::atomic<node*> head;

public:
    lock_free_stack() {}
    lock_free_stack(lock_free_stack &&) = default;
    lock_free_stack(const lock_free_stack &) = default;
    lock_free_stack &operator=(lock_free_stack &&) = default;
    lock_free_stack &operator=(const lock_free_stack &) = default;
    ~lock_free_stack() {}

    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));
    }
/*******************************修改部分pop*****************************************/
    std::shared_ptr<T> pop() {
        ++thread_in_pop; //【2】进入函数时增加计数
        node* old_head = head.load();
        while (old_head && !head.compare_exchange_weak(old_head, old_head->next));
        std::shared_ptr<T> res;
        if (old_head)
        {
            res.swap(old_head->data);//【3】从节点提取数据而不是复制指针
        }
        try_reclaim(old_head);//【4】如果可能回收删除的节点 具体实现见下方
        return res;
    }

    void print() {
        for (auto p = head.load(); p != nullptr; p = p->next) {
            std::cout << *(p->data) << std::endl;
        }
    }
/*****************************修改部分私有成员&私有方法**********************************************/
private:
    std::atomic<unsigned> thread_in_pop; //【1】原子变量线程计数器
    std::atomic<node*> to_be_deleted;
	//【A】
    static void delete_nodes(node* nodes) {
        while (nodes) //删除链表
        {
            node* next = nodes->next;
            delete nodes;
            nodes = next;
        }
    }
	/** 【B+C】找到链表头尾 **/
    void chain_pending_nodes(node* nodes) {
        node* last = nodes;
        while (node* const next = last->next) 
        {
            last = next;
        }
        chain_pending_nodes(nodes, last);
    }
	/** 【C】把 新的接到to_be_deleted待删除列表后,更新to_be_deleted**/
    void chain_pending_nodes(node* first, node* last) {
        last->next = to_be_deleted;//链表相接
        while (!to_be_deleted.compare_exchange_weak(last->next, first));//循环保证last->next的正确性
    }
    // 【D+C】
    void chain_pending_node(node* n) {
        chain_pending_nodes(n, n);
    }

    void try_reclaim(node* old_head) {
        if (thread_in_pop == 1) //【5】如果只有一个线程pop尝试删除
        { //删除内容包括正在出栈的结点以及之前出栈但是没有删除的结点
            //【6】列出将要删除的结点的清单
            node* nodes_to_delete = to_be_deleted.exchange(nullptr);
            if (!--thread_in_pop) //【7】是pop中唯一的线程吗?
            {
                delete_nodes(nodes_to_delete); //【8】是的话删除链表 【A】
            }
            else if (nodes_to_delete) //【9】如果链表不为空且减一后不为零
            {
                chain_pending_nodes(nodes_to_delete); //【10】将新增加的结点接到待删除列表【B+C】
            }
            delete old_head; //【11】由于5的判断可以安全删除
        }
        else
        {
            chain_pending_node(old_head); //【12】如果不止一个访问 【D+C】
            //找到链表头尾后接到to_be_deleted上,并更新to_be_deleted
            --thread_in_pop;
        }
    }

};


int main(int argc, char const *argv[])
{
    /* code */

    lock_free_stack<int> stack;

    for (int i = 0; i < 20000; i++) 
    {
        stack.push(i);
    }

    std::thread t1([&]{
        int n;
        for(int i = 0; i < 20000; i += 2) 
        {   
            std::cout << *(stack.pop()) << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });
    std::thread t2([&]{
        int n;
        for(int i = 0; i < 20000; i += 2) 
        {   
            std::cout << *(stack.pop()) << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    });
    t1.join();
    t2.join();

    return 0;
}
/************************运行结果******************************/
[wangs7@localhost 7th_chapter]$ ./a | wc -l
20000

这种设计的好处是当链表发生变化时,从链表尾部更新next指针,在链表中添加一个结点时一种特殊情况,即便链表中添加的第一个结点与最后一个结点时相同的。

在低负载的情况下,即当没有进程在调用pop这样一种合适的静态点的时候,这种方法很有效。但是在回收结点和删除刚转移出的结点之前都要检查threads_in_pop计数器是否减少为零,这是因为这种状态时很短暂的。删除会消耗一定时间的,并且别的线程修改列表窗口越小越好。在线程第一次发现threads_in_pop的值为1与试图删除结点之间的时间越长,别的线程调用pop以及threads_in_pop的值不再为1的可能性九月打,因此阻止了此节点被真正的删除。

在高负载的情况下,因为其他线程调用pop结束之前就会有别的线程调用pop,因此基本不可能有这种静态点。这种情况下,to_be_deleted列表很容易就越界了,并且再次内存泄漏。这就需要另外一种方法:风险指针(hazard pointers)

7.2.3 用风险指针检测不能被回收的结点

术语**风险指针(hazard pointers)**是Maged Michacel提出的一种技术。基本思想就是如果一个线程准备访问别的线程准备删除的对象,那么它会用风险指针来应用对象,因此就可以通知别的线程删除此对象可能是有风险的。如果别的线程引用此结点,并准备通过此引用来访问结点,那么删除一个可能仍然被别的线程引用的结点是有危险的,因此称它们为风险指针。一旦不需要此对象,风险指针就会被清除了。

当程序试图删除一个对象时,必须首先检查别的线程所持有的风险指针。如果没有风险指针引用此对象,那么就可以删除此对象。否则,它必须之后才能被处理。周期性地检查对象列表来确定现在是否可以删除它。

使用风险指针实现pop

//BUG 太多了编不过 回头再看

7.2.4 使用计数检测结点

删除结点的问题就在于检测那些结点正在被别的线程读取。如果可以精确识别出哪些节点正在被引用以及何时没有线程读取这些结点,那么就可以删除此节点。风险指针通过存储读取每个节点的线程数来处理此问题。引用技术通过存储一定数量的线程读取结点来处理这个问题。

这种方法看上去更好更直接,但是在实际中很难处理。首先,你可能认为std::shared_ptr<>可以处理这种问题,毕竟这是一个引用计数指针。不幸的是,尽管std::shared_ptr<>中的一些操作时原子的,但是它不能保证是无锁的。尽管这与原子类型上的任何操作并没有不同。但在许多情况下std::shared_ptr<>被使用时,且原子操作是无锁的,会导致使用这个类有花费。如果你的平台提供这样一个实现,即当std::atomic_is_lock_free(&some_shared_ptr)返回ture,所有的内存回收事件都离开。

template<typename T> //未测试代码
class lock_free_stack
{
private:
    struct node
    {
        std::shared_ptr<T> data;
        std::shared_ptr<node> next;
        node(T const& data_):data(std::make_shared<T>(data_)) {}
    };
    std::shared_ptr<node> head;
public:
    void push(T const& data)
    {
        std::shared_ptr<node> const new_node = std::make_shared<node>(data);
        new_node->next = head.load();
        while(!std::atomic_compare_exchange_weak(&head, &new_node->next, new_node));
    }
    std::shared_ptr<T> pop()
    {
        std::shared_ptr<node> old_head = std::atomic_load(&head);
        while (old_head && !std::atomic_compare_exchange_weak(&head, &old_node->next, old_node));
        return old_head ? old_head->data : std::shared_ptr<T>();
    }
}

在有些情况下std::shared_ptr<>实现并不是无锁的,需要手工处理引用计数,一种可能的计数器设计为每一个结点使用两个引用计数器,一个内部计数和一个外部计数。这两个计数值之和是结点总的引用数。外部计数始终与结点指针在一起,并且每次读取的时候外部计数加一。当读取结束时,内部计数减一。当内部计数/指针对不再被需要时(即多个线程不再访问结点时),内部计数器加一,外部计数器减一,并废除外部计数器。一旦内部计数值为零,就没有引用结点,此时可删除此节点。使用原子操作来更新共享数据也是很重要的。下面是一个使用这种计数来确保结点只会被安全收会的无锁数据结构。

template<typename T> //编不过 原子类型问题
class lock_free_stack
{

private:
    struct node;
    struct counted_node_ptr 
    {
        int external_count;
        node *ptr;
    };
    struct node 
    {
        std::shared_ptr<T> data;
        std::atomic<int> internal_count;
        counted_node_ptr next;

        node(T const& data_)
        {
            data = std::make_shared<T>(data_);
            internal_count = 0;
        }
    };
    std::atomic<counted_node_ptr> head;
public:
    lock_free_stack() 
    {
        // head.external_count = 0;
        // head.ptr = nullptr;
    }
    lock_free_stack(lock_free_stack &&) = default;
    lock_free_stack(const lock_free_stack &) = default;
    lock_free_stack &operator=(lock_free_stack &&) = default;
    lock_free_stack &operator=(const lock_free_stack &) = default;
    ~lock_free_stack() 
    {
        while (pop());
    }
    void push(T const& value)
    {
        counted_node_ptr new_node;
        new_node.ptr = new node(value);
        new_node.external_count = 1;
        new_node.ptr->next = head.load();
        while (!head.compare_exchange_weak(new_node.ptr->next, new_node));
    }

    std::shared_ptr<T> pop() 
    {
        counted_node_ptr old_head = head.load();
        for (;;)
        {
            increase_head_count(old_head);
            node* const ptr = old_head.ptr;
            if(!ptr)
            {
                return std::shared_ptr<T> ();
            }
            if (!head.compare_exchange_strong(old_head, ptr->next))
            {
                std::shared_ptr<T> res;
                res.swap(ptr->data);
                int const count_increase = old_head.external_count-2;
                if(ptr->internal_count.fetch_add(count_increase) == -count_increase)
                {
                    delete ptr;
                }
                return res;
            }
            else if (ptr->internal_count.fetch_sub(1) == 1)
            {
                delete ptr;
            }
        }
        
    }

private:
    void increase_head_count(counted_node_ptr& old_counter)
    {
        counted_node_ptr new_counter;
        do 
        {
            new_counter = old_counter;
            (new_counter.external_count)++;
        }
        while (!head.compare_exchange_strong(old_counter, new_counter));
        old_counter.external_count = new_counter.external_count;
    }
    
};

7.* 提前结束

鉴于后续部分都是基于原子类型的结构体,而这一部分由于本人对原子类型出现的问题较为陌生,排查起来比较耗时;而且我个人认为一个使用一个原子类型的结构体不是一个好主意,所以提前结束这一部分,后续有时间可能会回来加更。

【2022.04.06】
下一篇:待更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值