`unique_ptr` 上的原子操作

Atomic operations on `unique_ptr`

std::shared_ptr has specializations for atomic operations 喜欢 atomic_compare_exchange_weak 和家人,但我找不到关于 std::unique_ptr 的等效专业化的文档。有没有?如果不是,为什么不呢?

没有 std::unique_ptr 的标准原子函数。

我确实在 Herb Sutter

Atomic Smart Pointers(N4058) 中找到了为什么不这样做的论据

Lawrence Crowl responded to add:

One of the reasons that shared_ptr locking is the way it is is to avoid a situation in which we weaken the precondition on the atomic template parameter that it be trivial, and hence have no risk of deadlock.

That said, we could weaken the requirement so that the argument type only needs to be lockfree, or perhaps only non-recursively locking.

However, while trivial makes for reasonably testable traits, I see no effective mechanism to test for the weaker property.

该提案已分配给并发子组,目前尚未处理。您可以在 JTC1/SC22/WG21 - Papers 2014 mailing2014-07

查看状态

注意,在线程之间共享可修改的 unique_ptr 很少有意义,即使指针本身是原子的。如果它的内容改变了,其他线程怎么知道呢?他们不能。

考虑这个例子:

unique_ptr<MyObject> p(new MyObject);

// Thread A
auto ptr = p.get();
if (ptr) {
    ptr->do_something();
}

// Thread B
p.reset();

线程A如何在调用p.get()后避免使用悬挂指针?

如果您想在线程之间共享一个对象,请使用 shared_ptr,它具有用于此目的的引用计数。


如果您真的想要它,您可以随时推出自己的 atomic_unique_ptr,大致如下(简化):

#pragma once
#include <atomic>
#include <memory>

template<class T>
class atomic_unique_ptr
{
  using pointer = T *;
  std::atomic<pointer> ptr;
public:
  constexpr atomic_unique_ptr() noexcept : ptr() {}
  explicit atomic_unique_ptr(pointer p) noexcept : ptr(p) {}
  atomic_unique_ptr(atomic_unique_ptr&& p) noexcept : ptr(p.release()) {}
  atomic_unique_ptr& operator=(atomic_unique_ptr&& p) noexcept { reset(p.release()); return *this; }
  atomic_unique_ptr(std::unique_ptr<T>&& p) noexcept : ptr(p.release()) {}
  atomic_unique_ptr& operator=(std::unique_ptr<T>&& p) noexcept { reset(p.release()); return *this; }

  void reset(pointer p = pointer()) { auto old = ptr.exchange(p); if (old) delete old; }
  operator pointer() const { return ptr; }
  pointer operator->() const { return ptr; }
  pointer get() const { return ptr; }
  explicit operator bool() const { return ptr != pointer(); }
  pointer release() { return ptr.exchange(pointer()); }
  ~atomic_unique_ptr() { reset(); }
};

template<class T>
class atomic_unique_ptr<T[]> // for array types
{
  using pointer = T *;
  std::atomic<pointer> ptr;
public:
  constexpr atomic_unique_ptr() noexcept : ptr() {}
  explicit atomic_unique_ptr(pointer p) noexcept : ptr(p) {}
  atomic_unique_ptr(atomic_unique_ptr&& p) noexcept : ptr(p.release()) {}
  atomic_unique_ptr& operator=(atomic_unique_ptr&& p) noexcept { reset(p.release()); return *this; }
  atomic_unique_ptr(std::unique_ptr<T>&& p) noexcept : ptr(p.release()) {}
  atomic_unique_ptr& operator=(std::unique_ptr<T>&& p) noexcept { reset(p.release()); return *this; }

  void reset(pointer p = pointer()) { auto old = ptr.exchange(p); if (old) delete[] old; }
  operator pointer() const { return ptr; }
  pointer operator->() const { return ptr; }
  pointer get() const { return ptr; }
  explicit operator bool() const { return ptr != pointer(); }
  pointer release() { return ptr.exchange(pointer()); }
  ~atomic_unique_ptr() { reset(); }
};

注意:在此 post 中提供的代码特此发布到 Public 域中。

他们的签名中暗示了可以提供 std::shared_ptr 的原子实例而不可能为 std::unique_ptr 这样做的原因。比较:

  • std::shared_ptr<T> 对比
  • std::unique_ptr<T, D> 其中 D 是删除器的类型。

std::shared_ptr 需要分配一个控制块来保存强计数和弱计数,因此删除器的类型擦除成本很小(一个稍微大一点的控制块)。

因此,std::shared_ptr<T> 的布局大致类似于:

template <typename T>
struct shared_ptr {
    T* _M_ptr;
    SomeCounterClass<T>* _M_counters;
};

并且可以原子地执行这两个指针的交换。


std::unique_ptr 采用零开销策略;与使用原始指针相比,使用 std::unique_ptr 不应产生任何开销。

因此,std::unique_ptr<T, D> 的布局大致类似于:

template <typename T, typename D = default_delete<T>>
struct unique_ptr {
    tuple<T*, D> _M_t;
};

其中 tuple 使用 EBO(空基优化)以便每当 D 大小为零时 sizeof(unique_ptr<T>) == sizeof(T*)

但是,在 D 不是零大小的情况下,实现归结为:

template <typename T, typename D = default_delete<T>>
struct unique_ptr {
    T* _M_ptr;
    D _M_del;
};

这个D是这里的关键;通常,不可能保证 D 可以在不依赖互斥锁的情况下以原子方式进行交换。

因此,无法为通用 std::unique_ptr<T, D>.

提供一套 std::atomic_compare_exchange* 专用例程

请注意,该标准甚至不能保证 sizeof(unique_ptr<T>) == sizeof(T*) AFAIK,尽管这是一种常见的优化。