65.9K
CodeProject 正在变化。 阅读更多。
Home

测试读写锁

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2009年1月21日

CPOL

7分钟阅读

viewsIcon

85193

downloadIcon

760

本文描述了在 Windows XP/Vista 单核和多核 PC 上测试读写锁结果的研究。

引言

许多开发人员为了提供比现有 API 结构更好的性能,建议了各种自制的读写锁对象。最近,微软在 Windows Vista 中引入了 SRWLOCK,一种新的内置读写锁原语。

现有的 API 同步构造提供了对共享资源的简单快速访问。另一方面,API 原语(例如,临界区)在发生高锁竞争的情况下可能效果不佳。锁队列问题并非此处唯一的问题。如果一个写者与多个读者竞争,某个读者的性能可能无法保证,其性能变化范围很广,以至于由于应用程序的要求,最低性能都不可接受。通常,自定义读写锁对象比 API 构造慢。然而,使用自定义锁的好处是,手工编写的对象可以为所有读写线程提供合理的性能。我们将寻找一个在多读者最高性能和写者可接受性能之间取得折衷的解决方案。

测试结果总结在图 1 和图 6 中。一个写者和四个读者并发访问共享资源。写者线程获取独占锁,用随机字节填充共享缓冲区,并计算 CRC。每个读者线程获取共享锁,计算 CRC,并将其与写者保存的 CRC 进行比较(在 CRC 错误时应该抛出异常,这表明锁实现不正确)。测试了以下读写锁对象:“CSRW”对象是使用 Windows API 临界区的最简单实现;“RWFavorNeither”和“RWLockFavorWriters”对象来自 MSDN 杂志的文章“CONCURRENCY: Synchronization Primitives New To Windows Vista”,作者是 Robert Saccone 和 Alexander Taskov(http://msdn.microsoft.com/en-us/magazine/cc163405.aspx);“Ruediger_Asche_RWLock”对象来自 MSDN 文章“Compound Win32 Synchronization Objects”,作者是 Ruediger R. Asche(http://msdn.microsoft.com/en-us/library/ms810427.aspx)。此锁实现了“CRWLock”复合对象。“Jim_B_Robert_Wiener_RWLock”锁对象来自“Multithreading Applications in Win32: The Complete Guide to Threads”,作者是 Jim Beveridge 和 Robert Wiener。新的 Windows Vista API 用户空间锁 SRWLOCK 也被测试了。

图 1. 在单核 PC AMD Sempron(tm) 3000+,2.00 GHz,32 位 Windows XP Home Edition SP3 上测试一个写者和四个读者,使用 VC++ 2005 Express 编译。每秒操作的持续时间(微秒)和变异系数(%)。
Without locking

reader: t=0.213 (s=0.85%)
writer: t=3.234 (s=0.46%)

Critical section CSRW lock

reader: t1=41586.02 (s1=161.18%) t2=0.974 (s2=40.32%) 
writer: t1=83.78 (s1=202.08%) t2=6.938 (s2=0.69%)

RWFavorNeither lock

reader: t1=1.166 (s1=9.18%) t2=1.09 (s2=7.05%) 
writer: t1=230.71 (s1=118.71%) t2=7.741 (s2=0.86%) 

RWLockFavorWriters lock

reader: t1=1.553 (s1=19.52%) t2=1.431 (s2=10.41%) 
writer: t1=28.7240 (s1=11.4%) t2=8.623 (s2=1.86%)

Jim_B_Robert_Wiener_RWLock

reader: t1=30.202 (s=8.05%) t2=9.973 (s2=1.26%) 
writer: t1=6.303 (s=6.84%) t2=6.105 (s2=2.68%) 

Ruediger_Asche_RWLock

reader: t1=2.605 (s1=6.84%) t2=2.444 (s2=1.53%) 
writer: t1=4484987.15 (s1=76.96%) t2=14.438 (s2=1.22%)

平均每个读/写操作的持续时间“t”、“t1”和“t2”(微秒)以及变异系数“s”、“s1”和“s2”(%)是基于 50 次测试计算的;每次测试包括 1,000,000 次写者迭代和 4,000,000 次读者迭代。将“t”值(不获取锁的操作持续时间)与“t1”和“t2”值(线程并发存在时的持续时间)进行比较,可以看到由于锁的争用而导致的性能下降。平均持续时间“t2”是从工作线程开始到结束测量的。在所有线程完成给定次数的迭代后,计算每个读/写操作的总持续时间。以下伪代码说明了“t2”的计算方法。

图 2. 计算每秒平均操作持续时间。
worker_thread_procedure()
{
   ...
   worker.m_perf_counter.start();
   for (int i = 0; i < test_iteration; i++)
   {
      if( false == worker.simulate_work() )
      {
         ::RaiseException( STATUS_NONCONTINUABLE_EXCEPTION, 0, 0, 0);
      }   
      else
      {
      }
   }
   worker.m_perf_counter.end();
   worker.m_perf_counter.set_iteration_done(test_iteration);
}

图 1 中的持续时间“t2”表明锁的性能差异不大。虽然使用“t2”作为标准乍一看似乎简单合理,但它存在几个问题。如果许多读者线程持有共享锁,写者可能会由于竞态条件而完全无法获取独占锁。写者一开始几乎被阻塞,因此性能很差,但在测试结束时,当其他读者线程完成给定的“test_iteration”后,写者可能会显示“良好”的总体性能。“Ruediger_Asche_RWLock”对象就是这样一个锁的例子。当四个读者中的一个完成了所有 4,000,000 次迭代时,写者可能只完成了 1-7 次迭代(从 1,000,000 次开始),参见图 3。

图 3. 测试“Ruediger_Asche_RWLock”对象的个体结果。完成的迭代次数和每次操作的持续时间(毫秒)。
reader thread 1: n=3999973 (t=0.0025) writer thread: n=7 (t=1433.846) 
reader thread 2: n=4000000 (t=0.0025) 
reader thread 3: n=3999993 (t=0.0025) 
reader thread 4: n=3999901 (t=0.0025)

当所有读者都完成时,写者解锁并迅速完成剩余的 999,993 次迭代。虽然“Ruediger_Asche_RWLock”对象的“总体”性能可能看起来可以接受(每个写操作 t2=14.438 微秒),但它并不好。查看图 1 中每个写操作的持续时间 t1=4484987.15 微秒。“t1”的估算与“t2”类似,但它是在测试开始时,当共享/独占锁存在高争用时的一个短暂时期内计算的。下面的伪代码说明了“t1”的计算。

图 4. 计算所有线程并发工作时每秒平均操作持续时间。
worker_thread_procedure()
{
   ...
   worker.m_perf_counter.start();
   int i;
   for (i = 0; i < test_iteration; i++)
   {
      if( false == worker.simulate_work() )
      {
         ::RaiseException( STATUS_NONCONTINUABLE_EXCEPTION, 0, 0, 0);
      }
      else
      {
      }
      if(TRUE == g_stop_test)
      {
         break;
      }
      else
      {
      }
   }
   worker.m_perf_counter.end();
   ::InterlockedExchange(&g_stop_test, TRUE);
   worker.m_perf_counter.set_iteration_done(i);
}

“t1”和“s1”显示了在 Windows XP 上临界区对象“CSRW”的性能很差。在高度并发的条件下,几个读者线程可能几乎“被阻塞”。当一个读者线程完成了 4,000,000 次迭代时,其他线程可能只完成了 6-48 次迭代。图 5 说明了“CSRW”对象在个体线程和测试中的性能偏差。

图 5. 测试“CSRW”对象时的个体测试结果。完成的迭代次数和每次操作的持续时间(毫秒)。
...
test1:

reader thread 1: n=138302 (t=0.013412) writer thread: n= 4151 (t=0.449985) 
reader thread 2: n=4000000 (t=0.000464) 
reader thread 3: n=69812 (t=0.026571) 
reader thread 4: n=3513004 (t=0.000492)

test2:

reader thread 1: n=30 (t=64.038158) writer thread: n= 294219 (t=0.006664)
reader thread 2: n=30 (t=64.035178) 
reader thread 3: n=30 (t=64.032356) 
reader thread 4: n=4000000 (t=0.000480) 
...

虽然一些读者线程非常快(0.464-0.492 微秒),但平均读取成本却非常高(t1= 41586.02 微秒)。变异系数 s1=161.18% 的高值表明各个读者线程的性能变化很大。CSRW 很难保证所有读者的性能。当使用新的、更优越的 Windows Vista API SRWLOCK 对象时,高争用可能会对性能产生非常负面的影响。如果一个写者与许多在多核 PC 上运行的读者竞争,写者可能很难获取独占锁。图 6 显示了在四核 PC 上 SRWLOCK 的写者性能较差,t1=186.171 微秒,变异系数 s1=25.17% 相当高。写者只能完成 5,000-30,000 次迭代(从 1,000,000 次开始),而读者几乎完成了所有 4,000,000 次迭代。当改变并发读者线程的数量时,SRWLOCK 的写者性能可能会有很大差异。如果读者数量从 2 增加到 4,大多数锁对象的性能会下降 40% 到 100%,但 SRWLOCK 没有。SRWLOCK 的写者性能从 7.1 微秒下降到 186.2 微秒(约 26 倍)。

图 6. 在四核 CPU PC Intel Q6700,2.66 GHz,禁用超线程,64 位 Windows Vista Home Premium SP1 上测试一个写者和四个读者,使用 VC++ 2008 Express 编译。每秒操作的持续时间(微秒)和变异系数(%)。
Without locking

reader: t=0.106 (s=1.45%)
writer: t=1.416 (s=0.79%)

Critical section CSRW lock

reader: t1=19.337 (s1=48.99%) t2=1.322 (s2=5.65%) 
writer: t1=2.018 (s1=3.17%) t2=2.194 (s2=4.71%) 

Windows Vista SRWLOCK lock

reader: t1=0.991 (s1=11.77%) t2=0.881 (s2=8.2%) 
writer: t1=186.171 (s1=25.17%) t2=5.204 (s2=1.31%) 

RWFavorNeither lock

reader: t1=1.274 (s1=6.59%) t2=1.381 (s2=3.07%) 
writer: t1=72.13 (s1=5.31%) t2=6.981 (s2=1.64%) 

RWLockFavorWriters lock

reader: t1=6.311 (s1=4.83%) t2=5.511 (s2=3.48%) 
writer: t1=21.948 (s1=4.34%) t2=21.127 (s2=5.06%) 

Jim_B_Robert_Wiener_RWLock

reader: t1=61.688 (s1=1.03%) t2=59.166 (s2=0.59%) 
writer: t1=84.911 (s1=9.15%) t2=85.005 (s2=9.6%) 

Ruediger_Asche_RWLock

reader: t1=1.755 (s1=11.69%) t2=1.803 (s2=7.95%) 
writer: t1=2107697.58 (s1=108.77%) t2=10.607 (s2=5.17%)

选择哪个读写锁更好取决于硬件和软件架构以及特定的应用程序场景。然而,测试结果表明,“RWLockFavorWriters”对象可能是个不错的选择。锁的成本相对较低(分别为读者/写者的 1.6 - 6.3 微秒和 22 - 29 微秒)。与其它锁不同,“RWLockFavorWriters”对象在不同的硬件/软件平台和测试场景中表现一致。如果一个写者与两个读者竞争(而不是图 1 和图 6 中的四个读者),在单核和四核 PC 上,读者和写者的性能会提高约 30-60%。变异系数保持相对较低(RWFavorNeither 对象性能的提高会导致变异系数增加到 60-70%)。另一方面,根据并发读者线程的数量,内核 CPU 的负载可能很高(约 10-40%);每秒大量线程上下文切换(约 150,000-200,000 次)可能会导致额外的开销。与自定义锁相比,新的用户空间 Windows 瘦锁(slim lock)表现非常好,内核 CPU 负载为 0%,每秒线程上下文切换次数很少(约 10-1000 次)。如果写者性能可以接受,并且考虑随着读者线程数量的增加而扩展解决方案,那么 SRWLOCK 对象似乎是最好的读写锁选择;随着读者线程数量的增加,锁的高争用可能会对 SRWLOCK 的写者性能产生负面影响。

结论

  • 在有多个读者且锁争用较高的情况下,使用 Windows API 临界区锁 CSRW 作为读写锁可能是一个缺点。
  • 如果可以接受随着读者数量增加而可能出现的写者性能回退,那么 Windows Vista SRWLOCK 似乎是最好的选择。
  • RWLockFavorWriters”锁对象是在广泛的应用程序场景中实现读写锁对象的高性能、健壮的实现,它在单核和多核平台上表现一致,并提供了读者/写者性能的合理平衡。
© . All rights reserved.