std::shared_ptr 和 std::experimental::atomic_shared_ptr 有什么区别?
What is the difference between std::shared_ptr and std::experimental::atomic_shared_ptr?
我阅读了 Antony Williams 的 following 文章,据我所知,除了 std::shared_ptr
和 std::experimental::atomic_shared_ptr
中的原子共享计数指向共享对象的实际指针也是原子的?
但是当我读到 Antony 关于 C++ Concurrency 的书中描述的 lock_free_stack
的引用计数版本时,对我来说似乎同样适用于 std::shared_ptr
,因为像 std::atomic_load
、std::atomic_compare_exchnage_weak
应用于 std::shared_ptr
的实例。
template <class T>
class lock_free_stack
{
public:
void push(const T& data)
{
const std::shared_ptr<node> new_node = std::make_shared<node>(data);
new_node->next = std::atomic_load(&head_);
while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
}
std::shared_ptr<T> pop()
{
std::shared_ptr<node> old_head = std::atomic_load(&head_);
while(old_head &&
!std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
return old_head ? old_head->data : std::shared_ptr<T>();
}
private:
struct node
{
std::shared_ptr<T> data;
std::shared_ptr<node> next;
node(const T& data_) : data(std::make_shared<T>(data_)) {}
};
private:
std::shared_ptr<node> head_;
};
这两种类型的智能指针之间的确切区别是什么,如果 std::shared_ptr
实例中的指针不是原子的,为什么上面的无锁堆栈实现是可能的?
在 shared_ptr
上调用 std::atomic_load()
或 std::atomic_compare_exchange_weak()
在功能上等同于调用 atomic_shared_ptr::load()
或 atomic_shared_ptr::atomic_compare_exchange_weak()
。两者之间应该没有任何性能差异。在 atomic_shared_ptr
上调用 std::atomic_load()
或 std::atomic_compare_exchange_weak()
在语法上是多余的,可能会也可能不会导致性能下降。
shared_ptr
中的原子"thing"并不是共享指针本身,而是它指向的控制块。这意味着只要您不跨多个线程改变 shared_ptr
,就可以了。请注意 copying a shared_ptr
只会改变控制块,而不是 shared_ptr
本身。
std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block
}
改变共享指针本身,例如从多个线程为其分配不同的值,是一种数据竞争,例如:
std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
ptr = std::make_shared<int>(20);
});
在这里,我们改变了控制块(没关系),也改变了共享指针本身,使其指向来自多个线程的不同值。这不行。
这个问题的一个解决方案是用锁包裹shared_ptr
,但是这个解决方案在某些争用下不是那么可扩展,并且在某种意义上,失去了标准共享指针的自动感觉。
另一种解决方案是使用您引用的标准函数,例如std::atomic_compare_exchange_weak
。这使得同步共享指针的工作成为我们不喜欢的手动工作。
这就是原子共享指针发挥作用的地方。您可以改变来自多个线程的共享指针,而不必担心数据竞争,也无需使用任何锁。独立功能将是成员功能,它们的使用对用户来说将更加自然。这种指针对于无锁数据结构非常有用。
atomic_shared_ptr
is an API refinement. shared_ptr
already supports atomic operations, but only when using the appropriate atomic non-member functions。这是容易出错的,因为非原子操作仍然可用并且很容易被粗心的程序员意外调用。 atomic_shared_ptr
不易出错,因为它不会公开任何非原子操作。
shared_ptr
和 atomic_shared_ptr
公开了不同的 API,但它们不一定需要以不同的方式实现; shared_ptr
已经支持 atomic_shared_ptr
公开的所有操作。话虽如此,shared_ptr
的原子操作并没有达到应有的效率,因为它还必须支持非原子操作。因此,atomic_shared_ptr
可能会以不同的方式实现,这是出于性能方面的原因。这与单一职责原则有关。 "An entity with several disparate purposes... often offers crippled interfaces for any of its specific purposes because the partial overlap among various areas of functionality blurs the vision needed for crisply implementing each."(Sutter & Alexandrescu 2005,C++ 编码标准)
N4162(pdf),原子智能指针的提案,有很好的解释。以下是相关部分的引述:
Consistency. As far as I know, the [util.smartptr.shared.atomic]
functions are the only atomic operations in the standard that
are not available via an atomic
type. And for all types
besides shared_ptr
, we teach programmers to use atomic types
in C++, not atomic_*
C-style functions. And that’s in part because of...
Correctness. Using the free functions makes code error-prone
and racy by default. It is far superior to write atomic
once on
the variable declaration itself and know all accesses
will be atomic, instead of having to remember to use the atomic_*
operation on every use of the object, even apparently-plain reads.
The latter style is error-prone; for example, “doing it wrong” means
simply writing whitespace (e.g., head
instead of atomic_load(&head)
),
so that in this style every use of the variable is “wrong by default.” If you forget to
write the atomic_*
call in even one place, your code will still
successfully compile without any errors or warnings, it will “appear
to work” including likely pass most testing, but will still contain a
silent race with undefined behavior that usually surfaces as intermittent
hard-to-reproduce failures, often/usually in the field,
and I expect also in some cases exploitable vulnerabilities.
These classes of errors are eliminated by simply declaring the variable atomic
,
because then it’s safe by default and to write the same set of
bugs requires explicit non-whitespace code (sometimes explicit
memory_order_*
arguments, and usually reinterpret_cast
ing).
Performance. atomic_shared_ptr<>
as a distinct type
has an important efficiency advantage over the
functions in [util.smartptr.shared.atomic] — it can simply store an
additional atomic_flag
(or similar) for the internal spinlock
as usual for atomic<bigstruct>
. In contrast, the existing standalone functions
are required to be usable on any arbitrary shared_ptr
object, even though the vast majority of shared_ptr
s will
never be used atomically. This makes the free functions inherently
less efficient; for example, the implementation could require
every shared_ptr
to carry the overhead of an internal spinlock
variable (better concurrency, but significant overhead per
shared_ptr
), or else the library must maintain a lookaside data
structure to store the extra information for shared_ptr
s that are
actually used atomically, or (worst and apparently common in
practice) the library must use a global spinlock.
我阅读了 Antony Williams 的 following 文章,据我所知,除了 std::shared_ptr
和 std::experimental::atomic_shared_ptr
中的原子共享计数指向共享对象的实际指针也是原子的?
但是当我读到 Antony 关于 C++ Concurrency 的书中描述的 lock_free_stack
的引用计数版本时,对我来说似乎同样适用于 std::shared_ptr
,因为像 std::atomic_load
、std::atomic_compare_exchnage_weak
应用于 std::shared_ptr
的实例。
template <class T>
class lock_free_stack
{
public:
void push(const T& data)
{
const std::shared_ptr<node> new_node = std::make_shared<node>(data);
new_node->next = std::atomic_load(&head_);
while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
}
std::shared_ptr<T> pop()
{
std::shared_ptr<node> old_head = std::atomic_load(&head_);
while(old_head &&
!std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
return old_head ? old_head->data : std::shared_ptr<T>();
}
private:
struct node
{
std::shared_ptr<T> data;
std::shared_ptr<node> next;
node(const T& data_) : data(std::make_shared<T>(data_)) {}
};
private:
std::shared_ptr<node> head_;
};
这两种类型的智能指针之间的确切区别是什么,如果 std::shared_ptr
实例中的指针不是原子的,为什么上面的无锁堆栈实现是可能的?
在 shared_ptr
上调用 std::atomic_load()
或 std::atomic_compare_exchange_weak()
在功能上等同于调用 atomic_shared_ptr::load()
或 atomic_shared_ptr::atomic_compare_exchange_weak()
。两者之间应该没有任何性能差异。在 atomic_shared_ptr
上调用 std::atomic_load()
或 std::atomic_compare_exchange_weak()
在语法上是多余的,可能会也可能不会导致性能下降。
shared_ptr
中的原子"thing"并不是共享指针本身,而是它指向的控制块。这意味着只要您不跨多个线程改变 shared_ptr
,就可以了。请注意 copying a shared_ptr
只会改变控制块,而不是 shared_ptr
本身。
std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block
}
改变共享指针本身,例如从多个线程为其分配不同的值,是一种数据竞争,例如:
std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
ptr = std::make_shared<int>(20);
});
在这里,我们改变了控制块(没关系),也改变了共享指针本身,使其指向来自多个线程的不同值。这不行。
这个问题的一个解决方案是用锁包裹shared_ptr
,但是这个解决方案在某些争用下不是那么可扩展,并且在某种意义上,失去了标准共享指针的自动感觉。
另一种解决方案是使用您引用的标准函数,例如std::atomic_compare_exchange_weak
。这使得同步共享指针的工作成为我们不喜欢的手动工作。
这就是原子共享指针发挥作用的地方。您可以改变来自多个线程的共享指针,而不必担心数据竞争,也无需使用任何锁。独立功能将是成员功能,它们的使用对用户来说将更加自然。这种指针对于无锁数据结构非常有用。
atomic_shared_ptr
is an API refinement. shared_ptr
already supports atomic operations, but only when using the appropriate atomic non-member functions。这是容易出错的,因为非原子操作仍然可用并且很容易被粗心的程序员意外调用。 atomic_shared_ptr
不易出错,因为它不会公开任何非原子操作。
shared_ptr
和 atomic_shared_ptr
公开了不同的 API,但它们不一定需要以不同的方式实现; shared_ptr
已经支持 atomic_shared_ptr
公开的所有操作。话虽如此,shared_ptr
的原子操作并没有达到应有的效率,因为它还必须支持非原子操作。因此,atomic_shared_ptr
可能会以不同的方式实现,这是出于性能方面的原因。这与单一职责原则有关。 "An entity with several disparate purposes... often offers crippled interfaces for any of its specific purposes because the partial overlap among various areas of functionality blurs the vision needed for crisply implementing each."(Sutter & Alexandrescu 2005,C++ 编码标准)
N4162(pdf),原子智能指针的提案,有很好的解释。以下是相关部分的引述:
Consistency. As far as I know, the [util.smartptr.shared.atomic] functions are the only atomic operations in the standard that are not available via an
atomic
type. And for all types besidesshared_ptr
, we teach programmers to use atomic types in C++, notatomic_*
C-style functions. And that’s in part because of...Correctness. Using the free functions makes code error-prone and racy by default. It is far superior to write
atomic
once on the variable declaration itself and know all accesses will be atomic, instead of having to remember to use theatomic_*
operation on every use of the object, even apparently-plain reads. The latter style is error-prone; for example, “doing it wrong” means simply writing whitespace (e.g.,head
instead ofatomic_load(&head)
), so that in this style every use of the variable is “wrong by default.” If you forget to write theatomic_*
call in even one place, your code will still successfully compile without any errors or warnings, it will “appear to work” including likely pass most testing, but will still contain a silent race with undefined behavior that usually surfaces as intermittent hard-to-reproduce failures, often/usually in the field, and I expect also in some cases exploitable vulnerabilities. These classes of errors are eliminated by simply declaring the variableatomic
, because then it’s safe by default and to write the same set of bugs requires explicit non-whitespace code (sometimes explicitmemory_order_*
arguments, and usuallyreinterpret_cast
ing).Performance.
atomic_shared_ptr<>
as a distinct type has an important efficiency advantage over the functions in [util.smartptr.shared.atomic] — it can simply store an additionalatomic_flag
(or similar) for the internal spinlock as usual foratomic<bigstruct>
. In contrast, the existing standalone functions are required to be usable on any arbitraryshared_ptr
object, even though the vast majority ofshared_ptr
s will never be used atomically. This makes the free functions inherently less efficient; for example, the implementation could require everyshared_ptr
to carry the overhead of an internal spinlock variable (better concurrency, but significant overhead pershared_ptr
), or else the library must maintain a lookaside data structure to store the extra information forshared_ptr
s that are actually used atomically, or (worst and apparently common in practice) the library must use a global spinlock.