27

.NET 2.0 メモリ モデルでは、書き込みには常にリリース フェンスが使用されるとよく​​耳にします。これは本当ですか?これは、明示的なメモリバリアやロックがなくても、作成されたスレッドとは異なるスレッドで部分的に構築されたオブジェクト (参照型のみを考慮) を観察することは不可能であることを意味しますか? コンストラクターが参照をリークするケースは明らかに除外していthisます。

たとえば、不変の参照型があるとします。

public class Person
{
    public string Name { get; private set; }
    public int Age { get; private set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

次のコードを使用して、「John 20」と「Jack 21」以外の出力、たとえば「null 20」または「Jack 0」を観察することは可能でしょうか?

// We could make this volatile to freshen the read, but I don't want
// to complicate the core of the question.
private Person person;

private void Thread1()
{
    while (true)
    {
        var personCopy = person;

        if (personCopy != null)
            Console.WriteLine(personCopy.Name + " " + personCopy.Age);
    }
}

private void Thread2()
{
    var random = new Random();

    while (true)
    {
        person = random.Next(2) == 0
            ? new Person("John", 20)
            : new Person("Jack", 21);
    }
}

これはまた、深く不変の参照型のすべての共有フィールドを作成しvolatile、(ほとんどの場合) 仕事を続けることができるということですか?

4

3 に答える 3

10

.NET 2.0 メモリ モデルでは、書き込みには常にリリース フェンスが使用されるとよく​​耳にします。これは本当ですか?

どのモデルを参照しているかによって異なります。

まず、リリースフェンスバリアを正確に定義しましょう。解放セマンティクスでは、命令シーケンス内のバリアの前にある他の読み取りまたは書き込みは、そのバリアの後に移動することは許可されていません。

  • ECMA 仕様には、書き込みがこの保証を提供しない緩和されたモデルがあります。
  • Microsoft が提供する CLR 実装は、書き込みにリリース フェンス セマンティクスを持たせることでモデルを強化することがどこかで引用されています。
  • x86 および x64 アーキテクチャは、書き込みをリリース フェンス バリアにし、読み取りを取得フェンス バリアにすることで、モデルを強化します。

そのため、難解なアーキテクチャ (Windows 8 が対象とする ARM など) で実行される CLI (Mono など) の別の実装では、書き込み時にリリース フェンス セマンティクスが提供されない可能性があります。可能であると言ったが、確実ではないことに注意してください。しかし、さまざまなソフトウェア レイヤーやハードウェア レイヤーなど、使用されているすべてのメモリ モデル間で、コードを真に移植可能にしたい場合は、最も弱いモデルをコーディングする必要があります。つまり、ECMA モデルに照らしてコーディングし、仮定を行わないということです。

使用中のメモリ モデル レイヤーのリストを明示的に作成する必要があります。

  • コンパイラ: C# (または VB.NET など) は命令を移動できます。
  • ランタイム: 明らかに、JIT コンパイラを介した CLI ランタイムは命令を移動できます。
  • ハードウェア: もちろん、CPU とメモリのアーキテクチャも影響します。

これは、明示的なメモリバリアやロックがなくても、作成されたスレッドとは異なるスレッドで部分的に構築されたオブジェクト (参照型のみを考慮) を観察することは不可能であることを意味しますか?

はい (認定済み): アプリケーションが実行されている環境が十分にわかりにくい場合、部分的に構築されたインスタンスが別のスレッドから観察される可能性があります。これが、 を使用しないとダブルチェックされたロック パターンが安全でない理由の 1 つvolatileです。ただし、実際には、Microsoft の CLI の実装では命令をこのように並べ替えないため、これに遭遇することはないと思います。

次のコードを使用して、「John 20」と「Jack 21」以外の出力、たとえば「null 20」または「Jack 0」を観察することは可能でしょうか?

繰り返しますが、それは資格があります。しかし、上記の何らかの理由で、あなたがそのような行動を観察することはないと思います。

ただし、personはマークされvolatileていないため、読み取りスレッドが常にnull. ただし、実際には、この呼び出しにより、C# および JIT コンパイラは、読み取りをループConsole.WriteLineの外に移動する可能性のあるリフティング操作を回避することになるでしょう。personこのニュアンスについては、すでに十分に認識されていると思います。

これは、非常に不変な参照型のすべての共有フィールドを揮発性にし、(ほとんどの場合) 仕事を続けることができるという意味ですか?

私は知らない。それはかなり負荷の高い質問です。その背後にある文脈をよりよく理解せずに、どちらの方法で答えても安心できません。私が言えることは、通常、操作、、、、などのより明示的なメモリ命令を優先して使用することを避けているということvolatileです。繰り返しになりますが、ロックなしのコードを完全に回避して、.InterlockedThread.VolatileReadThread.VolatileWriteThread.MemoryBarrierlock

アップデート:

私が好きな視覚化の方法の 1 つは、C# コンパイラ、JITer などが可能な限り積極的に最適化すると仮定することです。つまりPerson.ctor、次の疑似コードを生成する (単純なので) インライン化の候補になる可能性があります。

Person ref = allocate space for Person
ref.Name = name;
ref.Age = age;
person = instance;
DoSomething(person);

また、書き込みには ECMA 仕様のリリース フェンス セマンティクスがないため、他の読み取りと書き込みは、割り当てを超えて「フロート」してperson、次の有効な一連の命令を生成する可能性があります。

Person ref = allocate space for Person
person = ref;
person.Name = name;
person.Age = age;
DoSomething(person);

したがって、この場合、person初期化される前に割り当てられることがわかります。これは、実行中のスレッドの観点から見ると、論理シーケンスが物理シーケンスと一貫しているため有効です。意図しない副作用はありません。しかし、明らかな理由から、このシーケンスは別のスレッドにとって悲惨なことになります。

于 2011-12-02T16:11:47.450 に答える
1

あなたには希望がありません。コンソールへの書き込みをエラー チェックに置き換え、Thread1() のコピーを多数設定し、4 コアのマシンを使用すると、部分的に構築された Person インスタンスがいくつか見つかるはずです。他の回答やコメントに記載されている保証された手法を使用して、プログラムを安全に保ちます。

コンパイラを書いている人も、CPU を作っている人も、スピードを求めて状況を悪化させようと共謀しています。明示的な指示がなければ、コンパイラの担当者は、ナノ秒を節約するためにコードを並べ替えます。CPUの人も同じことをしています。最後に読んだところによると、単一のコアは、可能であれば、4 つの命令を同時に実行する傾向があります。(そして、それができなくても。)

通常の状況では、これで問題が発生することはほとんどありません。しかし、6 か月に 1 回しか発生しない小さな問題が、本当に大きな問題になる可能性があることがわかりました。そして興味深いことに、10 億分の 1 の問題が 1 分間に数回発生する可能性があります。あなたのコードは後者のカテゴリに分類されると思います。

于 2011-12-02T20:53:09.497 に答える
1

少なくとも IL レベルでは、コンストラクターはスタック上で直接呼び出され、結果の参照は、構築が完了するまで生成されません (そして格納できます)。そのため、(IL) コンパイラ レベルで並べ替えることはできません (参照型の場合)。

ジッターレベルについては、よくわかりませんが、フィールドの割り当てとメソッド呼び出し (コンストラクターとは何か) の順序が変更された場合は驚くでしょう。フィールドが呼び出されたメソッドによって使用されないようにするには?

同様に、CPU レベルでは、分岐が「サブルーチン呼び出し」であるかどうかを CPU が知る方法がなく、したがって次の命令に戻るため、ジャンプ命令の前後で並べ替えが発生すると驚くでしょう。順不同で実行すると、「型にはまらない」ジャンプが発生した場合に、非常に不適切な動作が発生する可能性があります。

于 2011-12-02T21:11:38.567 に答える