マルチスレッドエンジンのスレッド間でメッセージの受け渡しを煩雑にしないようにするにはどうすればよいですか?


18

現在取り組んでいるC ++エンジンは、いくつかの大きなスレッドに分割されています。生成(手順コンテンツの作成用)、ゲームプレイ(AI、スクリプト、シミュレーション用)、物理学、レンダリングです。

スレッドは、スレッドからスレッドに渡される小さなメッセージオブジェクトを介して相互に通信します。ステップ実行前に、スレッドはすべての受信メッセージを処理します-変換の更新、オブジェクトの追加と削除など。時々、あるスレッド(世代)が何か(アート)を作成し、それを別のスレッド(レンダリング)に渡して永久所有権を取得します。

プロセスの初期段階で、いくつかのことに気付きました。

  1. メッセージングシステムは面倒です。新しいメッセージタイプの作成とは、ベースMessageクラスのサブクラス化、そのタイプの新しい列挙の作成、およびスレッドが新しいタイプのメッセージを解釈する方法のロジックを記述することを意味します。それは開発のためのスピードバンプであり、タイプミスのエラーを起こしやすいです。(Sidenote-これに取り組んでいると、動的言語がどれほど優れているかを理解できます!)

    これを行うためのより良い方法はありますか?boost :: bindのようなものを使用してこれを自動化する必要がありますか?そうすると、発言したり、タイプなどに基づいてメッセージを並べ替えたりすることができなくなります。そのような管理が必要になるかどうかはわかりません。

  2. これらのスレッドは非常に多くの通信を行うため、最初のポイントは重要です。メッセージを作成して渡すことは、物事を実現するための大きな部分です。私はそのシステムを合理化したいが、同じように役立つかもしれない他のパラダイムにも開かれたい。これを簡単にするために考えるべき異なるマルチスレッド設計はありますか?

    たとえば、まれにしか書き込まれないが、複数のスレッドから頻繁に読み取られるリソースがあります。すべてのスレッドがアクセスできる、ミューテックスによって保護された共有データを持つというアイデアに開かれるべきですか?

ゼロからマルチスレッドを念頭に置いて何かを設計するのは初めてです。この初期段階では、私は実際にそれが本当にうまくいくと考えています(考慮)が、スケーリングと、新しいものを実装する際の私自身の効率が心配です。


6
ここには単一の直接的な質問はありません。そのため、この投稿はこのサイトのQ&Aスタイルには適していません。投稿を質問ごとに個別の投稿に分割し、漠然としたヒントやアドバイスの代わりに実際に発生している特定の問題について質問するように、質問に焦点を合わせ直すことをお勧めします。

2
より一般的な会話をしたい場合は、gamedev.netのフォーラムでこの投稿を試してみることをお勧めします。Joshが言ったように、「質問」は特定の質問ではないため、StackExchange形式で対応することは非常に困難です。
サイファー

フィードバックをありがとう!私は、より多くの知識を持つ人が、私の問題のいくつかに一度に対処できる単一のリソース/経験/パラダイムを持っていることを望んでいた。私は、1つの大きなアイデアが私のさまざまな問題を1つの欠けているものに統合できると感じています。そして、私が認識しているよりも多くの経験を持つ誰かを考えていました。 !
ラプトルミート

「ヒント」タイプの質問は、解決すべき特定の問題がないことを意味するため、タイトルをメッセージパッシングにより具体的な名前に変更しました(したがって、最近では「本当の質問ではない」と考えています)。
テトラッド

物理学とゲームプレイに別々のスレッドが必要ですか?これら2つは非常に絡み合っているようです。また、それぞれが誰とどのように通信するかを知らずにアドバイスを提供する方法を知ることは困難です。
ニコルボーラス

回答:


10

より広範な問題については、スレッド間通信を可能な限り減らす方法を見つけることを検討してください。可能な場合は、同期の問題を完全に回避することをお勧めします。これは、データをダブルバッファリングすることで実現できます。これにより、単一の更新レイテンシが導入されますが、共有データを扱うタスクが大幅に軽減されます。

余談ですが、サブシステムごとにスレッド化するのではなく、スレッド生成、またはタスクごとに分岐するスレッドプールを使用することを検討しましたか?(スレッドプーリングについては、特定の問題に関してこれを参照してください。)この短いペーパーでは、プールパターンの目的と使用方法を簡潔に説明しています。これらの有益な回答をご覧くださいまた。そこに記載されているように、スレッドプールはボーナスとしてスケーラビリティを向上させます。そして、新しいゲームやエンジンを作成するたびにサブシステムベースのスレッドをうまくプレイする必要があるのとは対照的に、「一度書くだけで、どこでも使用する」ことができます。サードパーティのスレッドプーリングソリューションもたくさんあります。スレッドの生成と破棄のオーバーヘッドを軽減する必要がある場合は、スレッドの生成から始めて、後でスレッドプールに移動する方が簡単です。


1
チェックアウトする特定のスレッドプーリングライブラリに関する推奨事項はありますか?
8:13にimre

ニック-応答してくれてありがとう。あなたの最初の点に関しては、それは素晴らしいアイデアであり、おそらく私が進む方向だと思います。現時点では、ダブルバッファリングする必要があるかどうかはまだわかりません。時間が経つにつれて固まるので、このことを心に留めておきます。第二のポイントに-提案をありがとう!はい、スレッドタスクの利点は明らかです。リンクを読んで考えてみます。それが私のために働くかどうか/私のためにそれを機能させる方法は100%確信していませんが、私は間違いなく真剣に考えます。ありがとう!
ラプトルミート

1
@imreはBoostライブラリをチェックします-それらには先物があり、これはこれらの事柄にアプローチする良い/簡単な方法です。
ジョナサンディキンソン

5

さまざまなマルチスレッド設計について尋ねました。私の友人は、私がかなりクールだと思ったこの方法について教えてくれました。

アイデアは、すべてのゲームエンティティのコピーが2つあるということです(無駄だと思います)。1つのコピーが現在のコピーになり、もう1つのコピーが過去のコピーになります。現在のコピーは厳密に書き込み専用で、過去のコピーは厳密に読み取り専用です。更新するときは、エンティティリストの範囲を必要に応じてできるだけ多くのスレッドに割り当てます。各スレッドは割り当てられた範囲内の現在のコピーへの書き込みアクセス権を持ち、すべてのスレッドはエンティティの過去のすべてのコピーへの読み取りアクセス権を持っているため、過去のコピーのデータをロックなしで使用して、割り当てられた現在のコピーを更新できます。各フレーム間では、現在のコピーが過去のコピーになりますが、ロールの交換を処理する必要があります。


4

C#でのみ同じ問題が発生しました。新しいメッセージの作成の容易さ(またはその欠如)について長く一生懸命考えた後、私たちができる最善のことは、それらのコードジェネレータを作成することでした。それは少しugいですが、使用可能です:メッセージの内容の説明のみが与えられると、メッセージクラス、列挙、プレースホルダー処理コードなどを生成します。このすべてのコードは毎回ほぼ同じで、実際にタイプミスを起こしやすいです。

私はこれに完全に満足しているわけではありませんが、すべてのコードを手書きで書くよりはましです。

共有データに関して、最良の答えはもちろん「依存する」です。ただし、一般的に、一部のデータが頻繁に読み取られ、多くのスレッドで必要とされる場合は、共有する価値があります。スレッドセーフのために、最善の策は不変にすることですが、それが問題にならない場合は、mutexを使用できます。C#にはReaderWriterLockSlim、そのような場合のために特別に設計されたクラスがあります。C ++に相当するものがあるはずです。

おそらく最初の問題を解決するスレッド通信のもう1つのアイデアは、メッセージの代わりにハンドラーを渡すことです。私はC ++でこれをどのように解決するのかわかりませんが、C#ではdelegateオブジェクトを別のスレッドに送信して(たとえば、何らかの種類のメッセージキューに追加し)、実際に受信スレッドからこのデリゲートを呼び出すことができます。これにより、その場で「アドホック」メッセージを作成できます。私はこのアイデアをいじっただけで、実際に本番で試したことはないので、実際には悪い結果になるかもしれません。


すべての素晴らしい情報をありがとう!ハンドラーについての最後のビットは、関数を渡すためにバインディングまたはファンクターを使用することについて言及したものと似ています。私はちょっとアイデアが好きです-それを試してみて、それが吸うか素晴らしいかを確認するかもしれません:D
Raptormeat

1

私はいくつかのスレッド化されたゲームコードの設計段階にいるだけなので、自分の考えだけを共有でき、実際の経験は共有できません。そうは言っても、私は次の行に沿って考えています。

  • ほとんどのゲームデータは、読み取り専用アクセス用に共有する必要があります
  • データの書き込みは、一種のメッセージングを使用して可能です。
  • 別のスレッドが読み取り中にデータを更新しないようにするため、ゲームループには読み取りと更新の2つの異なるフェーズがあります。
  • 読み取りフェーズ:
  • すべての共有データは、すべてのスレッドに対して読み取り専用です。
  • スレッドは(スレッドローカルストレージを使用して)ものを計算し、更新要求を生成できます。更新要求は、基本的にコマンド/メッセージオブジェクトであり、キューに置かれ、後で適用されます。
  • 更新フェーズでは:
  • すべての共有データは書き込み専用です。データは、不明/不安定な状態であると想定されます。
  • これは、更新要求オブジェクトが処理される場所です。

理論的には、これは読み取りフェーズと更新フェーズの両方で、最小限の同期で任意の数のスレッドが同時に実行される可能性があることを意味するはずです(確かではありませんが)。読み取りフェーズでは、誰も共有データを書き込んでいないため、同時実行の問題は発生しません。更新フェーズは、それより難しいです。の並行更新同じデータの問題になるため、ここではいくつかの同期が適切に行われます。ただし、異なるデータセットで動作している限り、任意の数の更新スレッドを実行できます。

全体として、このアプローチはスレッドプーリングシステムに適していると思います。問題のある部分は次のとおりです。

  • 更新スレッドの同期(複数のスレッドが同じデータセットを更新しようとしないようにしてください)。
  • 読み取りフェーズで、スレッドが誤って共有データを書き込むことがないようにします。プログラミングミスの余地が多すぎるのではないかと心配しています。デバッグツールがそれらをどれだけ簡単にキャッチできるのかわかりません。
  • 中間結果がすぐに読めるようになることに頼ることができないような方法でコードを書く。つまり、x += 2; if (x > 5) ...xが共有されている場合は記述できません。xのローカルコピーを作成するか、更新要求を生成し、次の実行時にのみ条件を実行する必要があります。後者は、ボイラープレートコードを保持する追加のスレッドローカル状態を追加することを意味します。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.