web-dev-qa-db-ja.com

本質的に並列実行に適応させる関数型プログラミングについてはどうですか?

私は関数型言語が並列処理に理想的(または少なくとも非常に頻繁に役立つ)であることを何度も繰り返し読んでいます。どうしてこれなの?どのコアコンセプトとパラダイムが一般的に採用され、どの特定の問題を解決しますか?

たとえば、抽象レベルでは、不変性がリソースの競合に起因する競合状態やその他の問題の防止にどのように役立つかを理解できますが、それ以上に具体的に説明することはできません。ただし、私の質問の範囲はjust不変性よりも広いことに注意してください-適切な答えは、いくつかの関連する概念の例を提供します。

44
blz

主な理由は、参照順序の透明性(さらには遅延)が実行順序を抽象化するためです。これにより、評価の並列化が簡単になります。

たとえば、ab||の両方が参照透過である場合、次の場合は問題ではありません

a || b

aが最初に評価される、bが最初に評価される、またはbがまったく評価されない(atrueに評価されたため)。

a || a

aが1回または2回評価されるかどうかは関係ありません(または、5回でも評価されます…意味がありませんが、それでも関係ありません)。

したがって、それらが評価される順序が重要でなく、それらが不必要に評価されるかどうかも重要でない場合は、単純にすべてのサブ式を並行して評価できます。したがって、abを並行して評価し、次に、||は2つのスレッドのいずれかが完了するのを待って、返されたものを確認し、trueを返した場合、もう1つをキャンセルしてすぐにtrueを返すこともできます。

Every部分式は並行して評価できます。ささいなこと。

ただし、これは特効薬ではないことに注意してください。 GHCのいくつかの実験的な初期バージョンはこれを行い、それは災難でした:多すぎる潜在的な並列処理がありました。単純なプログラムでさえ、数百、数千、数百万のスレッドを生成する可能性があり、圧倒的多数のサブ式では、スレッドを生成するのに、最初に式を評価するよりもはるかに時間がかかります。非常に多くのスレッドがあるため、コンテキスト切り替え時間が、有用な計算を完全に支配します。

関数型プログラミングは頭を悩ませます。通常、問題はシリアルプログラムを適切なサイズの並列「チャンク」に分割する方法ですが、関数型プログラミングでは、問題は並列サブをグループ化する方法です。 -シリアル「チャンク」にプログラムします。

GHCが今日行う方法は、2つの部分式に手動で注釈を付けて、並行して評価できるようにすることです。これは実際には、2つの式を別々のスレッドに入れることにより、命令型言語でそれを行う方法と似ています。ただし、重要な違いがあります。この注釈を追加しても、プログラムの結果が変わることはありません。それはより速くすることができ、より遅くすることができ、より多くのメモリを使用することができますが、できません結果を変更します。これによりway並列処理の実験が容易になり、適切な量の並列処理と適切なサイズのチャンクを見つけることができます。

68
Jörg W Mittag

まず、同時実行スレッドで手続き型プログラミングが非常に悪い理由を見てみましょう。

並行プログラミングモデルでは、分離して実行されることを(デフォルトで)期待する順次命令を記述します。複数のスレッドを導入する場合、それらの変更が互いに影響し合う可能性がある場合に、共有変数への同時アクセスを防ぐために、アクセスを明示的に制御する必要があります。これは正しいプログラミングが難しいプログラミングであり、テストでは、それが安全に行われたことを証明することは不可能です。せいぜい、このテストの実行時に、観察可能な問題が発生していないことのみを確認できます。

関数型プログラミングでは、問題は異なります。共有データはありません。同じ変数への同時アクセスはありません。実際、変数を設定できるのは1回だけです。「forループ」はなく、特定の値のセットで実行すると常に同じ結果を実行するコードのブロックがあります。これにより、テストは予測可能であり、正確性の良い指標となります。

開発者が関数型プログラミングで解決しなければならない問題は、一般的な状態を最小化するソリューションをどのように設計するかです。設計の問題で高レベルの同時実行性と最小限の共有状態が必要な場合、機能の実装は非常に効果的な戦略です。

4
Michael Shaw

最小化された共有状態

本質的に並列実行に適応させる関数型プログラミングについてはどうですか?

関数の純粋な性質( 参照透過性 )、つまり副作用がないため、共有オブジェクトが少なくなり、共有状態が少なくなります。

簡単な例は次のとおりです。

_double CircleCircumference(double radius)
{
  return 2 * 3.14 * radius; // constants for illustration
}
_

出力は入力にのみ依存し、含まれる状態はありません。 GetNextStep();などの関数とは対照的に、出力は現在のステップが何であるかに依存します(通常、オブジェクトのデータメンバーとして保持されます)。

ミューテックスとロックを介してアクセスを制御する必要があるのは共有状態です。共有状態についてより強力な保証があると、並列最適化と並列構成の改善が可能になります。


参照の透明性と純粋な式

参照の透明性についての詳細は、 ここではProgrammers.SEにあります を参照してください。

[それ]は、プログラムの意味を変更せずに、プログラム内の任意の式をその式を評価した結果(またはその逆)で置き換えることができることを意味します。 JörgW Mittag

これにより、純粋な関数(同じ入力引数が与えられた場合に同じ結果に評価される関数)を構築するために使用される 純粋な式 が許可されます。したがって、各式を並行して評価できます。

3
Niall

並列コードを書く際の最も難しい部分は、別のスレッドによって更新されているデータを1つのスレッドが読み取らないようにすることです。

これに対する一般的な解決策は、不変オブジェクトを使用することです。これにより、オブジェクトが作成されると、オブジェクトは決して更新されません。しかし、実際のデータは変更する必要があるため、更新はすべて新しいオブジェクトを返す "persistence" data が使用されます。これは、データ構造を慎重に選択することで効率化できます。

関数型プログラミングでは副作用が許可されないため、通常、関数型プログラミングを行う場合は「永続オブジェクト」が使用されます。 そのため、副作用がないために関数型プログラマーが強いられる苦痛は、並列プログラミングでうまく機能するソリューションにつながります。

もう1つの利点は、関数型言語システムが規則に従っていることを確認し、そのために役立つ多くのツールがあることです。

1
Ian

純粋な関数コードはデフォルトでスレッドセーフです。

それ自体、すでに大きな勝利です。他のプログラミング言語では、完全にスレッドセーフなコードブロックを設計することは、非常に困難な場合があります。しかし、純粋な関数型言語forcesあなたはeverythingをスレッドセーフにしますが、スレッドセーフではない何かを明示的に行ういくつかの場所を除きます。命令型コードでは、スレッドセーフであることは明示的であり(したがって、通常はまれ)、一方、純粋な関数型コードでは、スレッド非安全であることが明示的です(したがって、通常はまれです)。

命令型言語では、問題のほとんどは、奇妙なデータの競合を防ぐために十分なロックを追加する方法ですが、パフォーマンスを殺したり、ランダムなデッドロックを引き起こしたりするほど多くはありません。純粋な関数型言語では、ほとんどの場合、そのような考慮事項は議論の余地があります。ここで問題となるのは、作業を均等に分割する方法を理解することだけです。

極端な場合、利用可能なコアをすべて使用するわけではない、ごく少数の並列タスクがあります。もう1つの極端な例では、何十億もの小さなタスクがあり、非常に多くのオーバーヘッドが発生するため、すべての速度が低下します。もちろん、特定の関数呼び出しが行う「作業量」を把握しようとすることは、しばしば非常に困難です。

これらの問題はすべて、命令コードにも存在します。命令型プログラマーは通常、物事をうまく動かそうとするだけで忙しすぎてまったくタスクサイズの微妙な微調整について心配するだけです。

1