web-dev-qa-db-ja.com

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

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

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

10
mrpyo

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

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

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

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

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

これがRustがそのような混乱を引き起こしている理由です。これは、エイリアシングと可変性をサポートするエンジニアリング言語ですが、エイリアシングと可変性をサポートしますが、それらが同時に発生しないことをコンパイラにチェックさせます。理想的ではありませんが、これにより、従来の多くのプログラムよりも大きなクラスのプログラムを安全に作成できます。

14
Alex

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

最初にC#とJavaがこれを行う方法について考えることは重要です。それらはundefinedの動作を変換することによってそうしますCまたはC++を定義された動作に変換:プログラムをクラッシュさせます。null逆参照と配列インデックスの例外がcaught正しい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、まあまあ。しかしこれは、現在の言語デザイナーがスレッド化問題の解決を支援している主な方法です。 C#のawait演算子(F#非同期ワークフローおよびその他の先行技術に触発された)は、ますます多くの言語に組み込まれています。

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

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

他に何ができますか?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

11
Eric Lippert