[C#] `lock` と `ReaderWriterLockSlim` のメモ

lockReaderWriterLockSlim による同期処理のメモ

lock で同期処理

var stopWatch = new Stopwatch();
stopWatch.Reset();
stopWatch.Start();
Parallel.For(0, 10, i =>
{
  lock (stopWatch)
  {
    for (int loop = 0; loop < 10; loop++)
    {
      Trace.Write(i);
      Thread.Sleep(10);
    }
    Trace.Write(Environment.NewLine);
  }
});
stopWatch.Stop();
Debug.WriteLine("Total time "
  + stopWatch.ElapsedMilliseconds); // 1000より大きくなる

※コレを実行すると出力される数値は同じ数値であればひとまとまりに出力される。

ReaderWriterLockSlim

なんかロックするのに便利なクラスらしい。 デッドロックしにくいとかなんとか。速度は lock よりも遅い。 Enter****Lock でロックを取得して、Exit****Lock でロックを手放す。

書き込みロック

var lockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
var stopWatch = Stopwatch.StartNew();
Parallel.For(0, 10, i =>
{
  lockSlim.EnterWriteLock();
  for (int loop = 0; loop < 10; loop++)
  {
    Trace.Write(i);
    Thread.Sleep(10);
  }
  Trace.Write(Environment.NewLine);
  lockSlim.ExitWriteLock();
});
stopWatch.Stop();
Debug.WriteLine("Total time "
  + stopWatch.ElapsedMilliseconds); // 1000より大きくなる

これの結果は lock の場合と同じ。 書き込みロックを取得した場合は、後続の処理はロックが外れるまで待つことになる。

読み込みロック

var lockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
var stopWatch = new Stopwatch();
stopWatch.Reset();
stopWatch.Start();
Parallel.For(0, 10, i =>
{
  lockSlim.EnterReadLock();
  for (int loop = 0; loop < 10; loop++)
  {
    Trace.Write(i);
    Thread.Sleep(10);
  }
  Trace.Write(Environment.NewLine);
  lockSlim.ExitReadLock();
});
stopWatch.Stop();
Debug.WriteLine("Total time "
  + stopWatch.ElapsedMilliseconds); // 100より大きくなる

これは出力の順番はバラバラとなる。 読み込みロックは、他の処理がロックを握ってても取得できるので。。

var lockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
try
{
  lockSlim.EnterReadLock();
  lockSlim.EnterWriteLock();
}
catch (LockRecursionException ex)
{
  Trace.WriteLine(ex.Message);
}
finally
{
  lockSlim.ExitReadLock();
}

例外が飛ぶ。

例外出力内容は :
読み取りロックを保持しながら、書き込みロックを取得することはできません。 このパターンでは、デッドロックの可能性が高まります。 書き込みロックを取得する前に、読み取りロックが解放されていることを確認してください。 アップグレードが必要な場合は、読み取りロックの代わりに、アップグレード可能なロックを使用します。

ReaderWriterLockSlim と例外と

var lockSlim = new ReaderWriterLockSlim();
Parallel.For(0, 10, i =>
{
  try
  {
    Trace.WriteLine("try " + i);
    lockSlim.EnterWriteLock();
    throw new Exception();
    lockSlim.ExitWriteLock();
  }
  catch
  {
    Trace.WriteLine("catch " + i);
  }
});

途中で例外が投げられると、ロックが解放されず、ロック解放待ちのスレッドが終わらない。

using (var lockSlim = new ReaderWriterLockSlim())
{
  Parallel.For(0, 10, i =>
  {
    try
    {
      Trace.WriteLine("try " + i);
      lockSlim.EnterWriteLock();
      throw new Exception();
    }
    catch
    {
      Trace.WriteLine("catch " + i);
    }
    finally
    {
      if (lockSlim.IsWriteLockHeld)
      {
        lockSlim.ExitWriteLock();
      }
    }
  }); // parallel for
} // using

finally できちんとロックを放す。また、ロックを持ってないのにExitすると System.Threading.SynchronizationLockException が投げられる。 また ReaderWriteLockSlimIDisposable を採用しているのでDisposeすること。 また、ロックを保持したまま ReaderWriteLockSlim インスタンスのファイナライズが実行される場合も SynchronizationLockException が投げられる。

組み合わせて実行してみる

var lockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
var watch = new Stopwatch();
watch.Start();
Parallel.For(0, 10, i =>
{
  if (i % 2 == 0)
  {
    lockSlim.EnterReadLock();
    Trace.WriteLine("enter read lock - " + watch.Elapsed.Seconds);
    Thread.Sleep(1000);
    lockSlim.ExitReadLock();
    Trace.WriteLine("exit read lock - " + watch.Elapsed.Seconds);
  }
  else
  {
    lockSlim.EnterWriteLock();
    Trace.WriteLine("enter write lock - " + watch.Elapsed.Seconds);
    Thread.Sleep(1000);
    lockSlim.ExitWriteLock();
    Trace.WriteLine("exit write lock- " + watch.Elapsed.Seconds);
  }
});

"enter read lock" の出力は実行してすぐに全部出力されるけど、 "enter write lock" の出力は順次出力される。

結果

enter read lock - 0
enter read lock - 0
enter read lock - 0
enter read lock - 0
enter read lock - 0
exit read lock - 1
exit read lock - 1
exit read lock - 1
exit read lock - 1
enter write lock - 1
exit read lock - 1
exit write lock- 2
enter write lock - 2
exit write lock- 3
enter write lock - 3
exit write lock- 4
enter write lock - 4
exit write lock- 5
enter write lock - 5
exit write lock- 6

再帰的なロックの取得

using (var l = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion))
{
  //
  // Read Lock 取得状態でロックを取得すると
  //
  l.EnterReadLock();
  TraceThrows(() => l.EnterReadLock()); // このモードでは、再帰的な読み取りロックは取得できません。
  TraceThrows(() => l.EnterWriteLock()); // 読み取りロックを保持しながら、書き込みロックを取得することはできません。このパターンでは、デッドロックの可能性が高まります。書き込みロックを取得する前に、読み取りロックが解放されていることを確認してください。アップグレードが必要な場合は、読み取りロックの代わりに、アップグレード可能なロックを使用します。
  l.ExitReadLock();

  //
  // Write Lock 取得状態でロックを取得すると
  //
  l.EnterWriteLock();
  TraceThrows(() => l.EnterReadLock()); // このモードでは、書き込みロックを保持しながら読み取りロックを取得することはできません。
  TraceThrows(() => l.EnterWriteLock()); // このモードでは、再帰的な書き込みロックは取得できません。
  l.ExitWriteLock();
} // using

using (var l = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion))
{
  //
  // Read Lock 取得状態でロックを取得すると
  //
  l.EnterReadLock();
  TraceThrows(() => l.EnterReadLock()); // No exception.
  TraceThrows(() => l.EnterWriteLock()); // 読み取りロックを保持しながら、書き込みロックを取得することはできません。このパターンでは、デッドロックの可能性が高まります。書き込みロックを取得する前に、読み取りロックが解放されていることを確認してください。アップグレードが必要な場合は、読み取りロックの代わりに、アップグレード可能なロックを使用します。
  l.ExitReadLock();
  l.ExitReadLock();

  //
  // Write Lock 取得状態でロックを取得すると
  //
  l.EnterWriteLock();
  TraceThrows(() => l.EnterReadLock()); // No exception.
  TraceThrows(() => l.EnterWriteLock()); // No exception.
  l.ExitWriteLock();
  l.ExitWriteLock();
  l.ExitReadLock();
} // using

ReaderWriterLockSlim の引数なしのコンストラクタは NoRecursion を渡した時と同じ。

アップグレード可能なロックはいまいち分からなかったのでまたいつか。。

Share
関連記事