この講演では 、Rich HickeyがClojureを紹介します。彼はAntのデモを行い、ソフトウェアトランザクションメモリ(STM)システムを実装する動機について語っています。
彼のSTMの推論は"locks are hard to reason about"
。
私はそれを、2つのスレッドが同じアイテムを更新しようとしていて、他のスレッドが必要とするリソースを保持しているというデッドロック状態を意味していると考えました。
アトミックであるためには、1つのスレッドをロールバックして再試行する必要があり、もう1つのスレッドはロールフォワードする必要があります。
これをオブジェクト指向言語で機能させるには、STMフレームワークを使用するか、手動でロールする必要があります。
それは私の理解ですが、私は問題のケースを1つだけカバーしたと思います。私が見落としているより一般的な原則があると想像します。おそらく問題は、ロックが構成されていないことです。
私の質問は:OOでロック(センチネル)を推論するのが難しい実際の理由は何ですか?
これは特にOO)とは関係ありません。問題は、問題の他の部分で使用されている他のパラダイムに関係なく、同時実行へのアプローチとしての「明示的ロック」パラダイムの問題です。
私はその2時間の話を見ていないので、ヒッキーの推論についてはコメントしません。以下は、私が最もよく聞くロックの批判であり、要約されています。
ロックは構成できません。独自に正しく(特にアトミックに)機能する2つのコードAとBがある場合、A; B
を記述して2つのアクションを実行することはできません一緒に、アトミックに。少なくとも、ロックAとBをいじくり回す(適切な理由でプライベートであることが多い)か、さらに別のロックを導入し、それを遵守する必要のあるすべての場所に導入することなくしてはいけません。
ロックは、通常実装されているように、保護する対象と取得する必要がある時期については正式化されていません。システムに問題がなければ、文書化されますが、プログラマーはルールを順守するための自動化された支援を受けません。
ロックは低レベルです。それらの相互作用と同期の意味を正しく理解するには、同時実行がインターリーブする可能性のあるすべての可能な方法と、コードを並べ替える可能性のあるすべての可能な方法を考慮する必要があります。フロー依存の推論は、単一の実行スレッドではすでにトリッキーですが、並行性により、検討すべきパスの数が急増します。
関数型プログラミングの支持者は、OOPもはるかに多くの同期を必要とすることを主張します。これは、はるかに多くの共有の可変状態を使用するためですが、これはここの要点の横にあります。必要な同期の量は、選択方法とは直交しています。同期します。
理解しにくいのはロックではありません。ロックは簡単です。実生活のロックと同じように機能します。 理解するのが難しいのは並行性です
ロックが人々の頭を回転させる唯一の理由は、並行システムでのみ使用することです。しかし、少しの間、同期がまったく行われていない並行性の高いシステムがあることを少し想像してみてください。システムの状態は、時間の経過とともに、陽気に無効な構成に変化します。これらの構成の多くは、理解するのと同じくらい難しいものです。彼らは人々に「ええ?地球上でどのようにこれが起こるのでしょうか?」と行きます。その理由は、現代のタイムスライシングシステムがもたらす同時および交互の変更の奇妙な組み合わせを理解するように構築されていないためです。実生活では他にほとんどそのように機能しないので、私たちの脳はそれで本当に苦労しています。
STMの経験はありませんが、ロックの部分について説明します。
シングルスレッドコードを正しく取得するのは困難です。普及している方法論は、単体テストの大規模なバッテリーを作成することであり、それらすべてが合格した場合、何も問題がないことを期待しています。生命の損失や数十億ドルの損失をもたらさないコードの場合、それは妥当な概算ですが、私が作ろうとしているポイントは証明コードを修正するのは難しいということです。ほとんどの人が努力を惜しみません。
次に、ロックを使用した並行コードを検討します。それが機能することを証明するには、スレッドを手動で適切に同期したことを証明する必要があります可能なすべてのインターリービング/スケジューリング。それは記念すべき仕事です。そして、もしあなたが失敗した場合、コードは非決定的であるため、バグのある動作も非決定的です!シングルスレッドコードでリソースを追跡する方が簡単である(ただし、正しく処理するのが難しい)ことを除いて、手動のメモリ管理と同じです。
ロックを使用するさらに悪いコードは構成できません。 2つのコレクションがあり、それらに対するすべての操作をスレッドセーフにする場合を考えます。ここで、1つのコレクションからアイテムを削除して、もう1つのコレクションに追加するとします。要素の削除がスレッドセーフであり、要素の追加がスレッドセーフであるという理由だけで、操作全体がスレッドセーフであるとは限りません。スレッドセーフコレクションのコードを記述し、同僚がプログラムを記述しなければならない場合、それはthemにあり、アクションの組み合わせが安全であることを確認します。挿入と削除のコードが正しいことを証明するシングルスレッドのシナリオとは対照的に、コレクション間での要素の移動が正しいことも証明されます。また、割り当てられたメモリの解放が関数呼び出し元の仕事である手動メモリ管理と比較してください。
OOがこれにどのように適合するかは本当にわかりません。ロックベースの同時実行は、どのパラダイムを選択しても難しいです。
OOPと並行性に関する特定の問題は、プログラマーがカプセル化を使用してスレッドセーフの実装を隠そうとする誘惑に駆られることです。
これをロックをオブジェクトに焼き付けることによって行う場合、ロックが引き起こす副作用のため、ほとんどの場合、それはリークのある抽象化です。
これらの隠されたロックがたくさんあると、競合状態に陥る可能性は低くなりますが、デッドロックを取得するのは非常に簡単になります(おそらく、それ自体が実際には単なる競合状態ですが、私が理解したと思います)。したがって、デッドロックを回避するために、特定のメソッドを呼び出す順序について考える必要があります。理由は簡単ではありません。そして、必要なカプセル化はありません。
または、ロックをよりまばらに使用して、デッドロックを回避しやすくします。しかし、奇妙な/予期しない動作を回避するためにロックをどこに置くべきかを判断することも困難です。これはまた、オブジェクトの実装をwhenと密接に結合しなければならないことがよくあることを意味します(単にhowとは対照的に)、それは外部によってインターフェースされます世界。したがって、実際のカプセル化もありません。
つまり、オブジェクトを持っているが同時実行性の扱いを難しくするということではありません。オブジェクトにそれらへの同時アクセスを処理させようとすると、それらのオブジェクトの処理が難しくなるということです。たとえば、常にビットセーフなスレッドセーフではないモデルを常に持つことができ、すべての相互作用がスレッドセーフティを扱うファサードを介して(リアクターパターンを介して)ルーティングされることを保証するだけです。次に、ファサードは並行性に対処する単一の責任を負うと述べた。
ロックベースの同時実行コードの特定の行の正確さを理解するには、プログラム全体を理解する必要があります。
これで少し節約できます。そのビットのロックベースの同時実行コードでアクセスされるすべてのものが特定のロック内でのみアクセスされることを自分で証明できた場合、そのロックにアクセスするプログラムのすべてのコードのビット、つまりその状態を理解する必要があるだけです。ロックアクセスを開始し、それを保持している間に行うすべてのこと。
これは、ロックベースの同時実行性がデータへのアクセスを一度に限られた数のスレッドにロックすることによって「スレッドの安全性」のみを維持するためです(場合によっては1、時にはリーダーのみなど)。したがって、ロックシステムが実際に望んでいることを実行しているかどうかを確認するには、すべてのロック動作とロック中の動作を理解する必要があります。
次に、ロックはデッドロックの影響を受けるため、複数のロックを理解するには、ロック内のコードだけでなく、ロックを取得できるすべてのシーケンスを理解する必要があります。
そして、私たちの言語と型システム助けにならないでくださいこれらのタスクでそれだけです。
悪くなる。追跡が難しい他のエラーや一般的なエラーと同様に、デッドロックや競合状態の問題は通常、確実に発生するわけではありません。コードを記述し、いくつかの簡単なテストを実行し、本番環境で実行して、ほとんど常に動作させることができます。ときどき見事に失敗することを除いて。
本当にまれな壮大な障害はデバッグが困難です無視理由を簡単に説明できます。ここでは、推論するのが難しい稀な、壮観な失敗があります。
そしてさらに悪化します。プログラミングの方法を学ぶとき、人々は通常いくつかのハードルにぶつかります。状態/割り当て/データフローを理解することは、一部の人が乗り越えられないハードルです。条件と関数呼び出しを理解することは別です。再帰と反復により、ループが発生する場合があります。間接参照とポインターは、プログラマーのセット全体が有用になるのを遮断します。資源管理。また、同時実行性と同期化もそのようなハードルの1つです。
これらのハードルを生産性の「邪魔になる」ところから取り除くことで、より便利なプログラマーを生み出すことができます。プログラマーから間接参照とポインターを隠し、大きなリソース管理タスク(メモリー)を処理する言語があり、そのハードルを乗り越えることなく、それらの言語で生産的なプログラマーになる方法を学ぶことができます。
ほとんどすべての言語で、並行性のハードルを乗り越えることなく「生産的なプログラマ」になることができます。有用なコードを作成したり、プロの開発者になったり、大学を卒業したりすることができます。
フロー制御、データフロー、反復を行わなかったものは、プロのプログラマーにはなりませんでした。同時実行性が得られないのは、多くの企業の生産的な上級開発者です。
ロックは理解するのが難しくなく、並行性も特に理解するのが難しくありません(私たちの周りで起こっているほとんどすべてが並行しています)。 ロックのスケジュールわかりづらいです。実際のブロックではほとんど何もありませんが、シングルスレッドのシーケンシャルコンピューティングではすべてが機能します。スレッドやプロセスを分割することで行うことは、実際には常にお互いをブロックしているときに、いくつかのふりをすることです。そのブロックのスケジュールを立てるのは本当に難しいです。 1つのCPU時間をスケジュールするのは難しいし、共有のステートフルリソースもスケジュールするのは難しいですが、両方を実行するのは本当の獣です。
Erlangランタイムなどを入力します。同時実行は簡単で、期待どおりに機能します。これは、ランタイムがスケジューリングを処理して、実際に解決しようとしている問題について考える時間を解放するためです。スケジューリングは大きな勝利であり、常に宣伝されている他のものではありません。簡単な同時実行のコストは、no状態を共有できることです。これは、ほとんどのプログラマが快適に感じるものではないようです。 「Erlangの本を半分読んで、あきらめた」ダンスをし、彼らが始めたところに巻き戻される人々の発生率は非常に高いです。
問題を解決するために選択する言語または環境が何であれ、問題の性質はロック、状態、または言語ではなく、状態が共有されるときは常にロックのスケジューリングであることを認識することが重要です。