MySQL自旋锁-spin lock

自旋锁

标签(空格分隔): innodb


简介

Innodb中大量使用自旋锁来避免锁等待时的上下文切换,影响性能的问题。自旋锁分为加锁和解锁两个过程,其中加锁分为尝试加锁与自旋的过程。

数据结构解析

其上层调用方式一般为如下:

    mutex_enter(&trx->undo_mutex);

mutex_enter宏定义如下

#define mutex_enter(M)          (M)->enter(         \
                    srv_n_spin_wait_rounds,     \
                    srv_spin_wait_delay,        \
                    __FILE__, __LINE__)

所以根据不同的mutex类型,有不同的实现。有点类似与C++中的多态。
而如上示例中的undo_mutex定义如下

UndoMutex   undo_mutex; /*!< mutex protecting the fields in this
                    section (down to undo_no_arr), EXCEPT
                    last_sql_stat_start, which can be
                    accessed only when we know that there
                    cannot be any activity in the undo
                    logs! */

UndoMutex的声明为

typedef ib_mutex_t UndoMutex;

ib_mutex_t的声明为

typedef SyncArrayMutex ib_mutex_t;

SyncArrayMutex的声明为

UT_MUTEX_TYPE(TTASEventMutex, GenericPolicy, SyncArrayMutex);

UT_MUTEX_TYPE的宏定义为

/** Create a typedef using the MutexType<PolicyType>
@param[in]  M       Mutex type
@param[in[  P       Policy type
@param[in]  T       The resulting typedef alias */

#define UT_MUTEX_TYPE(M, P, T) typedef PolicyMutex<M<P> > T;

展开就是

typedef PolicyMutex<TTASEventMutex<GenericPolicy> > SyncArrayMutex;

其中PolicyMutex的模版定义为

template <typename MutexImpl>
struct PolicyMutex
{
}

TTASEventMutex的模版定义为

template <template <typename> class Policy = NoPolicy>
struct TTASEventMutex 
{
}

GenericPolicy的模版定义为

template <typename Mutex>
struct GenericPolicy

好吧,我已经不敢再把声明展开了

我们看下函数调用吧

调用

如下,if (!try_lock()),则证明尝试加锁失败,进入自旋操作。

    /** Acquire the mutex.
    @param[in]  max_spins   max number of spins
    @param[in]  max_delay   max delay per spin
    @param[in]  filename    from where called
    @param[in]  line        within filename */
void enter(
        uint32_t    max_spins, //MySQL的参数设置innodb_sync_spin_loops
        uint32_t    max_delay, //MySQL的参数设置innodb_spin_wait_delay
        const char* filename,
        uint32_t    line)
        UNIV_NOTHROW
    {
        if (!try_lock()) {
            spin_and_try_lock(max_spins, max_delay, filename, line);
        }
    }

try_lock

从代码上来看是先进行一次try_lock,下面来看

    /** Try and lock the mutex. Note: POSIX returns 0 on success.
    @return true on success */
    bool try_lock()
        UNIV_NOTHROW
    {
        return(tas_lock());
    }

tas_lock()

    /** Try and acquire the lock using TestAndSet.
    @return true if lock succeeded */
    bool tas_lock() UNIV_NOTHROW
    {
        return(TAS(&m_lock_word, MUTEX_STATE_LOCKED)
            == MUTEX_STATE_UNLOCKED);
    }

TAS宏定义

#define TAS(l, n)           os_atomic_test_and_set((l), (n))

os_atomic_test_and_set

/** Do an atomic test and set.
@param[in,out]  ptr Memory location to set
@param[in]  new_val new value
@return old value of memory location. */
UNIV_INLINE
lock_word_t
os_atomic_test_and_set(
    volatile lock_word_t*   ptr,
    lock_word_t     new_val)
{
    lock_word_t ret;

    /* Silence a compiler warning about unused ptr. */
    (void) ptr;

#if defined(__powerpc__) || defined(__aarch64__)
    __atomic_exchange(ptr, &new_val,  &ret, __ATOMIC_SEQ_CST);
#else
    __atomic_exchange(ptr, &new_val,  &ret, __ATOMIC_RELEASE);
#endif

    return(ret);
}

最终调用是 __atomic_exchange(ptr, &new_val, &ret, __ATOMIC_RELEASE); __atomic_exchange是gcc的内置原子函数,用来实现原子操作。
意思为拿new_val去替换ptr中的值,然后返回ptr之前存储的值。__ATOMIC_RELEASE的意思是?

ok,我们返回到tas_lock(),如果返回值为0,则证明当前是没有其他线程占用锁的,则加锁成功,并且m_lock_word被设置为MUTEX_STATE_LOCKED,也就是1,下一次别的线程进行tas_lock,则会返回MUTEX_STATE_LOCKED。置于MUTEX_STATE_WAITERS,虽然知道其含义,但是暂时还没有发现其用途。

/** Try and acquire the lock using TestAndSet.
    @return true if lock succeeded */
    bool tas_lock() UNIV_NOTHROW
    {
        return(TAS(&m_lock_word, MUTEX_STATE_LOCKED)
            == MUTEX_STATE_UNLOCKED);
    }

    ---------
    /** Mutex states. */
enum mutex_state_t {
    /** Mutex is free */
    MUTEX_STATE_UNLOCKED = 0,

    /** Mutex is acquired by some thread. */
    MUTEX_STATE_LOCKED = 1,

    /** Mutex is contended and there are threads waiting on the lock. */
    MUTEX_STATE_WAITERS = 2
};

spin_lock

尝试加锁部分结束,下面来看自旋操作,也就是spin_and_try_lock(max_spins, max_delay, filename, line)部分;
函数实现和注释如下

    /** Spin while trying to acquire the mutex
    @param[in]  max_spins   max number of spins
    @param[in]  max_delay   max delay per spin
    @param[in]  filename    from where called
    @param[in]  line        within filename */
    void spin_and_try_lock(
        uint32_t    max_spins,
        uint32_t    max_delay,
        const char* filename,
        uint32_t    line)
        UNIV_NOTHROW
    {
        uint32_t    n_spins = 0;//当前自旋次数
        uint32_t    n_waits = 0;//等待次数
        const uint32_t  step = max_spins;

        os_rmb;//# define os_rmb    __atomic_thread_fence(__ATOMIC_ACQUIRE)

        for (;;) {

            /* If the lock was free then try and acquire it. */

            if (is_free(max_spins, max_delay, n_spins)) {

                if (try_lock()) {

                    break;
                } else {

                    continue;
                }

            } else {
                max_spins = n_spins + step;
            }

            ++n_waits;

            os_thread_yield();

            /* The 4 below is a heuristic that has existed for a
            very long time now. It is unclear if changing this
            value will make a difference.

            NOTE: There is a delay that happens before the retry,
            finding a free slot in the sync arary and the yield
            above. Otherwise we could have simply done the extra
            spin above. */

            if (wait(filename, line, 4)) {

                n_spins += 4;

                break;
            }
        }

        /* Waits and yields will be the same number in our
        mutex design */

        m_policy.add(n_spins, n_waits);
    }

聚焦到for(;;)
判断is_free,函数实现如下
解释:
在一个while循环中判断is_locked(),如果依然处于锁定状态,则执行ut_delay(ut_rnd_interval(0, max_delay)),这个也是自旋的根本所在,然后自增自旋次数。

    /** Spin and wait for the mutex to become free.
    @param[in]  max_spins   max spins
    @param[in]  max_delay   max delay per spin
    @param[in,out]  n_spins     spin start index
    @return true if unlocked */
    bool is_free(
        uint32_t    max_spins,
        uint32_t    max_delay,
        uint32_t&   n_spins) const
        UNIV_NOTHROW
    {
        ut_ad(n_spins <= max_spins);

        /* Spin waiting for the lock word to become zero. Note
        that we do not have to assume that the read access to
        the lock word is atomic, as the actual locking is always
        committed with atomic test-and-set. In reality, however,
        all processors probably have an atomic read of a memory word. */

        do {
            if (!is_locked()) {
                return(true);
            }

            ut_delay(ut_rnd_interval(0, max_delay));

            ++n_spins;

        } while (n_spins < max_spins);

        return(false);
    }

下面来看下ut_delay的实现

/*************************************************************//**
Runs an idle loop on CPU. The argument gives the desired delay
in microseconds on 100 MHz Pentium + Visual C++.
@return dummy value */
ulint
ut_delay(
/*=====*/
    ulint   delay)  /*!< in: delay in microseconds on 100 MHz Pentium */
{
    ulint   i, j;

    UT_LOW_PRIORITY_CPU();

    j = 0;

    for (i = 0; i < delay * 50; i++) {
        j += i;
        UT_RELAX_CPU();
    }

    UT_RESUME_PRIORITY_CPU();

    return(j);
}

猜你喜欢

转载自blog.csdn.net/sun_ashe/article/details/81291347