Antony Williamsによる following の記事を読み、std::shared_ptr
のアトミック共有カウントに加えてstd::experimental::atomic_shared_ptr
共有オブジェクトへの実際のポインタもアトミックですか?
しかし、 C++ Concurrency に関するAntonyの本に記載されている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_;
};
この2種類のスマートポインターの正確な違いは何ですか。また、std::shared_ptr
インスタンスのポインターがアトミックでない場合、上記のロックフリースタックの実装が可能なのはなぜですか。
shared_ptr
のアトミックな「もの」は、共有ポインタ自体ではなく、それが指す制御ブロックです。つまり、複数のスレッドで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
など、引用した標準関数を使用することです。これにより、共有ポインタを同期する作業が手動ではなくなりますが、これは望ましくありません。
これが、アトミックな共有ポインターの出番です。データの競合を心配することなく、ロックを使用せずに、共有ポインタを複数のスレッドから変更できます。スタンドアロン関数はメンバー関数になり、ユーザーにとってそれらの使用ははるかに自然になります。この種のポインタは、ロックのないデータ構造に非常に役立ちます。
N4162(pdf)、アトミックスマートポインターの提案は、適切な説明があります。関連部分の引用は次のとおりです。
整合性。私の知る限りでは、[util.smartptr.shared.atomic]関数は、
atomic
タイプを介して利用できない、標準で唯一のアトミック操作です。また、shared_ptr
以外のすべての型について、atomic_*
Cスタイルの関数ではなく、C++でアトミック型を使用するようにプログラマーに指示します。その理由の一部は...正しさ。無料の関数を使用すると、デフォルトでコードがエラーが発生しやすくなります。変数宣言自体に
atomic
を1回書き込み、すべてのアクセスがアトミックになることを知っておくと、オブジェクトの__everyの使用でatomic_*
演算を使用することを覚える必要がなく、はるかに優れています。明白に明白な読みでさえ。後者のスタイルはエラーが発生しやすいです。たとえば、「間違っている」とは、単に空白文字(たとえば、atomic_load(&head)
の代わりにhead
)を書き込むことを意味するため、このスタイルでは、変数のすべての使用が「デフォルトで間違っています」。atomic_*
の呼び出しを1か所に書き忘れても、コードはエラーや警告なしで正常にコンパイルされ、ほとんどのテストに合格することを含め、「機能しているように見えます」が、通常は未定義の動作を伴うサイレントレースが含まれます。多くの場合/通常は現場で、再現が困難な断続的な障害として表面化し、場合によっては悪用可能な脆弱性も予想されます。これらのエラーのクラスは、変数atomic
を宣言するだけで排除されます。デフォルトで安全であり、同じバグのセットを書き込むには、明示的な空白以外のコード(明示的なmemory_order_*
引数、通常はreinterpret_cast
ing)が必要なためです。パフォーマンス。特殊タイプとしての
atomic_shared_ptr<>
は、[util.smartptr.shared.atomic]の関数よりも効率的に重要な利点があります。これは、atomic_flag
の場合と同様に、内部スピンロック用の追加のatomic<bigstruct>
(または同様の)を格納するだけです。対照的に、shared_ptr
sの大部分はアトミックに使用されない場合でも、既存のスタンドアロン関数は任意のshared_ptr
オブジェクトで使用できる必要があります。これにより、フリー関数は本質的に効率が低下します。たとえば、実装では、すべてのshared_ptr
が内部スピンロック変数のオーバーヘッドを運ぶ必要があります(同時実行性は向上しますが、shared_ptr
ごとに大幅なオーバーヘッドが発生します)。そうでない場合、ライブラリは、実際に使用されるshared_ptr
sの追加情報を格納するためのルックアサイドデータ構造を維持する必要があります。アトミックに、または(最悪かつ実際には明らかに一般的)ライブラリはグローバルスピンロックを使用する必要があります。
_shared_ptr
_でstd::atomic_load()
またはstd::atomic_compare_exchange_weak()
を呼び出すことは、atomic_shared_ptr::load()
またはatomic_shared_ptr::atomic_compare_exchange_weak()
を呼び出すことと機能的に同等です。 2つの間にパフォーマンスの違いがあってはなりません。 _atomic_shared_ptr
_でstd::atomic_load()
またはstd::atomic_compare_exchange_weak()
を呼び出すと、構文的に冗長になり、パフォーマンスが低下する場合と低下する場合があります。
atomic_shared_ptr
はAPIの改良版です。 shared_ptr
はすでにアトミック操作をサポートしていますが、適切な アトミック非メンバー関数 を使用する場合のみです。非アトミックな操作は引き続き利用可能であり、不注意なプログラマが偶然に呼び出すのは簡単すぎるため、これはエラーが発生しやすくなります。 atomic_shared_ptr
は、非アトミック操作を公開しないため、エラーが発生しにくくなります。
shared_ptr
とatomic_shared_ptr
は異なるAPIを公開していますが、必ずしも別々に実装する必要はありません。 shared_ptr
は、atomic_shared_ptr
によって公開されるすべての操作をすでにサポートしています。そうは言っても、shared_ptr
のアトミック操作は非アトミック操作もサポートする必要があるため、可能な限り効率的ではありません。したがって、atomic_shared_ptr
を異なる方法で実装できるパフォーマンス上の理由があります。これは単一責任の原則に関連しています。 「複数の異なる目的を持つエンティティは、機能のさまざまな領域間の部分的なオーバーラップにより、それぞれを明確に実装するために必要なビジョンが不鮮明になるため、特定の目的のために機能不全のインターフェースを提供することがよくあります。」 (Sutter&Alexandrescu 2005、C++コーディング標準)