10.1.1 操作系统函数
由于原子递增和递减是许多重要机制(包括引用计数)的基石,因此提供某些设施以便以线程安全的方式来引导这些操作是非常重要的。正如我们将会在后面看到的,通常只需要原子整型操作能力就可以实现较复杂的组件线程安全性,1这种做法可以节省可观的性能开销。
Win32提供了InterLockedIncrement()和InterLockedDecrement()这两个系统函数,它们看起来像这样:
- LONG InterlockedIncrement(LONG *p);
- LONG InterlockedDecrement(LONG *p);
这两者分别实现了前置递增(++i)和前置递减(--i)语义。换句话说,其返回值反映了修改后的新值,而非旧值。Linux也提供了类似的函数[Rubi2001]:atomic_inc_and_test()和atomic_dec_and_test()。特定的操作系统对此会提供特定的函数。
现在,使用这些函数,我们就可以以彻底的线程安全的方式来重写先前的递增语句:
- atomic_inc_and_test(&i); // ++i
这种函数在Intel处理器上的实现简单地利用了LOCK指令前缀,像这样:- lock add dword ptr [i], 1
LOCK指令前缀会导致总线上出现一个LOCK#信号,阻止其他任何线程在该ADD指令的执行期间对i的内存位置作任何举动(当然,实际上比这要复杂得多,具体涉及了缓冲区行(cache line)和各种各样的鬼蜮伎俩,但从逻辑上说,它使得该指令成为原子的,不管是对于其他线程还是处理器而言3)。
使用锁语义的缺点是你需要付出速度上的代价。实际代价根据不同的架构有所不同。在Win32上使用锁的代价大体上是不使用的200-500%(在本章的后续部分我们会看到这一点)。因此,简单地让每个操作都成为线程安全的做法是不妥当的。事实上,多线程的全部语义在于允许独立无关的处理业务并发地执行。
在实践中,人们通常可以使用构建(build)设置来决定是否使用原子操作。在UNIX上,_REENTRANT预处理符号通常可以被用来告诉C和C++(www.cppentry.com)代码该连接单元是为多线程环境而构建(build)的。在Win32上则是_MT或__MT__,或类似的预处理符号,具体取决于编译器。自然,这种东西可以被抽象为一个平台/编译器无关的符号,例如ACMELIB_MULTI_THREADED,然后该符号被用于在编译期选择合适的操作。
- #ifdef ACMELIB_MULTI_THREADED
- atomic_increment(&i);
- #else /* ACMELIB_MULTI_THREADED */
- ++i;
- #endif /* ACMELIB_MULTI_THREADED */
由于将这些预处理指令散布在代码之中实在是件不雅的事,所以通常我们采用的方式是将操作抽象到一个公共函数里面,让该函数将这些预处理指令封装起来。在一些被广泛使用的库中可以找到这方面的许多例子,例如Boost和微软ATL(Active Template Library,活动模板库)。
我应该指出,并非所有的操作系统都提供对整型值的原子操作,在这种情况下,你可能需要求助于操作系统的同步对象(例如互斥体(mutex))来锁定对原子整型值API的访问,我们会在本章的后续部分看到这一点。