【问题标题】:Lock-free swap of two unique_ptr<T>两个 unique_ptr<T> 的无锁交换
【发布时间】:2013-03-17 12:43:55
【问题描述】:

不能保证交换两个unique_ptrs 是线程安全的。

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 中这样做的惯用方式是什么?

【问题讨论】:

  • 首先,您将如何处理T*
  • @JonathanWakely:老实说,我不知道。这就是我问这个问题的原因。我已经稍微放松了我的问题,T* 现在应该可以了。
  • 有一个建议在isocpp.org/blog/2014/06/n4058 添加 atomic (和 atomic )现在对您没有帮助,但评论可能有用。 (而且我认为通过与 atomic 一起工作的原始 T* 指针是目前你能做的最好的事情)。

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


【解决方案1】:

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

没有锁你不能为std::unique_ptr 做这件事。即使std::atomic&lt;int&gt; 也没有提供原子交换两个值的方法。您可以自动更新一个并取回其先前的值,但从概念上讲,交换是三个步骤,就std::atomic API 而言,它们是:

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

这是一个原子read,然后是一个原子read-modify-write,然后是一个原子write。每个步骤都可以原子地完成,但是如果没有锁,你不能原子地完成这三个步骤。

对于诸如std::unique_ptr&lt;T&gt; 之类的不可复制值,您甚至不能使用上面的loadstore 操作,但必须这样做:

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

这是三个 read-modify-write 操作。 (你不能真正使用 std::atomic&lt;std::unique_ptr&lt;T&gt;&gt; 来做到这一点,因为它需要一个可简单复制的参数类型,而 std::unique_ptr&lt;T&gt; 不是任何可复制的。)

要使用更少的操作来做到这一点,需要std::atomic 不支持的不同 API,因为它无法实现,因为正如 Stas 的回答所说,大多数处理器都不可能。 C++ 标准没有将功能标准化的习惯,这在所有当代架构上都是不可能的。 (反正不是故意的!)

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

atomic<T*> global;

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

这是一个单一的read-modify-write操作。

【讨论】:

  • 是的,你完全正确! CAS 在那里不需要。原子交换绰绰​​有余。看来我对原来的问题太着迷了,CAS 看起来已经很简单了)如果你不介意,我会把这个包含在我的答案中。
【解决方案2】:

两个指针的无锁交换

似乎没有针对此问题的通用无锁解决方案。为此,您需要能够将新值原子地写入两个非连续的内存位置。这称为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指针在temp
  2. 如果global 仍然等于temp(它没有被其他线程更改),则将local 保存到global。如果不是这样,请重试。

实际上,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 一起使用。

【讨论】:

  • 您将为std::atomic_exchange 操作设置什么类型的内存同步顺序?默认的memory_order_seq_cst 是否可以,或者可能是矫枉过正?
【解决方案3】:

这是一个有效的解决方案

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

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 最后的 sn-p。

希望这样的事情是安全的:

/*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 线程调用 ptr-&gt;update(...) 时,音频线程上没有任何东西可以让 t 保持活动状态

【讨论】:

  • 您的构造函数效率低下,并在未初始化的垃圾上使用delete。使用初始化列表将 arg 传递给 atomic&lt;T*&gt; 构造函数,例如 SmartAtomicPtr( T* newT ) : atomicTptr(newT) {}。您的析构函数也不需要执行原子 RMW,只需 get(),除非您希望多个线程可以安全地销毁对象...
  • 另外,您可能想要使用std::memory_order ord = memory_order_seq_cst 参数,以便您可以轻松更新或获取,但默认排序仍然是 seq-cst。
  • 为了我自己的理解(我对这个原子的东西很陌生),我在 Xcode 中挖掘了 std::atomic ,试图找到你所说的垃圾值来自哪里。 607: #define _Atomic(x) __gcc_atomic::__gcc_atomic_t&lt;x&gt;891:struct __atomic_base // false{mutable _Atomic(_Tp) __a_; // &lt;-- here?1103:template &lt;class _Tp&gt;struct atomic&lt;_Tp*&gt;struct atomic&lt;_Tp*&gt;: public __atomic_base&lt;_Tp*&gt;{typedef __atomic_base&lt;_Tp*&gt; __base;4@4341typedef __atomic_base&lt;_Tp*&gt; __base;987654341typedef __atomic_base&lt;_Tp*&gt; __base;987654341@987654341
  • 添加了推荐的解决方案@PeterCordes 感谢您的建议!!
  • 现在您已经为atomic&lt;T*&gt; 构造函数添加了{nullptr} 默认参数是安全的,因此您在构造它时只是在无用地删除nullptr,并执行了不必要的原子RMW。安全但缓慢。以前你让它默认构造,只有在静态存储中才会安全。在自动或动态存储中,atomicTptr 可能会在构造函数中的 update() 之前持有垃圾。
猜你喜欢
  • 1970-01-01
  • 2011-01-06
  • 1970-01-01
  • 1970-01-01
  • 2015-04-15
  • 2014-10-12
  • 2019-11-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多