スレッドセーフルールによって提案された非const引数を使用してコンストラクターをコピーしますか?


9

レガシーコードの一部のラッパーがあります。

class A{
   L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
   A(A const&) = delete;
   L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
   ... // proper resource management here
};

このレガシーコードでは、オブジェクトを「複製」する関数はスレッドセーフではない(同じ最初の引数を呼び出す場合)ためconst、ラッパーでマークされていません。私は現代のルールに従っていると思います:https : //herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

これduplicateは、そうではない詳細を除いて、コピーコンストラクタを実装する良い方法のように見えconstます。したがって、これを直接行うことはできません。

class A{
   L* impl_; // the legacy object has to be in the heap
   A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

それでは、この逆説的な状況から抜け出す方法は何でしょうか?

legacy_duplicateスレッドセーフではないが、オブジェクトが終了したときにオブジェクトを元の状態のままにしておくこともできます。C関数であるため、動作は文書化されているだけで、一貫性の概念はありません。)

私は多くの可能なシナリオを考えることができます:

(1) 1つの可能性は、通常のセマンティクスでコピーコンストラクターを実装する方法がないことです。(はい、オブジェクトを移動できますが、それは必要ありません。)

(2)一方、オブジェクトをコピーすることは、単純なタイプをコピーするとソースが半分変更された状態で見つかる可能性があるという意味で、本質的にスレッドセーフではありません。

class A{
   L* impl_;
   A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

(3)または、duplicateconstを宣言し、すべてのコンテキストでスレッドの安全性について嘘をつきます。(結局、レガシー関数は気にしないconstので、コンパイラーは文句を言うことさえありません。)

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate()}{}
   L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

(4)最後に、ロジックに従って、const以外の引数を取るコピーコンストラクタを作成できます。

class A{
   L* impl_;
   A(A const&) = delete;
   A(A& other) : L{other.duplicate()}{}
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

これらのオブジェクトは通常はそうではないため、これは多くのコンテキストで機能することがわかりますconst

問題は、これが有効なルートか、それとも一般的なルートか?

それらに名前を付けることはできませんが、非constコピーコンストラクターを使用することで、直感的に多くの問題が発生することが予想されます。おそらく、この微妙さのために、それは値タイプとして適格ではありません。

(5)最後に、これはやり過ぎで、実行時のコストが非常に高くなる可能性がありますが、ミューテックスを追加できます。

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate_locked()}{}
   L* duplicate(){
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   L* duplicate_locked() const{
      std::lock_guard<std::mutex> lk(mut);
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   mutable std::mutex mut;
};

しかし、これを強制されることは悲観化のように見え、クラスを大きくします。私はわかりません。私は現在(4)(5)、または両方の組み合わせに傾いています。

- 編集

別のオプション:

(6)重複するメンバー関数の意味がないことをすべて忘れてlegacy_duplicate、コンストラクターから呼び出すだけで、コピーコンストラクターはスレッドセーフではないことを宣言します。(必要に応じて、そのタイプの別のスレッドセーフバージョンを作成しますA_mt

class A{
   L* impl_;
   A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};

編集2

これは、レガシー機能が行うことの良いモデルになる可能性があります。入力に触れることによる呼び出しは、最初の引数で表される値に関してスレッドセーフではないことに注意してください。

void legacy_duplicate(L* in, L** out){
   *out = new L{};
   char tmp = in[0];
   in[0] = tmp; 
   std::memcpy(*out, in, sizeof *in); return; 
}

1
このレガシーコードでは、オブジェクトを複製する関数はスレッドセーフではありません(同じ最初の引数を呼び出す場合)。」それでよろしいですか?L新しいLインスタンスを作成することによって変更される状態に含まれていない状態はありますか?そうでない場合、なぜこの操作はスレッドセーフではないと思いますか?
Nicol Bolas

はい、それは状況です。最初の引数の内部状態が実行中に変更されたようです。何らかの理由(「最適化」または設計の誤り、または単に仕様による)のためlegacy_duplicate、2つの異なるスレッドから同じ最初の引数を使用して関数を呼び出すことはできません。
alfC

@TedLyngmoわかりました。技術的にはc ++ pre 11ですが、スレッドの存在下ではconstはより曖昧な意味を持ちます。
alfC

@TedLyngmoはい、それはかなり良いビデオです。ビデオが適切なメンバーのみを扱っており、構造の問題に触れていないのは残念です(また、一貫性は「その他」のオブジェクトにあります)。観点では、抽象化の別の層(および具体的なミューテックス)を追加せずに、このラッパースレッドをコピー時に安全にする固有の方法はない可能性があります。
alfC

ええ、まあ、それは私を混乱させました、そして私はおそらくconst本当の意味がわからない人々の一人でしょう。:-)私がconst&変更しない限り、私はコピーActor を取得することについて2度考えませんother。私は常にスレッドセーフティを、カプセル化によって複数のスレッドからアクセスする必要があるものに追加するものと考えており、答えを本当に楽しみにしています。
Ted Lyngmo

回答:


0

私はあなたのオプション(4)と(5)の両方を含めますが、パフォーマンスに必要だと思われる場合は、スレッド安全でない動作を明示的にオプトインします。

以下は完全な例です。

#include <cstdlib>
#include <thread>

struct L {
  int val;
};

void legacy_duplicate(const L* in, L** out) {
  *out = new L{};
  std::memcpy(*out, in, sizeof *in);
  return;
}

class A {
 public:
  A(L* l) : impl_{l} {}
  A(A const& other) : impl_{other.duplicate_locked()} {}

  A copy_unsafe_for_multithreading() { return {duplicate()}; }

  L* impl_;

  L* duplicate() {
    printf("in duplicate\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  L* duplicate_locked() const {
    std::lock_guard<std::mutex> lk(mut);
    printf("in duplicate_locked\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  mutable std::mutex mut;
};

int main() {
  A a(new L{1});
  const A b(new L{2});

  A c = a;
  A d = b;

  A e = a.copy_unsafe_for_multithreading();
  A f = const_cast<A&>(b).copy_unsafe_for_multithreading();

  printf("\npointers:\na=%p\nb=%p\nc=%p\nc=%p\nd=%p\nf=%p\n\n", a.impl_,
     b.impl_, c.impl_, d.impl_, e.impl_, f.impl_);

  printf("vals:\na=%d\nb=%d\nc=%d\nc=%d\nd=%d\nf=%d\n", a.impl_->val,
     b.impl_->val, c.impl_->val, d.impl_->val, e.impl_->val, f.impl_->val);
}

出力:

in duplicate_locked
in duplicate_locked
in duplicate
in duplicate

pointers:
a=0x7f85e8c01840
b=0x7f85e8c01850
c=0x7f85e8c01860
c=0x7f85e8c01870
d=0x7f85e8c01880
f=0x7f85e8c01890

vals:
a=1
b=2
c=1
c=2
d=1
f=2

これは、スレッドの安全性を伝えるGoogleスタイルガイドに従いconstますが、APIを呼び出すコードは、const_cast


答えてくれてありがとう、それはあなたの答えを変えるものではないと思います、そして私は確信が持てませんが、より良いモデル(つまり、非const )legacy_duplicateがあり得ますvoid legacy_duplicate(L* in, L** out) { *out = new L{}; char tmp = in[0]; /*some weird call here*/; in[0] = tmp; std::memcpy(*out, in, sizeof *in); return; }in
alfC

オプション(4)およびオプション(2)の明示的なバージョンと組み合わせることができるので、あなたの答えは非常に興味深いものです。つまり、A a2(a1)スレッドセーフになる(または削除される)可能性があり、スレッドセーフになることA a2(const_cast<A&>(a1))はまったくありません。
21:22

2
はい。Aスレッドセーフとスレッドアンセーフの両方のコンテキストで使用する場合はconst_cast、呼び出し元のコードにをプルして、スレッドセーフティに違反していることがわかっている箇所を明確にする必要があります。安全性をAPI(ミューテックス)の背後に押し込むことは問題ありませんが、安全性(const_cast)を非表示にすることは問題です。
Michael Graczyk

0

TLDR:複製機能の実装を修正するか、ミューテックス(または、より適切なロックデバイス、おそらくスピンロック)導入するか、今度は、何か重いことを行う前にミューテックスがスピンするように構成されていることを確認してから、複製の実装を修正しますロックが実際に問題になった場合は、ロックを解除してください。

注目すべき重要な点は、以前は存在しなかった機能を追加していることだと思います。複数のスレッドから同時にオブジェクトを複製する機能です。

明らかに、あなたが説明した条件下では、それはバグでした-何らかの外部同期を使用せずに以前にそれを行っていた場合の競合状態。

したがって、この新しい機能の使用は、既存の機能として継承するのではなく、コードに追加するものになります。この新しい機能を使用する頻度に応じて、追加のロックを追加すると実際にコストがかかるかどうかを知っている必要があります。

また、オブジェクトの知覚された複雑さに基づいて-あなたがそれを与えている特別な扱いによって、複製手順は簡単なものではないので、パフォーマンスの面ですでにかなり高価であると仮定します。

上記に基づいて、次の2つの方法があります。

A)複数のスレッドからこのオブジェクトをコピーすると、追加のロックのオーバーヘッドが高額になるほど頻繁には発生しないことを知っています。少なくとも、既存の複製手順を使用すると、それだけで十分高額であることを考えると、おそらく簡単です。スピンロック/プレスピニングミューテックス。競合はありません。

B)複数のスレッドからのコピーが頻繁に発生し、余分なロックが問題になると思われる。次に、本当に1つのオプションしかありません-複製コードを修正します。修正しない場合は、この抽象化レイヤーでもどこか他の場所でもロックが必要になりますが、バグが必要ない場合はロックが必要です。また、私たちが確立したように、このパスでは、そのロックはコストがかかりすぎるため、唯一の選択肢は複製コードを修正することです。

私はあなたが本当に状況Aにいると思います。そして、競合しないときにパフォーマンスペナルティがほとんどないスピンロック/スピンミューテックスを追加するだけで問題なく動作します(ただし、ベンチマークを行うことを忘れないでください)。

理論的には、別の状況があります。

C)見かけ上の複製機能の複雑さとは対照的に、それは実際には取るに足らないことですが、何らかの理由で修正することはできません。それは非常に些細なことであり、争われていないスピンロックでさえ、複製に許容できないパフォーマンス低下をもたらします。並列スレッドでの複製はめったに使用されません。シングルスレッドでの複製は常に使用されるため、パフォーマンスの低下は絶対に許容できません。

この場合は、次のことをお勧めします。デフォルトのコピーコンストラクター/演算子の削除を宣言して、誰かが誤って使用しないようにします。明示的に呼び出し可能な2つの複製メソッド、スレッドセーフなものとスレッドセーフでないものを作成します。コンテキストに応じて、ユーザーに明示的に呼び出させる。繰り返しますが、実際にこの状況にあり、既存の複製の実装を修正できない場合、許容可能なシングルスレッドのパフォーマンスと安全なマルチスレッドを実現する他の方法はありません。しかし、あなたが本当にそうである可能性は非常に低いと思います。

そのミューテックス/スピンロックとベンチマークを追加するだけです。


C ++でのスピンロック/プレスピニングミューテックスに関する資料を教えてもらえますか?それが提供するものよりも複雑なものstd::mutexですか?複製機能は秘密ではありません。問題を高レベルに保ち、MPIに関する回答を受け取らないように、私はそれを言及しませんでした。しかし、あなたはそれを深く行ったので、私はあなたにもっと詳細を与えることができます。レガシー関数はでMPI_Comm_dupあり、有効な非スレッドセーフ性はgithub.com/pmodels/mpich/issues/3234に記載されています(私はそれを確認しました)。これが、重複を修正できない理由です。(また、ミューテックスを追加すると、すべてのMPI呼び出しをスレッドセーフにしたくなるでしょう。)
alfC

悲しいことに、私はstd :: mutexをあまり知りませんが、プロセスをスリープさせる前に、いくらか回転していると思います。これを手動で制御できるよく知られた同期デバイスは次のとおりです。docs.microsoft.com / en-us / windows / win32 / api / synchapi / パフォーマンスを比較していませんが、std :: mutexは今、優れた:stackoverflow.com/questions/9997473/...と使用して実装:docs.microsoft.com/en-us/windows/win32/sync/...
DeducibleSteak

考慮に入れるべき一般的な考慮事項の適切な説明であると思われる:stackoverflow.com/questions/5869825/...
DeducibleSteak

改めて感謝します。問題があれば、私はLinuxを利用しています。
alfC

ここでは、やや詳細な性能比較である(異なる言語のためには、私は、これは有益で何を期待するの指標である推測):matklad.github.io/2020/01/04/... TLDRがある-スピンロックは非常に小さいで勝ちます競合がない場合のマージンは、競合がある場合に大幅に失う可能性があります。
DeducibleSteak
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.