8

标准库的互斥锁 std::mutex 是如何实现的

 3 years ago
source link: https://zhiqiang.org/coding/std-mutex-implement.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

标准库的互斥锁 std::mutex 是如何实现的

作者: 张志强

, 发表于 2020-01-07

, 共 3931 字 , 共阅读 1547 次

看到网上有片段,提到没有必要自己实现自旋锁,因为标准库的 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 也更顺手一些。

参考文章:

Q. E. D.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK