mardi 2 août 2016

A free-locking-stack. Misunderstanding

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;
    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,
                                            std::memory_order_acquire,
                                            std::memory_order_relaxed));
        old_counter.external_count=new_counter.external_count;
    }
public:
    ~lock_free_stack()
    {
        while(pop());
    }
    void push(T const& data)
    {
        counted_node_ptr new_node;
        new_node.ptr=new node(data);
        new_node.external_count=1;
        new_node.ptr->next=head.load(std::memory_order_relaxed)
        while(!head.compare_exchange_weak(new_node.ptr->next,new_node,
                                          std::memory_order_release,
                                          std::memory_order_relaxed));
    }
    std::shared_ptr<T> pop()
    {
        counted_node_ptr old_head= 
            head.load(std::memory_order_relaxed); // $1
        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::memory_order_relaxed)) // $2
            {
                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,
                       std::memory_order_release)==-count_increase)
                {
                    delete ptr;
                }
                return res;
            }
            else if(ptr->internal_count.fetch_add(-1,
                        std::memory_order_relaxed)==1)
            {
                ptr->internal_count.load(std::memory_order_acquire);
                delete ptr;
            }
        }
    }
};

Above piece of code is taken from Chapter 7, C++ Concurrency in Action. But, I focus on part of that, I mean:

    if(head.compare_exchange_strong(old_head,ptr->next,
                                    std::memory_order_relaxed))
    {
        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,
               std::memory_order_release)==-count_increase)
        {
            delete ptr;
        }
        return res;
    }
}

}

If the if will be evaluated to the true, it means that the head of the stack has not been changed by another thread ( in relate to load in $1), so current thread claimed a head ( node ) with a success. It can be evaluated to the true only if compare_exchange_strong returns true. So old_head must be equal to head. But, what about the following situation?

**Thread#1**

I've `load` the `head` in line `$1` and stored it in `old_head` ( see the comments) 
And I stopped after that fact ( I don't know why, just do it ;) ).
My `old_head` is following:
    old_head.external_count = 1;
    old_head.ptr = 0xfff;    


**Thread#2**

I've popped the head ( the same as Thread#1 ) and I deleted it.



**Thread#3**

I've pushed new head:
    new_head.external_count = 1;
    new_head.ptr = 0xfff;  

Note, that new_head.ptr is equal to old_head.ptr so in fact new_head and old_head are equal (*). Note, that it is possible that malloc ( implemented in stdlib) returns the same address- Thread#2 deleted old_head and in a result this area of memory was returend to allocator and from what I know the allocator keeps a list of free chunks of memory ( it depends on allocator). Even if the standard allocator doesn't allow this situation I'm sure that there are popular allocators that allow. Therefore, theoretically it is possible. I know that it is nearly impossible, but...

Thread#1 
I continue and I executed a `$2` line. Condition was evaluated to the true because of the (*). So, I assume that head is the same but actually it is something else. 

What about my reasoning? Do I misunderstand something. If yes, why? If no, does it mean that this piece of code isn't correct from general point of view?

Aucun commentaire:

Enregistrer un commentaire