まず
初投稿です。よろしくお願いします。
はじめに
リソースを作るのに時間がかかるときは、その処理を別スレッドに移して “非同期で” 行う方法がよくとられます。
また、リソースを複数のオブジェクトから参照するときは、作る処理は一度きりにして、みんながそれを参照し合うほうが効率的です。
今回はそれらを実現するコンテナを作ってみました。
ソースコードは Gist に投稿しましたので、ここではその一部と、使い道について解説していきます。
パブリックドメインですので自由にご活用くださいませ。
要約
- コンテナはリソースを直接管理するのではなく、リソースを生成するタスク (実際にはそのタスクから結果を受け取る
std::shared_future
オブジェクト) を管理する。 - 可変長引数テンプレートを使うことで、リソースのパラメータを自由に受け取れるだけでなく、
void *
で受け取ることによる危険性からも解放される。 - ムーブで複数のオブジェクトの排他制御が必要になる場合は慎重に。C++11 では
std::lock
を使うことができる。
自分にとって必要だった仕様
- パラメータを与えると、それに対応するリソースを非同期で生成する。
- 同じパラメータが来た場合は、以前返したリソースを返す。
- リソースがまだ生成されていなかったり、生成中の場合にも正常に動作する。
- リソースを返す際に、例えば参照カウントを増減するなど、何らかの処理を行うことができる。
- スレッドセーフである。
実装について
コンテナが管理するもの
SharedResourceContainer
はテンプレート型 Resource
のオブジェクトを直接管理するのではなく、オブジェクトを生成して返すタスクをラムダ式にして std::async
に渡し、そこから得られる std::future
を std::shared_future
化して管理しています。
std::async
は非同期実行する関数を受け、結果を受け取るための std::future
オブジェクトを返します。
別スレッドで実行させるだけでなく、同じスレッドで遅延実行させることもできる、非常に重宝する機能です。
今回は前者、別スレッドで実行させるために使います。
しかし std::future
はコピーができないため、同じパラメータが来たときに同じものを返すこのコンテナを実装するうえでは不便です。
そこで、share
関数で std::shared_future
にすることでこの問題を解決しています。
つまり、SharedResourceContainer
は 非同期生成タスクを管理するコンテナ を持っているわけです。
可変長引数テンプレート (Variadic Templates)
C++11 で「可変長引数テンプレート」が使えるようになったことも大きいです。
C++03 でも、同様の機能を持つコンテナを作ろうと思えば可能ですが、その場合、オブジェクトを生成する際のパラメータは固定にするか、あるいは void *
とキャストを駆使して対応することになります。
SharedResourceContainer
は request
関数に渡す引数とテンプレート型 Resource
のコンストラクタの引数が合っていれば、型や数に制限はありません。
「可変長引数テンプレート」は自由度だけでなく、「仕方なく void *
で受け取る」という選択肢を排除して、安全性を向上する面でも寄与しています。
コピーとムーブ
キーに一意のリソースを管理するコンテナの性質上、コピーは禁止しなければなりません。
よって、コピーコンストラクタと代入演算子はアクセス指定子を private
にして、なおかつ = delete
を記述し暗黙的な定義を無効化してあります。
ムーブは自分のオブジェクトと移動元のオブジェクトの両方のコンテナにアクセスが必要になるため、スレッドセーフを保証するための排他制御は慎重に行う必要があります。
ですが嬉しいことに C++11 にはそのための関数 std::lock
が用意されていて、デッドロックを防ぎつつ、与えられたミューテックスをすべてロックすることができます。1
使用例
ファイルの内容をすべて読み込むリソースを作ってみます。
class FileLoader
{
public:
FileLoader() = default;
FileLoader( const FileLoader & source ) = default;
FileLoader( FileLoader && source ) = default;
~FileLoader() = default;
FileLoader & operator=( const FileLoader & source ) = default;
FileLoader & operator=( FileLoader && source ) = default;
FileLoader( const std::string & path_to_file )
: blob_()
{
std::ifstream ifs( path_to_file, std::ifstream::binary );
if ( !ifs )
{
throw std::runtime_error( "Failed to open a file handle." );
}
auto length = ifs.seekg( 0, ifs.end ).tellg();
ifs.seekg( 0, ifs.beg );
std::shared_ptr< char > ptr( new char[ length ], std::default_delete< char[] >() );
ifs.read( ptr.get(), length );
ifs.close();
blob_.swap( ptr );
}
char * get() const
{
return blob_.get();
}
private:
std::shared_ptr< char > blob_;
};
たとえばこのように実装します。
std::shared_ptr
を使っているので、ファイル名を受けるコンストラクタをきちんと実装しておけば、残りのコンストラクタ等は = default
でコンパイラがおいしく料理してくれます。
int main()
{
SharedResourceContainer< std::string, FileLoader > container;
auto req0 = container.request( "path_to_file_a" );
auto req1 = container.request( "path_to_file_b" );
auto new_container = std::move( container );
auto req2 = new_container.request( "path_to_file_a" );
auto loader0 = req0.get();
auto loader1 = req1.get();
auto loader2 = req2.get();
if ( loader0.get() == loader1.get() )
{
std::cout << "loader0 と loader1 は同じリソースを持つ" << std::endl;
}
if ( loader0.get() == loader2.get() )
{
std::cout << "loader0 と loader2 は同じリソースを持つ" << std::endl;
}
if ( loader1.get() == loader2.get() )
{
std::cout << "loader1 と loader2 は同じリソースを持つ" << std::endl;
}
return 0;
}
req0
と req1
は違うファイルを指定しているわけですから、当然異なるリソースです。「同じリソースを持つ」は表示されません。
req2
はコンテナをムーブしてから request
関数に渡していますが、同じリソースとなり、その旨も表示されます。
req1
と req2
は言わずもがな、異なるリソースですね。
おわりに
自分の作ったものと C++11 のいいところを絡めながら紹介できればいいなぁと思ってここ数週間記事を考えましたが、少しでもお役に立てれば嬉しいです。
それにしても C++11 は色々な局面でかゆいところに手が届く仕様で、実装していて楽しくなりますね。
奥付
記事には自分なりにしっかりと目を通してはいますが、クラスや関数の使い方を間違えている、言語規格違反がある、誤解を招く表現がある、もっとスマートな方法があるといった場合は、コメント欄にてご指摘くださると幸いです。
- 初稿 「非同期生成したリソースを保持できるコンテナを作る」
- 2016/01/16
-
余談ですが、(MSVC では?)
std::lock
はすべてのミューテックスがロックできるまでstd::try_lock
をし続けるという実装になっていましたので、そのときの状況によってはロックまでに時間がかかることもあるでしょう。 ↩