スレッドの同時実行C ++ 11、その自転車技術(Apple)GCD

エントリー


こんばんは、ハブロビテス。 この記事では、私が出会ったマルチスレッド環境での作業の問題とそれらを解決する方法について説明したいと思います。 5年以上にわたり、主にiOSプラットフォーム向けに、C ++ / Objective C ++でゲームプロジェクトを開発してきました。 2年前、Objective-Cのみを使用して「ネイティブ」開発に挑戦することにしました。 同じ頃、私はAppleのGCDテクノロジーに興味がありました(次のWWDCを見た直後)。 まず、このテクノロジーでは、スレッド間で操作を委任する柔軟な機能に惹かれました。 かなり一般的なタスクは、優先度の低いストリームにゲームリソースをロードすることです。 しかし、かなり重要なタスクは、ダウンロード操作の最後にストリームをメインストリームに変更して、さらにVRAMにロードすることです。 もちろん、この問題に目を向けて、グラフィックコンテキストに共有コンテキストを使用することもできますが、当時の私のコードとグラフィックデザインソリューションでの完成度の向上により、それができなくなりました。 一般に、私が当時行っていた「ペット」プロジェクトでGCDを試してみることにしました。 そして、それはかなり悪くないことが判明しました。 ゲームリソースの読み込みを決定するタスクに加えて、GCDを適切な場所で使用するようになりました。



多くの時間が経ち、コンパイラはC ++ 11標準を完全にサポートするようになりました。 私は現在、コンピューターゲームを開発する会社で働いているため、C ++での開発には特別な要件があります。 ほとんどの従業員はObjective-Cとは無関係です。 はい、私自身はこの言語に特別な愛を持っているわけではありません(Smalltalk言語の原理に基づいて構築されたオブジェクトモデルを除きます)。



11番目の標準の仕様を読んだ後、ブルジョアのブログをたくさん勉強した後、Apple CGDに似た自転車を書くことにしました。 もちろん、「Pool of threads」パターンの実装と、セカンダリストリームのコンテキストからメインストリームのコンテキストへ、またはその逆の任意の時点で終了する能力だけに、広大で制限された自分を受け入れるという目標を設定していません。



これを行うには、次のC ++ 11の革新が必要でした。std::関数、可変長テンプレート、そしてもちろんstd :: threadで動作します。 (std :: shared_ptrは自己快適のためにのみ使用されます)。 もちろん、私自身が設定した別の目標はクロスプラットフォームです。 そして、VS 2012にバンドルされているMicrosoftコンパイラが可変長テンプレートをサポートしていないことに気付いたとき、非常に失望しました。 しかし、少しスタックオーバーフローを調べたところ、この問題はオプションのVisual C ++ 2012年11月CTPパッケージをインストールすることでも解決されることがわかりました。



実装


すでに述べたように、このアイデアはスレッドプールパターンに基づいています。 設計中、「gcdpp_t_task」の2つのクラスが区別され、実行可能タスク自体とgcdpp_t_queue(タスクを蓄積するキュー)を集約しました。



template<class FUCTION, class... ARGS> class gcdpp_t_task { protected: FUCTION m_function; std::tuple<ARGS...> m_args; public: gcdpp_t_task(FUCTION _function, ARGS... _args) { m_function = _function; m_args = std::make_tuple(_args...); }; ~gcdpp_t_task(void) { }; void execute(void) { apply(m_function, m_args); }; };
      
      







ご覧のとおり、このクラスはテンプレートです。 そして、これは私たちにとって問題を引き起こします-異なるタイプのタスクを1つのキューに格納するにはどうすればよいですか?



むかしむかし、なぜC ++にはインターフェース/プロトコルの本格的な実装がまだないのだろうか。 結局のところ、抽象化からのプログラミングの原則は、実装からよりも効果的です。 まあ、何も、抽象クラスを作成することはできません。



 class gcdpp_t_i_task { private: protected: public: gcdpp_t_i_task(void) { }; virtual ~gcdpp_t_i_task(void) { }; virtual void execute(void) { assert(false); }; };
      
      







これで、タスクの抽象化からタスククラスを継承し、すべてを1つのキューに簡単に入れることができます。



少し停止して、gcdpp_t_taskクラスを見てみましょう。 前述したように、クラスは定型です。 関数(ラムダ式の特定の実装)とパラメーターのセットへのポインターを受け取ります。 1つの実行メソッドのみを実装します。このメソッドでは、パラメーターが関数に渡されます。 これが頭痛の種です。 遅延呼び出しで渡すことができるようにパラメーターをスタックする方法。 std :: tupleを使用する決定が助けになりました。



 template<unsigned int NUM> struct apply_ { template<typename... F_ARGS, typename... T_ARGS, typename... ARGS> static void apply(std::function<void(F_ARGS... args)> _function, std::tuple<T_ARGS...> const& _targs, ARGS... args) { apply_<NUM-1>::apply(_function, _targs, std::get<NUM-1>(_targs), args...); } }; template<> struct apply_<0> { template<typename... F_ARGS, typename... T_ARGS, typename... ARGS> static void apply(std::function<void(F_ARGS... args)> _function, std::tuple<T_ARGS...> const&, ARGS... args) { _function(args...); } }; template<typename... F_ARGS, typename... T_ARGS> void apply(std::function<void(F_ARGS... _fargs)> _function, std::tuple<T_ARGS...> const& _targs) { apply_<sizeof...(T_ARGS)>::apply(_function, _targs); }
      
      







まあ、すべてが透明で明確になったようです。 優先順位を付けて「スレッドプール」を整理するのはあなた次第です。



  class gcdpp_t_queue { private: protected: std::mutex m_mutex; std::thread m_thread; bool m_running; void _Thread(void); public: gcdpp_t_queue(const std::string& _guid); ~gcdpp_t_queue(void); void append_task(std::shared_ptr<gcdpp_t_i_task> _task); };
      
      







タスクキューの集約とカプセル化を実装する実際のインターフェイスは次のとおりです。 コンストラクターで、各gcdpp_t_queueクラスオブジェクトは、割り当てられたタスクが実行される独自のスレッドを作成します。 当然、プッシュやポップなどの操作は、マルチスレッド環境での安全な操作のために相互排他同期オブジェクトにラップされます。 同様の機能を実装するクラスも必要でしたが、メインスレッドでのみ動作します。 gcdpp_t_main_queue-より平凡なため、コンテンツはより控えめです。



そして今、最も重要なことは、すべてを多かれ少なかれ機能する形で配置することです。



 class gcdpp_impl { private: protected: friend void gcdpp_dispatch_init_main_queue(void); friend void gcdpp_dispatch_update_main_queue(void); friend std::shared_ptr<gcdpp_t_queue> gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY _priority); friend std::shared_ptr<gcdpp_t_main_queue> gcdpp_dispatch_get_main_queue(void); template<class... ARGS> friend void gcdpp_dispatch_async(std::shared_ptr<gcdpp_t_main_queue> _queue, std::function<void(ARGS... args)> _function, ARGS... args); std::shared_ptr<gcdpp_t_main_queue> m_mainQueue; std::shared_ptr<gcdpp_t_queue> m_poolQueue[gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY_MAX]; static std::shared_ptr<gcdpp_impl> instance(void); std::shared_ptr<gcdpp_t_queue> gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY _priority); std::shared_ptr<gcdpp_t_main_queue> gcdpp_dispatch_get_main_queue(void); template<class... ARGS> void gcdpp_dispatch_async(std::shared_ptr<gcdpp_t_main_queue> _queue, std::function<void(ARGS... args)> _function, ARGS... args); public: gcdpp_impl(void); ~gcdpp_impl(void); };
      
      







クラスgcdpp_impl-シングルトンであり、外部の影響から完全にカプセル化されます。 3つのタスクプールの配列(優先順位付き、優先順位はスタブによって実装されます)、およびメインスレッドでタスクを実行するためのプールが含まれます。 このクラスには、5つのフレンド関数も含まれています。 関数gcdpp_dispatch_init_main_queueおよびgcdpp_dispatch_update_main_queueは寄生虫です。 今、私はそれらを切断するための不吉な計画を開発しています。 gcdpp_dispatch_update_main_queue-メインスレッドのタスク処理関数...そして、ユーザーがこの関数を実行ループにプラグインしないようにしたいのです。



残りの機能では、すべてが透明であるようです:



gcdpp_dispatch_get_global_queue-優先度キューを取得する

gcdpp_dispatch_get_main_queue-メインスレッドでキューを取得する

gcdpp_dispatch_async-遅延呼び出しのキュー操作を特定のスレッド、特定のキューに入れます。



申込み


そして、なぜこれがすべて必要なのでしょうか?

いくつかのテストでこの実装の利益を示してみます。



 std::function<void(int, float, std::string)> function = [](int a, float b, const std::string& c) { std::cout<<<<a<<b<<c<<std::endl; }; gcdpp::gcdpp_dispatch_async<int, float, std::string>(gcdpp::gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY_HIGH), function, 1, 2.0f, "Hello World");
      
      







この例では、ラムダ式で宣言された関数は、優先度の高いスレッドで遅延と呼ばれます。



 class Clazz { public: int m_varible; void Function(int _varible) { m_varible = _varible; }; }; std::shared_ptr<Clazz> clazz = std::make_shared<Clazz>(); clazz->m_varible = 101; std::function<void(std::shared_ptr<Clazz> )> function = [](std::shared_ptr<Clazz> clazz) { std::cout<<"call"<<clazz->m_varible<<std::endl; }; gcdpp::gcdpp_dispatch_async<std::shared_ptr<Clazz>>(gcdpp::gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY_HIGH), function, clazz);
      
      







これは、パラメーターとしてカスタムクラスを使用した遅延操作呼び出しの使用例です。



 void CParticleEmitter::_OnTemplateLoaded(std::shared_ptr<ITemplate> _template) { std::function<void(void)> function = [this](void) { std::shared_ptr<CVertexBuffer> vertexBuffer = std::make_shared<CVertexBuffer>(m_settings->m_numParticles * 4, GL_STREAM_DRAW); ... m_isLoaded = true; }; thread_concurrency_dispatch(get_thread_concurrency_main_queue(), function); }
      
      







そして最も重要なテストは、セカンダリスレッドからメインスレッドの操作を呼び出すことです。 _OnTemplateLoaded関数は、xml設定ファイルを解析するスレッドのバックグラウンドから呼び出されます。 その後、パーティクルバッファを作成し、テクスチャをVRAMに送信する必要があります。 この操作では、グラフィックスコンテキストが作成されたストリームでのみ実行する必要があります。



おわりに


一般に、問題は設定された目標内で解決されます。 もちろん、未完成でテストされていないものはまだたくさんありますが、今のところ、GCDの実装を改善していきます。 このプロジェクトには約18時間の作業が費やされ、主に作業中の煙幕が犠牲になりました。



ソースコードは、オープンアクセスのソースコードにあります 。 VS 2012では、プロジェクトはまだプッシュされていませんが、すぐに登場すると思います。



PS適切な批判を待っている...



All Articles