【Linux】读写锁机制

认识读写锁

读写锁本质是一个自旋锁,用于处理多读少写的情况。
读写锁的类型: pthread_rwlock_t lock 又分“读锁”(对内存进行读操作)和“写锁”(对内存进行写操作)
读写锁的特性
写时独占资源,读时共享资源,写锁优先级更高

当前锁的状态读锁请求写锁请求
无锁可以可以
读锁可以阻塞
写锁阻塞阻塞

读写锁与互斥锁的区别:
由上面内容可知,读写锁的读锁可并行,写锁只能串行;
而互斥锁的读写操作都是串行的。
所以两种锁的写操作是一样的,但读写锁有其并行的特性使得更适用于读操作多的场景,可以提高效率。

读写锁的场景练习
1)写时独占资源 线程 A 加写锁成功,若线程 B 请求读锁–B 读阻塞;若线程 B 请求写锁 --B 写阻塞。
2)读时共享资源 线程 A 加读锁成功,若线程 B 请求读锁–B 读锁请求成功;若线程 B 请求写锁 --B 写阻塞。
3)写锁优先级更高 线程 A 加写锁成功,然后线程 B 请求读锁,线程 C 请求读锁,然后线程D请求写锁 :B 读阻塞,C 读阻塞,D写阻塞;
A 解锁 :D 加写锁成功,B 继续读阻塞,C 读阻塞;
D 解锁 :B加读锁成功,C 继续读阻塞。
B 解锁 :C加读锁成功。

读写锁使用的练习
模拟三个线程不定时的写同一个全局变量,五个线程不定时期读这个变量。预期能够保证该变量每次被写后再进行的读/写操作永远都是基于最新值的操作。

  • 代码实现:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int g_number = 0;
pthread_rwlock_t lock;

void* writeFunc(void* arg);
void* readFunc(void* arg);

int main(int argc, char *argv[])
{
    pthread_t p[8];

    pthread_rwlock_init(&lock, nullptr); //初始化一个锁

    for (int i = 0; i < 3; i++) {  //创建写线程
        pthread_create(&p[i], nullptr, writeFunc, nullptr);
    }

    for (int i = 3; i < 8; i++) {  //创建读线程
        pthread_create(&p[i], nullptr, readFunc, nullptr);
    }

    for (int i = 0; i < 8; i++) {  //阻塞回收子线程的 pcb
        pthread_join(p[i], nullptr);
    }

    pthread_rwlock_destroy(&lock); //销毁读写锁,释放锁资源
    
    return 0;
}

void* writeFunc(void* arg)
{
    while (1) {
        pthread_rwlock_wrlock(&lock); //加写锁
        g_number++;
        printf("--write: %lu, %d\n", pthread_self(), g_number);
        pthread_rwlock_unlock(&lock);  //解锁
        usleep(500);
    }
    
    return nullptr;
}

void* readFunc(void* arg)
{
    while (1) {
        pthread_rwlock_rdlock(&lock);  //加读锁
        printf("--read : %lu, %d\n", pthread_self(), g_number);
        pthread_rwlock_unlock(&lock);  //解锁
        usleep(500);
    }

    return nullptr;
}

  • 运行结果:加读写锁后,任一线程当前操作的都是最新值,所以变量值的大值永远在小值后面。在这里插入图片描述
  • 不加读写锁时,无法保证线程每次操作的都是最新值,所以会出现小数在大数后面的情况。如下,是注释掉上面代码中的读写锁运行得到的结果。
    在这里插入图片描述

其他锁

悲观锁

在每次取数据时,总是担心数据会被其他线程修改,所以总会在取数据前先加锁,当其他线程想要访问时只能阻塞挂起。典型:互斥锁、自旋锁、读写锁
适用场景:适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。

乐观锁

每次取数据时,都不认为数据会被其他线程修改,因此不上锁,但是在更新数据前会判断该数据在更新前是否被修改过。主要采取两种方法:版本号机制和CAS操作。

CAS操作:当需要更新数据时,判断当前内存值与之前取到的值是否相等。若相等则更新,若不等则失败并不断尝试。

适用场景:适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现 , 不存在死锁 问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。

互斥锁

用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

自旋锁

是互斥锁的一种实现方式,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。

公平/ 非公平锁

在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配。
公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。或者依据某种优先级使得后面的线程可能会比前面的先获得锁。
优缺点分析: 公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况。

选择锁的方式:取决于等待获得临界资源的时间

若等待时间较长可以选择挂起等待;若等待时间较短,可以选择自旋锁。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值