浅析线程间通信二:读写锁和自旋锁
上文讨论了互斥量和条件变量用于线程的同步,本文将讨论读写锁和自旋锁的使用,并给出了相应的代码和注意事项,相关代码也可在我的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_lock或phread_spin_trylock来获得自旋锁,但需要注意的是,线程在获得自旋锁期间,不要调用任何可能使线程进入睡眠状态的函数,因为如果这样做,其他线程试图获得这个自旋锁时,就会相应消耗CPU资源。
许多互斥量实现非常高效,即使在使用自旋锁的场合,改成使用互斥量,程序性能几乎没有影响。事实上,有些互斥量在获得锁时,如果当前锁不能获得,线程并不马上睡眠,而是忙等一段时间,看能否获得(而自旋锁是一直忙等),若超过一定的时间,线程才进入睡眠状态。另外,现在的处理器切换线程的上下文速度越来越快,使得使用自旋锁的情况越来越少。到底是使用互斥量还是自旋锁,有人总结如下:
a、Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。
b、spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。
c、更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lock和rw lock)。
参考资料
《UNIX环境高级编程》 11.6线程的同步
《UNIX网络编程卷2:进程间通信》第7章、第8章和第10章
http://www.parallellabs.com/2010/01/31/pthreads-programming-spin-lock-vs-mutex-performance-analysis/
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。