C#だけに限った話ではないけど
余程小さなシステムでもない限り
マルチスレッドを大なり小なり使う機会は多い。
C#は割と手軽にスレッド処理を行えるので登場機会は多い。(と思う)
そうなるとよく問題になるのが排他制御。
データを複数スレッドから同時に操作するとすぐ落ちるし、
タイミングに依存することが多いので
バグ発見が遅れたりします。
この間も現地試験フェーズでうっかりバグが出てしまい、対応した時に使ったクラスが
『ReaderWriterLockSlim』。
特定のクラスが保持する定義データの変更と参照がぶつかって例外。
本来であれば設計段階でしっかり落としておくべきなんだろうけど
終盤ということもあり、影響範囲も考慮したかったので、
シンプルにlock制御しようかと思ったんだけど
データ参照の頻度が高いので何でもかんでも排他すると
GUIでの操作にも影響が出そうだったので調べていて発見。
lockと違い、読み取りロックと書き込みロックを持つので
参照同士は排他されないのがポイント。
class DataManager
{
// データ読み書き排他用
ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
/// <summary>
/// データリスト
/// </summary>
private List<int> listData = new List<int>();
~DataManager()
{
// しっかり破棄する
rwlock.Dispose();
}
/// <summary>
/// リストを参照する処理
/// </summary>
private IEnumerable<int> ListData
{
get
{
try
{
// 読み込みロック取得
rwlock.EnterReadLock();
return listData.ToList();
}
finally
{
// 読み込みロック解放
if(rwlock.IsReadLockHeld)
rwlock.ExitReadLock();
}
}
}
/// <summary>
/// リストにデータを入れる処理
/// </summary>
/// <returns></returns>
public void LoadMaster()
{
try
{
// 書き込みロック取得
_rwlock.EnterWriteLock();
// データを取得するような処理
listData = new List<int>{ 0, 1, 2 };
}
finally
{
// 書き込みロック解放
if(_rwlock.IsWriteLockHeld)
_rwlock.ExitWriteLock();
}
}
}
ポイントとしては以下。
- ReaderWriterLockSlimはIDisposableを持つので破棄必須
- ロック取得後に例外が発生すると解放されずにデッドロックする
- ロックを解放する前にさらに取得すると例外が発生する(これは当たり前か)
1はReaderWriterLockSlimがインターフェース持ってるんだからそりゃそうだろ、と言われれば
それまでですが、C#のメモリ管理は勝手にやってくれるし、いいだろ…と
C++とかよりもその辺の意識が薄い人もいるので念のため。
#過去の自分がそうでした。ダメですね。
2は発生するとヤバいので必ずTry-Finallyで括って確実に解放しましょう。
ロック解放時は念のため、IsReadLockHeld/IsWriteLockHeldで
ロックを保持しているか確認しておくと安心。
#ロック未保持で解放すると例外
3は普通はまぁやらないと思いますが、ロック取得後に関数をコールして
その関数内でもロック取得、とかやると起きがちです。
排他すべき範囲をしっかり考えて使えば問題ないと思います。
#要はちゃんと設計しろってことですね…
C#の排他制御も色んな方法があって
パフォーマンス等それぞれ異なるのでケースバイケースで採用していきたいですね。