JavaおよびC#によってメモリの安全性が提供されるのと同様のプログラミング言語によって、スレッドの安全性をどのように提供できますか?


10

JavaとC#は、配列の境界とポインターの逆参照をチェックすることにより、メモリの安全性を提供します。

競合状態やデッドロックの可能性を防ぐために、プログラミング言語にどのようなメカニズムを実装できますか?


3
Rustの機能に興味があるかもしれません:RustとのFearless Concurrency
Vincent Savard

2
すべてを不変にするか、すべてを安全なチャネルと非同期にします。GoErlangにも興味があるかもしれません。
Theraot

@Theraotは「すべてを安全なチャネルと非同期にする」-それについて詳しく説明できればと思います。
mrpyo

2
@mrpyoプロセスまたはスレッドを公開しません。すべての呼び出しは約束であり、すべてが同時に実行されます(ランタイムが実行をスケジュールし、必要に応じてバックグラウンドでシステムスレッドを作成/プールします)。状態を保護するロジックはメカニズムにあります情報を渡す...ランタイムはスケジューリングによって自動的にシリアル化できます。特に、プロデューサー/コンシューマーと集計が必要な、よりニュアンスのある動作のためのスレッドセーフなソリューションを備えた標準ライブラリがあります。
Theraot

2
ちなみに、別のアプローチとして、トランザクションメモリがあります
Theraot

回答:


14

競合は、オブジェクトのエイリアスが同時にあり、少なくとも1つのエイリアスが変化している場合に発生します。

したがって、競合を防ぐには、これらの条件の1つ以上を偽りにする必要があります。

さまざまなアプローチがさまざまな側面に取り組みます。関数型プログラミングは不変性に重点を置いており、それによって変異性が取り除かれます。ロック/アトミックは同時性を削除します。アフィンタイプはエイリアスを削除します(Rustは変更可能なエイリアスを削除します)。アクターモデルは通常、エイリアシングを削除します。

エイリアスを設定できるオブジェクトを制限することで、上記の条件を回避しやすくなります。そこにチャネルやメッセージパッシングスタイルが含まれます。任意のメモリにエイリアスを設定することはできません。競合のないように配置されたチャネルまたはキューの最後だけをエイリアス化することはできません。通常、同時性、つまりロックやアトミックを回避することによって。

これらのさまざまなメカニズムの欠点は、作成できるプログラムが制限されることです。制限がはっきりしないほど、プログラムは少なくなります。したがって、エイリアシングやミュータビリティーは機能せず、理由は簡単ですが、非常に限定的です。

それが、Rustがそのような動揺を引き起こしている理由です。エイリアシングとミュータビリティをサポートするエンジニアリング言語です(アカデミック言語とは異なります)が、それらが同時に発生しないことをコンパイラチェックします。理想的ではありませんが、これまでの多くのプログラムよりも大きなクラスのプログラムを安全に作成できます。


11

JavaとC#は、配列の境界とポインターの逆参照をチェックすることにより、メモリの安全性を提供します。

C#とJavaがこれをどのように行うかを最初に考えることが重要です。これは、CまたはC ++での未定義の動作を定義済みの動作に変換することによって行います。プログラムをクラッシュさせます。Null逆参照と配列インデックスの例外は、正しいC#またはJavaプログラムでキャッチされるべきではありません。プログラムにはそのバグがないはずなので、そもそもそれらは起こらないはずです。

しかし、それはあなたがあなたの質問で何を意味しているのではないと私は思います!相互に待機しているn個のスレッドがあるかどうかを定期的にチェックし、それが発生した場合にプログラムを終了する「デッドロックセーフ」ランタイムを簡単に作成できますが、満足できるとは思いません。

競合状態やデッドロックの可能性を防ぐために、プログラミング言語にどのようなメカニズムを実装できますか?

私たちがあなたの質問に直面する次の問題は、デッドロックとは異なり、「競合状態」は検出が難しいことです。スレッドセーフの目的は、レースをなくすことではないことを忘れないでください。私たちの狙いは、誰がレースに勝っても、プログラムを正しくすることです!競合状態の問題は、2つのスレッドが未定義の順序で実行されており、誰が最初に終了するかわからないことではありません。競合状態の問題は、開発者がいくつかのスレッド終了の順序が可能であることを忘れ、その可能性を説明できないことです。

したがって、質問は基本的に「プログラミング言語が私のプログラムが正しいことを保証できる方法はありますか?」に要約されます。その質問への答えは、実際には違います。

これまでのところ、私はあなたの質問を批判しただけです。ここでギアを切り替えて、あなたの質問の精神に取り組みましょう。言語設計者がマルチスレッドで私たちが直面している恐ろしい状況を緩和することができる選択はありますか?

状況は本当に恐ろしいです!マルチスレッドコードを正しく取得することは、特に弱いメモリモデルアーキテクチャでは非常に困難です。なぜそれが難しいのかを考えることは有益です。

  • 1つのプロセスで複数の制御スレッドを使用することは、論理的に考えるのが困難です。1本の糸で十分です!
  • 抽象化は、マルチスレッドの世界では非常に漏洩しやすくなります。シングルスレッドの世界では、プログラムが実際に順番に実行されなくても、プログラムが順番に実行されるかのように動作することが保証されています。マルチスレッドの世界では、そうではありません。単一のスレッドでは見えない最適化が見えるようになり、開発者はこれらの可能な最適化を理解する必要があります。
  • しかし、それはさらに悪化します。C#仕様では、実装は、すべてのスレッドで合意できる読み取りと書き込みの一貫した順序を持つ必要はないことを述べています。「人種」がまったくあり、勝者がはっきりしているという考えは、実際には真実ではありません。多くのスレッドでいくつかの変数に対する2つの書き込みと2つの読み取りがある状況を考えます。賢明な世界では、「まあ、誰がレースに勝つかはわかりませんが、少なくともレースはあり、誰かが勝つでしょう」と思うかもしれません。私たちはその賢明な世界にはいません。C#を使用すると、読み取りと書き込みの順序について、複数のスレッドで合意できません。誰もが観察している一貫した世界があるとは限りません。

したがって、言語デザイナーが物事をより良くすることができる明白な方法があります。現代のプロセッサのパフォーマンスの勝利を放棄します。マルチスレッドのプログラムであっても、すべてのプログラムに非常に強力なメモリモデルを持たせます。これにより、マルチスレッドプログラムの速度が大幅に低下し、パフォーマンスが向上するという理由から、マルチスレッドプログラムがそもそも存在しない理由に直接作用します。

メモリモデルを別にしても、マルチスレッド化が難しい理由は他にもあります。

  • デッドロックを防止するには、プログラム全体の分析が必要です。ロックが取り出されるグローバルな順序を知っている必要があり、プログラムが異なる組織によって異なる時点で作成されたコンポーネントで構成されている場合でも、プログラム全体にその順序を適用する必要があります。
  • マルチスレッドを管理するために提供する主なツールはロックですが、ロックを構成することはできません

その最後の点は、さらに説明があります。「構成可能」とは、以下を意味します。

doubleを指定してintを計算したいとします。計算の正しい実装を記述します。

int F(double x) { correct implementation here }

intを与えられた文字列を計算したいとします:

string G(int y) { correct implementation here }

ここで、与えられたdoubleで文字列を計算したい場合:

double d = whatever;
string r = G(F(d));

GとFは、より複雑な問題の正しい解に構成されます。

ただし、デッドロックのため、ロックにはこのプロパティがありません。L1、L2の順序でロックを取得する正しいメソッドM1、およびL2、L1の順序でロックを取得する正しいメソッドM2は、誤ったプログラムを作成せずに、同じプログラムで両方を使用することはできません。ロックは、「個々のメソッドがすべて正しいので、全体が正しい」とは言えないようにします。

では、言語デザイナーとして何ができるでしょうか?

まず、そこに行かないでください。1つのプログラムで複数の制御スレッドを使用することは悪い考えであり、スレッド間でメモリを共有することは悪い考えであるため、そもそも言語やランタイムに配置しないでください。

これは明らかにスターターではありません。

次に、より基本的な質問に注意を向けましょう:なぜ最初に複数のスレッドがあるのですか?主な理由は2つあり、非常に異なりますが、同じものに頻繁に混同されます。どちらもレイテンシの管理に関するものであるため、両者は混乱しています。

  • 誤ってスレッドを作成して、IOレイテンシを管理します。大きなファイルを書き、リモートデータベースにアクセスし、UIスレッドをロックするのではなく、ワーカースレッドを作成する必要があります。

悪いアイデア。代わりに、コルーチンを介してシングルスレッド非同期を使用します。C#はこれを美しく行います。Java、まあまあ。しかし、これが現在の言語デザイナーたちがスレッドの問題を解決するのに役立つ主な方法です。awaitC# のオペレーター(F#非同期ワークフローやその他の先行技術に触発された)は、ますます多くの言語に組み込まれています。

  • スレッドを適切に作成して、アイドル状態のCPUを計算量の多い作業で飽和させます。基本的に、軽量プロセスとしてスレッドを使用しています。

言語設計者は、並列処理でうまく機能する言語機能を作成することで支援できます。たとえば、LINQがPLINQに非常に自然に拡張される方法について考えます。あなたが賢明な人であり、TPL操作を高度に並列でメモリを共有しないCPUにバインドされた操作に制限する場合、ここで大きな利益を得ることができます。

他に何ができますか?

  • コンパイラに最も骨の折れる間違いを検出させ、それらを警告またはエラーに変えます。

C#ではロックで待機することはできません。これはデッドロックのレシピだからです。C#では、値の型をロックすることはできません。これは、常に行うのが間違っているためです。値ではなく、ボックスをロックします。エイリアスは取得/解放のセマンティクスを強制しないため、揮発性のエイリアスを作成すると、C#は警告を出します。コンパイラが一般的な問題を検出して防止する方法は他にもたくさんあります。

  • 「品質のピット」機能を設計します。最も自然な方法は、最も正しい方法でもあります。

C#とJavaは、参照オブジェクトをモニターとして使用できるようにすることで、大きな設計エラーを引き起こしました。これにより、デッドロックの追跡が困難になり、静的に防止することが困難になる、あらゆる種類の悪い習慣が奨励されます。そして、それはすべてのオブジェクトヘッダーのバイトを浪費します。モニターは、モニタークラスから派生する必要があります。

  • Microsoft Researchの膨大な時間と労力がC#のような言語にソフトウェアトランザクションメモリを追加する試みに費やされましたが、メインの言語に組み込むために十分なパフォーマンスを発揮できませんでした。

STMは素晴らしいアイデアであり、私はHaskellでのおもちゃの実装をいろいろと試しました。ロックベースのソリューションよりもはるかにエレガントに正しいパーツから正しいソリューションを作成できます。しかし、詳細については、なぜそれを大規模に機能させることができなかったのかについて十分に理解していません。次に会うときはジョー・ダフィーに聞いてください。

  • 別の答えはすでに不変性について述べています。不変性と効率的なコルーチンを組み合わせると、アクターモデルなどの機能を直接言語に構築できます。たとえば、Erlangを考えてみてください。

プロセス計算ベースの言語については多くの研究が行われており、私はその領域をよく理解していません。自分でいくつかの論文を読んでみて、洞察が得られるかどうかを確認してください。

  • サードパーティが優れたアナライザーを簡単に作成できるようにする

MicrosoftでRoslynに携わった後、Coverityで働きました。私がしたことの1つは、Roslynを使用してアナライザーフロントエンドを取得することでした。Microsoftが提供する正確な字句解析、構文解析、および意味解析を行うことで、一般的なマルチスレッドの問題を検出する検出器を作成するというハードワークに集中できます。

  • 抽象化のレベルを上げる

私たちが人種やデッドロックなどを抱えている根本的な理由何をすべきかを示すプログラムを書いているためです。コンピュータはあなたが言うことをします、そして私たちは間違ったことをするようにそれを伝えます。最近のプログラミング言語の多くは、宣言型プログラミングにますます重点を置いています。どのような結果を望んでいるかを言い、その結果を達成するための効率的で安全な正しい方法をコンパイラーに理解させてください。もう一度、LINQについて考えてみましょう。意図from c in customers select c.FirstNameを表現したいのです。コンパイラーにコードの記述方法を理解させます。

  • コンピューターを使用してコンピューターの問題を解決します。

機械学習アルゴリズムは、手作業でコーディングしたアルゴリズムよりも一部のタスクで優れていますが、正確さ、トレーニングにかかる​​時間、不適切なトレーニングによって生じるバイアスなど、多くのトレードオフがあります。しかし、現在「手動」でコーディングしている非常に多くのタスクが、すぐに機械で生成されたソリューションに対応できるようになる可能性があります。人間がコードを書いていなければ、バグを書いていません。

申し訳ありませんが少し混乱していました。これは巨大で難しいトピックであり、私がこの問題の分野で進展を追い続けてきた20年の間に、PLコミュニティでは明確なコンセンサスが生まれていません。


「つまり、あなたの質問は基本的に「プログラミング言語が私のプログラムが正しいことを保証できる方法があるのか​​」ということになります。その質問に対する答えは、実際にはありません。」-実際、それはかなり可能です-これは正式な検証と呼ばれ、不便ですが、重要なソフトウェアで日常的に行われていると確信しているので、実用的ではありません。しかし、言語デザイナーであるあなたはおそらくこれを知っています...
mrpyo

6
@mrpyo:私はよく知っています。多くの問題があります。最初に、私はかつて正式な検証会議に参加しました。そこではMSFT研究チームがエキサイティングな新しい結果を発表しました。彼らはテクニックを拡張して長さが最大20行のマルチスレッドプログラムを検証し、ベリファイアを1週間未満で実行できました。これは興味深いプレゼンテーションでしたが、私には役に立ちませんでした。分析する2000万行のプログラムがありました。
Eric Lippert

@mrpyo:第二に、私が述べたように、ロックの大きな問題は、スレッドセーフメソッドで作成されたプログラムが必ずしもスレッドセーフプログラムであるとは限らないことです。個々のメソッドを正式に検証することは必ずしも効果的ではありません。プログラム全体の分析は、重要なプログラムでは困難です。
Eric Lippert

6
@mrpyo:第三に、形式分析の大きな問題は、基本的に私たちが何をしているのかということです。前提条件と事後条件の仕様を提示し、プログラムがその仕様を満たしていることを確認しています。すごい; 理論的にはそれは完全に実行可能です。仕様はどの言語で書かれていますか?明確で検証可能な仕様言語がある場合は、すべてのプログラムをその言語で記述し、コンパイルます。なぜこれをしないのですか?スペック言語で正しいプログラムを書くことも本当に難しいことが判明したからです!
Eric Lippert

2
事前条件/事後条件を使用してアプリケーションの正確さを分析することが可能です(たとえば、コーディングコントラクトを使用)。ただし、このような分析は、条件が構成可能でロックがそうでない場合にのみ実行可能です。また、分析を可能にする方法でプログラムを作成するには、注意深い規律が必要であることにも注意します。たとえば、Liskov置換原理に厳密に準拠していないアプリケーションは、分析に抵抗する傾向があります。
ブライアン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.