.NET 的快速 ReaderWriterLock(译)

409 阅读4分钟

原文地址

在多线程应用程序中,对共享资源的访问必须在线程之间同步。如果您更频繁地读取数据,那么您可以更改它,您可以使用特殊的读写锁来更有效地利用您的多核甚至多 CPU 环境。读写锁的原始 .NET 实现是System.Threading.ReaderWriterLock. 从 .NET 3.5 开始,您可以选择使用System.Threading.ReaderWriterLockSlim这几乎是原始版本的两倍。但即使是精简版也几乎比 lock(…) {…} 语句使用的 Monitor 慢两倍。在我的一个项目中,我必须同步对字典的调用。在这种环境中,在字典的单个添加或获取操作期间,锁会保持很短的时间(纳秒)。锁实现的性能对整个系统性能非常重要。我最终使用Interlocked.CompareExchange.NET 环境提供的内在机制编写了自己的读写锁实现。结果非常有趣:

以下是我在笔记本电脑、较旧的双 CPU Xeon 服务器和带有 QPI 接口的较新双 CPU Xeon 上的实现测试。测试启动 10 个读取器线程和 10 个写入器线程。写入者在锁定下将单个整数增加 5 次。读者检查共享整数的当前值是否可以被 5 整除,如果锁正常工作应该是这样。测试在一个循环中重复 10M 次。总是有 3 次运行:一次没有锁定,一次使用ReaderWriterLockSlim ,最后一次使用我的自定义ReaderWriterLockTiny.

第一行以毫秒为单位显示经过的时间。第二行显示了阅读器在无效状态下看到共享整数的大致次数(如果正确同步,则应为 0)。第三行显示了增量操作的结果(期望值为:5增量10M迭代10个线程=500M)。

这是我的笔记本电脑上的测试输出(i5-2520M @ 2.5GHz)

Executing units: 4

Unlocked:
msec: 1511,1511
read collisions: 37768728
result: 392446846
expected result: 500000000

ReaderWriterLockSlim:
msec: 10687,0686
read collisions: 0
result: 500000000
expected result: 500000000

ReaderWriterLockTiny:
msec: 4932,4932
read collisions: 0
result: 500000000
expected result: 500000000

自定义实现的性能好于ReaderWriterLockSlim 2 倍。内核通过 L2 缓存同步锁定值,速度非常快。

这是旧服务器的结果(至强 E5450 @ 3.0 GHz)

Executing units: 8

Unlocked:
msec: 1143,1143
read collisions: 12173642
result: 298697846
expected result: 500000000

ReaderWriterLockSlim:
msec: 46072,6068
read collisions: 0
result: 500000000
expected result: 500000000

ReaderWriterLockTiny:
msec: 47045,7041
read collisions: 0
result: 500000000
expected result: 500000000

由于旧款至强 E5450 的 FSB 较慢,我们显然在这里遇到了瓶颈。ReaderWriterLockTiny 甚至比 ReaderWriterLockSlim 慢一点,可能是因为 SpinLocks 使 CPU 消耗达到 100%。

这是较新服务器的结果(至强 E5-2680 @ 2.7 Ghz)

Executing units: 32

Unlocked:
msec: 1772,1772
read collisions: 16776995
result: 164617248
expected result: 500000000

ReaderWriterLockSlim:
msec: 85963,5955
read collisions: 0
result: 500000000
expected result: 500000000

ReaderWriterLockTiny:
msec: 21710,1708
read collisions: 0
result: 500000000
expected result: 500000000

微型版本的性能是超薄版本的 4 倍。CPU 使用 QPI 接口来同步锁定值。

结论:ReaderWriterLockTiny 在细粒度锁定场景中是一个理想的解决方案,在这种情况下,锁经常被应用,而且时间很短(如果你没有使用带有 FSB 接口的旧多 CPU 服务器)。

您可以从我的Github存储库下载整个源代码。这是一个简短的片段,显示了锁是如何实现的:

struct ReaderWriterLockTiny
{
    // if lock is above this value then somebody has a write lock
    const int _writerLock = 1000000;
    // lock state counter
    int _lock;
 
    public void EnterReadLock()
    {
        var w = new SpinWait();
        var tmpLock = _lock;
        while (tmpLock >= _writerLock ||
            tmpLock != Interlocked.CompareExchange(ref _lock, tmpLock + 1, tmpLock))
        {
            w.SpinOnce();
            tmpLock = _lock;
        }
    }
 
    public void EnterWriteLock()
    {
        var w = new SpinWait();
 
        while (0 != Interlocked.CompareExchange(ref _lock, _writerLock, 0))
        {
            w.SpinOnce();
        }
    }
 
    public void ExitReadLock()
    {
        Interlocked.Decrement(ref _lock);
    }
 
    public void ExitWriteLock()
    {
        _lock = 0;
    }
 
}