理論はわかるけど、結局どんなコードで書けばいいの!?と悩む、同志たちに捧ぐ。
#基本
複数のスレッドが共有する変数を操作するときは、適切な排他制御のもとに行う必要がある。
例えば、mutex によるクリティカルセクションを設けて、その中で共有変数への読み書きを安全に実行する。
#失敗
全ての実装(シングルスレッドで動作するもの)を終えたあとに、基本アイデアに従って、対象の共有変数へアクセスしている全てのコードブロックをクリティカルセクションで包んでいては骨が折れる。
#アイデア
もし、対象の変数へアクセスする度に、必ずクリティカルセクションを設けてくれる機能があるのなら、安心して実装を進められるだろう。
##★共有変数制御テンプレート
###コード
//--------------------------------------------------------------------------------------------
extern std::recursive_mutex thread_mutex;
//--------------------------------------------------------------------------------------------
template<typename T>
T Delegate_Mutex( const std::function<T()>& mutex_event )
{
// ロックする
std::lock_guard<std::recursive_mutex> lock( thread_mutex );
return mutex_event();
}
//--------------------------------------------------------------------------------------------
template<typename T>
class Critical
{
public:
// 値を書き換える版
template<typename R>
R Invoke( const std::function<R( T& )>& critical_event )
{
return Delegate_Mutex<R>( [&]() { return critical_event( m_sharedValue); } );
}
// 値を書き換えない版
template<typename R>
R Invoke( const std::function<R( const T& )>& critical_event ) const
{
return Delegate_Mutex<R>( [&]() { return critical_event( m_sharedValue); } );
}
private:
T m_sharedValue;
};
//--------------------------------------------------------------------------------------------
###解説
共有変数をprivate 修飾で宣言しているので、この変数への操作はInvoke 関数を経由するほかない。さらにInvoke 関数は、引数に渡された関数オブジェクトをコールするが、その前に必ずmutex によるロック(※ここでは再帰的なロックとした。)を行う。
こうすることで、うっかり共有変数を排他制御なしに操作してしまう危険性を排除できる。
##使い方
struct Counter
{
uint32_t count = 0;
};
{
Critical<Counter> criticalCounter;
criticalCounter.Invoke<void>( []( Counter& counter ) {
// 安全なインクリメント
counter++;
} );
}
#おわりに
スレッドセーフを実現するための道具には、atomic 変数や、他のロック管理クラスなどたくさんある。今回は排他制御の記述漏れ問題に対しての解決手段を提案した。何を優先し、どこにコストを払うのか、常にトレードオフを意識することが大切だと思う。