上文讨论了互斥量和条件变量用于线程的同步,本文将讨论读写锁和自旋锁的使用,并给出了相应的代码和注意事项,相关代码也可在我的github上下载。
读写锁
对于互斥量要么是锁住状态要么是不加锁锁状态,而且一次只有一个线程可以对其加锁,而读写锁对线程的读数据加锁请求和写数据加锁请求进行了区分,从而在某些情况下,程序有更高的并发性。对于读写锁,一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式的请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。读写锁也叫共享-独占锁(shared-exclusive)。下面有读写锁来解决经典的读出者和写入者的问题,代码如下:
#include
#include
#include
#include
#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/