別々のスレッドで更新してレンダリングする


11

シンプルな2Dゲームエンジンを作成しています。スプライトをさまざまなスレッドで更新してレンダリングし、その方法を学びたいと思います。

更新スレッドとレンダリングスレッドを同期する必要があります。現在、2つのアトミックフラグを使用しています。ワークフローは次のようになります。

Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done

この設定では、レンダリングスレッドのFPSを更新スレッドのFPSに制限しています。さらにsleep()、レンダリングと更新の両方のスレッドのFPSを60に制限するために使用するので、2つの待機関数はそれほど待機しません。

問題は:

平均CPU使用率は約0.1%です。場合によっては、25%に達することがあります(クアッドコアPCの場合)。これは、待機関数がテストおよび設定関数を使用したwhileループであり、whileループがすべてのCPUリソースを使用するため、スレッドが他のスレッドを待機していることを意味します。

私の最初の質問は:2つのスレッドを同期する別の方法はありますか?私は気づいたstd::mutex::lock、それは、whileループではないので、リソースをロックするために待っている間、CPUを使用しないでください。どのように機能しますか?std::mutex1つのスレッドでロックし、別のスレッドでロック解除する必要があるため、使用できません。

他の質問です。プログラムは常に60 FPSで実行されるため、CPU使用率が25%に急上昇することがあるのはなぜですか。(2つのスレッドはどちらも60fpsに制限されているため、理想的には多くの同期は必要ありません)。

編集:すべての返信をありがとう。まず、レンダリングのためにフレームごとに新しいスレッドを開始しないようにしたいと思います。最初に、更新とレンダーループの両方を開始します。マルチスレッディングで時間を節約できると思います。FastAlg()とAlg()という関数があります。Alg()は私の更新objとrender objの両方であり、Fastalg()は私の「レンダーキューを「レンダラーに送信」」しています。シングルスレッドでは:

Alg() //update 
FastAgl() 
Alg() //render

2つのスレッド:

Alg() //update  while Alg() //render last frame
FastAlg() 

したがって、マルチスレッド化によって同じ時間を節約できる可能性があります。(実際にはそれが行う単純な数学アプリケーションでは、algは長いアルゴリズムであり、fastalgはより高速なアルゴリズムです)

睡眠に問題があることは一度もありませんが、睡眠は良い考えではありません。これは良くなりますか?

While(true) 
{
   If(timer.gettimefromlastcall() >= 1/fps)
   Do_update()
}

しかし、これはすべてのCPUを使用する無限のwhileループになります。使用を制限するためにスリープ(15未満の数値)を使用できますか?このようにして、たとえば100 fpsで実行され、更新関数は1秒あたり60回だけ呼び出されます。

2つのスレッドを同期するには、createSemaphoreでwaitforsingleobjectを使用して、別のスレッドで(whileループを使用せずに)ロックおよびロック解除できるようにしますか?


5
「この場合、マルチスレッドが役に立たないと言ってはいけません。その方法を学びたいだけです」 -その場合、適切に学習する必要があります。つまり、(a)sleep()を使用してフレームを制御しない、決して決して、および(b)コンポーネントごとのスレッド設計を回避し、ロックステップの実行を回避します。代わりに、タスクを作業に分割し、作業キューからタスクを処理します。
デイモン

1
@Damon(a)sleep()はフレームレートメカニズムとして使用でき、実際には非常に人気がありますが、はるかに優れたオプションがあることに同意する必要があります。(b)ここのユーザーは、更新とレンダリングの両方を2つの異なるスレッドに分けたいと考えています。これはゲームエンジンの通常の分離であり、「コンポーネントごとのスレッド」ではありません。明確な利点がありますが、正しく行わないと問題が発生する可能性があります。
Alexandre Desbiens 2014

@AlphSpirit:何かが「一般的」であるという事実は、それが間違っていないことを意味するものではありませ。発散タイマーにさえ踏み込むことなく、少なくとも1つの一般的なデスクトップオペレーティングシステムでのスリープの粒度は、既存のすべてのコンシューマーシステムでの設計ごとの信頼性ではないにしても、十分な理由です。説明されているように更新とレンダリングを2つのスレッドに分離することが賢明ではなく、時間がかかりすぎるよりも多くのトラブルを引き起こす理由を説明する。OPの目標は、として記載されているどのように行うのを学ぶどのように行うのを学習すべきか、正しく。最新のMTエンジン設計に関する記事がたくさんあります。
デイモン

@Damon私がそれが人気がある、または一般的であると言ったとき、私はそれが正しいと言うつもりはありませんでした。私はそれが多くの人々によって使用されたことを意味しました。「...はるかに優れたオプションがあることには同意する必要がありますが」というのは、実際には時間を同期するのにあまり良い方法ではないということです。誤解してすみません。
Alexandre Desbiens 2014

@AlphSpirit:心配する必要はありません:-)世界は多くの人がすることでいっぱいです(そして、常に正当な理由があるわけではありません)。
デイモン

回答:


25

スプライトを使用した単純な2Dエンジンの場合、シングルスレッドアプローチが最適です。しかし、マルチスレッド化の方法を学びたいので、正しく行う方法を学ぶ必要があります。

しない

  • 多かれ少なかれロックステップを実行する2つのスレッドを使用して、いくつかのスレッドでシングルスレッドの動作を実装します。これには同じレベルの並列処理(ゼロ)がありますが、コンテキストの切り替えと同期のオーバーヘッドが追加されます。さらに、ロジックを理解するのが困難です。
  • sleepフレームレートを制御するために使用します。決して。誰かがあなたに言うなら、それらを叩いてください。
    まず、すべてのモニターが60Hzで動作するわけではありません。第2に、同じ速度で2つのタイマーが並んで実行されると、常に最終的に同期が外れます(2つのピンポンボールを同じ高さからテーブルにドロップして聴きます)。3つ目sleep、設計によるもので、正確でも信頼性でもありません。細分性は15.6ms(実際には、Windowsのデフォルト[1])と同じくらい悪くなる可能性があり、フレームは60fpsでわずか16.6msであり、それ以外はすべて1msしか残りません。さらに、16.6を15.6の倍数にすることは困難です。
    また、sleep30ミリ秒、50ミリ秒、100ミリ秒、またはそれ以上の時間が経過した後にのみ戻ることが許可されています(場合によっては!)。
  • std::mutex別のスレッドに通知するために使用します。これはそのためのものではありません。
  • TaskManagerは何が起こっているかを伝えるのに長けていると仮定します。特に、「25%CPU」のような数値から判断すると、コード、ユーザーモードドライバー、その他のどこかで費やされる可能性があります。
  • 高レベルのコンポーネントごとに1つのスレッドがあります(もちろん、いくつかの例外があります)。
  • タスクごとに「ランダムな時間」にアドホックにスレッドを作成します。スレッドの作成は驚くほど費用がかかる可能性があり、実際にスレッドが伝えた内容を実際に実行するまでに驚くほど長い時間がかかる可能性があります(特に多くのDLLがロードされている場合!)。

行う

  • マルチスレッドを使用して、できる限り非同期に実行します。速度はスレッド化の主要なアイデアではありませんが、並行して処理を実行します(そのため、全体で時間がかかる場合でも、すべての合計はまだ少なくなります)。
  • 垂直同期を使用してフレームレートを制限します。それが唯一の正しい(そして失敗しない)方法です。ユーザーがディスプレイドライバーのコントロールパネルであなたを上書きする場合( "強制オフ")、そうする必要があります。結局のところ、あなたのコンピュータではなく、彼のコンピュータです。
  • 定期的に何かを「チェック」する必要がある場合は、タイマーを使用します。タイマーには、精度と信頼性がsleep[2]に比べてはるかに優れているという利点があります。また、繰り返しタイマーは時間(その間の経過時間を含む)を正しく考慮しますが、16.6ms(または16.6msからmeasured_time_elapsedを差し引いた時間)は考慮しません。
  • 固定タイムステップで数値積分を含む物理シミュレーションを実行します(または、方程式が爆発します!)、ステップ間でグラフィックスを補間します(これ、コンポーネントごとのスレッドごとに言い訳になる場合がありますが、なくても実行できます)。
  • 使用std::mutex(「相互排除」)同時に複数のスレッドにアクセスリソースを持っている、との奇妙なセマンティクスを遵守しますstd::condition_variable
  • スレッドがリソースを奪い合うのを避けます。必要最低限​​のロック(ただしそれ以下ではありません!)を行い、絶対に必要な間だけロックを保持します。
  • スレッド間で読み取り専用データを共有します(キャッシュの問題やロックは必要ありません)が、同時にデータを変更しないでください(同期が必要で、キャッシュを強制終了します)。これには、他の誰かが読む可能性のある場所の近くにあるデータの変更が含まれます。
  • std::condition_variableある条件が満たされるまで別のスレッドをブロックするために使用します。std::condition_variableその余分なミューテックスのセマンティクスは確かにかなり奇妙でねじれています(主にPOSIXスレッドから継承された歴史的な理由による)が、条件変数はあなたが望むものに使用するための正しいプリミティブです。
    あなたが見つけた場合にはstd::condition_variableそれで快適にするにはあまりにも奇妙な、あなたにも単に代わりに(わずかに遅い)Windowsイベントを使用することができますか、あなたは勇気があるならば、NtKeyedEvents周り独自のシンプルなイベントを構築する(怖い低レベルのものを必要とします)。DirectXを使用しているので、とにかく既にWindowsにバインドされているので、移植性が失われても大きな問題にはなりません。
  • 作業を固定サイズのワーカースレッドプールによって実行される適度なサイズのタスクに分割します(ハイパースレッドコアはカウントせず、コアあたり1つ以下)。終了タスクが依存タスクをキューに入れるようにします(無料の自動同期)。それぞれに少なくとも数百の重要な操作(またはディスク読み取りのような1つの長いブロック操作)があるタスクを作成します。キャッシュ隣接アクセスを優先します。
  • プログラムの開始時にすべてのスレッドを作成します。
  • OSまたはグラフィックAPIが提供する非同期機能を利用して、プログラムレベルだけでなく、ハードウェア(PCIe転送、CPU-GPU並列処理、ディスクDMAなど)の並列処理を向上させます。
  • 私が言及することを忘れていた他の10,000の事柄。


[1]はい、スケジューラのレートを1ミリ秒に設定できますが、コンテキストの切り替えが多くなり、消費電力が多くなるため(モバイルデバイスが増えている世界では)、これは好ましくありません。それはまたそれがまだより信頼できる睡眠を作らないのでそれは解決策ではありません。
[2]タイマーはスレッドの優先度を引き上げます。これにより、同じ優先度の別のスレッドを量子の途中で中断し、最初にスケジュールすることができます。これは、準RTの動作です。もちろん本当のRTではありませんが、とても近いです。スリープからの復帰とは、スレッドがいつでもスケジュールできるようになることを意味します。


「高レベルコンポーネントごとに1つのスレッドを持つ」べきではない理由を説明していただけますか?2つの別々のスレッドで物理とオーディオのミキシングを行うべきではないということですか?そうしない理由はないと思います。
Elviss Strazdins 2017

3

更新とレンダリングの両方のFPSを60に制限することで何を達成したいのかわかりません。それらを同じ値に制限すると、同じスレッドに入れるだけで済みます。

異なるスレッドで更新とレンダリングを分離するときの目標は、GPUが500 FPSをレンダリングでき、更新ロジックが60 FPSのままになるように、両方を互いに「ほぼ」独立させることです。そうすることで、非常に高いパフォーマンスの向上を達成することはできません。

しかし、あなたはそれがどのように機能するのか知りたいだけだと言いました、そしてそれは問題ありません。C ++では、mutexは他のスレッドの特定のリソースへのアクセスをロックするために使用される特別なオブジェクトです。言い換えると、ミューテックスを使用して、一度に1つのスレッドのみが機密データにアクセスできるようにします。そのための手順は非常に簡単です。

std::mutex mutex;
mutex.lock();
// Do sensible stuff here...
mutex.unlock();

ソース:http : //en.cppreference.com/w/cpp/thread/mutex

編集:与えられたリンクのように、ミューテックスがクラス全体またはファイル全体であることを確認してください。そうしないと、各スレッドが独自のミューテックスを作成し、何も実行しません。

mutexをロックする最初のスレッドは、内部のコードにアクセスできます。2番目のスレッドがlock()関数を呼び出そうとすると、最初のスレッドがロックを解除するまでブロックされます。つまり、mutexは、whileループとは異なり、ブロッシング関数です。ブロック機能はCPUにストレスをかけません。


そして、ブロックはどのように機能しますか?
Liuka 14

2番目のスレッドがlock()を呼び出すと、最初のスレッドがmutexのロックを解除するまで辛抱強く待機し、次の行(この例では、賢明なもの)を続行します。編集:2番目のスレッドは、ミューテックス自体をロックします。
Alexandre Desbiens


1
/ std::lock_guardではなく、同様のものを使用します。RAIIはメモリ管理だけではありません!.lock().unlock()
bcrist 2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.