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

作者: , 共 3925 字 , 共阅读 0

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

类似文章:
由 Facebook 开发和维护的 C++库 Folly 提供了自旋锁的实现folly::MicroSpinLock,代码文件地址:https://github.com/facebook/folly/blob/master/folly/synchronization/MicroSpinLock.h
由 Facebook 开发和维护的 C++库 Folly 提供了锁folly::MicroLock,代码文件地址:https://github.com/facebook/folly/blob/master/folly/MicroLock.h
智能指针在现代 C++里用得越多。以前只知道它大致的原理,比如使用引用计数。但很多实现细节并不清楚,最直接的,它是如何实现多线程安全的?今天找了 gnu c++ library 的实现好好看了一下。
std::thread是 C++ 11 新引入的标准线程库。在同样是 C++ 11 新引入的 lambda 函数的辅助下,std::thread用起来特别方便:
编程 » C++
有两种方法,一种在线程的调用函数内部设置,还有一种是在外部对指定线程变量做设置。
编程 » C++, C++标准库
std::tuple的原理并不复杂,但有些细节非常有意思。其中有一个是至少在gnu C++ std的实现中,std::tuple是倒序存储的:
编程 » C++, 异步
C++11 的标准异步库至少包含下面内容:
C++对一个有序序列[first, last)firstlast都是iterator,可简单理解为位置指针),以及指定值v,标准库直接提供二分查找的函数std::lower_boundstd::upper_bould
编程 » C++, 智能指针
前面已经提到std::shared_ptr有三个缺陷:
智能指针在现代 C++里用得越多。以前只知道它大致的原理,比如使用引用计数。但很多实现细节并不清楚,最直接的,它是如何实现多线程安全的?今天找了 gnu c++ library 的实现好好看了一下。