実用的なマルチワードの比較と交換操作


10

この質問と同じタイトルで、著者は構築する方法について説明しノンブロッキング 線形化 マルチワードCASの唯一のシングルワードCASを使用して操作を。彼らは最初に、以下のように二重比較単一交換操作-RDCSSを紹介します。

word_t RDCSS(RDCSSDescriptor_t *d) {
  do {
    r = CAS1(d->a2, d->o2, d);
    if (IsDescriptor(r)) Complete(r);
  } while (IsDescriptor(r));
  if (r == d->o2) Complete(d); // !!
  return r;
}

void Complete(RDCSSDescriptor_t *d) {
  v = *(d->a1);
  if (v == d->o1) CAS1(d->a2, d, d->n2);
  else CAS1(d->a2, d, d->o2);
}

ここで、RDCSSDescriptor_tは次のフィールドを持つ構造です。

  • a1 -最初の条件のアドレス
  • o1 -最初のアドレスで期待される値
  • a2 -2番目の条件のアドレス
  • o2 -2番目のアドレスで期待される値
  • n2 -2番目のアドレスに書き込まれる新しい値

この記述子は、RDCSS操作を開始するスレッドで一度作成および初期化されます。関数の最初のCAS1がRDCSS成功し、記述子が到達可能になる(または論文の用語でアクティブになる)まで、他のスレッドはそれを参照しません。

アルゴリズムの背後にある考え方は次のとおりです。2番目のメモリ位置を、何をしたいかを示す記述子に置き換えます。次に、記述子が存在する場合、最初のメモリ位置をチェックして、その値が変更されたかどうかを確認します。そうでない場合は、2番目のメモリ位置の記述子を新しい値で置き換えます。それ以外の場合は、2番目のメモリ位置を古い値に戻します。

著者は、!!コメント付きの行が論文内で必要な理由を説明していません。同時変更がない限り、このチェックの後CAS1Complete関数の命令は常に失敗するようです。また、チェックとのCASの間に同時変更があった場合、同時変更は同じ記述子を使用してはならないため、Completeチェックを実行するスレッドはCAS in で失敗しCompleteますd

私の質問は:缶関数内のチェックはRDCSSSif (r == d->o2)...RDCSSがまだあるダブル比較する、単一スワップ命令のセマンティクス維持して、省略され線形化ロックフリーを?(!!コメント付きの行)

そうでない場合、正確性を保証するためにこの行が実際に必要であるシナリオを説明できますか?

ありがとうございました。


まず、何が起こっているのかを理解するために、データ構造RDCSSDescriptor_tを確認する必要があります。第二に、これは理論的なコンピューターサイエンスを扱っていないため、ここではおそらくトピックから外れています。stackoverflow.comでこれを確認することをお勧めします。
デイブクラーク

論文へのリンクが壊れています。
アーロンスターリング

1
リンクをお詫び申し上げます。リンクは機能するはずです。記述子が何であるかを説明するために質問を更新しました。私がこれをstackoverflow.comに投稿しなかった理由は、FAQがこのサイトはコンピューターサイエンスの研究レベルの質問用であると述べているためです。アルゴリズムのロックフリー性と線形化可能性の問題はそのように当てはまると思いました。よくある質問を間違って理解したと思います。
axel22

FAQで見逃したキーワードは「理論的」でした。一部の人々は質問を興味深いと思うので、私はそれを開いたままにしておきます。
デイブクラーク

3
@Dave:私はこのサブエリアの専門家ではありませんが、私にはこれは非常に典型的なTCS質問のように聞こえます。2つの計算モデル(A:シングルワードCASを使用、B:マルチワードCASを使用)と複雑さの測定(CASの数)が与えられ、モデルAでモデルBをシミュレートできるかどうか尋ねられます。そして最悪の場合のオーバーヘッドで。(ここで、シミュレーションが疑似コードではなくCコードの一部として与えられていることは少し誤解を招くかもしれません。これは、これが現実のプログラミングの課題に関連していることを理論担当者に示唆するかもしれません。)
Jukka Suomela

回答:


9

同時実行環境では、単純なことは奇妙に見えることがあります。これが役立つことを願っています。

我々は持っているBUILT-IN ATOMIC CAS1はこのセマンティックを持ちます:

int CAS1(int *addr, int oldval, int newval) {
  int currval = *addr;
  if (currval == oldval) *addr = newval;
  return currval;
}

CAS1を使用し、次の意味を持つATOMIC RDCSS関数を定義する必要があります。

int RDCSS(int *addr1, int oldval1, int *addr2, int oldval2, int newval2) {
  int res = *addr;
  if (res == oldval2 && *addr1 == oldval1) *addr2 = newval2;
  return res;
}

直観的に:* addr1 == oldval1 ...の場合のみ、addr2の値を同時に変更する必要があります。別のスレッドが変更している場合、他のスレッドが操作を完了できるようにしてから、再試行できます。

RDCSS関数を使用して(記事を参照)、CASNを定義します。ここで、次の方法でRDCSS記述子を定義します。

RDCSSDESCRI
int *addr1   
int oldval1
int *addr2   
int oldval2
int newval2

次に、RDCSSを次のように実装します。

int RDCSS( RDCSSDESCRI *d ) {
  do {
    res = CAS1(d->addr2, d->oldval2, d);  // STEP1
    if (IsDescriptor(res)) Complete(res); // STEP2
  } while (IsDescriptor(res);             // STEP3
  if (res == d->oldval2) Complete(d);     // STEP4
  return res;
}

void Complete( RDCSSDESCRI *d ) {
  int val = *(d->addr1);
  if (val == d->oldval1) CAS1(d->addr2, d, d->newval2);
    else CAS1(d->addr2, d, d->oldval2);  
}
  • ステップ1:最初に* addr2の値を(独自の)記述子dに変更しようとします。CAS1が成功した場合、res == d-> oldval2(つまり、resは記述子ではありません)
  • STEP2:resが記述子かどうかを確認します。つまり、STEP1が失敗しました(別のスレッドがaddr2を変更しました)...別のスレッドが操作を完了するのを助けます
  • STEP3:記述子の保存に失敗した場合は、STEP1を再試行しますd
  • STEP4:addr2から期待値をフェッチした場合、記述子(ポインター)をaddr2に格納することに成功し、newval2を* addr2 iif * addr1 == oldval1に格納するタスクを完了することができます。

あなたの質問への回答

STEP4を省略した場合、RDCSSセマンティックスのif(... && * addr1 == oldval1)* addr2 = newval2部分は実行されません(...またはそれ以上:他のスレッドを支援することによって予測できない方法で実行される可能性があります)現在のもの)。

コメントで指摘したように、STEP4 の条件if(res == d1-> oldval2)は不要です。省略しても、*(d-> addr2)!= dのため、Complete()の両方のCAS1が失敗します。 。その唯一の目的は、関数呼び出しを避けることです。

例T1 = thread1、T2 = thread2:

remember that addr1 / addr2 are in a shared data zone !!!

T1 enter RDCSS function
T2 enter RDCSS function
T2 complete STEP1 (and store the pointer to its descriptor d2 in addr2)
T1 at STEP1 the CAS1 fails and res = d2
T2 or T1 completes *(d2->addr2)=d2->newval2 (suppose that *(d2->addr1)==d2->oldval1)
T1 execute STEP1 and now CAS1 can fail because *addr2 == d2->newval2
   and maybe d2->newval2 != d1->oldval2, in every case at the end 
   res == d2->newval2 (fail) or
   res == d1->oldval2 (success)
T1 at STEP2 skips the call to Complete() (because now res is not a descriptor)
T1 at STEP3 exits the loop (because now res is not a descriptor)
T1 at STEP4 T1 is ready to store d1->newval2 to addr2, but only if
   *(d1->addr2)==d (we are working on our descriptor) and *(d1->addr1)==d1->oldval1
   ( Custom() function)

いい説明ありがとうございます。CAS1が新しい値ではなく古い値を返すという点を完全に逃しました。
axel22

ただし、このシナリオでは、最後の2行で、STEP4の条件がない場合、addr2containsが含まれてd2->newval2いるため、T1は値を格納できます。しかし、Complete古い値が記述子であることを期待しているため、CAS1のCAS1は失敗するようです。T1はd1何も書き込みません。正しい?
axel22

@ axel22:Complete()でCAS1を逃しました:-D。はい、あなたは正しいです...私の例は間違っています、if()を何も変更しない場合、if条件は関数呼び出しを避けるためにのみ使用されます。明らかにSTEP4の完了(d)が必要です。ここで例を変更します。
Marzio De Biasi

実際のハードウェアではキャッシュラインのフラッシュやキャッシュラインへの排他的アクセスの取得などのマイナスの影響があるため、私が知る限り、失敗すると予想されるCASを回避することはキャッシュ最適化手法です。この論文の著者は、アルゴリズムが正しいことに加えて、可能な限り実用的なものであることを望んでいたと思います。
Tim Seguine
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.