MySQL InnoDB mutex 实现分析


generated by 通义万相

InnoDB 中的 mutex 和 rw_lock
在早期的版本都是通过系统提供的 cas, tas 语义自己进行实现, 并没有使用pthread_mutex_t
, pthread_rwlock_t
, 这样实现的好处在于便于统计, 以及为了性能考虑, 还有解决早期操作系统的一些限制.

基本原理:

mutex_enter
之后, 在 spin 的次数超过 innodb_sync_spin_loops=30
每次最多 innodb_spin_wait_delay
=6 如果还没有拿到 Mutex, 会主动 yield() 这个线程, 然后wait 在自己实现的wait array 进行等待.

这里每次spin 时候, 等待的时候执行的是 ut_delay
, 在 ut_delay
中是执行 “pause” 指定, 当innodb_spin_wait_delay
= 6 的时候, 在当年1 00MHz Pentium cpu, 这个时间最大是1us.

wait array 也是InnoDB 实现的一种cond_wait 的实现, 为什么要自己实现?

早期的MySQL 需要wait array 是因为操作系统无法提供超过100000 event, 因此wait array 在用户态去进行这些event 维护, 但是到了MySQL 5.0.30 以后, 大部分操作系统已经能够处理100000 event, 那么现在之所以还需要 wait array, 主要是为了统计.

在wait array 的实现里面其实有一把大wait array mutex, 是一个pthread_mutex_t
, 然后在wait array 里面的每一个wait cell 中, 包含了os_event_t
, wait 的时候调用了os_event_wait_low()
, 然后在 os_event_t
里面也包含了一个mutex, 因此在一次wait 里面就有可能调用了两次 pthread_mutex_t
的wait.

并且在 os_event_t
唤醒的机制中是直接通过 pthread_cond_boradcast()
, 当有大量线程等待在一个event 的时候, 会造成很多无谓的唤醒.

代码实现:

  void enter(uint32_t max_spins, uint32_t max_delay, const char *filename,<br>             uint32_t line) UNIV_NOTHROW {<br>    // 在try_lock 中通过 TAS 比较是否m_lock_word = LOCKED<br>    // TAS(&m_lock_word, MUTEX_STATE_LOCKED) == MUTEX_STATE_UNLOCKED<br>    // 在InnoDB 自己实现的mutex 中, 使用m_lock_word = 0, 1, 2 分别来比较unlock,<br>    // lock, wait 状态<br>    // 在InnoDB 自己实现的rw_lock 中, 同样使用 m_lock_word 来标记状态,<br>    // 不过rw_lock 记录的状态就不止lock, unlock, 需要记录有多少read 等待,<br>    // 多少write 等待等待, 不过大体都一样<br>    if (!try_lock()) {<br>      // 如果try_lock 失败, 就进入spin 然后同时try_lock 的逻辑<br>      spin_and_try_lock(max_spins, max_delay, filename, line);<br>    }<br>  }<br><br>  void spin_and_try_lock(uint32_t max_spins, uint32_t max_delay,<br>                         const char *filename, uint32_t line) UNIV_NOTHROW {<br>    for (;;) {<br>      /* If the lock was free then try and acquire it. */<br><br>      // is_free 的逻辑很简单, 每spin 一次, 就检查一下这个lock 是否可以获得,<br>      // 如果不可以获得, 那么就delay (0, max_delay] 的时间<br>      if (is_free(max_spins, max_delay, n_spins)) {<br>......<br>      }<br>        // 如果尝试了max_spins 次, 那么就将当前cpu 时间片让出<br>      os_thread_yield();<br><br>      // 最后进入到wait 逻辑, 这个wait 是基于InnoDB 自己实现的wait array 来实现<br>      if (wait(filename, line, 4)) {<br>        n_spins += 4;<br><br>  }<br>

Innam 在底下回复: 当然只唤醒一个也并不能完全解决问题, 如果使用 pthread_cond_signal
, 那么等待的线程就是一个一个的被唤醒, 那么所有等待的线程执行的时间就是串行的

在当前InnoDB 的实现中, 如果使用 pthread_cond_boradcase
会让所有的线程都唤醒, 然后其中的一个线程获得mutex, 但是其他线程并不会因为拿不到mutex马上进入wait, 而是依然会通过spin 一段时间再进入wait,这样就可以减少一些无谓的wait.

所以这里官方到现在一直也都没有改.