web-dev-qa-db-ja.com

2つのunique_ptr <T>のロックフリースワップ

2つの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でこれを行う慣用的な方法は何ですか?

38
user1181282

2つのポインターのロックフリースワップ

この問題に対する一般的なロックフリーの解決策はないようです。これを行うには、新しい値を2つの非連続メモリ位置にアトミックに書き込む可能性が必要です。これは DCAS と呼ばれますが、Intelプロセッサでは使用できません。

ロックなしの所有権の譲渡

これは、新しい値を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. localglobalと等しい場合(他のスレッドによって変更されていない場合)、globaltempに保存します。これが当てはまらない場合は、再試行してください。

実際には、変更前の古い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では使用できません。

21
Stas

2つの変数をアトミックに変更する慣用的な方法は、ロックを使用することです。

ロックなしではstd::unique_ptrを実行できません。 std::atomic<int>でも、2つの値をアトミックに交換する方法は提供されていません。 1つをアトミックに更新して以前の値を取り戻すことができますが、スワップは、std::atomic AP​​Iに関して、概念的には3つのステップです。

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

これはアトミックreadの後にアトミックread-modify-writeが続き、その後にアトミックwriteが続きます。各ステップはアトミックに実行できますが、ロックなしでは3つすべてをアトミックに実行できません。

std::unique_ptr<T>などのコピー不可能な値の場合、上記のloadおよびstore操作を使用することもできませんが、次のようにする必要があります。

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

これは3つのread-modify-write操作です。 (これを行うためにstd::atomic<std::unique_ptr<T>>を実際に使用することはできません。それは、簡単にコピーできる引数の型が必要であり、std::unique_ptr<T>はいかなる種類のコピーもできないためです。)

少ない操作でそれを行うには、std::atomicでサポートされていない別のAPIが必要になります。これは、Stasの回答にあるように、ほとんどのプロセッサーでは実現できないため、実装できないためです。 C++標準は、現代のすべてのアーキテクチャでは不可能な機能を標準化する習慣はありません。 (とにかく意図的ではありません!)

編集:更新された質問は、非常に異なる問題について尋ねています。2番目の例では、2つのオブジェクトに影響を与えるアトミックスワップは必要ありません。スレッド間で共有されるのはglobalだけなので、localへの更新がアトミックであるかどうかは気にせず、globalをアトミックに更新して古い値を取得するだけで済みます。これを行う標準的なC++ 11の方法はstd:atomic<T*>を使用することであり、2番目の変数も必要ありません。

atomic<T*> global;

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

これは、単一のread-modify-write操作です。

21
Jonathan Wakely

これは有効な解決策ですか

独自のスマートポインタを記述する必要があります

_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スレッドがptr->update(...)を呼び出したときに、オーディオスレッドでtを維持するものはありません

0
MatkatMusic