ソフトウェアを開発するとき、いつ並行セクションを考えたり設計したりしますか?


8

最適化が早すぎないという原則に従って、ソフトウェアの設計/開発のどの時点で、同時実行の機会について考え始めますか?

シングルスレッドのアプリケーションを作成し、プロファイリングを通じて、並列実行の候補となるセクションを特定することが1つの戦略となることはよく想像できます。私が少し見てきたもう1つの戦略は、タスクのグループごとにソフトウェアを検討し、独立したタスクを並列化することです。

もちろん、尋ねる理由の1つは、最後まで待ってソフトウェアをリファクタリングするだけで同時に動作する場合、可能な限り最悪の方法で構造化し、大きなタスクを抱えることになるということです。

設計で並列化を検討するときに、どのような経験が役立ちましたか?

回答:


7

スレッドタスクについての重要な点は、高い凝集性、低い結合、および優れたカプセル化を実現することです。興味深いことに、これらはシングルスレッドアプリケーションの設計目標にもなります。今日はもともと並列化を計画していなかったタスクがありましたが、そのときは、関数の名前run()を変更し、その呼び出し方法を変更するだけでした。

最適化が早すぎないということは、「念のため」にすべてをスレッドに入れないことを意味しますが、必要に応じて最適化するのが難しいので、アーキテクチャコーナーに自分自身をペイントする必要もありません。


はい、リエントラント関数、ワークキューなどは、シングルスレッドアプリケーションでもマルチスレッドアプリケーションでも役立ちますが、後で並列処理に向けて拡張性を高めることもできます。これにより、後で同期の問題が発生して頭痛が大幅に軽減されます。
Coder

2

Javaプログラマーは、 Callable、作業単位インターフェースをます。アプリケーション全体がCallableの作成、Executorへのすべてのユニットの出荷、ポスト生成タスクの処理のループで構成されている場合、シリアル処理、「3つのワークキュー」、「すべて実行」に非常に簡単に形作ることができるものがあります。すぐに」正しい執行者を選ぶだけで。

シリアルアプローチが1つの時点で遅くなりすぎて、とにかくそれを行う必要があることが非常に一般的であるため、このパターンをゆっくりと適応させています。


1

プロジェクトによって異なります。並列化できるものを確認するのが非常に簡単な場合があります。おそらく、プログラムがファイルのバッチを処理します。各ファイルの処理が他のすべてのファイルから完全に独立していると仮定すると、一度に1つのファイル、つまり10、または100を処理でき、これらのジョブのいずれも他に影響を与えないことは明らかです。

潜在的な並列ジョブが同じでない場合は、少し複雑になります。画像ファイルを処理する場合、ヒストグラムを作成するジョブ、サムネイルを作成するジョブ、EXIFメタデータを抽出するジョブ、そしてこれらすべてのジョブの出力を取得してデータベースに保存する最終ジョブを使用できます。この例では、これらを並行して実行する必要があるのか​​、それとも実行する必要があるのか​​が明確でない場合があります(最後のジョブは、前のジョブがすべて正常に完了するまで待機する必要があります)。

私の経験では、何かを並列化する最も簡単な方法は、可能な限り独立して実行できるプロセスを探し(最初の例のように)、それらから始めることです。2番目の例を並行して実行しようとするのは、それを使用してパフォーマンスを大幅に向上させると考えた場合のみです。


1

最初からアプリケーションへの同時実行性を設計する必要があります。通常、最適化として、本質的に明白ではないにしても、後でそれを残すべきであることに同意します。問題は、最悪の場合、並行性のためにアプリケーションをゼロから再構築する必要がある可能性があることです。システムによっては、並行性を実現することが事実上不可能です。これの簡単な例は、データを共有するシステムです。たとえば、ゲームのシミュレーションとレンダリングの側面です。


0

スレッド化はアプリケーションのアーキテクチャの一部であると私は言うでしょう。ですから、それは私が最初に考えなければならないことの1つです。

たとえば、GUIアプリケーションを実行すると、GUIコードはシングルスレッドになるため、長時間実行されるタスク(XML処理など)によってGUIがブロックされ、代わりにバックグラウンドスレッドで実行する必要があります。

たとえば、サーバーは、各要求が新しいスレッドによって処理されるスレッドベースであるか、サーバーがイベント駆動型で、cpu-coreごとに1つのスレッドのみを使用できますが、長時間実行されるタスクは、バックグラウンドスレッドまたは小さなタスクに分割されます。


0

私が物事に取り組む方法では、マルチスレッド化は無料で、後から適用するのは比較的簡単です。しかし、私は最初にデータについて考えています。これがすべてのドメインで機能するかどうかはわかりませんが、どのように対処するかについて説明します。

したがって、最初に頻繁に処理されるソフトウェアに必要な最も粗い種類のデータについてです。メッシュ、サウンド、モーション、パーティクルエミッタ、ライト、テクスチャなどのようなゲームの場合。そしてもちろん、メッシュだけにドリルダウンして、それらがどのように表現されるかを考える場合、多くのことを考慮する必要がありますが、ここではそれをスキップします。現在、最も広いアーキテクチャレベルで考えています。

そして、私が最初に考えたのは、「これらすべてのものの表現を統合して、これらすべての種類のものに対して比較的均一なアクセスパターンを実現するにはどうすればよいのか」ということです。そして、私の最初の考えは、空のスペースを再利用するための無料のリストタイプの方法で、すべてのタイプのモノを独自の連続した配列に格納することかもしれません。そして、APIを統一する傾向があるので、たとえば、ライトやテクスチャと同じ種類のコードを使用して、少なくともこれらのコンポーネントがアクセスされる場所と方法まで、メッシュをシリアル化できます。すべてがどのように表現されるかを統一できるほど、それらにアクセスするコードはより統一された形をとる傾向があります。

カッコいい。これで、32ビットインデックスでこれらのことを指すことができ、64ビットポインターの半分のメモリしか使用できなくなります。そして、ねえ、並列ビットセットを関連付けることができれば、交差を線形時間で設定できるようになります。たとえば、すべてのインデックスを作成しているため、データをこれらのいずれかに非常に安価に並列に関連付けることもできます。ああ、そのビットセットは、メモリアクセスパターンを改善するために一連のソートされたインデックスを順番にトラバースするように戻すことができ、単一のループで同じキャッシュラインを複数回リロードする必要がありません。一度に64ビットをテストできます。すべての64ビットが設定されていない場合は、64要素を一度にスキップできます。すべて設定されていれば、一度に処理できます。すべてではなく一部が設定されている場合、FFS命令を使用して、設定されているビットをすばやく判別できます。

しかし、まあ、数万の内の数百のものにデータを関連付けたいだけの場合、それは一種の高価です。代わりに、スパース配列を使用してみましょう。

ここに画像の説明を入力してください

そして、まあ、すべてが疎な配列に格納され、それらにインデックスが付けられたので、これを永続的なデータ構造にするのは非常に簡単です。

ここに画像の説明を入力してください

変更されていないものをディープコピーする必要がないため、副作用のないより安価な関数を作成できます。

ここで、ECSエンジンについて学習した後、チートシートは既に与えられていますが、ここで、各タイプのコンポーネントで動作する必要がある広範な機能のタイプについて考えてみましょう。これらを「システム」と呼ぶことができます。「SoundSystem」は「Sound」コンポーネントを処理できます。各システムは、1つ以上のタイプのデータを操作する幅広い機能です。

ここに画像の説明を入力してください

これにより、特定のコンポーネントタイプについて、通常1つまたは2つのシステムのみがアクセスする多くのケースが残ります。うーん、それは確かにそれがスレッドの安全性を助け、絶対にスレッドの競合を最小限に抑えるように思えます。

さらに、データを均一に渡す方法を検討します。のような代わりに:

for each thing:
    play with it
    cuddle it
    kill it

私はそれを複数のより単純なパスに分割しようとしています:

for each thing:
    play with it
for each thing:
    cuddle it
for each thing:
    kill it

そのため、次の均質な据え置き遅延処理のために中間状態を保存する必要がある場合がありますが、各ループのロジックがよりシンプルで均一であることを知っているため、コードの維持と推論に本当に役立ちます。そしてねえ、それはそれがスレッドの安全性を簡素化し、スレッドの競合を減らすように思えます。

そして、スレッドの安全性と正確さについて自信を持って並列化することが本当に簡単なアーキテクチャが見つかるまで、このように続けますが、最初はすべて、データ表現を統一し、より予測可能なメモリアクセスパターンを持ち、メモリ使用量、制御フローをより均一なパスに簡略化、非常に高価なディープコピーコストを発生させずに副作用を引き起こすシステム内の関数の数を減らし、APIを統一します。

これらすべてを組み合わせると、並行性に非常に配慮した設計に偶然遭遇した共有状態の量を最小限に抑えるシステムになってしまう傾向があります。また、状態を共有する必要がある場合、スレッドのトラフィックジャムを引き起こさずに同期を使用する方が安く、さらに、表現を統合する中央データ構造で処理できることが多いため、多くの競合がないことがよくあります。システム内のすべてのものの数百の異なる場所にスレッド同期を適用する必要はなく、ほんの一握りです。

次に、メッシュなどのより複雑なコンポーネントの1つにドリルダウンするとき、最初にデータについて考えることから始めて、それを設計する同じプロセスを繰り返します。そうすれば、1つのメッシュの処理を簡単に並列化することさえできるかもしれませんが、すでに構築した幅広いアーキテクチャ設計により、複数のメッシュの処理を並列化できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.