Published by orzz.org(). (https://orzz.org/%e8%b7%a8%e5%b9%b3%e5%8f%b0%e7%9a%84%e5%8e%9f%e5%ad%90%e6%93%8d%e4%bd%9c%e5%8f%8a%e7%ae%80%e5%8d%95%e7%9a%84%e5%be%aa%e7%8e%af%e9%94%81%e5%ae%9e%e7%8e%b0/)
原子操作一直是多线程编程中的重要杀器之一。Win32里我们有Interlocked系列API,其他平台下也有各自的原子操作接口。如果想要让我们的程序能够拥有跨平台且统一的多线程调度方案,那么就必须得把不同的操作接口统一(C++11中已经有了跨平台的原子操作接口,不过当不方便使用C++11的时候,自己简单的写一套还是有一定需要的)。
首先,我们需要定义一套平台判断宏,来方便我们决定使用何种接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#if defined(WINCE) || defined(_WIN32_WCE) # define NX_OS_WINCE #elif defined(WIN64) || defined(_WIN64) || defined(__WIN64__) # define NX_OS_WIN64 #elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) # define NX_OS_WIN32 #elif defined(__linux__) || defined(__linux) # define NX_OS_LINUX #else # error "This OS is unsupported" #endif #if defined(NX_OS_WIN32) || defined(NX_OS_WIN64) || defined(NX_OS_WINCE) # define NX_OS_WIN #else # define NX_OS_UNIX #endif |
考虑到编译的差异(gcc下提供了一套跨平台的原子操作接口),编译器也需要区分开:
1 2 3 4 5 6 7 8 9 10 |
#if defined(_MSC_VER) # if (_MSC_VER <= 1200) # define NX_CC_MSVC6 # endif # define NX_CC_MSVC #elif defined(__GNUC__) # define NX_CC_GNUC #else # error "This CC is unsupported" #endif |
这里我们就只简单的考虑Windows、Linux,以及VC、gcc之间的区分。
下面,我们需要定义跨平台的接口兼容层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
namespace atomic { #if defined(NX_OS_WIN32) || defined(NX_OS_WINCE) typedef volatile LONG type_t; typedef LONG norm_t; #elif defined(NX_OS_WIN64) typedef volatile LONG64 type_t; typedef LONG64 norm_t; #else typedef volatile long type_t; typedef long norm_t; #endif } ////////////////////////////////////////////////////////////////////////// #if defined(NX_CC_GNUC) # define nx_atomic_inc(var) __sync_fetch_and_add (&(var), 1) # define nx_atomic_dec(var) __sync_fetch_and_sub (&(var), 1) # define nx_atomic_add(var, val) __sync_fetch_and_add (&(var), (val)) # define nx_atomic_sub(var, val) __sync_fetch_and_sub (&(var), (val)) # define nx_atomic_set(var, val) __sync_lock_test_and_set (&(var), (val)) # define nx_atomic_cas(var, cmp, val) __sync_bool_compare_and_swap(&(var), (cmp), (val)) #elif defined(NX_OS_WIN32) || defined(NX_OS_WINCE) # define nx_atomic_inc(var) InterlockedExchangeAdd (&(var), 1) # define nx_atomic_dec(var) InterlockedExchangeAdd (&(var),-1) # define nx_atomic_add(var, val) InterlockedExchangeAdd (&(var), (val)) # define nx_atomic_sub(var, val) InterlockedExchangeAdd (&(var),-(val)) # define nx_atomic_set(var, val) InterlockedExchange (&(var), (val)) # define nx_atomic_cas(var, cmp, val) ((cmp) == InterlockedCompareExchange(&(var), (val), (cmp))) #elif defined(NX_OS_WIN64) # define nx_atomic_inc(var) InterlockedExchangeAdd64 (&(var), 1) # define nx_atomic_dec(var) InterlockedExchangeAdd64 (&(var),-1) # define nx_atomic_add(var, val) InterlockedExchangeAdd64 (&(var), (val)) # define nx_atomic_sub(var, val) InterlockedExchangeAdd64 (&(var),-(val)) # define nx_atomic_set(var, val) InterlockedExchange64 (&(var), (val)) # define nx_atomic_cas(var, cmp, val) ((cmp) == InterlockedCompareExchange64(&(var), (val), (cmp))) #else # error "This platform is unsupported" #endif |
这里28、29行和37、38行并没有使用现成的InterlockedIncrement、InterlockedDecrement接口。是因为这里提供的接口均返回操作之前的原始值,而InterlockedIncrement、InterlockedDecrement接口返回操作之后的值。为了统一,此处使用InterlockedExchangeAdd来实现功能。
考虑到宏的调试性和安全性不够好,我们可以使用函数对这些接口做一个简单的封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static inline atomic::norm_t atomic_inc(atomic::type_t& v) { return nx_atomic_inc(v); } static inline atomic::norm_t atomic_inc(atomic::type_t& v, atomic::norm_t n) { return nx_atomic_add(v, n); } static inline atomic::norm_t atomic_dec(atomic::type_t& v) { return nx_atomic_dec(v); } static inline atomic::norm_t atomic_dec(atomic::type_t& v, atomic::norm_t n) { return nx_atomic_sub(v, n); } static inline atomic::norm_t atomic_set(atomic::type_t& v, atomic::norm_t n) { return nx_atomic_set(v, n); } static inline bool atomic_cas(atomic::type_t& v, atomic::norm_t c, atomic::norm_t n) { return nx_atomic_cas(v, c, n); } |
下面,我们尝试使用这些原子操作实现一个简单的循环锁。
要实现原子循环锁,还另外需要一个跨平台接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
namespace thread { static inline void yield(void) { # if defined(NX_OS_WINCE) Sleep(5); # elif defined(NX_OS_WIN) SwitchToThread(); # else pthread_yield(); # endif } } |
它的作用是切换线程操作,让当前线程放弃剩余的时间片,把时间片让给其他线程。
那么循环锁可以简单的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace atomic { typedef type_t lock_t; class Locker { protected: lock_t& _lc; public: Locker(lock_t& lc) : _lc(lc) { while(atomic_set(_lc, 1)) thread::yield(); } ~Locker() { atomic_set(_lc, 0); } }; typedef Locker locker_t; } |
当Locker::_lc为1时,即表示处于加锁状态;为0则表示没有加锁。当处于加锁状态时,atomic_set(_lc, 1)的返回值将永远是1,此时Locker的构造函数将被锁住,直到有其他持有同一个lock_t对象的Locker析构并释放锁。
为了方便的使用它,我们可以为它写一些便捷的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class AtomicLock { protected: atomic::lock_t& get_lock(void) { static atomic::lock_t lc; // 这里的实现是粗糙的 // 若在实际项目中运用,需使用线程安全的单例 return lc; } }; #ifndef nx_atomic_lock #define nx_atomic_lock() nx::atomic::locker_t locker(get_lock()) #endif |
我们可以像下面这样使用它:
1 2 3 4 5 6 7 8 9 |
class RefBase : AtomicLock { public: void opt(void) { nx_atomic_lock(); // ... } }; |
只需要让我们的类继承自AtomicLock,然后这个类就可以使用nx_atomic_lock锁住某一段操作了。
上面的原子锁虽然简单方便,但是还有两大缺点:
- 1. 使用锁除了定义lock_t之外,还必须要使用Locker,lock_t自身不具备锁功能
- 2. 锁是不可重入的,即递归中不能使用此锁
为了克服上面所述的缺点,我们还需要再定义一个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace thread { #if defined(NX_OS_WIN) typedef DWORD type_t; #else typedef pthread_t type_t; #endif static inline type_t current(void) { # if defined(NX_OS_WIN) return GetCurrentThreadId(); # else return pthread_self(); # endif } } |
作用是获得当前线程的id。
接下来,我们需要实现一个可重入的lock_t:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
namespace atomic { class Lock : NonCopyable { protected: long _rc; type_t _id; public: Lock(void) : _rc(0), _id(0) {} ~Lock() { nx_assert(_rc <= 0); } public: void lock() { thread::type_t tid = thread::current(); if (_id == (norm_t)tid) ++_rc; else { while(!atomic_cas(_id, 0, (norm_t)tid)) thread::yield(); _rc = 1; } } void unlock() { if (_id == (norm_t)thread::current()) if (--_rc <= 0) { atomic_set(_id, 0); _rc = 0; } } }; typedef Lock lock_t; class Locker { protected: lock_t& _lc; public: Locker(lock_t& lc) : _lc(lc) { _lc.lock(); } ~Locker() { _lc.unlock(); } }; typedef Locker locker_t; } |
lock_t提供了lock和unlock接口,内部通过判断当前线程id,并使用计数器来支持可重入。
代码第21行,使用一个CAS操作判断是否有其他线程已加锁,若未加锁,那么将_id设为当前的线程id。
使用上面的代码,我们前面写的AtomicLock和nx_atomic_lock均不需要调整,使用方法也和原来一致。但此时locker_t已可以独立于Locker等辅助类单独存在,并在我们需要的时候手动加锁解锁。
文中代码请参考:
http://neonx.googlecode.com/svn/trunk/neoncore/platform/atomic.h
http://neonx.googlecode.com/svn/trunk/neoncore/thread/locker.h