Linux下的多线程编程二(线程的同步与互斥)

 

一、什么叫做线程的同步与互斥?为什么需要同步与互斥?

1、同步与互斥
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

2、线程的同步与异步
线程同步是多个线程同时访问同一资源,等待资源访问结束,浪费时间,效率低 。
线程异步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制 。异步处理就是,你现在问我问题,我可以不回答你,等我用时间了再处理你这个问题。

同步信息被立即处理 – 直到信息处理完成才返回消息句柄;
异步信息收到后将在后台处理一段时间 – 而早在信息处理结束前就返回消息句柄。

3、多线程的同步与互斥的区别
假如把整条道路看成是一个【进程】的话,那么马路中间白色虚线分隔开来的各个车道就是进程中的各个【线程】了。
①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。
②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。
③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。
④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。
⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。

注:由于用于互斥的信号量sem与所有的并发进程有关,所以称之为公有信号量。公有信号量的值反映了公有资源的数量。只要把临界区置于P(sem)和V(sem)之间,即可实现进程间的互斥。就象火车中的每节车厢只有一个卫生间,该车厢的所有旅客共享这个公有资源:卫生间,所以旅客间必须互斥进入卫生间,只要把卫生间放在P(sem)和V(sem)之间,就可以到达互斥的效果。

二、mutex(互斥量)

1、多线程的冲突举例
我们知道,多个线程同时访问共享数据时,可能会产生冲突。比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:
1> 从内存读变量值到寄存器
2>寄存器的值加1
3>将寄存器的值写回内存
假设两个线程在多处理器平台上同时执行这三条指令,则可能导致下图所示的结果,最后变量只加了一次而非两次。
这里写图片描述

数据二义性产生原因:不同的线程访问同一份临界资源,其次线程的操作不是原子操作。
为了验证多线程同时访问临界资源产生的冲突与数据的二义性,创建两个线程,各自把global增加5000次,正常情况下最后global应该等于10000,但事实上每次运行该程序的结果都不一样,代码如下(为了使现象更容易观察到,我们把上述三条指令做的事情用更多条指令来做):

  1 /************************************** 
  2 *文件说明:mutex.c 
  3 *作者:段晓雪 
  4 *创建时间:2017年05月29日 星期一 16时11分42秒 
  5 *开发环境:Kali Linux/g++ v6.3.0 
  6 ****************************************/ 
  7 #include<stdio.h> 
  8 #include<stdlib.h> 
  9 #include<pthread.h> 
 10  
 11 int global = 0;//定义全局变量 
 13 {
 14     int i = 5000;//数据太小,线程发生冲突的概率比较小
 15     //int i = 5000000;//1、增大循环次数,发生冲突较多
 16     //while(i--)
 17     //{
 18     //  global++;
 19     //}
 20     //2、增多线程切换:内核态---->用户态,增大冲突概率
 21     while(i--)
 22     {
 23         int tmp = global;
 24         printf("%d\n",global);
 25         global = tmp + 1;
 26     }
 27     //printf("\n");
 28     return (void*)0;
 29 }
 30 int main()
 31 {
 32     pthread_t tid1;
 33     pthread_t tid2;
 34     pthread_create(&tid1,NULL,pthread_add,NULL);//创建线程1
 35     pthread_create(&tid2,NULL,pthread_add,NULL);//创建线程2
 36     pthread_join(tid1,NULL);//等待线程1
 37     pthread_join(tid2,NULL);//等待线程2
 38     printf("the global is %d\n",global);
 39     return 0;
 40 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

第一次运行结果—-5101:
这里写图片描述

第二次运行结果—-5091:
这里写图片描述

第三次运行结果—-5000:
这里写图片描述

3次运行结果均不一样,足以说明多线程在同步访问全局变量global时发生了冲突。

2、解决多线程的冲突问题 —-引入互斥锁
(1)互斥锁:(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得 锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

(2)互斥锁的创建和销毁:
Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁。
1>定义一把锁

pthread_mutex_t lock;
  • 1

2>互斥锁lock的初始化
如果lock是局部变量—可用初始化函数pthread_mutex_init初始化。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
  • 1

返回值:成功返回0,失败返回错误号。

如果lock是静态或全局变量—可用 PTHREAD_MUTEX_INITIALIZER初始化 ,相当于用pthread_mutex_init初始化并且attr参数为NULL。

3>加锁与解锁

int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞式加锁
int pthread_mutex_lock(pthread_mutex_t* mutex)//阻塞式加锁
int pthread_mutex_unlock(pthread_mutex_t* mutex)//解锁
  • 1
  • 2
  • 3

如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被 另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
Mutex的两个基本操作lock和unlock是如何实现的呢?
假设Mutex变量 的值为1表示互斥锁空闲,这时某个进程调用lock可以获得锁,而Mutex的值为0表示互斥锁已经被某个线程获得,其它线程再调用lock只能挂起等待。

4>销毁锁资源

int pthread_mutex_destroy(pthread_mutex_t* mutex);
  • 1

返回值:成功返回0,失败返回错误号。

(3)解决多线程的冲突问题

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

   int global = 0;//定义全局变量
   //加入互斥锁解决线程冲突
  pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//定义全局锁并初始化
  void* pthread_add(void* val)
  {
      int i = 5000;
      while(i--)
      {
          pthread_mutex_lock(&lock);//加锁
          int tmp = global;
          printf("%d\n",global);
          global = tmp + 1;
          pthread_mutex_unlock(&lock);//解锁
      }
      return (void*)0;
  }
  int main()
  {
      pthread_t tid1;
      pthread_t tid2;
      pthread_create(&tid1,NULL,pthread_add,NULL);//创建线程1
      pthread_create(&tid2,NULL,pthread_add,NULL);//创建线程2
      pthread_join(tid1,NULL);//等待线程1
      pthread_join(tid2,NULL);//等待线程2
      printf("the global is %d\n",global);
      pthread_mutex_destroy(&lock);//销毁锁资源
      return 0;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

第一次运行结果:
这里写图片描述

第二次运行结果:
这里写图片描述

第三次运行结果:
这里写图片描述

可见,加了互斥锁之后,运行结果是正确的,解决了多线程的冲突问题。

(4)互斥锁的加锁和解锁是原子的。
加锁解锁指令必须依赖于一条汇编指令:swap和exchange(原子操作)。大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

“挂起等待”和“唤醒等待线程”的操作如何实现?
每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一 项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有 可能切换到被唤醒的线程。
操作系统中同一时间可以有多个进程处于running状态,需要把其PCB组织起来。

注意:互斥锁的加入有可能引起“饥饿”和死锁问题。
死锁的概述和总结

三、Condition Variable (条件变量)

1、条件变量:描述某些资源就绪与否的状态,为了实现同步而引入。
同步则是为了协调多线程之间的饥饿问题。
大多数情况下,同步是以互斥为前提的。少数情况可实现无锁同步。
一个Condition Variable总是和一个Mutex搭配使用的。一个线程可以调用pthread_cond_wait在一个Condition Variable上阻塞等待,这个函数做以下三步操作:
1>释放Mutex
2>阻塞等待
3>当被唤醒时,重新获得Mutex并返回

2、条件变量的使用
1>创建条件变量condition

pthread_cond_t condition;
  • 1

2>条件变量condition的初始化
如果condition是局部变量—可用初始化函数pthread_mutex_init初始化。

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
  • 1

返回值:成功返回0,失败返回错误号。

如果condition是静态或全局变量—可用 PTHEAD_COND_INITIALIZER初始化 ,相当于用pthread_cond_init初始化并且attr参数为NULL。

3>操作条件变量

int pthread_cond_broadcast(pthread_cond_t* cond);//唤醒条件变量等待的所有线程
int pthread_cond_signal(pthread_cond_t* cond)//唤醒条件变量上等待的一个线程
int pthread_cond_wait(pthread_cond_t* cond)//解锁互斥量并等待条件变量触发
int pthread_cond_timewait(pthread_cond_t* cond,int abstime)//pthread_cond_wait,但可设定等待超时 
  • 1
  • 2
  • 3
  • 4

pthread_cond_wait 自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发;
pthread_cond_timedwait与pthread_cond_wait,区别在于,如果达到或是超过所引用的参数*abstime,它将结束并返回错误ETIMEDOUT;
当调用pthread_cond_signal时一个在相同条件变量上阻塞的线程将被解锁;
pthread_cond_broadcast,则将通知阻塞在这个条件变量上的所有线程。

4>销毁条件变量

int pthread_cond_destroy(pthread_cond_t* cond);
  • 1

返回值:成功返回0,失败返回错误号。

3、条件变量的实例
生产者-消费者模型
生产者从单链表头部生产节点,消费者从单链表头部消费节点:

  1 /************************************** 
  2 *文件说明:list.c 
  3 *作者:段晓雪 
  4 *创建时间:2016年07月26日 星期二 10时59分31秒 
  5 *开发环境:Kali Linux/g++ v6.3.0 
  6 ****************************************/ 
  7  
  8 #include<stdio.h> 
  9 #include<pthread.h> 
 10 #include<stdlib.h> 
 11 #include<unistd.h> 
 12  
 13 typedef struct list_node 
 14 { 
 15     struct list_node* _next; 
 16     int _data; 
 17 }Node,*pNode,**ppNode; 
 18  
 19 pNode head = NULL;//单链表的头节点
 20 
 21 static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//定义锁
 22 static pthread_cond_t condition = PTHREAD_COND_INITIALIZER;//定义条件变量
 23 
 24 static pNode CreateNode(int data)//创建链表的新节点
 25 {
 26     pNode newNode = (pNode)malloc(sizeof(Node));
 27     if(NULL == newNode)
 28     {
 29         perror("malloc error");
 30         return NULL;
 31     }
 32     newNode->_data = data;
 33     newNode->_next = NULL;
 34     return newNode;
 35 }
 36 
 37 static void FreeNode(pNode node)//删除节点 
 38 {
 39     if(node != NULL)
 40     {
 41         free(node);
 42         node = NULL;
 43     }
 44 }
 45 
 46 void InitList(ppNode list)//单链表的初始化 
 47 {
 48     *list = CreateNode(0);
 49 }
 50 
 51 void PushFront(pNode node,int data)//头插
 52 {
 53     pNode newNode = CreateNode(data);
 54     newNode->_next = node->_next;
 55     node->_next = newNode;
 56 }
 57 
 58 int Empty(pNode node)//判断链表是否为空 
 59 {
 60     return node->_next==NULL ? 1:0;
 61 }
 62 
 63 void PopFront(pNode node,int* data)//头删
 64 {
 65     if(!Empty(node))
 66     {
 67          pNode del = node->_next;
 68          node->_next = node->_next->_next;
 69          *data = del->_data;
 70          FreeNode(del);
 71     }
 72 }
 73 
 74 void ShowList(pNode node)//打印链表
 75 {
 76     if(Empty(node))
 77     {
 78         printf("list empty!\n");
 79     }
 80     else
 81     {
 82         pNode cur = node->_next;
 83         while(cur) 
 84         {
 85             printf("%d->",cur->_data);
 86             cur = cur->_next;
 87         }
 88         printf("NULL\n");
 89     }
 90 }
 91 
 92 void DestroyList(pNode node)//销毁链表
 93 {   
 94     int data = 0;
 95     while(!Empty(node))
 96     {
 97         PopFront(node,&data);
 98     }   
 99     FreeNode(node);
100 }
101 
102 void* product(void* arg)//生产者句柄
103 {
104     int data = 0;
105     while(1)
106     {
107         sleep(1);
108         data = rand() % 1234;
109         pthread_mutex_lock(&lock);//加锁
110         PushFront(head,data);
111         pthread_mutex_unlock(&lock);//解锁
112         pthread_cond_signal(&condition);//唤醒等待的进程
113         printf("product:%d\n",data);
114     }
115 }
116 
117 void* consumer(void* arg)//消费者句柄
118 {
119     int data = 0;
120     while(1)
121     {
122         pthread_mutex_lock(&lock);//加锁
123         while(Empty(head))
124         {
125             pthread_cond_wait(&condition,&lock);//如果为空,释放锁,等待被唤醒之后获得锁
126         }
127         PopFront(head,&data);
128         pthread_mutex_unlock(&lock);//解锁
129         printf("consumer:%d\n",data);
130     }
131 }
132 
133 int main()
134 {
135     InitList(&head);//初始化单链表
136     pthread_t tid1,tid2;
137     pthread_create(&tid1,NULL,product,NULL);//生产者线程 
138     pthread_create(&tid2,NULL,consumer,NULL);//消费者线程
139     pthread_join(tid1,NULL);//生产者线程等待
140     pthread_join(tid2,NULL);//消费者线程等待
141 
142     DestroyList(head);
143     pthread_mutex_destroy(&lock);//销毁锁
144     pthread_cond_destroy(&condition);//销毁条件变量
145 
146     return 0;
147 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147

运行结果:
这里写图片描述

四、Semaphore(信号量)

1、什么是信号量
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

2、信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

3、Linux的信号量机制
Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件semphore.h中。

4、信号量的使用
1>创建信号量sem

sem_t sem;
  • 1

2>初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 1

pshared参数为0表示信号量用于同一进程的线程间同步。
3>操作信号量
P操作:

int sem_wait(sem_t *sem);//信号量的P操作
int sem_trywait(sem_t *sem);//信号量的P操作(非阻塞版本)
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//abs_timeout是UTC时间戳
  • 1
  • 2
  • 3

sem_wait()可以获得资源(P操作),使semaphore的值减1,如果调⽤用sem_wait()时
semaphore的值 已经是0,则挂起等待

V操作:

int sem_post(sem_t *sem);//信号量的V操作
  • 1

调用sem_post() 可以释放资源(V操作),使semaphore 的值加1,同时唤醒挂起等待的线程。

4> 销毁信号量

int sem_destroy(sem_t *sem);
  • 1

5、代码实例
基于环形队列的生产者消费者模型:

  1 /************************************** 
  2 *文件说明:ring_queue.c 
  3 *作者:段晓雪 
  4 *创建时间:2017年06月07日 星期三 17时00分44秒 
  5 *开发环境:Kali Linux/g++ v6.3.0 
  6 ****************************************/ 
  7  
  8 #include<stdio.h> 
  9 #include<unistd.h> 
 10 #include<pthread.h> 
 11 #include<semaphore.h> 
 12 #include<stdlib.h> 
 13  
 14 int ring[64]; 
 15 sem_t blank_sem;//生产者关心的格子信号量 
 16 sem_t data_sem;//消费者关心的数据信号量 
 17  
 18 void* producter(void* arg)//生产者句柄 
 19 {  
 20     int step = 0; 
 21     int data = 0; 
 22     while(1) 
 23     { 
 24         sleep(1); 
 25         sem_wait(&blank_sem);//格子数-1 
 26         data = rand() % 123; 
 27         ring[step] = data; 
 28         ++step; 
 29         step %= 64; 
 30         printf("producter done:%d\n",data); 
 31         sem_post(&data_sem);//数据+1 
 32     } 
 33 } 
 34 void* consumer(void* arg)//消费者句柄 
 35 { 
 36     int step = 0; 
 37     int data = 0; 
 38     while(1) 
 39     { 
 40         sem_wait(&data_sem);//数据-1 
 41         data = ring[step]; 
 42         ++step; 
 43         step %= 64; 
 44         printf("consumer done:%d\n",data); 
 45         sem_post(&blank_sem);//格子+1 
 46     } 
 47 } 
 48  
 49 int main() 
 50 { 
 51     sem_init(&blank_sem,0,64);//格子信号量的初始化 
 52     sem_init(&data_sem,0,0);//数据信号量的初始化 
 53  
 54     pthread_t tid1,tid2; 
 55     pthread_create(&tid1,NULL,producter,NULL);//生产者线程 
 56     pthread_create(&tid2,NULL,consumer,NULL);//消费者线程 
 57     pthread_join(tid1,NULL); 
 58     pthread_join(tid2,NULL); 
 59      
 60     sem_destroy(&blank_sem); 
 61     sem_destroy(&data_sem); 
 62     return 0; 
 63 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

运行结果:
这里写图片描述

五、rwlock(读写锁)

1、读写锁的概念
在编写多线程的时候,有一种情况是〸分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。
为了解决上述问题,我们引入了读写锁的概念。

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多
个读者(与CPU数相关),但不能同时既有读者又有写者。

2、读写锁的”三二一“记忆原则
三二一原则:三种关系两个角色一个交易场所
写者与写者:互斥
读者与写者:同步、互斥
读者与读者:没关系

读者和写者同步(读写)过程:要么读者优先,要么写者优先—〉读写锁
读的次数非常多而写的次数非常少适用于读写锁。
单CPU在任何时刻只能有一个线程在运行。
并发:在一个时间段内多个线程交替轮流执行。
并行:在多CPU机器中能够实现多线程真正意义上的同时运行。
自旋锁:当申请锁资源的线程占用临界资源的时间比较短,一直询问不进行挂起的锁。(读写锁)
挂起等待锁:二元信号量,互斥锁。

3、读写锁的使用
1>定义读写锁

pthread_rwlock_t rwlock;
  • 1

2>初始化读写锁
静态或全局的读写锁可用宏PTHREAD_RWLOCK_INITIALIZER初始化,相当于pthread_rwlock_init中的attr为NULL。

局部定义的读写锁需要用函数初始化:

int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
  • 1

如果执行成功返回0,失败返回错误码。

3> 读写锁的操作

int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);//以阻塞方式获取读出锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);//以阻塞方式获取写入锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);//以非阻塞方式获取读出锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);//以非阻塞方式获取写入锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);//释放读出锁或写入锁
  • 1
  • 2
  • 3
  • 4
  • 5

若调用成功则返回0,失败就返回错误码。

4>销毁读写锁

int pthread_rwlockattr_destroy(pthread_rwlockatttr_t *attr);
  • 1

如果执行成功返回0,失败返回错误码。

4、读写锁的实例

 1 /**************************************
  2 *文件说明:rwlock.c
  3 *作者:段晓雪
  4 *创建时间:2017年06月08日 星期四 11时15分00秒
  5 *开发环境:Kali Linux/g++ v6.3.0
  6 ****************************************/
  7 
  8 #include<stdio.h>
  9 #include<pthread.h>
 10 #include<unistd.h>
 11 
 12 pthread_rwlock_t rwlock;//定义一把读写锁
 13 int buf = 0;
 14 
 15void* reader(void* arg)
 16 {
 17     pthread_detach(pthread_self());//线程分离
 18     while(1)
 19     {
 20         //try lock by reader
 21         if(pthread_rwlock_trywrlock(&rwlock) != 0)//读者获取锁资源失败
 22         {
 23             printf("writer is writing,reader waiting...\n");
 24         }   
 25         else
 26         {
 27             printf("reader is %lu,value is %d\n",pthread_self(),buf);
 28             pthread_rwlock_unlock(&rwlock);//读者读完数据之后释放锁资源
 29         }   
 30         sleep(2);
 31     }   
 32 }   
 33 
 34 void* writer(void* arg)//写者句柄
 35 {
 36     pthread_detach(pthread_self());//线程分离
 37     while(1)
 38     {
 39         //try lock by writer
 40         if(pthread_rwlock_tryrdlock(&rwlock) != 0)//获取锁资源失败
 41         {
 42             printf("reader is reading,writer waiting...\n");
 43             sleep(1);
 44         }
 45         else
 46         {
 47             buf++;//write
 48             printf("writer is %lu,the value is %d\n",pthread_self(),buf);
 49             pthread_rwlock_unlock(&rwlock);//写者写完之后释放锁资源
 50         }
 51         sleep(2);
 52     }
 53 }
 54 
 55 int main()
 56 {
 57     pthread_rwlock_init(&rwlock,NULL);//初始化锁
 58     pthread_t read,write;
 59     int i = 0;
 60     for(; i < 2; ++i)//创建2个读者线程
 61     {
 62         pthread_create(&read,NULL,reader,NULL);
 63     }
 64     for(i = 0;i < 3; ++i)//创建3个写者线程
 65     {
 66         pthread_create(&write,NULL,writer,NULL);
 67     }
 68     for(i = 0; i < 2; ++i)//对读者线程进行等待
 69     {
 70         pthread_join(read,NULL);
 71     }
 72     for(i = 0;i < 3; ++i)//对写者线程进行等待
 73     {
 74         pthread_join(write,NULL);
 75     }
 76     pthread_rwlock_destroy(&rwlock);//销毁读写锁
 77     return 0;
 78 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

运行结果:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/wlf_go/article/details/82627040