回答:
返信が遅くなって申し訳ありませんが、質問(確かに質問)を見つけました。私は並行性についても勉強しています。いくつかのアイデアを皆さんと共有するように努めます。
まず、シーケンシャルな一貫性から始めましょう。操作がプログラムの順序で有効に見える場合、モデルにはこのプロパティがあります。つまり、コードの行が実行される順序は、ソースファイルで指定された順序です。この前提条件はスレッド固有です。異なるスレッドを含む操作は、プログラムの順序とは無関係です。したがって、このプロパティは次のことを許可します。同じスレッドによって発行された操作は、スレッドのソースコードで指定された同じ順序で実行されます。異なるスレッドによって発行された操作は、任意の順序で発生する可能性があります。この前提条件は明白に思えるかもしれませんが、常にそうであるとは限りません(コンパイラーの最適化により、操作が発行される順序が変更され、プログラムの順序がソースと異なる場合があります)。
あなたの例は正しいですが、他の例を挙げましょう。このプログラムP1について考えてみましょう(簡単に参照できるように各行にタグを付けます)。
int x = 1;
void ThreadA()
{
A1: x = x * 2;
A3: int a = x;
}
void ThreadB()
{
B2: x = 20;
}
最後にa = 40の順次実行はありますか?はい:B2、A1、A3。
B2とA1は任意の順序で実行できます(これらは異なるスレッドに属しています)。A1はA3の前に実行されます(プログラムの順序=ソースコードの順序)。
次に、このプログラムP2について考えます。
int y = 1;
void ThreadA()
{
A2: y = 40;
}
void ThreadB()
{
B1: y = y / 2;
B3: int b = y;
}
最後にb = 20の順次実行はありますか?はい:A2、B1、B3。
作曲はどうですか?P1とP2を構成しましょう。P1∘P2が得られます。
int x = 1;
int y = 1;
void ThreadA()
{
A1: x = x * 2;
A2: y = 40;
A3: int a = x;
}
void ThreadB()
{
B1: y = y / 2;
B2: x = 20;
B3: int b = y;
}
逐次一貫性が構成的である場合、最後にa = 40およびb = 20の実行が必要です。これは本当ですか?いいえ。それが事実ではない理由を正式に証明しましょう。「o1→o2」と書いて、操作o2の前に操作o1を実行する必要があることを示します。
P1∘P2では、逐次一貫性により、A1-> A2およびB1-> B2(スレッドプログラムごとの順序)が成り立つ必要があります。a = 40を得るには、B2-> A1も保持する必要があります。b = 20を取得するには、A2-> B1も保持する必要があります。優先チェーンを確認できますか?A1-> A2-> B1-> B2-> A1-> ...
P1とP2は連続して一貫していたが、それらの構成P1∘P2は一致していませんでした。逐次一貫性は構成的ではありません。あなたが提供した例は、この事実を示すほどトリッキーではありませんでした。
それでは、静止一貫性について考えてみましょう。オブジェクト指向のパラダイムを使わずにこの特性を説明するのは難しいです。実際、静止時の一貫性は、オブジェクトに対する保留中のメソッド呼び出しに関して簡単に理解できます。それにもかかわらず、私は命令パラダイムに固執しようとします(保留中のメソッド呼び出しは関数が開始されますが、完了されず、オブジェクトは関数に含まれる変数になります)。
関数呼び出しは、呼び出しと応答で構成されます。スレッドによって呼び出された関数は保留中ですが、そのようなスレッドにまだ応答を返していません。変数の休止期間は、(スレッドによる)保留中の関数呼び出しが動作していない時間間隔です。同じ変数で動作し、休止期間で区切られた関数呼び出しがリアルタイムの順序(ソースコードで指定された順序)で有効に見える場合、モデルには休止整合性プロパティがあります。逆の観点から見ると、定義は、異なるスレッドによって同時に呼び出された関数によって実行された同じ変数に対する操作(静止によって分離されていない)は任意の順序で発生する可能性があると述べています。
私は何時間にもわたって、読み書き操作のみを使用して意味のある例を設計し、静止と順次の一貫性の違いを示してきましたが、成功しませんでした。セットを含む別の例を使用してみましょう。ビットベクトルを使用して、セットに含まれる整数(0〜4)を追跡します。
int set[5] = {0, 0, 0, 0, 0}; // 0 if i-th item is absent, 1 otherwise
void add(int number) {
L1: set[number] = 1;
L2: temp foo = set[0];
}
void remove(int number) {
set[number] = 0;
}
int contains(int number) {
return set[number] == 1;
}
void ThreadA()
{
A1: add(1);
}
void ThreadB()
{
B1: add(2);
B2: remove(2);
B3: int res = contains(2);
}
最後にres = 1となる順次実行はありますか?いいえ:B1-> B2およびB2-> B3の順次整合性により、B1-> B3となります。したがって、remove(2)は常にadd(2)の後に実行され、contains(2)は常に0を返します。
最後に、res = 1の静止実行はありますか?はい!この実行を検討してください:
スレッドAはA1でadd(1)を呼び出します。
スレッドAはL1を実行します(L2は実行しません)。//セットに保留中の呼び出しがあるため、これらの呼び出しにもセットが含まれるため、スレッドBはB1の前にB2を自由に実行できます。
スレッドBはB2を呼び出します。//呼び出し+応答
スレッドBがB1を呼び出します。//呼び出し+応答
スレッドAがL2を実行し、add(1)が応答します。//セットに静止があります
スレッドBはB3を実行します。//呼び出し+応答
現在、res = 1です。
残念ながら、静止整合性が構成的である理由に関する最新の質問にはまだ答えられません。この正確な答えを確かに探していたときに、あなたの質問を見つけました。何か思いついたらお知らせします。
このトピックに関する良い読み物は、HerlihyとShavitによる本「マルチプロセッサプログラミングの芸術」の3.3章を参照してください。
編集:静止整合性が構成的である理由を説明する素晴らしいページを見つけました。そして、これは別の非常に良い読書です!
EDIT2:逐次一貫性構成可能性の例の小さなエラーを修正しました。