C++性能优化(十二)——自旋锁

C++性能优化(十二)——自旋锁

一、互斥锁

1、互斥锁简介

互斥锁属于sleep-waiting类型锁。Linux Kernel 2.6.x稳定版开始,Linux的互斥锁都是futex (Fast Usermode Mutex)锁。
Futex是一个在Linux上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具。
Futex由Hubertus Franke(IBM Thomas J. Watson 研究中心),Matthew Kirkwood,Ingo Molnar(Red Hat)和 Rusty Russell(IBM Linux 技术中心)等人创建。
Futex是由用户空间的一个对齐的整型变量和附在其上的内核空间等待队列构成。多进程或多线程绝大多数情况下对位于用户空间的futex的整型变量进行操作(汇编语言调用CPU提供的原子操作指令来增加或减少),而其它情况下则需要通过代价较大的系统调用来对位于内核空间的等待队列进行操作(如唤醒等待的进程/线程或将当前进程/线程放入等待队列)。除了多个线程同时竞争锁的少数情况外,基于futex的lock操作是不需要进行代价昂贵的系统调用操作的。
Futex核心思想是通过将大多数情况下非同时竞争lock的操作放到在用户空间执行,而不是代价昂贵的内核系统调用方式来执行,从而提高了效率。
互斥锁禁止多个线程同时进入受保护的代码临界区(critical section)。在任意时刻,只有一个线程被允许进入代码保护区。互斥锁实际上是count=1情况下的semaphore。




2、互斥锁特点

互斥锁缺点:
(1)等待互斥锁会消耗时间,等待延迟会损害系统的可伸缩性。
(2)优先级倒置。低优先级的线程可以获得互斥锁,因此会阻碍需要同一互斥锁的高优先级线程。
(3)锁护送(lock convoying)。如果持有互斥锁的线程分配的时间片结束,线程被取消调度,则等待同一互斥锁的其它线程需要等待更长时间。


3、互斥锁API

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

二、自旋锁

1、自旋锁简介

自旋锁(spin lock)属于busy-waiting类型锁。在多处理器环境中,自旋锁最多只能被一个可执行线程持有。如果一个可执行线程试图获得一个被其它线程持有的自旋锁,那么线程就会一直进行忙等待,自旋(空转),等待自旋锁重新可用。如果自旋锁未被争用,请求锁的执行线程便立刻得到自旋锁,继续执行。
多处理器操作系统中某些资源是有限的,不同线程需要互斥访问,因此需要引入锁概念,只有获取锁的线程才能够对资源进行访问。多线程的核心是CPU的时间分片,同一时刻只能有一个线程获取到锁。对于没有获取到锁的线程通常有两种处理方式:自旋锁,没有获取到锁的线程会一直循环等待判断资源是否已经释放锁,不用将线程阻塞起来;互斥锁,把未获取到锁的线程阻塞起来,等待重新调度请求。
自旋锁(spin lock)是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但并没有执行任何有效的任务,使用自旋锁会造成busy-waiting。


2、自旋锁的特点

自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁时需要从内核态恢复,导致线程在用户态与内核态之间来回切换,严重影响锁的性能。

3、自旋锁原理

自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,只需要等一等(自旋),等到持有锁的线程释放锁后即可获取,避免用户进程和内核切换的消耗。
自旋锁避免了操作系统进程调度和线程切换,通常适用在时间极短的情况,因此操作系统的内核经常使用自旋锁。但如果长时间上锁,自旋锁会非常耗费性能。线程持有锁时间越长,则持有锁的线程被 OS调度程序中断的风险越大。如果发生中断情况,那么其它线程将保持旋转状态(反复尝试获取锁),而持有锁的线程并不打算释放锁,导致结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。
自旋锁的目的是占着CPU资源不进行释放,等到获取锁立即进行处理。如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能,因此可以给自旋锁设定一个自旋时间,等时间一到立即释放自旋锁。

4、自旋锁API

#include <pthread.h>

int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

5、自旋锁与互斥锁

spinlock不会使线程状态发生切换,mutex在获取不到锁的时候会选择sleep。
spinlock优点:没有耗时的系统调用,一直处于用户态,执行速度快。
spinlock缺点:一直占用CPU,而且在执行过程中还会锁bus总线,锁总线时其它处理器不能使用总线。
mutex获取锁分为两阶段,第一阶段在用户态采用spinlock锁总线的方式获取一次锁,如果成功立即返回;否则进入第二阶段,调用系统的futex锁去sleep,当锁可用后被唤醒,继续竞争锁。
mutex优点:不会忙等,得不到锁会sleep。
mutex缺点:sleep时会陷入到内核态,需要昂贵的系统调用。




三、自旋锁实现

1、raw_spinlock

当某个处理器上的内核执行线程申请自旋锁时,如果锁可用,则获得锁,然后执行临界区操作,最后释放锁;如果锁已被占用,线程并不会转入睡眠状态,而是忙等待该锁,一旦锁被释放,则第一个感知此信息的线程将获得锁。

typedef struct {
    unsigned int slock;
} raw_spinlock_t;

传统自旋锁本质是用一个整数来表示,值为1代表锁未被占用,为0或者为负数表示被占用。
在单处理机环境中可以使用特定的原子级汇编指令swap和test_and_set实现进程互斥,但由于中断只能发生在两条机器指令之间,而同一指令内的多个指令周期不可中断,从而保证swap指令或test_and_set指令的执行不会交叉进行。
多处理器环境中利用test_and_set指令实现进程互斥,硬件需要提供进一步的支持,以保证test_and_set指令执行的原子性,目前多以锁总线形式提供,由于test_and_set指令对内存的两次操作都需要经过总线,在执行test_and_set指令前锁住总线,在执行test_and_set指令后释放总线,即可保证test_and_set指令执行的原子性。

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    __asm__ __volatile__(
        __raw_spin_lock_string
        :"=m" (lock->slock) : : "memory");
}
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    __asm__ __volatile__(
        __raw_spin_unlock_string
    );
}

由于传统自旋锁无序竞争的本质特点,内核执行线程无法保证何时可以取到锁,某些执行线程可能需要等待很长时间,导致锁竞争不公平。
(1)随着处理器个数增加,自旋锁竞争也在加剧,自然导致更长等待时间。释放自旋锁时的重置操作将无效化所有其它正在忙等待的处理器的缓存,那么在处理器拓扑结构中临近自旋锁拥有者的处理器可能会更快地刷新缓存,因而增大获得自旋锁的机率。
(2)由于每个申请自旋锁的处理器均在全局变量slock上忙等待,系统总线将因为处理器间的缓存同步而导致繁重的流量,从而降低了系统整体性能。

2、ticket spinlock

Linux Kernel 2.6.25版本中引入了排队自旋锁,通过保存执行线程申请锁的顺序信息来解决不公平问题。
排队自旋锁仍然使用raw_spinlock_t 数据结构,但是赋予slock字段新含义。为了保存顺序信息,slock字段被分成两部分Owner和Next,分别保存锁持有者和未来锁申请者的票据序号(Ticket Number),只有Owner和Next相等时,才表明锁处于未使用状态。
排队自旋锁初始化时slock被置为0,即Owner和Next置为0。Linux内核执行线程申请自旋锁时,原子地将Next加1,并将原值返回作为自己的票据序号。如果返回的票据序号等于申请时Owner值,说明自旋锁处于未使用状态,则直接获得锁;否则,线程忙等待检查Owner是否等于自己持有的票据序号,一旦相等,则表明锁轮到自己获取。线程释放锁时,原子地将Owner加1即可,下一个线程将会发现这一变化,从忙等待状态中退出。线程将严格地按照申请顺序依次获取排队自旋锁,从而完全解决了不公平问题。

typedef struct arch_spinlock {
    union {
        __ticketpair_t head_tail;
        struct __raw_tickets {
            __ticket_t head, tail;
        } tickets;
    };
} arch_spinlock_t;

申请自旋锁时,原子地将tail加1,释放时,head加1。只有head域和tail域的值相等时,才表明锁处于未使用的状态。

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    asm volatile("\n1:\t"
             LOCK_PREFIX " ; decb %0\n\t"
             "jns 3f\n"
             "2:\t"
             "rep;nop\n\t"
             "cmpb $0,%0\n\t"
             "jle 2b\n\t"
             "jmp 1b\n"
             "3:\n\t"
             : "+m" (lock->slock) : : "memory");
}
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");
}

在大规模多处理器系统和NUM系统中,排队自旋锁(包括传统自旋锁)存在一个比较严重的性能问题:由于执行线程均在同一个共享变量slock上自旋,申请和释放锁的时候必须对slock进行修改,将导致所有参与排队自旋锁操作的处理器的缓存变得无效。如果排队自旋锁竞争比较激烈的话,频繁的缓存同步操作会导致繁重的系统总线和内存的流量,从而大大降低了系统整体的性能。

3、mcs spinlock

每个锁的申请者(处理器)只在一个本地变量上自旋。MCS Spinlock是一种基于链表结构的自旋锁。
MCS Spinlock的设计目标如下:
(1)保证自旋锁申请者以先进先出的顺序获取锁(FIFO)
(2)只在本地可访问的标志变量上自旋。
(3)在处理器个数较少的系统中或锁竞争并不激烈的情况下,保持较高性能。
(4)自旋锁的空间复杂度(即锁数据结构和锁操作所需的空间开销)为常数。
(5)在没有处理器缓存一致性协议保证的系统中也能很好地工作。
MCS Spinlock采用链表结构将全体锁申请者的信息串成一个单向链表。每个锁申请者必须提前分配一个本地mcs_lock_node,其中至少包括2个字段:本地自旋变量waiting和指向下一个申请者 mcs_lock_node结构的指针变量next。waiting初始值为1,申请者自旋等待其直接前驱释放锁;为0时结束自旋。
自旋锁数据结构mcs_lock是一个永远指向最后一个申请者 mcs_lock_node的指针,当且仅当锁处于未使用(无任何申请者)状态时为NULL值。MCS Spinlock依赖原子的swap和CAS(compare_and_swap)操作,如果缺乏CAS支持,MCS Spinlock 就不能保证以先进先出的顺序获取锁。
每个锁有NR_CPUS个元素node数组,mcs_lock_node结构可以在处理器所处节点的内存中分配,从而加快访问速度。








typedef struct _mcs_lock_node {
    volatile int waiting;
    struct _mcs_lock_node *volatile next;
} ____cacheline_aligned_in_smp mcs_lock_node;

typedef mcs_lock_node *volatile mcs_lock;

typedef struct {
    mcs_lock slock;
    mcs_lock_node nodes[NR_CPUS];
} raw_spinlock_t;
static __always_inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    int cpu;
    mcs_lock_node *me;
    mcs_lock_node *tmp;
    mcs_lock_node *pre;

    cpu = raw_smp_processor_id();                                 
    me = &(lock->nodes[cpu]);
    tmp = me;
    me->next = NULL;

    pre = xchg(&lock->slock, tmp);                              
    if (pre == NULL) {
        /* mcs_lock is free */
        return;                                                
    }

    me->waiting = 1;                                               
    smp_wmb();                                                      
    pre->next = me;                                                

    while (me->waiting) {                                            
        asm volatile ("pause");
    }   
}
static __always_inline int __raw_spin_trylock(raw_spinlock_t *lock)
{
    int cpu;
    mcs_lock_node *me;

    cpu = raw_smp_processor_id();
    me = &(lock->nodes[cpu]);
    me->next = NULL;

    if (cmpxchg(&lock->slock, NULL, me) == NULL)            
        return 1;
    else
        return 0;
}
static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    int cpu;
    mcs_lock_node *me;
    mcs_lock_node *tmp;

    cpu = raw_smp_processor_id();
    me = &(lock->nodes[cpu]);
    tmp = me;

    if (me->next == NULL) {                                     
        if (cmpxchg(&lock->slock, tmp, NULL) == me) {   
            /* mcs_lock I am the last. */
            return;
        }
        while (me->next == NULL)                            
            continue;
    }

    /* mcs_lock pass to next. */
    me->next->waiting = 0;                                       
}

mcs spinlock 锁占用空间大。

4、qspinlock

qspinlock在Linux Kernel 4.2引入,基于mcs spinlock设计思想但解决了mcs spinlock接口不一致或空间太大的问题。
qspinlock数据结构体比mcs lock大大减小,与ticket spinlock大小相同。

struct __qspinlock {
    union {
        atomic_t val;
#ifdef __LITTLE_ENDIAN
        struct {
            u8  locked;
            u8  pending;
        };
        struct {
            u16 locked_pending;
            u16 tail;
        };
#else
        struct {
            u16 tail;
            u16 locked_pending;
        };
        struct {
            u8  reserved[2];
            u8  pending;
            u8  locked;
        };
#endif

static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
        u32 val;

        val = atomic_cmpxchg_acquire(&lock->val, 0, _Q_LOCKED_VAL);
        if (likely(val == 0))
                return;
        queued_spin_lock_slowpath(lock, val);
}

qspinlock采用mcs lock机制, 每一个CPU都定义有一个struct mcs spinlock数据结构,在大规模多处理器系统和NUM架构中, 使用qspinlock可以较好的提高锁的性能。

5、性能比较

 写一个spinlock的性能测试驱动,在等待相同时间后比较spinlock 临界区域的值, 从而比较各个锁的性能差异。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/kernel/h>
#include <linux/spinlock.h>
#include <linux/random.h>
#include <linux/slab.h>
#incude <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/atomic.h>

int spinlock_num;

struct worker {
    int burns;
    struct task_struct *task;
}
static struct worker *workers;
static int threads = 2;
module_param(threads, int, 0);

static spinlock_t lock;
static int runtime = 10;
module_param(runtime, int, 0);

static int bench_running;
static task_struct *monitor_task;

static int rerun, done;
module_param(rerun, int, S_IRUGO|S_ISUSR);
module_param(done, int, S_IRUGO|S_ISUSR);

static int work(void *data)
{
    struct worker *wk = (struct worker*)arg;
    while(!kthread_should_stop()) {
        cond_resched();

    if (!ACCESS_ONCE(bench_running))
        continue;
    spin_lock(&lock)
    spinlock_num++;
    spin_unlock(&lock);
    }
    return 0;
}

static int monitor(void *unused)
{
    int i, c;
    int total, min, max, avg;

repeat:
    total = 0, min = INT_MAX, max = 0, avg = 0;

    spinlock_num = 0;

    workers = (struct worker *)kzalloc(sizeof(struct worker) * threads, GFP_KERNEL);
    for (i = 0; i < threads; i++) {
        c = i %num_online_cpus();
        workers[i].task = kthread_create(work, &workers, "locktest/%d:%d", c, i);
        kthread_bind(workers[i].task, c);
        wake_up_process(workers[i].task);
    }
    bench_running = 0;
    for (i = 0; i < threads; i++) {
        if (workers[i].task)
            kthread_stop(workers[i].task);
    }
    kfree(workers);
    printk("lockresult:%6d %8d %12d\n", num_online_cpus(), threads, spinlock_num);
    done = 1;
    while(!kthread_should_stop()) {
        schedule_timeout(1);
        if (cmpxchg(&rerun, done, 0)) {
            done = 0;
            goto repeat;
        }
    }
    return 0;
}

static int locktest_init(void)
{
    monitor_task = kthread_run(monitor, NULL, "monitor");
    return 0;
}

static void locktest_exit(void)
{
    kthread_stop(monitor_task);
}

module_init(locktest_init);
module_exit(locktest_exit);
MODULE_LICENSE("GPL");

在CPU较少的情况下, qspinlock的性能和ticket spinlock的性能差不多, 在CPU较多的情况下,qspinlock的性能远好于ticket spinlock。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章