浅析线程间通信二:读写锁和自旋锁

    上文讨论了互斥量和条件变量用于线程的同步,本文将讨论读写锁和自旋锁的使用,并给出了相应的代码和注意事项,相关代码也可在我的github上下载。

 读写锁

 对于互斥量要么是锁住状态要么是不加锁锁状态,而且一次只有一个线程可以对其加锁,而读写锁对线程的读数据加锁请求和写数据加锁请求进行了区分,从而在某些情况下,程序有更高的并发性。对于读写锁,一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式的请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。读写锁也叫共享-独占锁(shared-exclusive)。下面有读写锁来解决经典的读出者和写入者的问题,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define MAXNTHREADS 100
#define MIN(a,b) (((a) < (b))?(a):(b))

void *reader(void *); 
void *writer(void *); 

int nloop = 1000, nreaders = 6, nwriters = 4;

struct {
  pthread_rwlock_t  rwlock;
  pthread_mutex_t   rcountlock;
  int               nreaders;
  int               nwriters;
} shared = { PTHREAD_RWLOCK_INITIALIZER, PTHREAD_MUTEX_INITIALIZER };

int main(int argc, char **argv)
{
    int     c, i;
    pthread_t   tid_readers[MAXNTHREADS], tid_writers[MAXNTHREADS];

    while ( (c = getopt(argc, argv, "n:r:w:")) != -1) {
        switch (c) {
        case 'n':
            nloop = atoi(optarg);
            break;

        case 'r':
            nreaders = MIN(atoi(optarg),MAXNTHREADS);
            break;

        case 'w':
            nwriters = MIN(atoi(optarg),MAXNTHREADS);
            break;
        }   
    }   
    if (optind != argc)
    {   
        printf("usage: read_write_lock_example [-n #loops ] [ -r #readers ] [ -w #writers ]");
        return 1;
    }   

    /* create all the reader and writer threads */
    for (i = 0; i < nreaders; i++)
        pthread_create(&tid_readers[i], NULL, reader, NULL);
    for (i = 0; i < nwriters; i++)
        pthread_create(&tid_writers[i], NULL, writer, NULL);

    /* wait for all the threads to complete */
    for (i = 0; i < nreaders; i++)
        pthread_join(tid_readers[i], NULL);
    for (i = 0; i < nwriters; i++)
        pthread_join(tid_writers[i], NULL);

    exit(0);
}

void* reader(void *arg)
{
    int     i;

    for (i = 0; i < nloop; i++) {
        pthread_rwlock_rdlock(&shared.rwlock);

        pthread_mutex_lock(&shared.rcountlock);
        shared.nreaders++;  /* shared by all readers; must protect */
        pthread_mutex_unlock(&shared.rcountlock);

        if (shared.nwriters > 0)
        {
            printf("reader: %d writers found", shared.nwriters);
            return (void*)0;
        }

        pthread_mutex_lock(&shared.rcountlock);
        shared.nreaders--;  /* shared by all readers; must protect */
        pthread_mutex_unlock(&shared.rcountlock);

        pthread_rwlock_unlock(&shared.rwlock);
    }
    return(NULL);
}

void* writer(void *arg)
{
    int     i;

    for (i = 0; i < nloop; i++) {
        pthread_rwlock_wrlock(&shared.rwlock);
        shared.nwriters++;  /* only one writer; need not protect */

        if (shared.nwriters > 1)
        {
            printf("writer: %d writers found", shared.nwriters);
            return (void*)0;
        }
        if (shared.nreaders > 0)
        {
            printf("writer: %d readers found", shared.nreaders);
            return (void*)0;
        }

        shared.nwriters--;  /* only one writer; need not protect */
        pthread_rwlock_unlock(&shared.rwlock);
    }
    return(NULL);
}

上面程序实现是读入线程和写入线程同步,有以下几个地方值得注意:

I)在reader中,在修改shared.nreaders值时(尽管之前用pthread_rwlock_rdlock加了读锁),需要对互斥量加锁,因为可以有多个线程获得读锁,修改这个值。

II)在writer中,修改shared.nwriters值时,不需要用互斥量加锁保护了,因为只可能有一个线程获得写锁。

 自旋锁(spin locks

 自旋锁类似于互斥量,在获得互斥量的锁线程阻塞时,线程会进入睡眠状态,而在获取自旋锁时,线程会处于忙等待(busy-waiting)状态,即不会让出CPU,消耗CPU资源,反复尝试是否能获得自旋锁,直到得到为止。自旋锁适用于这样的情况:线程持有自旋锁的时间比较短并且线程不想消耗重新调度的花费。自旋锁通常可以使用test-and-set指令高效实现。

可以使用pthread_spin_lockphread_spin_trylock来获得自旋锁,但需要注意的是,线程在获得自旋锁期间,不要调用任何可能使线程进入睡眠状态的函数,因为如果这样做,其他线程试图获得这个自旋锁时,就会相应消耗CPU资源。

许多互斥量实现非常高效,即使在使用自旋锁的场合,改成使用互斥量,程序性能几乎没有影响。事实上,有些互斥量在获得锁时,如果当前锁不能获得,线程并不马上睡眠,而是忙等一段时间,看能否获得(而自旋锁是一直忙等),若超过一定的时间,线程才进入睡眠状态。另外,现在的处理器切换线程的上下文速度越来越快,使得使用自旋锁的情况越来越少。到底是使用互斥量还是自旋锁,有人总结如下:

aMutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。

bspin locklock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)

c、更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lockrw lock)。

 参考资料

《UNIX环境高级编程》 11.6线程的同步 
《UNIX网络编程卷2:进程间通信》第7章、第8章和第10章
http://www.parallellabs.com/2010/01/31/pthreads-programming-spin-lock-vs-mutex-performance-analysis/



郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。