はじめに
.NET 9.0 で同期処理用の System.Threading.Lock が追加されました。
任意のオブジェクトを lock できるのはオーバーヘッドが大きい https://ufcpp.net/blog/2024/4/lock-class/ らしく、パフォーマンス改善のために導入されたようです。
今回は現時点 (.NET 9.0-rc.1) でどのくらいパフォーマンスに違いがあるか検証してみます。
// Lock の使い方
using System.Threading;
var lockHandle = new Lock();
lock (lockHandle)
{
    // Do something
}
サンプルコード
テストコード
using System.Threading;
public class _LockClassTest
{
    void HowToUse()
    {
        var lockHandle = new Lock();
        lock (lockHandle)
        {
            // Do something
        }
    }
    static void IncrementTest(Performance p)
    {
        p.AddTest("lock_object", () =>
        {
            var count = 0;
            var lockHandle = new object();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    ++count;
            }
        });
        p.AddTest("lock_Lock", () =>
        {
            var count = 0;
            var lockHandle = new Lock();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    ++count;
            }
        });
        p.AddTest("Lock.EnterScope()", () =>
        {
            var count = 0;
            var lockHandle = new Lock();
            for (int n = 0; n < 10000; n++)
            {
                using (lockHandle.EnterScope())
                    ++count;
            }
        });
        p.AddTest("Monitor_object", () =>
        {
            var count = 0;
            var lockHandle = new object();
            for (int n = 0; n < 10000; n++)
            {
                Monitor.Enter(lockHandle);
                ++count;
                Monitor.Exit(lockHandle);
            }
        });
        p.AddTest("InterLocked", () =>
        {
            var count = 0;
            for (int n = 0; n < 10000; n++)
            {
                Interlocked.Increment(ref count);
            }
        });
        p.AddTest("NoLock", () =>
        {
            var count = 0;
            for (int n = 0; n < 10000; n++)
            {
                ++count;
            }
        });
    }
    static void GetHashCodeTest(Performance p)
    {
        p.AddTest("lock_lockHandle", () =>
        {
            var hash = 0;
            var lockHandle = new object();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    hash += lockHandle.GetHashCode();
            }
        });
        p.AddTest("lock_obj", () =>
        {
            var hash = 0;
            var lockHandle = new object();
            var obj = new object();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    hash += obj.GetHashCode();
            }
        });
        p.AddTest("Lock_lockHandle", () =>
        {
            var hash = 0;
            var lockHandle = new Lock();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    hash += lockHandle.GetHashCode();
            }
        });
        p.AddTest("NoLock", () =>
        {
            var hash = 0;
            var obj = new object();
            for (int n = 0; n < 10000; n++)
            {
                hash += obj.GetHashCode();
            }
        });
    }
}
パフォーマンス比較
テストその1:インクリメント
単純なインクリメントを行う、同期処理のテストです。
var count = 0;
var lockHandle = new object();
for (int n = 0; n < 10000; n++)
{
    lock (lockHandle)
        ++count;
}
| Test | Score | % | CG0 | 
|---|---|---|---|
| lock_object | 1,195 | 100.0% | 0 | 
| lock_Lock | 1,256 | 105.1% | 0 | 
| Lock.EnterScope() | 1,188 | 99.4% | 0 | 
| Monitor_object | 1,479 | 123.8% | 0 | 
| InterLocked | 5,598 | 468.5% | 0 | 
| NoLock | 42,696 | 3,572.9% | 0 | 
実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- ロックハンドルが 
objectの場合と比較して、Lockは 5% くらいパフォーマンスが良いです - 
lock(Lock)とusing(Lock.EnterScope())はコンパイラによって同じコードになります(糖衣構文) - スコアはブレがあります(10% 程度)
 - インクリメント等の数値の単純な操作は、
System.Threading.Interlockedを使うのが高速です 
テストその2:GetHashCode() を使う場合
ロック用の値とハッシュ値は共有しているらしく、ロック中はハッシュコードの取得が遅くなるとのことで、その検証です。
var hash = 0;
var lockHandle = new object();
for (int n = 0; n < 10000; n++)
{
    lock (lockHandle)
        hash += lockHandle.GetHashCode();
}
| Test | Score | % | CG0 | 
|---|---|---|---|
| lock_lockHandle | 885 | 100.0% | 0 | 
| lock_obj | 1,018 | 115.0% | 0 | 
| Lock_lockHandle | 1,036 | 117.1% | 0 | 
| NoLock | 3,907 | 441.5% | 0 | 
実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- ロック中にロックしたオブジェクトからハッシュコードを取得した場合、15% 程度パフォーマンスが悪いです
 - 
Lockオブジェクトの場合、ロック中にハッシュコードを取得しても影響なさそうです - とはいえ、ロック中のオブジェクトのハッシュコードを取得することはまずなさそうです
 
おわりに
検証の結果、現時点 (.NET 9.0-rc.1) では lock(Lock) は従来の方法よりも 5% くらいパフォーマンスに優れるようです。既存のコードをリファクタリングするほどではないですが、新しいコードでは取り入れてもいいでしょう。
マルチスレッドのプログラムは難しくパフォーマンスより正しさ・読みやすさを優先したいところですが、Lock クラスは構文的にも既存のコードにそのまま置き換えられるのが良さげです。
パフォーマンス改善に加えて、そのオブジェクトの用途がスレッドのロックに限定されるため、コードの可読性も上がるかもしれません。