两个unique_ptr <t> </t>的无锁交换

时间:2013-03-17 12:43:56

标签: c++ c++11 unique-ptr lock-free atomic-swap

交换两个unique_ptr并不保证是线程安全的。

std::unique_ptr<T> a, b;
std::swap(a, b); // not threadsafe

由于我需要原子指针交换,因为我喜欢unique_ptr的所有权处理,是否有一种简单的方法将它们组合在一起?


编辑:如果无法做到这一点,我愿意接受替代方案。我至少想做这样的事情:

threadshared_unique_ptr<T> global;

void f() {
   threadlocal_unique_ptr<T> local(new T(...));
   local.swap_content(global); // atomically for global
}

在C ++ 11中这样做的惯用方法是什么?

3 个答案:

答案 0 :(得分:19)

无锁交换两个指针

似乎没有针对此问题的通用无锁解决方案。为此,您需要有可能将新值原子地写入两个非连续的内存位置。这称为DCAS,但在英特尔处理器中不可用。

无锁转让所有权

这是可能的,因为只需要将新值原子地保存到global并接收其旧值。我的第一个想法是使用CAS操作。看看下面的代码来了解一下:

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

步骤

  1. 记住global
  2. 中的当前temp指针
  3. 如果local仍然等于global(其他线程未更改),则global保存到temp。如果不是这样,请再试一次。
  4. 实际上,CAS在那里是过度的,因为在更改之前我们没有对旧global值做任何特殊操作。所以,我们只能使用原子交换操作:

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

    请参阅Jonathan的answer以获得更简短,更优雅的解决方案。

    无论如何,你必须编写自己的智能指针。您不能将此技巧用于标准unique_ptr

答案 1 :(得分:19)

以原子方式修改两个变量的惯用方法是使用锁。

没有锁定,您无法为std::unique_ptr执行此操作。即使std::atomic<int>也没有提供以原子方式交换两个值的方法。您可以原子方式更新一个并返回其先前的值,但是交换在概念上是三个步骤,就std::atomic API而言:

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

这是一个原子读取,后跟原子读取 - 修改 - 写入,后跟原子读取。每个步骤都可以原子方式完成,但如果没有锁定,你就无法完成所有三个步骤。

对于std::unique_ptr<T>等不可复制的值,您甚至无法使用上述loadstore操作,但必须这样做:

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

这是三个读 - 修改 - 写操作。 (你不能真正使用std::atomic<std::unique_ptr<T>>来做这件事,因为它需要一个简单的可复制参数类型,std::unique_ptr<T>不是任何可复制的。)

要用较少的操作来完成它需要std::atomic不支持的不同API,因为它无法实现,因为正如Stas的回答所说,大多数处理器都无法实现。 C ++标准不习惯标准化所有现代架构都不可能实现的功能。 (无论如何都不是故意的!)

编辑:您更新的问题询问了一个非常不同的问题,在第二个示例中,您不需要影响两个对象的原子交换。线程之间只共享global,因此您不关心local的更新是否是原子的,您只需要以原子方式更新global并检索旧值。规范的C ++ 11方法是使用std:atomic<T*>,你甚至不需要第二个变量:

atomic<T*> global;

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

这是一个读 - 修改 - 写操作。

答案 2 :(得分:0)

这是

的有效解决方案
  

您必须编写自己的智能指针

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

它是基于最后的@Jonathan Wakely的片段。

希望这样的事情会很安全:

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

问题在于您可以执行以下操作:

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

当GUI线程调用t

时,音频线程上没有任何东西ptr->update(...)保持活动状态