Lock-free swap of two unique_ptr<T>

為{幸葍}努か 提交于 2019-12-02 17:26:55
Stas

Lock-free swapping of two pointers

It seems there is no general lock-free solution for this problem. To do this, you need a possibility to atomically write new values into two non-continous memory locations. This is called DCAS, but it is not available in Intel processors.

Lock-free transfer of ownership

This one is possible, as it is only needed to atomically save new value into global and receive its old value. My first idea was to use CAS operation. Take a look at the following code to get an idea:

std::atomic<T*> global;

void f() {
   T* local = new T;
   T* temp = nullptr;
   do {
       temp = global;                                                   // 1
   } while(!std::atomic_compare_exchange_weak(&global, &temp, local));  // 2

   delete temp;
}

Steps

  1. Remember current global pointer in temp
  2. Save local to global if global is still equal to temp (it wasn't changed by other thread). Try again if this is not true.

Actually, CAS is overkill there, as we do not do anything special with old global value before it is changed. So, we just can use atomic exchange operation:

std::atomic<T*> global;

void f() {
   T* local = new T;
   T* temp = std::atomic_exchange(&global, local);
   delete temp;
}

See Jonathan's answer for even more short and elegant solution.

Anyway, you will have to write your own smart pointer. You can't use this trick with standard unique_ptr.

The idiomatic way to modify two variables atomically is to use a lock.

You can't do it for std::unique_ptr without a lock. Even std::atomic<int> doesn't provide a way to swap two values atomically. You can update one atomically and get its previous value back, but a swap is conceptually three steps, in terms of the std::atomic API they are:

auto tmp = a.load();
tmp = b.exchange(tmp);
a.store(tmp);

This is an atomic read followed by an atomic read-modify-write followed by an atomic write. Each step can be done atomically, but you can't do all three atomically without a lock.

For a non-copyable value such as std::unique_ptr<T> you can't even use the load and store operations above, but must do:

auto tmp = a.exchange(nullptr);
tmp = b.exchange(tmp);
a.exchange(tmp);

This is three read-modify-write operations. (You can't really use std::atomic<std::unique_ptr<T>> to do that, because it requires a trivially-copyable argument type, and std::unique_ptr<T> isn't any kind of copyable.)

To do it with fewer operations would need a different API that isn't supported by std::atomic because it can't be implemented because as Stas's answer says, it isn't possible with most processors. The C++ standard is not in the habit of standardising functionality that is impossible on all contemporary architectures. (Not intentionally anyway!)

Edit: Your updated question asks about a very different problem, in the second example you don't need an atomic swap that affects two objects. Only global is shared between threads, so you don't care if updates to local are atomic, you just need to atomically update global and retrieve the old value. The canonical C++11 way to do that is with std:atomic<T*> and you don't even need a second variable:

atomic<T*> global;

void f() {
   delete global.exchange(new T(...));
}

This is a single read-modify-write operation.

Is this a valid solution to the

You'll have to write your own smart pointer

template<typename T>
struct SmartAtomicPtr
{
    SmartAtomicPtr( T* newT )
    {
        update( newT );
    }
    ~SmartAtomicPtr()
    {
        update(nullptr);
    }
    void update( T* newT, std::memory_order ord = memory_order_seq_cst ) 
    {
        delete atomicTptr.exchange( newT, ord );
    }
    std::shared_ptr<T> get(std::memory_order ord = memory_order_seq_cst) 
    { 
        keepAlive.reset( atomicTptr.load(ord) );
        return keepAlive;
    }
private:
    std::atomic<T*> atomicTptr{nullptr};
    std::shared_ptr<T> keepAlive;
};

it's based on @Jonathan Wakely's snippet at the end.

the hope is that things like this would be safe:

/*audio thread*/ auto t = ptr->get() ); 
/*GUI thread*/ ptr->update( new T() );
/*audio thread*/ t->doSomething(); 

the issue is that you could do something like this:

/*audio thread*/ auto* t = ptr->get(); 
/*GUI thread*/ ptr->update( new T() );
/*audio thread*/ t->doSomething(); 

and there's nothing to keep t alive on the audio thread when the GUI thread calls ptr->update(...)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!