linux线程同步方式5——读写锁(rwlock)

读写锁

#include <pthread.h>

1、背景和定义

1. 背景:读者和写者问题

有一群写者和一群读者,写者在写同一本书,读者也在读这本书,多个读者可以同时读这本书,但是,只能有一个写者在写书。特征:

  • (1)任意多的读进程可以同时读这个文件;
  • (2)一次只允许一个写进程往文件中写;
  • (3)如果一个写进程正在往文件中写,禁止任何读进程或写进程访问文件;
  • (4)写进程执行写操作前,应让已有的写者或读者全部退出。这说明当有读者在读文件时不允许写者写文件。

有时候,在多线程中,有一些公共数据修改的机会比较少,而读的机会却是非常多的,此公共数据的操作基本都是读,如果每次操作都给此段代码加锁,太浪费时间了而且也很浪费资源,降低程序的效率,因为读操作不会修改数据,只是做一些查询,所以在读的时候不用给此段代码加锁,可以共享的访问,只有涉及到写的时候,互斥的访问就好了

2. 定义

读写锁是一种特殊的自旋锁,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作。读写锁一个读写锁同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。

3. 读写锁三种状态

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获的读写锁,否则必须自旋,直到没有任何的写锁或者读锁存在。如果读写锁没有写锁,那么读锁可以立马获取,否则必须等待写锁释放。

  • 所以读写锁具备三种状态:
    1. 读模式下加锁状态 (读锁)
    2. 写模式下加锁状态 (写锁)
    3. 不加锁状态

4. 读写锁特性

  • 读写锁是"写模式加锁"时, 解锁前,所有对该锁加锁的线程都会被阻塞;
  • 读写锁是"读模式加锁"时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞;
  • 读写锁是"读模式加锁"时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。

写独占,读共享;写锁优先级高

2、初始化

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

成功返回0, 失败直接返回错误号

  • pthread_rwlock_t:定义读写锁的结构体
    • PTHREAD_RWLOCK_INITIALIZER静态初始化读写锁
    • pthread_rwlock_init 函数动态初始化锁
  • attr:通常使用默认属性,NULL
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);

3、请求读锁(阻塞)

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

成功,返回 0。否则,将返回用于指明错误的错误号

  • 如果写入器未持有读锁,并且没有任何写入器基于该锁阻塞,则调用线程会获取读锁。
  • 如果写入器未持有读锁,但有多个写入器正在等待该锁时,调用线程是否能获取该锁是不确定的。
  • 如果一个线程写锁定了 读写锁后,又调用了pthread_rwlock_rdlock来读锁定同一个读写锁,结果无法预测。
  • 如果某个写入器持有读锁,则调用线程无法获取该锁。
  • 调用线程必须获取该锁之后,才能从 pthread_rwlock_rdlock() 返回。
  • 为避免写入器资源匮乏,允许在多个实现中使写入器的优先级高于读取器。
  • pthread_rwlock_rdlock() n 次。该线程必须调用pthread_rwlock_unlock() n 次才能执行匹配的解除锁定操作。
  • 线程信号处理程序可以处理传送给等待读写锁的线程的信号。从信号处理程序返回后,线程将继续等待读写锁以执行读取,就好像线程未中断一样。

4、请求读锁(非阻塞)

读取非阻塞读写锁中的锁

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);//尝试加读锁,没锁上就立即返回

无论加锁是否成功,上面的函数都会立即返回,成功返回 0,失败返回 EBUSY。

  • 如果任何线程持有 rwlock 中的写锁或者写入器基于 rwlock 阻塞,则 pthread_rwlock_tryrdlock()
    函数会失败。

5、请求写锁(阻塞)

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

成功返回0, 失败直接返回错误号

  • 如果没有其他读取器线程或写入器线程持有读写锁 rwlock,则调用线程将获取写锁,否则,调用线程将阻塞。
  • 调用线程必须获取该锁之后,才能从 pthread_rwlock_wrlock() 调用返回。
  • 如果在进行调用时,调用线程持有读写锁(读锁或写锁),则结果是不确定的。
  • 为避免写入器资源匮乏,写入器的优先级高于读取器。 如果针对未初始化的读写锁调用 pthread_rwlock_wrlock(),则结果是不确定的。
  • 线程信号处理程序可以处理传送给等待读写锁以执行写入的线程的信号。从信号处理程序返回后,线程将继续等待读写锁以执行写入,就好像线程未中断一样。

6、请求写锁(非阻塞)

int pthread_rwlock_trywrlock(pthread_rwlock_t  *rwlock);//尝试(没锁上就立即返回)加锁

成功返回0, 失败直接返回错误号

  • 如果任何线程当前持有用于读取和写入的 rwlock,则pthread_rwlock_trywrlock() 函数会失败。
  • 如果针对未初始化的读写锁调用 pthread_rwlock_trywrlock(),则结果是不确定的

7、解锁

释放在 rwlock 引用的读写锁对象中持有的锁。

int pthread_rwlock_unlock (pthread_rwlock_t  *rwlock);

成功返回 0,否则返回用于指明错误的错误号

  • 如果调用线程未持有读写锁 rwlock,则结果是不确定的。
  • 如果通过调用 pthread_rwlock_unlock()来释放读写锁对象中的读锁,并且其他读锁当前由该锁对象持有,则该对象会保持读取锁定状态。
  • 如果 pthread_rwlock_unlock()释放了调用线程在该读写锁对象中的最后一个读锁,则调用线程不再是该对象的属主。
  • 如果 pthread_rwlock_unlock()释放了该读写锁对象的最后一个读锁,则该读写锁对象将处于无属主、解除锁定状态。
  • 如果通过调用 pthread_rwlock_unlock() 释放了该读写锁对象的最后一个写锁,则该读写锁对象将处于无属主、解除锁定状态。
  • 如果 pthread_rwlock_unlock()解除锁定该读写锁对象,并且多个线程正在等待获取该对象以执行写入,则通过调度策略可确定获取该对象以执行写入的线程。
  • 如果多个线程正在等待获取读写锁对象以执行读取,则通过调度策略可确定等待线程获取该对象以执行写入的顺序。
  • 如果多个线程基于rwlock 中的读锁和写锁阻塞,则无法确定读取器和写入器谁先获得该锁。
  • 如果针对未初始化的读写锁调用 pthread_rwlock_unlock(),则结果是不确定的。

8、销毁

销毁 rwlock 引用的读写锁对象并释放该锁使用的任何资源

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

成功,返回 0。否则,返回用于指明错误的错误号

  • 尝试销毁未初始化的读写锁会产生不确定的行为。
  • 已销毁的读写锁对象可以使用 pthread_rwlock_init() 来重新初始化。
  • 销毁读写锁对象之后,如果以其他方式引用该对象,则结果是不确定的。

互斥锁、读写锁、自旋锁区别

  • 互斥锁:
    用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒

  • 读写锁:
    分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。

  • 自旋锁:
    在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。

应用举例

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

pthread_rwlock_t rwlock;//创建读写锁
long int love;

void *pth_wr(void *arg)//写操作
{
    int i = (int)arg;//参数类型转化
    while (love <= 520)
    {
        pthread_rwlock_wrlock(&rwlock);//请求写锁
        printf("write================love = %ld, threadID = %d\n", love += 40, i + 1);//写操作,love每次加40
        pthread_rwlock_unlock(&rwlock);//写锁释放
        sleep(1);
    }
    return NULL;
}
void *pth_rd(void *arg)//读操作
{
    int i = (int)arg;
    while (love <= 520)
    {
        pthread_rwlock_rdlock(&rwlock);//请求读锁
        printf("love = %ld, threadID = %d-------------------- read\n", love, i + 1);
        pthread_rwlock_unlock(&rwlock);//读锁释放
        sleep(1);
    }
    return NULL;
}
int main(void)
{
    pthread_t pth[10];
    int i;
    pthread_rwlock_init(&rwlock, NULL);
    for (i = 0; i != 5; i++)
    {
        pthread_create(&pth[i], NULL, pth_wr, (void *)i);
    }
    for (i = 0; i != 5; i++)

    {
        pthread_create(&pth[5 + i], NULL, pth_rd, (void *)i);
    }
    while (1)
    {
        if (love >= 520)
        {
            for (int j = 0; j != 10; j++)
            {
                pthread_join(pth[j], NULL);
            }
            break;
        }
    }
   pthread_rwlock_destroy(&rwlock);
   return 0;
}

gcc rwlock.c -o rwlock -lpthread
./rwlock

在这里插入图片描述
读的时候时一致的,并且读出来的数据与前面写进去的数据保持一致,并没有出现读出的数据和写入的数据不一致的情况。虽然线程执行顺序没有顺序,但是这并不重要,这只是内核调度和线程争夺资源的结果。

参考

1、https://www.cnblogs.com/wait-pigblog/p/9350569.html
2、https://www.cnblogs.com/love-DanDan/p/8723931.html
3、https://blog.csdn.net/bluehawksky/article/details/41212539
4、https://www.cnblogs.com/dins/p/pthread-rwlock-t.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值