http://www.ddj.com/dept/cpp/184403815
これ面白かった.この記事,以前読んだことがあったのだけれど,そのときは記事の前半だけ流し読みしてて「知ってる話っぽいからいーや」とか思って途中で読むのやめてた.しかし,後半が例外安全かつスレッド安全なインタフェースっつーなかなか高度な取り合わせになっていて,読み飛ばしていたことを大後悔時代.勝手に要約すると「スタックの実装を例にしながら,モニタオブジェクトの外部に同期のためのロックの構文を曝すことなくスレッド安全性を達成し,同時に例外安全も獲得したいけれどどーすりゃえーの?」な感じの記事.っていうか同期境界がオブジェクトのメソッドに一致するのがモニタパタンの中核的特徴だから前者の要求は至極妥当で,それに例外安全という要求も加わります的な話.
「top() の戻り値の型を参照にすると race condition 起こりうるよね」ってところ
class MoeStack { public: ..... void push( Charactor const &c ) { boost::mutex::scoped_lock lk( mtx_ ); vec_.push_back( c ); } void pop() { boost::mutex::scoped_lock lk( mtx_ ); vec_.pop_back(); } Charactor const &top() const { boost::mutex::scoped_lock lk( mtx_ ); return vec_.back(); } private: std::vector< Charactor > vec_; mutable boost::mutex mtx_; }; void functionWithPotentialRaceCondition() { MoeStack stk; ..... Charactor c = stk.top(); // マルチスレッド環境でふぎゃっ!! }
はまったく見落としていて思わず「ふぎゃっ!!」とか言っちゃったり, "More Exceptional C++" で Herb Sutter に
Unfortunately, I do not know of any good and safe use for uncaught_exception(). My advice: Don't use it.とまで書かれていた std::uncaught_exception が真っ当に陽の目を見ていたりして「良かったにぇ,ホントに良かったにぇ」とかちょっとお涙頂戴物語な感じだったり,色々勉強になったよ〜な気がする.気がするだけかもしれない.
しかし,記事中でやっているような苦労をするくらいなら out な引数を指定する形のインタフェースにしてやって手っ取り早く片付ければ良い問題じゃないかという話もあるんだけれど,文中にあるとおり out な引数なとして指定できるためには(コピー)代入と(たいていの場合)デフォルトコンストラクタを用意しなければならないんだよにぇ.
class Charactor { public: Charactor(); // 必要 void assign( Charactor const & ); // 必要 ..... }; class MoeStack { public: ..... void top( Charactor &out ) { boost::mutex::scoped_lock lk( mtx_ ); out.assign( vec_.back() ); // 代入のための構文が必要 } ..... }; int main() { Charactor c; // デフォルトコンストラクタ(もしくは「オブジェクトの入れ物を作るための構文」)が必要 stk.top( c ); }
out な引数として指定できるように代入とデフォルトコンストラクタ(resourceless な状態)を用意することついてもうちょっと思うところ書かせてもらうと, std::string 程度の汎用かつ primitive なクラスならともかくも,アプリケーションレベルのコードに出現するクラスの場合,むしろデフォルトコンストラクタ(resourceless な状態)を持たせないほうが自然なもののほうが,むしろ多いくらいなのではないかという個人的な感覚がある.そして,そういうクラスに対して out な引数として関数に渡せるようにするためだけにデフォルトコンストラクタ(resourceless な状態)を用意するとする.すると,クラスの利用者側に本来曝さなくて良い無効なオブジェクトの状態を曝すことになる.これは,自分でもやや潔癖な感覚かとは思うのだけれど,ちょっと容認しかねるにゃーっていう.以上から,関数の結果を値返しにしたがるという嗜好が私の中で形成されている今日この頃,皆様いかがお過ごしでしょうか?
out な引数として指定できるために値の代入のなんらかの構文も必要になる.代入についは多くのクラスで自然な機能であるけれど,一方である種の設計,特にオブジェクトが生成されたのち一貫して状態が変化しないことを強制する設計と衝突する場合もある.そして,こうした設計 (Immutable パタン) はまさに今問題としているマルチスレッド環境での典型的な設計パタンなのでこちらもやはりうむむむむとなってしまう文脈があるかも知れない.
もし例外安全かつ(ロックの構文をモニタオブジェクトの外部に暴露しない)スレッド安全を優先する場合,もしくは free store の確保・開放のコストが(クラス自体の機能のコストに対して相対的に)小さいならば上のジレンマを解消する方策がある.関数内で free store にオブジェクトを乗せて std::auto_ptr もしくは boost::shared_ptr の値を返す.
class MoeStack { public: ..... boost::shared_ptr< Charactor > popTop() { boost::mutex::scoped_lock lk( mtx_ ); boost::shared_ptr< Charactor > p = boost::shared_ptr< Charactor >( new Charactor( vec_.back() ) ); vec_.pop_back(); return p; // ここのコピーは no-throw } ..... private: std::vector< Charactor > vec_; mutable boost::mutex mtx_; }; void processInMultiThreadEnvironment() { MoeStack stk; ..... boost::shared_ptr< Charactor > p = stk.popTop(); assert( p->getName() == "Vita" || p->getName() == "geboko" ); }
もしくは pimpl で no-throw なコピーコンストラクタを持たせたクラスにして値返しするつーのも選択肢に入ってくるように思う.
class Charactor { public: Charactor( Charactor const &rhs ) //throw() <- これが no-throw なのが重要 : pimpl_( rhs.pimpl_ ) {} private: class Impl; boost::shared_ptr< Impl > pimpl_; }; class MoeStack { public: ..... Charactor popTop() //throw() { boost::mutex::scoped_lock lk( mtx_ ); Charactor c = vec_.back(); vec_.pop(); return c; } ..... private: std::vector< Charactor > vec_; mutable boost::mutex mtx_; }; void processInMultiThreadEnvironment() { MoeStack stk; ..... Charactor c = stk.popTop(); assert( c.getName() == "Louise Françoise le Blanc de la Vallière" ); }
いずれにせよコピーコンストラクタが no-throw であるという要求が例外安全性と(lock の構文をモニタオブジェクトの外に暴露せずに達成できる)スレッド安全性を同時に獲得するための鍵と言えるような気がするようなしないような?
余談だけれども, Move Semantics があれば効率に関する犠牲を払わずにこの種の問題が解消できるため,この文脈からも Move Semantics イチオシなんですが,今 Move Semantics が手に入るわけではないので本当に余談で終わってしまうという…….←これ後で考えたら大ウソやん.少なくとも前者の問題に対しては,Move Semantics を持ったクラスはデフォルトコンストラクタは持たなくて良いかも知れないけれど, valid resourceless state は依然として必要というか必須だから実質何も解決されていないジャマイカ.
関数の結果を値で返すことの別の側面としては,関数言語志向の構文との親和性の話もあって,もちろんちょっと考えれば関数合成などと相性が良くなるなど利点は明らかなんだけれど,ここら辺はさらに深くて面白い議論となるとちょっと見当たらなくてしょぼーんという印象.
関数の純粋性と例外安全性っつー取り合わせの話としては "Exception Safety Analysis" なんかも面白い*1けれど,まだちょっと academism に寄り過ぎてるかな?本当にちゃんと自動で例外安全性を推論しようとすると,変数間の依存性解析とかに似た話になりそうだよにぇとか門外漢が勝手に色々休むに似たりな感じに思ったり.
最後,なんか話逸れてしまった.ま〜,えっか.
*1:話逸れるけれど,この article の導入部の話は切り離しても良かったような?導入部の話は単体でも面白い話なんだれど,後ろの本論とのつながりがあまりないっぽ