web-dev-qa-db-ja.com

マルチスレッドおよびマルチプロセッサプログラミングで使用すべきでない廃止されたプラクティスはありますか?

FORTRANとBASICの初期の頃は、基本的にすべてのプログラムがGOTOステートメントで記述されていました。その結果がスパゲッティコードで、ソリューションは構造化プログラミングでした。

同様に、ポインターはプログラムの特性を制御するのが難しい場合があります。 C++は多くのポインタで始まりましたが、参照の使用をお勧めします。 STLのようなライブラリは、依存関係の一部を減らすことができます。より優れた特性を持つスマートポインタを作成するためのイディオムもあり、C++の一部のバージョンでは、参照とマネージコードを許可しています。

継承やポリモーフィズムなどのプログラミングプラクティスでは、舞台裏で多くのポインタを使用します(構造化プログラミングが分岐命令で満たされたコードを生成するのと同じように)。 Java=のような言語では、プログラマーにすべてのnewステートメントとdeleteステートメントを一致させるのではなく、ポインターを排除し、ガベージコレクションを使用して動的に割り当てられたデータを管理します。

私の読書では、セマフォを使用していないように見えるマルチプロセスおよびマルチスレッドプログラミングの例を見てきました。同じものを異なる名前で使用していますか、それとも、リソースの同時使用からの保護を構造化する新しい方法がありますか?

たとえば、マルチコアプロセッサを使用したマルチスレッドプログラミングのシステムの具体例は、OpenMPです。これは、環境に含まれていないように見えるセマフォを使用せずに、次のように重要な領域を表しています。

th_id = omp_get_thread_num();
#pragma omp critical
{
  cout << "Hello World from thread " << th_id << '\n';
}

この例は以下からの抜粋です http://en.wikipedia.org/wiki/OpenMP

あるいは、関数wait()とsignal()を持つセマフォを使用して、スレッドを互いに同様に保護する方法は次のようになります。

wait(sem);
th_id = get_thread_num();
cout << "Hello World from thread " << th_id << '\n';
signal(sem);

この例では、物事は非常に単純であり、単純なレビューだけでwait()呼び出しとsignal()呼び出しが一致していることを示し、多くの同時実行性があってもスレッドセーフティが提供されます。しかし、他のアルゴリズムはより複雑で、複数のセマフォ(バイナリとカウントの両方)を使用し、多くのスレッドから呼び出すことができる複雑な条件を持つ複数の関数にまたがっています。デッドロックを作成したり、スレッドセーフにすることに失敗した場合の結果は、管理が困難な場合があります。

OpenMPのようなこれらのシステムはセマフォの問題を解消しますか?
問題を別の場所に移動しますか?
アルゴリズムを使用してお気に入りのセマフォを変換し、セマフォを使用しないようにするにはどうすればよいですか?

36
DeveloperDon

もはや使用すべきではない並行プログラミングの技法と実践はありますか? はいと言います。

今日では珍しいと思われる初期の並行プログラミング手法の1つは、割り込み駆動型プログラミングです。これは、1970年代にUNIXが機能した方法です。 LionsCommentary on UNIXまたはBach'sDesign of the UNIX Operating Systemを参照してください。簡単に言えば、この手法は、データ構造を操作している間、一時的に割り込みを一時停止し、その後、割り込みを復元することです。 BSD spl(9)man page には、このコーディングスタイルの例があります。割り込みはハードウェア指向であり、コードはハードウェア割り込みの種類とそのハードウェアに関連付けられたデータ構造の間の暗黙的な関係を具体化することに注意してください。たとえば、ディスクI/Oバッファーを操作するコードは、それらのバッファーを操作している間、ディスクコントローラーハードウェアからの割り込みを中断する必要があります。

このプログラミングスタイルは、ユニプロセッサハードウェアのオペレーティングシステムで採用されていました。アプリケーションが割り込みを処理することははるかにまれでした。一部のOSにはソフトウェアの割り込みがあり、その上にスレッドシステムまたはコルーチンシステムを構築しようとした人がいると思いますが、これはあまり普及していませんでした。 (確かにUNIXの世界ではそうではありません。)今日、割り込みスタイルのプログラミングは、小さな組み込みシステムまたはリアルタイムシステムに限定されていると思います。

セマフォは、ソフトウェア構成体(ハードウェアに関連しない)であり、ハードウェア機能の抽象化を提供し、マルチスレッディングとマルチプロセッシングを可能にするため、割り込みよりも進んでいます。主な問題は、それらが構造化されていないことです。プログラマーは、プログラム全体にわたって、各セマフォとそれが保護するデータ構造との間の関係を維持する責任があります。このため、ベアセマフォは現在ほとんど使用されていません。

もう1つの小さな前進は、monitorです。これは、並行性制御メカニズム(ロックと条件)をカプセル化し、データを保護します。これは Mesa(alternate link) システムに引き継がれ、そこからJavaに引き継がれました。 (このMesaペーパーを読むと、Javaのモニターのロックと状態がMesaからほぼ逐語的にコピーされていることがわかります。)モニターは、十分に注意深く勤勉なプログラマーがコードとデータに関するローカルな推論のみを使用して並行プログラムを安全に作成できるという点で役立ちます。モニター内。

JavaのJava.util.concurrentパッケージのライブラリなど、追加のライブラリコンストラクトがあり、高度な並行処理を行うさまざまなデータ構造とスレッドプーリングコンストラクトが含まれています。これらは、スレッドの制限や効果的な不変性などの追加技術と組み合わせることができます。 GoetzらによるJava Concurrency In Practiceを参照してください。 al。さらなる議論のために。残念ながら、多くのプログラマーは ConcurrentHashMap のようなものを実際に使用する必要があるときに、ロックと条件を使用して独自のデータ構造をまだローリングしています。

上記のすべてはいくつかの重要な特性を共有します:それらは、グローバルに共有され、変更可能な状態を介して相互作用する複数の制御スレッドを持っています。問題は、このスタイルでのプログラミングは依然としてエラーが発生しやすいことです。小さな間違いが気付かれずに済むことは非常に簡単で、その結果、再現と診断が困難な誤動作が発生します。この方法で大規模システムを開発するために「十分に注意深く勤勉」なプログラマーはいないでしょう。少なくとも、ごくわずかです。したがって、共有で変更可能な状態のマルチスレッドプログラミングは、可能な限り回避する必要があると思います

残念ながら、すべてのケースで回避できるかどうかは完全には明らかではありません。多くのプログラミングはまだこの方法で行われています。これが他のものに取って代わられるのを見るのはいいことです。 Jarrod Roberson および davidk01 からの回答は、不変データ、関数型プログラミング、STM、メッセージパッシングなどの手法を指します。それらを推奨することはたくさんあり、すべてが積極的に開発されています。しかし、それらが古き良き共有の可変状態を完全に置き換えたとはまだ思っていません。

編集:最後に特定の質問に対する私の応答です。

OpenMPについてはあまり知りません。私の印象は、数値シミュレーションなどの高度に並列化された問題に対して非常に効果的である可能性があることです。しかし、それは汎用的ではないようです。セマフォの構造はかなり低レベルのように見え、プログラマがセマフォと共有データ構造の間の関係を維持する必要があり、上記で説明したすべての問題があります。

セマフォを使用する並列アルゴリズムがある場合、それを変換するための一般的なテクニックは知りません。それをオブジェクトにリファクタリングして、その周りにいくつかの抽象化を構築できる場合があります。ただし、メッセージパッシングなどを使用する場合は、問題全体を再概念化する必要があると思います。

15
Stuart Marks

質問への回答

一般的なコンセンサスは共有の可変状態はBad™で、不変状態はGood™です。これは、関数型言語や命令型言語によっても何度も正確で真であることが証明されています。

問題は、主流の命令型言語がこのように機能するように設計されていないだけであり、それらの言語の状況が一晩で変わることはないということです。これがGOTOとの比較に欠陥があるところです。不変の状態とメッセージパッシングは優れたソリューションですが、万能薬でもありません。

欠陥のある前提

この質問は、欠陥のある前提との比較に基づいています。そのGOTOは実際の問題であり、言語デザイナーおよびソフトウェアエンジニアリングユニオンの銀河系普遍的委員会©によって、どのようにして普遍的に廃止されましたか。 GOTOメカニズムがないと、ASMはまったく機能しません。生のポインタはCまたはC++の問題であり、一部のスマートポインタは万能薬であるという前提と同じですが、そうではありません。

GOTOは問題ではなく、プログラマーが問題でした。 共有された変更可能な状態についても同様です。それ自体は問題ではありません問題、問題はそれを使用しているプログラマーです。 共有の可変状態を使用するコードを生成する方法があった場合never競合状態またはバグの場合、問題にはなりません。 GOTOまたは同等の構文を使用してスパゲッティコードを記述したことがない場合と同様に、問題でもありません。

教育は万能薬です

ばかプログラマーはdeprecatedでした。人気のあるすべての言語には、依然としてGOTO構成要素が直接的または間接的にあり、best practice whenこのタイプの構成を持つすべての言語で適切に使用されている

例:Javaには labelstry/catch/finallyどちらもGOTOステートメントとして直接機能します。

ほとんどのJava私が話をしているプログラマーはimmutableが実際に何を意味するのかさえ知らないので、繰り返しthe String class is immutable彼らの目に見えるようなゾンビを持ちます。彼らは finalキーワードを適切に使用する方法 を知らず、immutableクラスを作成します。したがって、不変メッセージを使用したメッセージングパッシングがなぜ素晴らしいのか、共有された可変状態がそれほど優れていないのか、彼らにはわからないと確信しています。

28
user7519

学術界での最近の大流行は ソフトウェアトランザクションメモリ (STM)のようであり、十分にスマートなコンパイラテクノロジーを使用することで、マルチスレッドプログラミングのすべての細かい詳細をプログラマーの手から取り除くことを約束します。舞台裏ではまだロックとセマフォですが、プログラマとしてあなたはそれを心配する必要はありません。そのアプローチの利点はまだ明確ではなく、明白な候補はありません。

Erlang は、メッセージの受け渡しとエージェントを使用して並行性を実現します。これは、STMよりも作業が簡単なモデルです。メッセージパッシングでは、各エージェントが独自のミニユニバースで動作するため、データに関連する競合状態がないため、ロックやセマフォを心配する必要はありません。あなたはまだいくつかの奇妙なEdgeケースを持っていますが、それらはライブロックやデッドロックほど複雑ではありません。 JVM言語は Akka を利用でき、メッセージパッシングとアクターのすべての利点を得ることができますが、Erlangとは異なり、JVMにはアクターの組み込みサポートがないため、Akkaは依然として利用します。スレッドとロックですが、プログラマとしてのあなたはそれについて心配する必要はありません。

私が知っている、ロックとスレッドを使用しないもう1つのモデルは、非同期プログラミングのもう1つの形式である futures を使用することです。

このテクノロジーがC++でどの程度利用可能かはわかりませんが、スレッドとロックを明示的に使用していないものが見つかる場合は、上記の同時実行性を管理する手法の1つになります。

27
davidk01

これは主に抽象化のレベルについてだと思います。プログラミングではかなり頻繁に、より安全またはより読みやすい方法で、またはそのようなものでいくつかの詳細を抽象化することが有用です。

これは制御構造に適用されます。ifs、fors、およびtry-catchブロックでさえ、gotosを抽象化したものです。これらの抽象化は、コードを読みやすくするため、ほとんど常に有用です。ただし、gotoを使用する必要がある場合もあります(たとえば、手動でアセンブリを作成している場合)。

これはメモリ管理にも当てはまります。C++スマートポインタとGCは、生のポインタと手動のメモリ割り当て/割り当てを抽象化したものです。そして、時には、これらの抽象化は適切ではありません。最大のパフォーマンスが本当に必要な場合。

同じことがマルチスレッドにも当てはまります。フューチャーやアクターのようなものは、スレッド、セマフォー、ミューテックス、CAS命令の単なる抽象化です。このような抽象化は、コードをより読みやすくするのに役立ち、エラーを回避するのにも役立ちます。しかし、時々、それらは単に適切ではありません。

使用可能なツールと、それらの利点と欠点を知る必要があります。次に、タスクの正しい抽象化を選択できます(ある場合)。より高いレベルの抽象化はより低いレベルを非難しません、抽象化が適切ではない場合が常にあり、最良の選択は「古い方法」を使用することです。

3
svick

AppleのGrand Central Dispatchは、並行性についての私の考えを変えたエレガントな抽象概念です。キューに重点を置いているため、私の控えめな経験では、非同期ロジックの実装が桁違いに単純になります。

使用可能な環境でプログラミングすると、スレッド、ロック、およびスレッド間通信の使用方法のほとんどが置き換えられました。

2
orip

はい、しかしあなたはそれらのいくつかに遭遇する可能性は低いです。

昔は、良いmutexを正しく書くのが難しいため、ブロッキングメソッド(バリア同期)を使用するのが一般的でした。最近の同時実行ライブラリーを使用すると、並列化とプロセス間調整のための、非常に豊富で十分にテストされたツールのセットが提供されるため、この状況の痕跡は依然として見られます。

同様に、以前の慣例では、手作業で並列化する方法を理解できるように、厄介なコードを記述することでした。この形式の(間違った場合は有害である可能性があります)最適化も、これを行うコンパイラーの出現、必要に応じてループを巻き戻し、分岐を予測的に追跡するなどの出現により、大きな影響を及ぼしました。これは新しいテクノロジーではありませんが、 、市場に少なくとも15年いること。スレッドプールのようなものを利用することで、昨年の本当にトリッキーなコードを回避することもできます。

したがって、おそらく非推奨の方法は、十分にテストされた最新のライブラリを使用する代わりに、並行コードを自分で作成することです。

2
Alex Feinman

並列プログラミングの主な変更点の1つは、CPUが以前よりも非常に高速であるということですが、そのパフォーマンスを実現するには、適切に満たされたキャッシュが必要です。複数のスレッドを同時に実行してそれらを継続的に交換しようとすると、ほとんどの場合、各スレッドのキャッシュが無効になり(つまり、各スレッドで操作するために異なるデータが必要になります)、パフォーマンスが大幅に低下します。遅いCPUで使用されていました。

これは、非同期またはタスクベース(Grand Central Dispatch、IntelのTBBなど)のフレームワークがより一般的な理由の1つです。一度に1つのコードのタスクを実行し、次のタスクに進む前に終了します。ただし、それぞれをコーディングする必要があります。設計を台無しにしたくない場合(つまり、並列タスクが実際にキューに入れられている場合)を除き、各タスクにはほとんど時間がかかりません。 CPUを集中的に使用するタスクは、すべてのタスクを処理する単一スレッドで処理されるのではなく、代替CPUコアに渡されます。本当にマルチスレッド処理が行われていない場合も、管理が容易です。

1
gbjbaanb