29-线程同步——读写锁和自旋锁

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82811588

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资源浪费。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82811588