标准库的互斥锁 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;

其中上锁类型有下面几种取值:

  • 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_lockPTHREAD_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 也更顺手一些。

参考文章:

标准库的互斥锁 std::mutex 是如何实现的 - 阅微堂 (zhiqiang.org)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值