web-dev-qa-db-ja.com

不変性はマルチプロセッサプログラミングでのロックの必要性を完全に排除しますか?

パート1

明らかに不変性は、マルチプロセッサプログラミングでのロックの必要性を最小限に抑えますが、その必要性はなくなりますか、それとも不変性だけでは不十分な場合がありますか?ほとんどのプログラムが実際に何かを行う(データストアの更新、レポートの生成、例外のスローなど)必要がある限り、処理とカプセル化の状態を延期することしかできないようです。そのようなアクションは常にロックなしで実行できますか?各オブジェクトを破棄し、元のオブジェクトを変更する代わりに新しいオブジェクトを作成するという単なるアクション(不変性の大雑把なビュー)は、プロセス間の競合からの絶対的な保護を提供しますか、それともロックを必要とするコーナーケースはありますか?

私は多くの関数型プログラマーや数学者が「副作用なし」について話したいと思っていますが、「現実の世界」では、機械命令の実行にかかる時間であっても、すべてに副作用があります。理論的/学術的な答えと実際的/現実的な答えの両方に興味があります。

不変性が安全である場合、特定の境界または仮定が与えられれば、「セーフティゾーン」の境界が正確に何であるかを知りたいのです。可能な境界の例:

  • I/O
  • 例外/エラー
  • 他の言語で書かれたプログラムとの相互作用
  • 他のマシンとの相互作用(物理、仮想、または理論)

@JimmaHoffaに特に感謝します 彼のコメント この質問の始まりです!

パート2

マルチプロセッサプログラミングは、一部のコードをより高速に実行するための最適化手法としてよく使用されます。不変オブジェクトに対してロックを使用する方が速いのはいつですか?

アムダールの法則 で設定された制限を前提として、不変オブジェクトと可変オブジェクトのロックを使用して、(ガベージコレクターを考慮に入れて、または考慮せずに)全体的なパフォーマンスを向上させることができるのはいつですか?

概要

私はこれらの2つの質問を1つにまとめて、スレッド化問題の解決策として、境界ボックスが不変性のためにある場所に到達しようとしています。

39
GlenPeterson

これは奇妙な言い回しの質問であり、完全に回答すると、本当に、非常に幅広い質問になります。私はあなたが尋ねているいくつかの詳細を明らかにすることに焦点を当てます。

不変性は設計上のトレードオフです。一部の操作が困難になります(大きなオブジェクトの状態をすばやく変更する、オブジェクトを断片的に構築する、実行状態を維持するなど)。他の操作(デバッグが容易、プログラムの動作を簡単に推論できる、作業中に自分の下で変化することについて心配する必要がない)同時に、など)。これがこの質問で気になる最後の質問ですが、それがツールであることを強調したいと思います。 (ほとんどのmodernプログラムでは)発生する問題よりも多くの問題を解決できる優れたツールですが、特効薬ではありません...本質的なものを変えるものではありませんプログラムの動作。

さて、何があなたを手に入れますか?不変性はあなたに一つのことをもたらします:あなたはその下で状態が変化することを心配することなく、不変オブジェクトを自由に読むことができます(それは本当に深く不変であると仮定します...可変メンバーを持つ不変オブジェクトを持つことは通常取引のブレーカーです)。それでおしまい。 (ロック、スナップショット、データ分割、またはその他のメカニズムによって)同時実行性を管理する必要がなくなります。元の質問のロックへの焦点は...質問の範囲を考えると不正解です)。

しかし、多くのことがオブジェクトを読み取ることがわかりました。 IOは動作しますが、IO自体は同時使用自体をうまく処理しない傾向があります。ほとんどすべての処理は動作しますが、他のオブジェクトは変更可能であるか、または処理自体が同時実行に適さない状態を使用します。オブジェクトのコピーは、完全なコピーが(ほとんど)アトミック操作ではないため、一部の言語では大きな隠れた問題点です。これは、不変オブジェクトが役立つ場所です。

パフォーマンスについては、アプリによって異なります。ロックは(通常)重いです。他の同時実行管理メカニズムは高速ですが、設計に大きな影響を与えます。 一般的に、不変オブジェクトを使用する(およびその弱点を回避する)高度な並行設計は、可変オブジェクトをロックする高度な並行設計よりも優れたパフォーマンスを発揮します。あなたのプログラムが軽く並行しているなら、それは依存している、そして/または問題ではありません。

しかし、パフォーマンスは最大の関心事ではありません。並行プログラムの作成はhardです。並行プログラムのデバッグはhardです。不変オブジェクトは、同時実行管理を手動で実装する際のエラーの可能性を排除することにより、プログラムの品質を向上させるのに役立ちます。並行プログラムで状態を追跡する必要がないため、デバッグが容易になります。彼らはあなたのデザインをより単純にし、そしてそこでバグを取り除きます。

まとめると、不変性は役立ちますが、並行性を適切に処理するために必要な課題は解消されません。その助けは広く行き渡る傾向がありますが、最大の利点はパフォーマンスではなく品質の観点からです。そして、いいえ、不変性はアプリの同時実行性を魔法のように免除するものではありません。

35
Telastyn

ある値を受け入れて別の値を返す関数で、関数の外部に影響を与えず、副作用がないため、スレッドセーフです。関数の実行方法が消費電力にどのように影響するかなどを検討する場合、それは別の問題です。

私は、実装の詳細が重要ではない、ある種の明確に定義されたプログラミング言語を実行しているチューリング完全なマシンを参照していると想定しています。言い換えれば、選択したプログラミング言語で書いている関数が言語の範囲内での不変性を保証できる場合、スタックが何をしているかは問題ではありません。私が高級言語でプログラミングしているときは、スタックについて考えませんし、そうする必要もありません。

これがどのように機能するかを説明するために、C#でいくつかの簡単な例を紹介します。これらの例が真実であるためには、いくつかの仮定を行う必要があります。 1つ目は、コンパイラーがエラーなしでC#仕様に従っていること、2つ目は、正しいプログラムが生成されることです。

文字列コレクションを受け入れ、コンマで区切られたコレクション内のすべての文字列を連結した文字列を返す簡単な関数が必要だとしましょう。 C#での単純で素朴な実装は次のようになります。

_public string ConcatenateWithCommas(ImmutableList<string> list)
{
    string result = string.Empty;
    bool isFirst = false;

    foreach (string s in list)
    {
        if (isFirst)
            result += s;
        else
            result += ", " + s;
    }
    return result;
} 
_

この例は不変です。どうやってそれを知るのですか? stringオブジェクトは不変だからです。ただし、実装は理想的ではありません。 resultは不変なので、ループを通じて毎回新しい文字列オブジェクトを作成し、resultが指す元のオブジェクトを置き換える必要があります。余分な文字列をすべてクリーンアップする必要があるため、これは速度に悪影響を及ぼし、ガベージコレクタに圧力をかける可能性があります。

今、私がこれを行うとしましょう:

_public string ConcatenateWithCommas(ImmutableList<string> list)
{
    var result = new StringBuilder();
    bool isFirst = false;

    foreach (string s in list)
    {
        if (isFirst)
            result.Append(s);
        else
            result.Append(", " + s);
    }
    return result.ToString();
} 
_

stringresultを可変オブジェクトStringBuilderに置き換えたことに注意してください。これは最初の例よりもmuch高速です。これは、ループのたびに新しい文字列が作成されるわけではないためです。代わりに、StringBuilderオブジェクトは、各文字列の文字を文字のコレクションに追加するだけで、最後にすべてを出力します。

StringBuilderは変更可能ですが、この関数は不変ですか?

はい、そうです。どうして? この関数が呼び出されるたびに、その呼び出しのために新しいStringBuilderが作成されます。これで、スレッドセーフな純粋な関数ができました。しかし、変更可能なコンポーネントが含まれています。

しかし、私がこれをした場合はどうなりますか?

_public class Concatenate
{
    private StringBuilder result = new StringBuilder();
    bool isFirst = false;

    public string ConcatenateWithCommas(ImmutableList<string> list)
    {
        foreach (string s in list)
        {
            if (isFirst)
                result.Append(s);
            else
                result.Append(", " + s);
        }
        return result.ToString();
    } 
}
_

このメソッドはスレッドセーフですか?いいえ、そうではありません。どうして? クラスは現在、私のメソッドが依存する状態を保持しています。競合状態がメソッドに存在します。1つのスレッドがIsFirstですが、別のスレッドが最初のAppend()を実行する場合があります。その場合、文字列の先頭にコンマがあり、そこにあるとは想定されていません。

なぜこのようにしたいのですか?まあ、スレッドが順序に関係なく、またはスレッドが入ってくる順序に関係なく文字列をresultに蓄積するようにしたいと思うかもしれません。たぶんそれはロガーですよね。

とにかく、それを修正するために、メソッドの内部をlockステートメントで囲みました。

_public class Concatenate
{
    private StringBuilder result = new StringBuilder();
    bool isFirst = false;
    private static object locker = new object();

    public string AppendWithCommas(ImmutableList<string> list)
    {
        lock (locker)
        {
            foreach (string s in list)
            {
                if (isFirst)
                    result.Append(s);
                else
                    result.Append(", " + s);
            }
            return result.ToString();
        }
    } 
}
_

再びスレッドセーフになりました。

私の不変メソッドがスレッドセーフにならない可能性がある唯一の方法は、メソッドが何らかの方法で実装の一部をリークする場合です。これは起こりますか?コンパイラが正しく、プログラムが正しい場合は違います。そのようなメソッドにロックが必要になることはありますか?番号。

同時実行シナリオで実装がリークされる可能性のある方法の例については、 ここを参照 です。

13
Robert Harvey

私はあなたの質問を理解したかどうかわかりません。

私見答えはイエスです。すべてのオブジェクトが不変の場合、ロックは必要ありません。ただし、状態を保持する必要がある場合(たとえば、データベースを実装するか、複数のスレッドからの結果を集約する必要がある場合)は、可変性を使用する必要があるため、ロックも必要です。不変性はロックの必要性を排除しますが、通常、完全に不変のアプリケーションを持つ余裕はありません。

パート2の回答-ロックは、ロックがない場合よりも常に遅くなるはずです。

4
Maros

あなたは

明らかに不変性により、マルチプロセッサプログラミングでのロックの必要性が最小限に抑えられます

違う。使用するすべてのクラスのドキュメントを注意深く読む必要があります。たとえば、C++のconst std :: stringはnotスレッドセーフです。不変オブジェクトには、アクセス時に変化する内部状態があります。

しかし、あなたはこれを全く間違った視点から見ています。オブジェクトが不変であるかどうかは関係ありません。重要なのは、オブジェクトを変更するかどうかです。あなたが言っていることは、「運転テストを受けなければ、飲酒運転の運転免許を失うことは決してない」ということです。本当ですが、要点がありません。

さて、誰かが "ConcatenateWithCommas"という名前の関数を使って書いたコード例では、入力が可変で、ロックを使用した場合、何が得られますか?文字列を連結しようとしているときに他の誰かがリストを変更しようとした場合、ロックによってクラッシュを防ぐことができます。ただし、他のスレッドが文字列を変更する前または後に文字列を連結するかどうかはまだわかりません。だからあなたの結果はかなり役に立たない。ロックに関連しない問題があり、ロックで修正できない。ただし、不変オブジェクトを使用し、他のスレッドがオブジェクト全体を新しいオブジェクトに置き換える場合、新しいオブジェクトではなく古いオブジェクトを使用しているため、結果は役に立ちません。これらの問題について、実際の機能レベルで考える必要があります。

4
gnasher729

一連の関連する状態を、不変オブジェクトへの単一の可変参照にカプセル化すると、さまざまな種類の状態変更を、パターンを使用してロックフリーで実行できます。

do
{
   oldState = someObject.State;
   newState = oldState.WithSomeChanges();
} while (Interlocked.CompareExchange(ref someObject.State, newState, oldState) != oldState;

2つのスレッドが両方ともsomeObject.stateを同時に更新しようとすると、両方のオブジェクトが古い状態を読み取り、互いの変更なしで新しい状態を決定します。 CompareExchangeを実行する最初のスレッドは、次の状態がどうあるべきかを格納します。 2番目のスレッドは、状態が以前に読み取った状態と一致していないことを検出し、最初のスレッドの変更を有効にして、システムの適切な次の状態を再計算します。

このパターンには、ウェイレイドされたスレッドが他のスレッドの進行をブロックできないという利点があります。競合が激しい場合でも、一部のスレッドが常に進行しているという利点もあります。ただし、競合が存在する場合、多くのスレッドが作業に多くの時間を費やす可能性があり、結果として破棄されるという欠点があります。たとえば、別々のCPU上の30個のスレッドがすべて同時にオブジェクトを変更しようとすると、1回目は1回目、2回目は3回目、1回目は成功し、各スレッドは平均して約15回試行します。データを更新します。 「アドバイザリー」ロックを使用すると、状況を大幅に改善できます。スレッドが更新を試みる前に、「競合」インジケーターが設定されているかどうかを確認する必要があります。その場合、更新を行う前にロックを取得する必要があります。スレッドが更新に失敗した場合、競合フラグを設定する必要があります。ロックを取得しようとするスレッドが待機している人がいない場合、競合フラグをクリアする必要があります。ここでのロックは「正確さ」のために必要ではないことに注意してください。コードはそれなしでも正しく動作します。ロックの目的は、成功する可能性が低い操作にコードが費やす時間を最小限に抑えることです。

4
supercat