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


39

パート1

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

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

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

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

この質問を始めたコメントに対して@JimmaHoffaに感謝します!

パート2

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

アムダールの法則で定められた制限を考えると、不変オブジェクトと可変オブジェクトのロックで全体的なパフォーマンスを向上させることができます(ガベージコレクターの有無にかかわらず)。

概要

これら2つの質問を1つにまとめて、スレッド化の問題の解決策として不変性の境界ボックスがどこにあるのかを探ろうとしています。


21
but everything has a side effect-ええ、そうではありません。ある値を受け入れ、他の値を返し、関数の外部に影響を与えず、副作用がないため、スレッドセーフな関数。コンピューターが電気を使用していても構いません。必要に応じて、メモリセルに衝突する宇宙線についても説明できますが、議論は現実的なものにしましょう。関数の実行方法が電力消費にどのように影響するかなどを検討したい場合、それはスレッドセーフプログラミングとは異なる問題です。
ロバートハーベイ

5
@RobertHarvey-副作用の定義を変えているだけかもしれませんが、代わりに「実世界の副作用」と言っておくべきでした。はい、数学者には副作用のない機能があります。実際のマシンで実行されるコードは、データを変換するかどうかに関係なく、マシンリソースを実行します。この例の関数は、ほとんどのマシンアーキテクチャの戻り値をスタックに置きます。
グレンペターソン

1
あなたが実際にそれを得ることができる場合、私はあなたの質問は、この悪名高い紙の中心に行くと思いますresearch.microsoft.com/en-us/um/people/simonpj/papers/...
ジミー・ホッファ

6
私たちの議論の目的のために、あなたは、ある種の明確に定義されたプログラミング言語を実行しているチューリング完全なマシンについて言及していると仮定しています。実装の詳細は無関係です。 言い換えれば、選択したプログラミング言語で記述している関数が言語の範囲内で不変性を保証できるのであれば、スタックが何をしているのかは問題ではありません 高級言語でプログラミングしているとき、スタックについては考えませんし、そうする必要もありません。
ロバートハーベイ

1
@RobertHarvey spoonerism; モナドhehそして、あなたは最初の数ページからそれを集めることができます。全体的に見て、副作用を事実上純粋な方法で処理するためのテクニックを詳しく説明しているので、グレンの質問に答えると確信しているので、この質問を見つけた人に良い脚注として投稿しましたさらに読むための未来。
ジミー・ホッファ

回答:


35

これは奇妙に表現された質問であり、完全に答えられれば本当に広範です。私はあなたが尋ねている詳細のいくつかを片付けることに集中するつもりです。

不変性は設計上のトレードオフです。いくつかの操作を難しくします(大きなオブジェクトの状態をすばやく変更したり、オブジェクトを断片的に構築したり、実行状態を維持したりするなど)。同時に、など)。この質問で私たちが関心を持っているのはこれが最後ですが、私はそれがツールであることを強調したいと思います。多くの場合、原因となるよりも多くの問題を解決する優れたツールです(ほとんどの最新のプログラムで)が、特効薬ではありません...プログラムの本質的な動作を変更するものではありません。

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

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

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

ただし、パフォーマンスが最大の懸念事項ではありません。並行プログラムの作成は困難です。並行プログラムのデバッグは困難です。不変オブジェクトは、同時実行管理を手動で実装するエラーの機会を排除することにより、プログラムの品質を向上させるのに役立ちます。並行プログラムで状態を追跡しようとしないため、デバッグが容易になります。彼らはあなたの設計をよりシンプルにし、したがってそこのバグを取り除きます。

要約すると、不変性は並行性を適切に処理するために必要な課題を解決しますが、排除しません。その助けは普及する傾向がありますが、最大の利点はパフォーマンスよりも品質の観点からです。そして、不変性は魔法のようにアプリの同時実行を管理することを許しません。申し訳ありません。


+1これは理にかなっていますが、深く不変の言語のどこで、並行性を適切に処理することをまだ心配する必要があるのか​​例を示していただけますか?もしあなたが、しかし、そのようなシナリオは私には明確ではないという状態
ジミー・ホッファ

@JimmyHoffa不変の言語では、スレッド間で状態を更新するために何らかの方法が必要です。私が知っている最も不変な2つの言語(ClojureとHaskell)は、スレッド間で変更された状態を出荷する方法を提供する参照型(atomsとMvars)を提供します。ref型のセマンティクスにより、特定の種類の同時実行エラーが防止されますが、他のエラーも引き続き可能です。
ストーンメタル

@stonemetalは興味深いです。Haskellとの4か月間、私はMvarsについても聞いていませんでした。Erlangのメッセージパッシングのように振る舞う並行状態通信にSTMを使用すると聞いていました。私が考えることができる同時性の問題を解決できない不変性の完璧な例はUIの更新ですが、異なるバージョンのデータでUIを更新しようとする2つのスレッドがある場合、1つは新しい可能性があるため、2つ目の更新を取得する必要があります詳細については、あなたが...何とか興味深い考えをシーケンシングを保証しなければならない競合状態...おかげ
ジミー・ホッファ

1
@jimmyhoffa-最も一般的な例はIOです。言語が不変であっても、データベース/ウェブサイト/ファイルは不変です。もう1つは、典型的なmap / reduceです。不変性とは、マップの集約がより確実であることを意味しますが、「すべてのマップが並行して行われたら、調整」を処理する必要があります。
Telastyn

1
@JimmyHoffa:MVarsは低レベルの可変並行性プリミティブ(技術的には、可変ストレージの場所への不変の参照)であり、他の言語で見られるものとあまり変わらない。デッドロックと競合状態が非常に起こりやすい。STMは、ロックフリーの可変共有メモリ(メッセージの受け渡しとは非常に異なる)の高レベルの並行性抽象化であり、デッドロックや競合状態の可能性のない構成可能なトランザクションを可能にします。不変データはスレッドセーフであり、他に何も言うことはありません。
CAマッキャン

13

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

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

これがどのように機能するかを説明するために、C#でいくつかの簡単な例を提供します。これらの例が真実であるためには、いくつかの仮定をしなければなりません。まず、コンパイラがエラーなしで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();
} 

string result可変オブジェクトに置き換えたことに注目してくださいStringBuilder。これは最初の例よりもはるかに高速です。ループを通過するたびに新しい文字列が作成されないためです。代わりに、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();
        }
    } 
}

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

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

並行性シナリオで実装がどのようにリークされる可能性があるかの例については、こちらを参照してください


2
誤解しない限り、a Listは可変であるため、「純粋」と主張した最初の関数では、別のスレッドがforeachループ内にあるリストからすべての要素を削除するか、さらに多くの要素を追加できます。ed IEnumeratorwhile(iter.MoveNext())edでどのように再生されるかIEnumeratorは定かではありませんが、不変(疑わしい)でない限り、foreachループをスラッシングする恐れがあります。
ジミー・ホッファ

確かに、スレッドが読み取り中にコレクションが書き込まれることはないと想定する必要があります。メソッドを呼び出す各スレッドが独自のリストを作成する場合、これは有効な仮定になります。
ロバートハーヴェイ

参照によって使用している可変オブジェクトが含まれている場合、「純粋」と呼ぶことはできないと思います。それはIEnumerableをを受け取った場合、あなたは可能性がある追加またはIEnumerableをから要素を削除することはできませんので、その主張をすることができるが、それが配列されたり、IEnumerableを契約が任意のフォームを保証するものではありませんので、リストには、IEnumerableをとしてで手渡し純度の。その関数を純粋にする実際の手法は、コピーによるパスの不変性です。C#はこれを行わないため、関数が受け取ったときにリストをすぐにコピーする必要があります。しかし、それを行うための唯一の方法は...それにforeachのである
ジミー・ホッファ

1
@JimmyHoffa:くそー、私はこの鶏と卵の問題に夢中になった!どこかで解決策を見つけたら、私に知らせてください。
ロバートハーベイ

1
今、この答えに出くわしましたが、これは私が出会ったトピックに関する最も良い説明の1つです。例は非常に簡潔で、本当に簡単に理解できます。ありがとう!
スティーブンバーン

4

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

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

パート2への回答-ロックは、ロックなしよりも常に遅いはずです。


3
パート2では、「ロックと不変の構​​造の間のパフォーマンスのトレードオフは何ですか?」答えさえあるなら、おそらくそれはそれ自身の質問に値するでしょう。
ロバートハーベイ

4

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

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回目は1回目、3回目は1回目などに成功します。データを更新します。「助言」ロックを使用すると、状況を大幅に改善できます。スレッドが更新を試みる前に、「競合」インジケータが設定されているかどうかを確認する必要があります。もしそうなら、更新を行う前にロックを取得する必要があります。スレッドが更新にいくつか失敗した場合、競合フラグを設定する必要があります。ロックを取得しようとするスレッドが、他に誰も待機していないことを検出した場合、競合フラグをクリアする必要があります。ここでのロックは「正確さ」のために必要ではないことに注意してください。コードがなくてもコードは正しく機能します。ロックの目的は、成功する可能性が低い操作にコードが費やす時間を最小限に抑えることです。


4

で始まる

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

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

しかし、あなたはこれをまったく間違った観点から見ています。オブジェクトが不変かどうかは関係なく、重要なのはオブジェクトを変更するかどうかです。あなたが言っていることは、「運転免許試験を一度も受けなければ飲酒運転の運転免許証を決して失うことはできない」と言っているようなものです。本当ですが、むしろポイントがありません。

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


2
const std::string貧しい例であり、赤いニシンのビットです。C ++文字列は可変であり、constとにかく不変性を保証することはできません。それがするすべては、const関数だけが呼び出されるかもしれないということです。ただし、これらの関数は内部状態を変更する可能性があり、constキャストすることができます。最後に、他の言語と同じ問題があります。私の参照があなたの参照も同じというconstわけではありません。いいえ、真に不変のデータ構造を使用する必要があります。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.