lockステートメント

指定のコードブロックに対して、複数のスレッドが同時にアクセスできないように制限できます。ただし、同一のスレッドからのアクセスは制限されません。

構文

lock (obj)
    statement
lock ステートメント - C# リファレンス | Microsoft Learn

ロック オブジェクト (lock object)

objは、同期が必要なリソースでロックのスコープを定義するために使用されます。複数のスレッドが同一のオブジェクトをロックすることによるデッドロックを防ぐため、publicなオブジェクトは避けます。また同期ブロック インデックスのメンバが必要なため、参照型とします。同期の対象がコレクションならば、それのSyncRootプロパティがこれの代用となります。

  • Array.SyncRootの実装ではreturn this;となっており、Array自身が返される。
  • Queue<T>.SyncRootではSystem.Threading.Interlocked.CompareExchange(ref _syncRoot, new Object(), null);となっており、スレッドセーフに生成されたObjectが返される。このインスタンスはprivate Object _syncRoot;としてprivateなクラスフィールドとして保持される。

また以下のクラスのオブジェクトは、アプリケーション ドメインの境界を超えてアクセスされる恐れがあるため、利用を避けます。

  • MarshalByRefObject
  • ExecutionEngineException
  • OutOfMemoryException
  • StackOverflowException
  • String
  • MemberInfo
  • ParameterInfo
  • Thread

これらのクラスから派生するクラスのオブジェクトをロック用のオブジェクトとすると、コード分析で「ID が不十分なオブジェクトをロックしないでください」としてCA2002で警告されます。

statementには複数のスレッドからのアクセスを認められない文を記述し、あるスレッドがこのstatementを抜けるまで、他のスレッドはこの直前で待機させられます

lockでは無制限に待機するため、これを制限するタイムアウトを指定するにはMonitor.TryEnter()を用います。

サンプルコード

たとえばあるオブジェクトを初期化する場合を考えます。

private readonly object lockObj = new object(); // ロック用のオブジェクト
if (target == null)
{
    lock (lockObj)
    {
        // lockの直前に他のスレッドによって書き換えられていないか再確認する
        if (target == null)
        {
            target = new MyClass();
        }
    }
}

実装

lockステートメントはMonitorクラスのEnterとExitを呼ぶショートカットに過ぎず、

lock (x)
{
    // 共有リソースへのアクセス
}

と記述することは、

Monitor.Enter(x);

try
{
    // 共有リソースへのアクセス
}
finally
{
    Monitor.Exit(x);
}

とすることと同義です。Thread Synchronization (C# and Visual Basic) | MSDN

ただしC# 4.0以降は、次のように変更されているようです。 c# - What does a lock statement do under the hood? - Stack Overflow lockWasTaken - Locks and exceptions do not mix | Fabulous adventures in coding

bool lockTaken = false;

try
{
    Monitor.Enter(x, ref lockTaken);
    // 共有リソースへのアクセス
}
finally
{
    // ロックを得られていたならば、それを解放する
    if (lockTaken) Monitor.Exit(x);
}
lock ステートメント - C# リファレンス | Microsoft Learn

lock以外の方法

lockステートメントを、1つのスレッドだけにアクセスさせる場合

lockではロックが解放されるのを待って同期ブロックが順に実行されますが、1つのスレッドが実行すれば良いだけならば、次のようにMonitor.TryEnter()を用います。

if (Monitor.TryEnter(x))
{ // ロックを取得できた
    try
    {
        // 共有リソースへのアクセス
    }
    finally
    {
        Monitor.Exit(x);
    }
}

この場合ロックを取得したスレッドが共有リソースを処理し、他のスレッドは何もしません。

lockステートメントで、スレッドが変更される場合

ロックは、それを取得したスレッドで解放しなければなりません。このような制約を受けたくないならば、SemaphoreSlimを用います。

object lockObj = new Object();
lock (lockObj)
{
    await Task.Delay(10); // error CS1996: lock ステートメントの本体で待機することはできません。
}

デッドロックが発生する状況

他方がロックを取得しているリソースの解放を、それぞれが待つとデッドロックします。

object o1 = new object();
object o2 = new object();

Action action1 = delegate
{
    lock (o1)
    {
        Task.Delay(10).Wait();
        lock (o2) { } // o2の解放を待つが、o1をロックしているため解放されない
    }
};

Action action2 = delegate
{
    lock (o2)
    {
        Task.Delay(10).Wait();
        lock (o1) { } // o1の解放を待つが、o2をロックしているため解放されない
    }
};

Parallel.Invoke(action1, action2);
第4回 デッドロックの回避とスレッド間での同期制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御(後編) ―:連載.NETマルチスレッド・プログラミング入門(1/3 ページ) - @IT 高木健一 (2005/06/15)
Microsoft Learnから検索