请问Interlocked Variable Access、 Critical Section和Event 和 Mutex 的执行效率有何差异?

wormli 2007-10-01 12:33:08
如题,谢谢。
...全文
238 3 打赏 收藏 转发到动态 举报
写回复
用AI写文章
3 条回复
切换为时间正序
请发表友善的回复…
发表回复
DentistryDoctor 2007-10-07
  • 打赏
  • 举报
回复
在同一进程内用临界区进行同步是比较好的选择在,跨进程就得用Event,Mutex了。
Juchiyufei 2007-10-07
  • 打赏
  • 举报
回复
mark.
qiaoying 2007-10-01
  • 打赏
  • 举报
回复
首先要明确一点:同步器的相对效率,是在虽然进行多线程同步,但并没有发生“冲突”的
前提下而言的。就是说,一个线程独占了一项资源,随后很快就释放了,而在此期间并没有
其它线程也试图访问该资源而进入等待状态——真正需要进行等待的情况,发生概率必须很
小。良好的多线程设计必须保证这一点,否则如果你的几个线程总是我等你、你等我,说明
设计上有很严重的问题(这样严重的问题几乎可以视为Bug)。如果它们常常在同一时刻只
能有一个在运行,那要多线程有什么意思?还不如用单线程,起码节省同步的时间。

现在来看CriticalSection。虽然,它内部使用了Event,但并不是每次都用。第一个成功
进入CriticalSection的线程就根本不需要使用内核对象。虽然不能看到源代码,但我考虑
过,如果我来设计,我会怎样做,而且我相信,这也正是Windows中实际所做的(真要实现
起来,还会有更多细节问题需要考虑,我这里只提一些要点,如有兴趣,可以试试自己实现
一个CriticalSection,也是个不错的练习):
1.使用InterlockedExchange(或者某个相关函数),在CRITICAL_SECTION结构里保存本线
程的ID号。
2.如果原来的ID号是NULL,或者就是本线程ID(别忘了CriticalSection是允许同一线程重
复进入的),那么调用就成功了。
3.如果进入CriticalSection失败,则本线程的ID号已经存入,直接用WaitForSingleObject
等待内含的Event即可(因此,只有进入等待的时候,才需要使用内核级的等待函数。实际
上,需要令保存的线程ID构成一个链表,以便应付多个线程同时进入等待的情况)。
4.成功进入CriticalSection的线程在离开的时候,再次使用InterlockedExchange恢复所保
存的线程ID。如果发现保存的线程ID不是NULL也不是自己,就说明另外一个线程在等待中,
调用一次SetEvent即可(但要注意,这也是一个慢速得多的函数,好在它只有在存在等待线
程的时候才需要)。

在MSDN中,对Windows2000新增的函数InitializeCriticalSectionAndSpinCount的说明里,
所透露的一些细节也可以成为我的以上推测的佐证。

最后,要完全解决这个问题,需要了解一下386以上汇编语言,再综合MSDN和《Windows核心
编程》中得来的星点知识。我的估计如下:

◎最快的当然是C语言的加减运算,它们直接对应简单的机器指令;
◎Interlocked****等函数其次,它们对应的指令其实也很简单(起码在x86架构上是这样,
通常只需1~3个指令)。但是,它们需要CPU放弃通常的指令优化,还可能需要通过系统总
线通知其它CPU(CPU内部的一级、二级缓存,直到内存都可能牵连到)。我估计,这会减慢
速度5~10倍;
◎CrititalSection应该与Interlocked****相当,只是稍微慢一点。因为它实际上调用前者。
◎其它如互斥器、信号器之类最慢,我的估计是也许比CrititalSection慢几十甚至100倍。
因为它们需要切换到内核模式,再切换回来,这需要执行大量指令(在X86上看上去指令不
多,但这些指令一个就对应RISC类型的CPU,如Alpha上的一个子程序,一个指令就是几十个
时钟周期)。也因此,互斥器等内核对象本身的速度差别是微不足道的,没有必要考虑,因
为内核模式切换才是效率瓶颈。

正好我也想对这个问题有个定量的了解(因为我正在一段代码中是用CriticalSection还是
Interlocked***拿不定主意),以下是我的实测结果:

The system performance counter's frequency is:3579545
Repeat times for each test is:1000000

General ++/-- operators: t1(++ only)=0.00371197, t2(-- only)=0.00372959
Both time=0.00754256, t1+t2-both:-0.000101004, NET TIME COST:0.00754256
Interlocked(In/De)crement:0.115052
Critical section:0.140891
Mutex:1.47912

General ++/-- operators: t1(++ only)=0.00376848, t2(-- only)=0.0037205
Both time=0.00756789, t1+t2-both:-7.89067e-005, NET TIME COST:0.00756789
Interlocked(In/De)crement:0.114687
Critical section:0.141372
Mutex:1.49058

General ++/-- operators: t1(++ only)=0.00371739, t2(-- only)=0.00380439
Both time=0.00749592, t1+t2-both:2.58552e-005, NET TIME COST:0.00747007
Interlocked(In/De)crement:0.114999
Critical section:0.14113
Mutex:1.47861

General ++/-- operators: t1(++ only)=0.00379342, t2(-- only)=0.00371602
Both time=0.00754006, t1+t2-both:-3.06324e-005, NET TIME COST:0.00754006
Interlocked(In/De)crement:0.114092
Critical section:0.141929
Mutex:1.48663

General ++/-- operators: t1(++ only)=0.00380472, t2(-- only)=0.00377649
Both time=0.00752712, t1+t2-both:5.40851e-005, NET TIME COST:0.00747303
Interlocked(In/De)crement:0.113746
Critical section:0.140481
Mutex:1.48115

测试程序是一个单线程命令行程序,使用VC2005编译,Release版,无调试信息、优先为速
度优化。运行时,不考虑其它线程,只管用一个线程反复锁定与释放资源,看其执行速度。
实测用的机器是P4 2.4G(有3、4年的较旧机器了),每种调用重复一百万次,轮回测试了
5趟。
第一行打出的performance frequency没什么大用,这是硬件相关的值,说明我用的这个平
台上的计时精度而已。后面每趟测试中给出的就是跑完循环所用的时间,单位为秒。其中的
++和--运算就是基本的C运算符,但作用目标是volatile LONG型(如果不加上volatile,编
译器优化会把整个循环都省略掉,而且,这也就不成为对多线程环境的测试了)。
开始先只做++和--,然后在每趟循环中++和--各执行一次,前两次的时间之和,再减后一个
的差值应该就是空循环的时间,所以在随后所有测试的结果中,一般都会扣除空循环所需的
时间。但不幸这个时间太微小,甚至有时会成为负数(这时我就忽略该项)。其它循环都是
每次两项调用——一增一减,或者一锁一放。

结论:我的估计大体没什么意外,但在具体的速度比值上,就有些出入。实际看来:
InterlocdedIncrement和InterlockedDecrement的速度是++和--的大约16分之一(可能因为
我忘了考虑函数调用时,入栈出栈和指令跳转的开销)。
Critical section只比前者慢大约25~30%,这点差异通常几乎可以忽略。这是最接近我的
估计的,也说明我对其实现方式的推测正确。Critical section的技术本质,就是基于
Interlocked****,从而以几乎相同的时间代价完成更复杂的同步需要。但也不要忘了,在
现实中,如果在可以用Interlocked****处理的简单数值上动用Critical section,往往用
一个Interlocked****单独调用能解决的事就需要进入和离开Critical section的两次调用,
所以实际差异恐怕还得加倍。另一方面,任何需要同步处理比单个数值更复杂情况的时候,
Interlocked****就肯定不值得考虑(而且几乎肯定根本就无法适用)。
最后,互斥器比我的估计快很多,只比前者慢10倍(充分证明我对汇编编程的了解都是纸上
谈兵,呵呵)。当然,一个数量级的差距在编程时仍然是不可忽略的差异。所以,内核同步
器应该尽量只在跨进程等迫不得已的情况下加以利用。

15,471

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC 进程/线程/DLL
社区管理员
  • 进程/线程/DLL社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧