看到网上有片段,提到没有必要自己实现自旋锁,因为标准库的 std::mutex 和现在的自旋锁的实现没有两样。比较好奇,翻了一些资料,试图找到答案。
在 C++里,标准库std::mutex
只是一个pthread_mutex_t
的封装:
class mutex {
pthread_mutex_t _M_mutex;
public:
mutex() { _M_mutex = PTHREAD_MUTEX_INITIALIZER; }
~mutex() { pthread_mutex_destroy(&_M_mutex); }
void lock() { pthread_mutex_lock(&_M_mutex); }
bool try_lock() { return pthread_mutex_trylock(&_M_mutex) == 0; }
void unlock() { pthread_mutex_unlock(&_M_mutex); }
}
因此我们需要把眼光挪向pthread
库的pthread_mutex_t
以及相关函数。
pthread_mutex_t
在 x86 下占据 32 个字节,内部布局如下:
typedef union {
struct __pthread_mutex_s {
int __lock; //!< mutex状态,0:未占用;1:占用。
unsigned int __count; //!< 可重入锁时,持有线程上锁的次数。
int __owner; //!< 持有线程的线程ID。
unsigned int __nusers;
int __kind; //!< 上锁类型。
int __spins;
__pthread_list_t __list;
} __data;
} pthread_mutex_t;
其中上锁类型有下面几种取值:
- PTHREAD_MUTEX_TIMED_NP ,这是缺省值,也就是普通锁。
- PTHREAD_MUTEX_RECURSIVE_NP ,可重入锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
- PTHREAD_MUTEX_ERRORCHECK_NP ,检错锁,如果同一个线程重复请求同一个锁,则返回 EDEADLK ,否则与 PTHREAD_MUTEX_TIMED_NP 类型相同。
- PTHREAD_MUTEX_ADAPTIVE_NP ,自适应锁,自旋锁与普通锁的混合。
下面我们看最关键的加锁函数pthread_mutex_lock
,它的第一个参数是锁对象指针,第二个参数是上面的锁类型。如果是普通锁,它的实现就是:
pthread_mutex_lock(pthread_mutex_t* mutex, int type) {
if (type == PTHREAD_MUTEX_TIMED_NP) {
LLL_MUTEX_LOCK (mutex);
}
// else ...
}
下面我们看宏LLL_MUTEX_LOCK
的实现:
# define LLL_MUTEX_LOCK(mutex) \
lll_lock ((mutex)->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex))
其中PTHREAD_MUTEX_PSHARED
用来区分线程锁还是进程锁。我们可以只关心线程锁,此时第二个参数就是LLL_PRIVATE=0
。 而lll_lock
的实现如下:
#define lll_lock(futex, private) \
(void) \
({ int ignore1, ignore2; \
if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \
__asm __volatile ("cmpxchgl %1, %2\n\t" \
"jnz _L_lock_%=\n\t" \
".subsection 1\n\t" \
".type _L_lock_%=,@function\n" \
"_L_lock_%=:\n" \
"1:\tleal %2, %%ecx\n" \
"2:\tcall __lll_lock_wait_private\n" \
"3:\tjmp 18f\n" \
"4:\t.size _L_lock_%=, 4b-1b\n\t" \
".previous\n" \
LLL_STUB_UNWIND_INFO_3 \
"18:" \
: "=a" (ignore1), "=c" (ignore2), "=m" (futex) \
: "0" (0), "1" (1), "m" (futex), \
"i" (MULTIPLE_THREADS_OFFSET) \
: "memory"); \
这一段是直接用汇编实现的。其核心指令是 cmpxchgl ,汇编级别的CAS
( compare and swap )。如果 swap 不成功,则调用__lll_lock_wait_private
让调度线程(进入操作系统的同优先级的任务队列末尾)。
从这里看,std::mutex
的并没有加入自旋等待的实现。那么大家说的又是什么呢?其实是pthread_mutex_lock
的PTHREAD_MUTEX_ADAPTIVE_NP
锁方式。我们来看它的实现:
if (type == PTHREAD_MUTEX_ADAPTIVE_NP) {
if (LLL_MUTEX_TRYLOCK (mutex) != 0) {
int cnt = 0;
int max_cnt = MIN (MAX_ADAPTIVE_COUNT, mutex->__data.__spins * 2 + 10);
do {
if (cnt++ >= max_cnt) {
LLL_MUTEX_LOCK (mutex);
break;
}
BUSY_WAIT_NOP;
} while (LLL_MUTEX_TRYLOCK (mutex) != 0);
mutex->__data.__spins += (cnt - mutex->__data.__spins) / 8;
}
}
这个实现已经和folly::MicroSpinLock
基本没有两样了,甚至细节处理得更好一些,比如它每次的最大尝试次数是动态的,会根据之前的尝试次数调整。
但我还是更喜欢用folly::MicroSpinLock
,因为内存结构更简单(只占一个字节,而pthread_mutex_t
需要 32 个字节!), API 相比起 C 风格的 pthread 也更顺手一些。
参考文章:
Q. E. D.