6

ゲーム ループのスレッド セーフに関する軽度の難問に直面しています。私が以下に持っているのは、一緒に動作することを意図した 3 つのスレッド (メインを含む) です。1 つはイベント管理 (メイン スレッド) 用、もう 1 つはロジック用、もう 1 つはレンダリング用です。以下に示すように、これら 3 つのスレッドはすべて独自のクラス内に存在します。基本的なテストでは、構造は問題なく動作します。このシステムは SFML を使用し、OpenGL でレンダリングします。

int main(){
    Gamestate gs;
    EventManager em(&gs);
    LogicManager lm(&gs);
    Renderer renderer(&gs);

    lm.start();
    renderer.start();
    em.eventLoop();

    return 0;
}

ただし、お気づきかもしれませんが、スレッド間で共有する必要があるすべてのリソースのコンテナーとして機能する「Gamestate」クラスがあります (ほとんどの場合、LogicManager をライターとして、Renderer をリーダーとして使用します。EventManager は主にウィンドウイベントのみ)。私の質問は次のとおりです:(1と2が最も重要です)

1) これは物事を進める良い方法ですか? 「グローバル」な Gamestate クラスを使用することをお勧めしますか? それについてもっと良い方法はありますか?

2) 私の意図は、Gamestate のゲッター/セッターにミューテックスを持たせることでした。ただし、ロックされている間はオブジェクトを返すことができないため、読み取りには機能しません。つまり、同期をゲッターの外部に配置する必要があります。 /setters を作成し、ミューテックスを公開します。それはまた、さまざまなリソースすべてに対して大量のミューテックスが必要になることも意味します。この問題を解決する最もエレガントな方法は何ですか?

3) すべてのスレッドが「bool run」にアクセスして、ループを続行するかどうかを確認しています。

while(gs->run){
....
}

EventManager で終了メッセージを受信すると、run が false に設定されます。その変数をまったく同期する必要がありますか? 揮発性に設定しますか?

4) 常にポインタを逆参照することなどは、パフォーマンスに影響を与えますか? 例 gs->objects->entitylist.at(2)->move(); これらすべての '->' と '.' を実行します。大きな減速を引き起こしますか?

4

2 に答える 2

6

グローバル状態

1) これは物事を進める良い方法ですか? 「グローバル」な Gamestate クラスを使用することをお勧めしますか? それについてもっと良い方法はありますか?

ゲームの場合、再利用可能なコードとは対照的に、グローバルな状態で十分だと思います。ゲームステート ポインターを渡すのを避けて、代わりに実際にグローバル変数にすることもできます。

同期

2) 私の意図は、Gamestate のゲッター/セッターにミューテックスを持たせることでした。ただし、ロックされている間はオブジェクトを返すことができないため、読み取りには機能しません。つまり、同期をゲッターの外部に配置する必要があります。 /setters を作成し、ミューテックスを公開します。それはまた、さまざまなリソースすべてに対して大量のミューテックスが必要になることも意味します。この問題を解決する最もエレガントな方法は何ですか?

これをトランザクションの観点から考えてみます。単一の状態変更を独自のミューテックス ロック コードにラップすると、パフォーマンスに影響を与えるだけでなく、コードが 1 つの状態要素を取得し、その要素に対して何らかの計算を実行し、後で値を設定する場合に、実際には正しくない動作につながる可能性があります。間の同じ要素。そのため、Gamestate とのすべてのやり取りがいくつかの場所にまとめて発生するように構造化しようとしLogicManagerました。Rendererその相互作用の間、スレッドは状態のミューテックスを保持する必要があります。

ミューテックスの使用を強制したい場合は、少なくとも 2 つのクラスを持つ構成を作成できます。GameStateDataそれらを と と呼びましょうGameStateAccessGameStateDataすべての状態が含まれますが、パブリック アクセスは提供されません。GameStateAccessの友人にGameStateDataなり、その個人データへのアクセスを提供します。のコンストラクターはGameStateAccess、への参照またはポインターを取り、GameStateDataそのデータのミューテックスをロックします。デストラクタはミューテックスを解放します。そうすれば、状態を操作するコードは、GameStateAccessオブジェクトがスコープ内にあるブロックとして単純に記述されます。

ただし、まだ抜け穴があります。このGameStateAccessクラスから返されるオブジェクトが可変オブジェクトへのポインターまたは参照である場合、このセットアップでは、コードがそのようなポインターをミューテックスによって保護されているスコープ外に持ち出すことはできません。これを防ぐには、書き方に注意するかGameStateAccess、スコープ外になったらクリアできるカスタム ポインターのようなテンプレート クラスを使用するか、参照ではなく値でのみ渡すようにします。

C++11 を使用すると、上記のロック管理のアイデアは次のように実装できます。

class GameStateData {
private:
  std::mutex _mtx;
  int _val;
  friend class GameStateAccess;
};
GameStateData global_state;

class GameStateAccess {
private:
  GameStateData& _data;
  std::lock_guard<std::mutex> _lock;
public:
  GameStateAccess(GameStateData& data)
    : _data(data), _lock(data._mtx) {}
  int getValue() const { return _data._val; }
  void setValue(int val) { _data._val = val; }
};

void LogicManager::performStateUpdate {
  int valueIncrement = computeValueIncrement(); // No lock for this computation
  { GameStateAccess gs(global_state); // Lock will be held during this scope
    int oldValue = gs.getValue();
    int newValue = oldValue + valueIncrement;
    gs.setValue(newValue); // still in the same transaction
  } // free lock on global state
  cleanup(); // No lock held here either
}

ループ終了インジケータ

3) すべてのスレッドが「bool run」にアクセスして、ループを続行するかどうかを確認しています。

while(gs->run){
....
}

EventManager で終了メッセージを受信すると、run が false に設定されます。その変数をまったく同期する必要がありますか? 揮発性に設定しますか?

このアプリケーションでは、揮発性ではあるが同期されていない変数で問題ありません。コンパイラがその値をキャッシュするコードを生成しないようにするために、それを揮発性として宣言する必要があります。これにより、別のスレッドによる変更が隠されます。

std::atomic別の方法として、これに変数を使用することもできます。

ポインター間接オーバーヘッド

4) 常にポインタを逆参照することなどは、パフォーマンスに影響を与えますか? たとえばgs->objects->entitylist.at(2)->move();、これらすべてを実行する->.、大幅な速度低下が発生しますか?

それは代替案に依存します。多くの場合、コンパイラーは、gs->objects->entitylist.at(2)繰り返し使用される場合、上記のコードなどの値を保持することができ、何度も何度も計算する必要はありません。一般に、このすべてのポインターの間接化によるパフォーマンスの低下は小さな問題だと考えていますが、それを確実に判断するのは困難です。

于 2012-10-02T06:07:54.733 に答える
5

それは物事を進める良い方法ですか?( class Gamestate)

1) これは物事を進める良い方法ですか?

はい。

「グローバル」な Gamestate クラスを使用することをお勧めしますか?

はい、getter/setter がスレッドセーフである場合。

それについてもっと良い方法はありますか?

いいえ。データはゲーム ロジックと表現の両方に必要です。サブルーチンに入れると、グローバルゲームステートを削除できますが、これは問題を別の関数に転送するだけです。グローバル Gamestate により、現在の状態を非常に簡単に保護することもできます。

Mutex とゲッター/セッター

2) 私の意図は、Gamestate のゲッター/セッターにミューテックスを持たせることでした [...]。この問題を解決する最もエレガントな方法は何ですか?

これをリーダー/ライター問題と呼びます。これには公開ミューテックスは必要ありません。多くの読者を持つことができますが、書き込み者は 1 人だけであることに注意してください。リーダー/ライターのキューを実装し、ライターが終了するまで追加のリーダーをブロックできます。

while(gs->run)

その変数をまったく同期する必要がありますか?

変数への非同期アクセスによって不明な状態になる可能性がある場合は常に、変数を同期する必要があります。そのため、レンダリング エンジンが次のイテレーションを開始し、Gamestate が破棄された直後にrunが設定されると、混乱が生じます。falseただし、gs->runこれがループを継続するかどうかの指標にすぎない場合は、安全です。

ロジックとレンダリング エンジンの両方を同時に停止する必要があることに注意してください。両方を同時にシャットダウンできない場合は、フリーズを防ぐためにレンダリング エンジンを最初に停止します。

ポインターの逆参照

4) 常にポインタを逆参照することなどは、パフォーマンスに影響を与えますか?

最適化には 2 つのルールがあります。

  1. 最適化しない
  2. まだ最適化しません。

コンパイラはおそらくこの問題を処理します。プログラマは、自分にとって最も読みやすいバージョンを使用する必要があります。

于 2012-10-02T05:55:33.623 に答える