29-线程同步——读写锁和自旋锁
日期: 2018-09-23 分类: 个人收藏 384次阅读
1. 读写锁
读写锁跟互斥量是类似的,也是一种锁,但读写锁相当于互斥锁的加强版。
因为互斥锁缺乏并发性,例如有多个线程要对数据进行读取操作,互斥锁每次只能支持一个线程访问,而读写锁则支持多个线程同时进行读取操作。
还是拿银行取钱的例子来说,假如当前有10个人去银行查看银行账户,如果使用互斥锁的话每次只能允许一个人查看存款的话,其他人只能排队等待,且每个人查看存款需要1分钟的时间操作,那么10个人查看存款就需要10分钟了。
而使用读写锁的话每次能支持多个人查看存款,如果有10个人需要去查看存款的话,他们可以同时去查看银行存款,整个过程只需要1分钟,不得不说,在某些情况下互斥量极大的降低了线程的并发性。
也就是说,如果访问的数据有大量的读操作,写操作较少时,使用读写锁可以提高程序的执行效率
,因为读写锁解决了互斥锁的弊端,允许更高的并发性,所以读写锁的这种特性比较适用于写操作较少,读操作较多
的场景。
2. 读写锁主要操作函数
//初始化一把读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock , const pthread_rwlockattr_t *restrict attr);
//销毁一把读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//以读方式进行加锁,如果加锁失败会阻塞
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//以写方式进行加锁,如果加锁失败会阻塞
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//解锁同时唤醒所有阻塞在这的线程
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//以非阻塞,读方式请求读写锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//以非阻塞,写方式请求读写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
从以上这些读写锁操作函数可以看出,读写锁(rwlock)的数据类型为pthread_rwlock_t,通常是一个结构体,用于定义一个读写锁,pthread_rwlockattr_t 则表示读写锁属性的数据类型。
3. 读写锁示例1
一个线程分别以不同方式加锁两次:r r ,w w ,w r,r w, 以验证读写锁的特性。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//创建全局读写锁
pthread_rwlock_t rwlock;
int main(int argc , char *args[]){
if(argc < 3){
puts("argc < 3");
return -1;
}
//初始化读写锁
pthread_rwlock_init(&rwlock , NULL);
//以读方式加锁 args[1]
if(!strcmp(args[1] , "r")){
if(pthread_rwlock_rdlock(&rwlock) != 0){
puts("first read lock failed");
}else{
puts("first read lock succes");
}
//以写方式加锁 args[1]
}else if(!strcmp(args[1] , "w")){
if(pthread_rwlock_wrlock(&rwlock) != 0){
puts("first write lock failed");
}else{
puts("first write lock succes");
}
}
//以读方式加锁 args[2]
if(!strcmp(args[2] , "r")){
if(pthread_rwlock_rdlock(&rwlock) != 0){
puts("second read lock failed");
}else{
puts("second read lock succes");
}
}
//以写方式加锁 args[2]
if(!strcmp(args[2] , "w")){
if(pthread_rwlock_wrlock(&rwlock) != 0){
puts("second write lock failed");
}else{
puts("second write lock succes");
}
}
//加锁几次,就要释放几次锁
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_unlock(&rwlock);
return 0;
}
以读方式对读写锁加锁2次,程序执行结果如下:
两次读方式加锁都能成功
第一次以写方式对读写锁加锁,第二次以读模式对读写锁加锁,程序执行结果如下:
为什么第二次以读方式加锁会失败?
原因在于,如果A线程以写方式加锁和B线程以读方式加锁都加锁成功的话,那么问题来了,A线程和B线程谁先执行呢?B线程是读取A线程修改前的数据还是修改后的数据,应该是不确定的,这种情况可能导致数据出现混乱,为了避免出现这种情况,所以不允许读和写都加锁成功了,包括后面两种情况都是如此。
以写方式对读写锁加锁2次,程序执行结果如下:
第一次以r方式加锁,第二次以写模式加锁,程序执行结果如下:
从上面这个读写锁示例来看,读写锁也称为共享互斥锁(shared exclusive lock)
,当以读方式加锁时,锁(rwlock)是共享的,所有线程都能加锁成功;但以写方式加锁时,锁(rwlock)是互斥的,同一时刻只能有一个线程能加锁成功。
4. 读写锁状态
读写锁只有一把,但是具有多种状态:
1 . 读方式加锁
2 . 写方式加锁
3 . 不加锁状态(初始状态)
5. 自旋锁
自旋锁与互斥量有些类似,自旋锁也是一种锁。
互斥量中是一种阻塞锁,当一个线程获取互斥量失败时,会陷入阻塞等待状态,让出cpu资源。
但自旋锁是一种非阻塞锁,当一个线程需要获取自旋锁,但该锁已经被其他线程占用,那么该线程在获取自旋锁之前不会阻塞
,而是在不断的自旋,处于一种忙碌的等待
(消耗CPU的时间),反复检查锁是否可用。
所以自旋锁适用于线程持有锁的时间短,这意味着线程不会长时间自旋,一旦锁被释放,其他线程可以马上检测并获得锁。如果线程长时间持有锁的话,其他线程将不断自旋,反复尝试获取锁,这将导致cpu时间的浪费。
而互斥量相反,适用于线程持有锁的时间长,这也意味着线程将会长时间阻塞。
6. pthread自旋锁的相关函数
//初始化一把自旋锁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
//销毁自旋锁
int pthread_spin_destroy(pthread_spinlock_t *lock);
//加锁
int pthread_spin_lock(pthread_spinlock_t *lock);
//以非阻塞加锁
int pthread_spin_trylock(pthread_spinlock_t *lock);
//解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
pthread_spinlock_t是pthead自旋锁的一种数据类型,pthread_spin_init函数用来初始化一个自旋锁,其参数pshared的值含义为:
1.PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享
2.PTHREAD_PROCESS_PRIVATE: 仅初始化本自旋锁的线程所在的进程内的线程
才能够使用该自旋锁
注意,pthread_spin_lock函数用于给指定自旋锁进行加锁,如果锁被其他线程持有的话,那么该线程调用该函数会在获取到自旋锁之前一直处于自旋(检测自旋锁的状态,试图获取自旋锁)。如果调用该函数的线程已经持有自旋锁的话,再次调用该函数结果是未知的,可能返回错误号。
7. 自旋锁示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//定义全局自旋锁
pthread_spinlock_t spin_lock;
//线程主控函数
void *tfn(void *arg){
srand(time(NULL));
while(1){
//加锁
pthread_spin_lock(&spin_lock);
printf("a");
sleep(rand()%3);
printf("b\n");
//解锁
pthread_spin_unlock(&spin_lock);
sleep(rand()%3);
}
return (void *)0;
}
int main(void){
pthread_t tid;
srand(time(NULL));
//初始化自旋锁
pthread_spin_init(&spin_lock , PTHREAD_PROCESS_PRIVATE);
int ret;
//创建线程
ret = pthread_create(&tid , NULL , tfn , (void *)0);
if(ret != 0){
perror("pthread_create");
return -1;
}
while(1){
//加锁
pthread_spin_lock(&spin_lock);
printf("A");
sleep(rand()%3);
printf("B\n");
//解锁
pthread_spin_unlock(&spin_lock);
sleep(rand()%3);
}
//回收子线程
pthread_join(tid , NULL);
//销毁锁
pthread_spin_destroy(&spin_lock);
return 0;
}
程序执行结果:
8. 总结
在此之前我们先回忆一下所学的互斥锁,读写锁,自旋锁的特点,主要从以下几点进行总结:
1.共同点
互斥锁,读写锁,自旋锁的作用都是用于线程同步。
2.锁的状态
互斥锁和自旋锁的状态:加锁和非加锁两种状态
读写锁状态:非加锁,读方式加锁状态,写方式加锁状态
3.优缺点
优点:互斥锁保证了线程同步。读写锁保证了线程同步的同时,还提高了线程的并发性,提高了程序的执行效率。自旋锁适用于线程持有锁的时间短,多核处理器的场景。
缺点:互斥锁极大的降低了线程的并发性,程序执行效率不高。读写锁应用于读操作并发较大,写操作并发较少的场景(好像也不算缺点)。自旋锁不适用于线程长时间阻塞,这将会导致cpu资源浪费。
除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog
精华推荐