UNIX环境高级编程——线程同步之读写锁以及属性

时间:2021-03-17 04:43:16

     读写锁和互斥量(互斥锁)很类似,是另一种线程同步机制,但不属于POSIX标准,可以用来同步同一进程中的各个线程。当然如果一个读写锁存放在多个进程共享的某个内存区中,那么还可以用来进行进程间的同步,

     互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有三种状态:读模式下的加锁状态,写模式下的加锁状态,不加锁状态

     一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

  • 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁(读或写)的线程都会被阻塞。
  • 当读写锁在读加锁状态时,所有试图以读模式对它加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。
  • 当读写锁在读加锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。(后面有代码验证发现ubuntu 10.04系统下,不会阻塞随后的读模式的请求,最终导致请求写锁的线程饿死状态这样可以避免读模式锁长期占有,而等待的写模式锁请求一直得不到满足,出现饿死情况。后面会测试ubuntu 10.04系统的情况。(注意前篇关于记录锁fcntl中,ubuntu 10.04系统下,进程拥有读锁,然后优先处理后面的读锁,再处理写锁,导致写锁出现饿死)

     读写锁也称为共享-独占(shared-exclusive)锁,当读写锁以读模式加锁时,它是以共享模式锁住,当以写模式加锁时,它是以独占模式锁住读写锁非常适合读数据的频率远大于写数据的频率从的应用中。这样可以在任何时刻运行多个读线程并发的执行,给程序带来了更高的并发度。

     需要提到的是:读写锁到目前为止仍然不是属于POSIX标准。


1读写锁的初始化和销毁

#include <pthread.h>int pthread_rwlock_init (pthread_rwlock_t *rwlock,const pthread_rwlockattr_t *attr);

int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);

返回值:成功返回0,否则返回错误代码
           与互斥量一样,读写锁在使用之前必须初始化,在释放它们底层的内存前必须销毁。

     上面两个函数分别由于读写锁的初始化和销毁。和互斥量,条件变量一样,如果读写锁是静态分配的,可以通过常量进行初始化,如下:

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

         也可以通过pthread_rwlock_init()进行初始化。对于动态分配的读写锁由于不能直接赋值进行初始化,只能通过这种方式进行初始化pthread_rwlock_init()第二个参数是读写锁的属性,如果采用默认属性,可以传入空指针NULL

      那么当不在需要使用时及释放(自动或者手动)读写锁占用的内存之前,需要调用pthread_rwlock_destroy()进行销毁读写锁占用的资源。


2读写锁的使用

/* 读模式下加锁  */int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);/* 非阻塞的读模式下加锁  */int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);/*  限时等待的读模式加锁 */int pthread_rwlock_timedrdlock (pthread_rwlock_t *rwlock,const struct timespec *abstime);/* 写模式下加锁  */int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);/* 非阻塞的写模式下加锁 */int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);/* 限时等待的写模式加锁 */int pthread_rwlock_timedwrlock (pthread_rwlock_t *rwlock,const struct timespec *abstime);/* 解锁 */int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);                                                   返回值:成功返回0,否则返回错误代码
1pthread_rwlock_rdlock()系列函数

     pthread_rwlock_rdlock()用于以读模式即共享模式获取读写锁,如果读写锁已经被某个线程以写模式占用,那么调用线程就被阻塞。如果读写锁已经被某个线程以写模式占用,那么调用线程将获得读锁。如果读写锁未没有被占有,但有多个写锁正在等待该锁时,调用线程现在试图获取读锁,是否能获取该锁是不确定的。在实现读写锁的时候可以对共享模式下锁的数量进行限制(目前不知如何限制)。

     pthread_rwlock_tryrdlock()pthread_rwlock_rdlock()的唯一区别就是,在无法获取读写锁的时候,调用线程不会阻塞,会立即返回,并返回错误代码EBUSY

   针对未初始化的读写锁调用pthread_rwlock_rdlock/pthread_rwlock_tryrdlock,则结果是不确定的。

     pthread_rwlock_timedrdlock()是限时等待读模式加锁,时间参数struct timespec * abstime也是绝对时间。

2pthread_rwlock_wrlock()系列函数

     pthread_rwlock_wrlock()用于写模式即独占模式获取读写锁,如果读写锁已经被其他线程占用,不论是以共享模式还是独占模式占用,调用线程都会进入阻塞状态。

     pthread_rwlock_trywrlock()在无法获取读写锁的时候,调用线程不会进入睡眠,会立即返回,并返回错误代码EBUSY

   针对未初始化的读写锁调用pthread_rwlock_wrlock/pthread_rwlock_trywrlock,则结果是不确定的。

    pthread_rwlock_timedwrlock()是限时等待写模式加锁。

3pthread_rwlock_unlock()

     无论以共享模式还是独占模式获得的读写锁,都可以通过调用pthread_rwlock_unlock()函数进行释放该读写锁。

   针对未初始化的读写锁调用pthread_rwlock_unlock,则结果是不确定的。

注意:当读写锁以读模式被占有N次,即调用pthread_rwlock_rdlock() N次,且成功。则必须调用N次pthread_rwlock_unlock()才能执行匹配的解锁操作。


示例代码:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <pthread.h>#include <errno.h>#define MAXDATA     1024#define MAXREDER    100#define MAXWRITER   100struct{    pthread_rwlock_t   rwlock;   //读写锁    char datas[MAXDATA];          //共享数据域}shared = {    PTHREAD_RWLOCK_INITIALIZER};void *reader(void *arg);void *writer(void *arg);int main(int argc,char *argv[]){    int i,readercount,writercount;    pthread_t tid_reader[MAXREDER],tid_writer[MAXWRITER];    if(argc != 3)    {        printf("usage : <reader_writer> #<readercount> #<writercount>\n");        exit(0);    }    readercount = atoi(argv[1]);  //读者个数    writercount = atoi(argv[2]);   //写者个数    pthread_setconcurrency(readercount+writercount);    for(i=0;i<writercount;++i)        pthread_create(&tid_writer[i],NULL,writer,NULL);    sleep(1); //等待写者先执行    for(i=0;i<readercount;++i)        pthread_create(&tid_reader[i],NULL,reader,NULL);    //等待线程终止    for(i=0;i<writercount;++i)        pthread_join(tid_writer[i],NULL);    for(i=0;i<readercount;++i)        pthread_join(tid_reader[i],NULL);    exit(0);}void *reader(void *arg){    pthread_rwlock_rdlock(&shared.rwlock);  //获取读出锁if( pthread_rwlock_rdlock(&shared.rwlock) ==0 ) //获取读出锁   printf("pthread_rwlock_rdlock OK\n");    printf("Reader begins read message.\n");    printf("Read message is: %s\n",shared.datas);    pthread_rwlock_unlock(&shared.rwlock);  //释放锁if( pthread_rwlock_unlock(&shared.rwlock) != 0 );    printf("pthread_rwlock_unlock fail\n");    return NULL;}void *writer(void *arg){    char datas[MAXDATA];    pthread_rwlock_wrlock(&shared.rwlock);  //获取写锁if( pthread_rwlock_wrlock(&shared.rwlock) != 0)//再次获取写锁    perror("pthread_rwlock_wrlock");if( pthread_rwlock_rdlock(&shared.rwlock) !=0 ) //获取读出锁    perror("pthread_rwlock_rdlock");    printf("Writers begings write message.\n");sleep(1);    printf("Enter the write message: \n");    scanf("%s",datas);   //写入数据    strcat(shared.datas,datas);    pthread_rwlock_unlock(&shared.rwlock);  //释放锁    return NULL;}
运行结果:
huangcheng@ubuntu:~$ gcc 2.c -lpthreadhuangcheng@ubuntu:~$ ./a.out 5 3pthread_rwlock_wrlock: Successpthread_rwlock_rdlock: SuccessWriters begings write message.Enter the write message:hupthread_rwlock_wrlock: Successpthread_rwlock_rdlock: SuccessWriters begings write message.Enter the write message:1pthread_rwlock_wrlock: Successpthread_rwlock_rdlock: SuccessWriters begings write message.Enter the write message:2pthread_rwlock_rdlock OKReader begins read message.Read message is: hu12pthread_rwlock_unlock failpthread_rwlock_rdlock OKReader begins read message.Read message is: hu12pthread_rwlock_unlock failpthread_rwlock_rdlock OKReader begins read message.Read message is: hu12pthread_rwlock_unlock failpthread_rwlock_rdlock OKReader begins read message.Read message is: hu12pthread_rwlock_unlock failpthread_rwlock_rdlock OKReader begins read message.Read message is: hu12pthread_rwlock_unlock fail

结果说明:

(1)当一个线程获得读写锁的写模式,其他线程试图获得该读写锁的读模式或者是写模式,都将会阻塞,直到该线程释放该读写锁。
(2)当一个线程获得读写锁的写模式,该线程试图获得该读写锁的读模式或写模式,都会立即返回失败,不会导致失败。
(3)当一个线程获得读写锁的读模式,如果该线程试图获得该读写锁的读模式,则返回成功,并在该线程释放该读写锁只需要释放一次,第二次会释放失败。
(4)当一个线程获得读写锁的读模式,且还没有释放该读写锁,如果该线程试图获得该读写锁的写模式,将导致阻塞,直到该线程释放该读写锁。
(5)当一个线程获得读写锁的写模式,该线程试图释放该写锁两次,将导致不可预测的问题。
(6)当一个线程获得读写锁的写模式或者读模式,不能读该读写锁释放两次。即不管该线程获得读锁几次,都只需要释放该读写锁一次就OK。



3读写锁的属性设置

/* 初始化读写锁属性对象 */int pthread_rwlockattr_init (pthread_rwlockattr_t *attr);/* 销毁读写锁属性对象 */int pthread_rwlockattr_destroy (pthread_rwlockattr_t *attr);/* 获取读写锁属性对象在进程间共享与否的标识*/int pthread_rwlockattr_getpshared (__const pthread_rwlockattr_t *attr,int *pshared);/* 设置读写锁属性对象,标识在进程间共享与否  */int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *attr, int pshared);                                                    返回值:成功返回0,否则返回错误代码

     pthread_rwlockattr_setpshared()函数的第二个参数pshared用于设定是否进程间共享,其值可以是PTHREAD_PROCESS_PRIVATE或PTHREAD_PROCESS_SHARED,后者是设置进程间共享。


示例代码:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <pthread.h>#include <errno.h>struct{    pthread_rwlock_t rwlock;    int product;}sharedData = {PTHREAD_RWLOCK_INITIALIZER, 0};void * produce(void *ptr){    int i;    for ( i = 0; i < 5; ++i)    {        pthread_rwlock_wrlock(&sharedData.rwlock);        sharedData.product = i;printf("produce:%d\n",i);        pthread_rwlock_unlock(&sharedData.rwlock);        sleep(1);    }}void * consume1(void *ptr){    int i;for ( i = 0; i < 5;)    {        pthread_rwlock_rdlock(&sharedData.rwlock);        printf("consume1:%d\n",sharedData.product);        pthread_rwlock_unlock(&sharedData.rwlock);        ++i;        sleep(1);    }}void * consume2(void *ptr){    int i;for ( i = 0; i < 5;)    {        pthread_rwlock_rdlock(&sharedData.rwlock);        printf("consume2:%d\n",sharedData.product);        pthread_rwlock_unlock(&sharedData.rwlock);        ++i;        sleep(1);    }}int main(){    pthread_t tid1, tid2, tid3;    pthread_create(&tid1, NULL, produce, NULL);    pthread_create(&tid2, NULL, consume1, NULL);    pthread_create(&tid3, NULL, consume2, NULL);    void *retVal;    pthread_join(tid1, &retVal);    pthread_join(tid2, &retVal);    pthread_join(tid3, &retVal);    return 0;}
运行结果:

huangcheng@ubuntu:~$ ./a.outconsume2:0consume1:0produce:0consume2:0consume1:0produce:1consume2:1consume1:1produce:2consume2:2consume1:2produce:3consume2:3consume1:3produce:4huangcheng@ubuntu:~$

如果把 consume1 的解锁注释掉,如下:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <pthread.h>#include <errno.h>struct{    pthread_rwlock_t rwlock;    int product;}sharedData = {PTHREAD_RWLOCK_INITIALIZER, 0};void * produce(void *ptr){    int i;    for ( i = 0; i < 5; ++i)    {        pthread_rwlock_wrlock(&sharedData.rwlock);        sharedData.product = i;printf("produce:%d\n",i);        pthread_rwlock_unlock(&sharedData.rwlock);        sleep(1);    }}void * consume1(void *ptr){    int i;for ( i = 0; i < 5;)    {        pthread_rwlock_rdlock(&sharedData.rwlock);        printf("consume1:%d\n",sharedData.product);    //   pthread_rwlock_unlock(&sharedData.rwlock);        ++i;        sleep(1);    }}void * consume2(void *ptr){    int i;for ( i = 0; i < 5;)    {        pthread_rwlock_rdlock(&sharedData.rwlock);        printf("consume2:%d\n",sharedData.product);        pthread_rwlock_unlock(&sharedData.rwlock);        ++i;        sleep(1);    }}int main(){    pthread_t tid1, tid2, tid3;    pthread_create(&tid1, NULL, produce, NULL);    pthread_create(&tid2, NULL, consume1, NULL);    pthread_create(&tid3, NULL, consume2, NULL);    void *retVal;    pthread_join(tid1, &retVal);    pthread_join(tid2, &retVal);    pthread_join(tid3, &retVal);    return 0;}
程序运行结果:

huangcheng@ubuntu:~$ ./a.outconsume2:0consume1:0consume2:0consume1:0consume2:0consume1:0consume2:0consume1:0consume2:0consume1:0最后程序保持阻塞状态
      从执行结果可以看出Ubuntu 10.04提供的读写锁函数是优先考虑等待读模式占用锁的线程,这种实现的一个很大缺陷就是出现写入线程饿死的情况。