标准库的互斥锁 std::mutex 是如何实现的
看到网上有片段,提到没有必要自己实现自旋锁,因为标准库的 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;
其中上锁类型有下面几种取值:
- PTHREADMUTEXTIMED_NP ,这是缺省值,也就是普通锁。
- PTHREADMUTEXRECURSIVE_NP ,可重入锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
- PTHREADMUTEXERRORCHECKNP ,检错锁,如果同一个线程重复请求同一个锁,则返回 EDEADLK ,否则与 PTHREADMUTEXTIMEDNP 类型相同。
- PTHREADMUTEXADAPTIVE_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 也更顺手一些。
参考文章:
作者暂无likerid, 赞赏暂由本网站代持,当作者有likerid后会全部转账给作者(我们会尽力而为)。Tips: Until now, everytime you want to store your article, we will help you store it in Filecoin network. In the future, you can store it in Filecoin network using your own filecoin.
Support author:
Author's Filecoin address:
Or you can use Likecoin to support author: