在多线程应用程序中,对共享资源的访问必须在线程之间同步。如果您更频繁地读取数据,那么您可以更改它,您可以使用特殊的读写锁来更有效地利用您的多核甚至多 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;
}
}