lock ステートメント - 共有リソースへの排他的なアクセスを保証

lock ステートメントは、指定のオブジェクトに対する相互排他ロックを取得し、ステートメント ブロックを実行してからロックを解放します。 ロックが保持されている間、ロックを保持するスレッドではロックを再度取得し、解放することができます。 他のスレッドはブロックされてロックを取得できず、ロックが解放されるまで待機します。 lock ステートメントにより常に、最大で 1 つだけのスレッドでその本文が実行されます。

lock ステートメントの形式は次のとおりです。

lock (x)
{
    // Your code...
}

変数 x は、System.Threading.Lock 型の式、または参照型です。 コンパイル時に xSystem.Threading.Lock 型であることがわかっている場合は、次とまったく同じになります。

using (x.EnterScope())
{
    // Your code...
}

Lock.EnterScope() によって返されるオブジェクトは、Dispose() メソッドを含む ref struct です。 生成された using ステートメントにより、lock ステートメントの本文で例外がスローされた場合でも、スコープが解放されることが保証されます。

それ以外の場合、lock ステートメントは次とまったく同じです。

object __lockObj = x;
bool __lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
    // Your code...
}
finally
{
    if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}

このコードでは try-finally ステートメントが使用されているため、lock ステートメントの本文内で例外がスローされた場合でもロックは解放されます。

lock ステートメントの本体で awaitを使用することはできません。

ガイドライン

.NET 9 および C# 13 以降では、最適なパフォーマンスを得るために、System.Threading.Lock 型の専用オブジェクト インスタンスをロックします。 さらに、既知の Lock オブジェクトが別の型にキャストされてロックされている場合、コンパイラは警告を発行します。 以前のバージョンの .NET と C# を使用している場合は、別の目的で使用されていない専用オブジェクト インスタンスをロックします。 異なる共有リソースに対して同じロック オブジェクト インスタンスを使用することは避けてください。デッドロックやロックの競合が発生する可能性があります。 特に、次のインスタンスをロック オブジェクトとして使用しないでください。

  • this: 呼び出し元も this をロックする可能性があるためです。
  • Type インスタンス。typeof 演算子またはリフレクションによって取得される可能性があります。
  • 文字列リテラルを含む文字列インスタンス。インターン処理される可能性があります。

ロックの競合を減らすために、できるだけ短い時間ロックを保持します。

次の例では、専用 balanceLock インスタンスをロックすることでそのプライベート balance フィールドへのアクセスを同期する Account クラスが定義されます。 ロックに同じインスタンスを使用すると、2 つの異なるスレッドが、Debit メソッドまたは Credit メソッドを同時に呼び出して balance フィールドを更新できないことが保証されます。 このサンプルでは、C# 13 と新しい Lock オブジェクトを使用します。 以前のバージョンの C# または以前の .NET ライブラリを使用している場合は、object のインスタンスをロックします。

using System;
using System.Threading.Tasks;

public class Account
{
    // Use `object` in versions earlier than C# 13
    private readonly System.Threading.Lock _balanceLock = new();
    private decimal _balance;

    public Account(decimal initialBalance) => _balance = initialBalance;

    public decimal Debit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "The debit amount cannot be negative.");
        }

        decimal appliedAmount = 0;
        lock (_balanceLock)
        {
            if (_balance >= amount)
            {
                _balance -= amount;
                appliedAmount = amount;
            }
        }
        return appliedAmount;
    }

    public void Credit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "The credit amount cannot be negative.");
        }

        lock (_balanceLock)
        {
            _balance += amount;
        }
    }

    public decimal GetBalance()
    {
        lock (_balanceLock)
        {
            return _balance;
        }
    }
}

class AccountTest
{
    static async Task Main()
    {
        var account = new Account(1000);
        var tasks = new Task[100];
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = Task.Run(() => Update(account));
        }
        await Task.WhenAll(tasks);
        Console.WriteLine($"Account's balance is {account.GetBalance()}");
        // Output:
        // Account's balance is 2000
    }

    static void Update(Account account)
    {
        decimal[] amounts = [0, 2, -3, 6, -2, -1, 8, -5, 11, -6];
        foreach (var amount in amounts)
        {
            if (amount >= 0)
            {
                account.Credit(amount);
            }
            else
            {
                account.Debit(Math.Abs(amount));
            }
        }
    }
}

C# 言語仕様

詳細については、「C# 言語仕様」の lock ステートメントに関するセクションを参照してください。

関連項目