これは、純粋にC++のコンテキストにおける設計哲学の質問です。
コンストラクター内からスレッドを開始することは大丈夫な設計哲学ですか?公開されているAPIによる一部の要求を非同期で処理するのが唯一の責任であるライブラリーがあります。これが機能するには、内部でスレッドを開始する必要があります。
私のライブラリから公開されているパブリッククラスであるMyClass
があります。
//MyClass.h
MyClass {
private:
std::thread m_member_thread;
}
このようにコンストラクター内からスレッドを開始しても大丈夫ですか?
//MyClass.cpp
MyClass::MyClass() {
// Initialize variables of the class and then start the thread
m_member_thread = std::thread(&MyClass::ThreadFunction, this);
}
または、私のライブラリのユーザーコードがスレッドを開始するために明示的に呼び出すメソッドを公開する方が良いですか?
//MyClass.cpp
MyClass::MyClass() {
}
MyClass::StartThread() {
m_member_thread = std::thread(&MyClass::ThreadFunction, this);
}
それともそれは本当に重要ではありませんか?
PS:
はい、スレッドは軽量で、それほど重い操作を行いません。私が言う答えは避けたいです、それはスレッドが何をするかなどに依存します。これは純粋にデザイン哲学の質問です。
コンストラクターでオブジェクトを完全に初期化すると、状態の数が減り、オブジェクトの推論が容易になるため、通常はそれをお勧めします。デザインが本当にこのステートフル性を必要としない限り、開始および停止メソッドはありません!同様に、コンストラクターの後に呼び出す必要があるinitメソッドは大規模なデザインの匂いです–オブジェクトを初期化できない場合、まだ存在していてはならず、nullポインターでモデル化できます。ステートフルな操作により、const
を適切に使用できなくなる場合もあります。
「コンストラクターは実際の作業を行うべきではない」というアドバイスは、C++にはあまり当てはまりません。その目的は、コンストラクターの実行が、C++の「自明に構築可能な」概念と同様に、非常に安価であることです。そのアドバイスに従って、構築前にリソースを取得し、コンストラクタに引数として渡す必要があります。プライベートコンストラクターを呼び出すパブリックスタティックファクトリメンバー関数を使用します。しかし、これはRAII(リソース取得is初期化であり、それより前ではない)に違反しており、例外セーフなコードの記述を難しくしています。これは、C++では適切な方法ではありません。
したがって、コンストラクター内でスレッドを作成すること(および何か問題が発生した場合に例外をスローすること)は、完全に正しいことです。
標準ライブラリには、関連する動作の例がいくつか含まれています。 std::ifstream
などのファイルオブジェクトは、コンストラクタ内でファイルを正しく開いたり作成したりします。残念ながら、エラーが発生した場合、結果のオブジェクトが使用可能な状態にならない可能性があるという点で、設計に欠陥があります。より優れた設計は、エラーをスローするか、オプションの値を返すファクトリ関数を使用することでした。次に、std::vector
やstd::string
などのバッファを検討します。それらはコンストラクタでメモリを割り当てるかもしれませんが、そうする必要はありません。代わりに、割り当ては、文字/アイテムがバッファに追加されてその容量を使い果たすまで延期される場合があります。設計では、実際に使用されるまでスレッドの初期化を延期することが望ましい場合があります。ある作業単位がスレッドにスケジュールされるまで。
一般的なデザイン哲学としては悪い考えです。 これにより競合状態が発生します。
新しいスレッドはすぐに開始され、まだ初期化されていないオブジェクトのリソースにアクセスできます。さらに、スレッドは仮想メソッドを使用しない場合があります。この問題の詳細については、この記事をお読みください: https://rafalcieslak.wordpress.com/2014/05/16/c11-stdthreads-managed-by-a-designated-class/
もちろん、いくつかのルールに従うことで問題はないと主張することもできますが、その場合、コードには永久に「特別な注意」が必要になります。
それは、コンストラクターが実際の作業を行うべきかどうかに関する設計哲学に依存します。許可されている場合は、スレッドが何を行っていても問題ありません。そうでない場合は、そうではありません。
クラスの不変条件についても考えてください。バックグラウンドで実行されているスレッドはクラスの不変ですか?コンストラクターで不変条件を確立する必要があります。
最後に、これを考慮してください:std::thread
自身のコンストラクターがスレッドを開始します。これは、標準ライブラリの設計者の設計哲学に関するヒントになるかもしれません。
私にとっての質問は、あなたのニーズに関して、スレッドの存続期間がオブジェクトの存続期間にどのように結びついているかです。考慮事項として、スレッドプールを検討してください。
class ThreadPool
{
...
private:
MyClass threads[max_threads];
};
その場合、スレッドプールがN
スレッドと対応するオブジェクトを事前に割り当て/初期化し、それらを開始および停止する可能性があるため、オブジェクトの存続期間がスレッドの存続期間に関連付けられている場合、負担になる可能性があります。 ctorおよびdtorとは無関係にスレッドを作成します。必要に応じてこれら2つのライフタイム(スレッドライフタイムとオブジェクトライフタイム)の分離を必要とする場合、言語と調和して作業するのではなく、特に頑固に結び付けようとしながら、言語との戦いを開始できます。 2つ一緒に。
既存のスレッドリソースを再利用し、新しいスレッドの状態と関数でもう一度起動して、実際に新しいスレッドを作成したり、対応する新しいオブジェクトをインスタンス化したりせずに呼び出す場合は、これら2つを互いに独立させることができます。
上記のような場合、別のstart_thread
またはstart
またはrun
ラッパーの構築とは無関係にスレッドの実行を開始するための一種のメソッドと、オブジェクトを破棄する前にスレッドを終了するメソッド(ただし、オブジェクトを破棄するときにスレッドを確実に終了させることは、おそらくまだ良い考えです。
それが、この特定のケースで私が検討する主要な関心事です。オブジェクトの存続期間をスレッドの存続期間に関連付けることができれば、それは間違いなく事態を単純化するでしょう。しかし、2つを分離しないと、より多くの設計オプションが開かれる可能性があります。 "starting a operation" from "constructing a object" ... or not not and not it it canize a more code into more than required required to実際にはその区別は必要ありません。