82-读写锁 rwlock

1. 问题提出

假设,大多数人都是查询火车票的余票数量,只有少量的人是购买火车票。比方说同时有 99 个人查访余票,而只有 1 个人买票……如果此时还用互斥量,就会显的效率非常低了,比方有任何一个人在查票的时候,其它 98 个想查票的人只能等待,这说不通。

为了提高读并发性,也就是说允许多个人同时查票(这并不会带来什么问题),于是发明了读写锁 rwlock。

  • 只要有买票的人拿到了读写锁,即锁处于写模式的加锁状态,其它任何人都只能等待。
  • 只要有查票的人拿到了读写锁,即锁处于读模式的加锁状态,这时候:
    • 在没有取票人加锁的情况下,其它想查票的人都可以拿到锁,并继续加读模式的锁。
    • 其它任何想取票的人只能等待锁,同时在此取票人之后,想查票的人也只能等待锁。(这是为了防止线程处于饥饿状态,即长时间不能加写模式锁)

从上面的叙述中我们可以知道,读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态。同学们需要好好的把前面的读写锁的特性读上几遍。

2. 读写锁

2.1 读写锁的数据类型

读写锁的数据类型是 pthread_rwlock_t,通常它是一个结构体。同样也有两种方法可以对读写锁进行初始化:

  • 使用宏 PTHREAD_RWLOCK_INITIALIZER静态初始化读写锁。
  • 使用 pthread_rwlock_init 函数动态初始化锁。
// 初始化读写锁的函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
  const pthread_rwlockattr_t *restrict attr);

// 回收读写锁  
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

上面的函数和互斥量中的初始化函数是类似的,同样有关读写锁的属性暂时不讨论,现阶段 attr 参数传 NULL 值就行了。读写锁用完了,记得需要使用 destroy 函数进行回收。

2.2 读写锁的加锁和解锁

  • 读模式加锁函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 写模式加锁函数
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • 解锁函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

上面所有的函数返回 0 表示成功,否则失败。

上面的两个加锁函数都是阻塞版本,当然读写锁也提供了非阻塞版本,即 try 方式加锁,通常不推荐使用它们,不过这里也列出来:

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

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

3. 读写锁示例

程序 trainticket 中,有 100 个线程,其中 90 个线程是查余票数量的,只有 10 个线程抢票,每个线程一次买 10 张票。初始状态下一共有 1000 张票。因此执行完毕后,还会剩下 900 张票。

程序 trainticket 在运行的时候需要传入参数,即 ./trainticket <0|1|2>:

  • 参数 0:表示不加任何锁
  • 参数 1:表示使用读写锁
  • 参数 2:表示使用互斥量

在后面将会演示这三种情况,它们的结果也是不同的。

3.1 程序清单

// trainticket.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct Ticket{
  int remain; // 余票数,初始化为 1000
  pthread_rwlock_t rwlock; // 读写锁
  pthread_mutex_t mlock; // 互斥锁,主要是为了和读写锁进行对比
}ticket;

// 通过命令行传参数来取得这个值,用来控制到底使用哪一种锁
// 0:不加锁 1:加读写锁 2:加互斥锁
int lock = 0; 

// 查票线程
void* query(void* arg) {
  int name = (int)arg;
  sleep(rand() % 5 + 1); 
  if (lock == 1)
    pthread_rwlock_rdlock(&ticket.rwlock); // 读模式加锁
  else if (lock == 2)
    pthread_mutex_lock(&ticket.mlock);

  int remain = ticket.remain;
  sleep(1);
  printf("%03d query: %d\n", name, remain);

  if (lock == 1)  
    pthread_rwlock_unlock(&ticket.rwlock);
  else if (lock == 2)
    pthread_mutex_unlock(&ticket.mlock);

  return NULL;
}

// 抢票线程
void* buy(void* arg) {
  int name = (int)arg;

  if (lock == 1)
    pthread_rwlock_wrlock(&ticket.rwlock); // 写模式加锁
  else if (lock == 2)
    pthread_mutex_lock(&ticket.mlock);

  int remain = ticket.remain;
  remain -= 10; // 一次买 10 张票
  sleep(1);
  ticket.remain = remain;
  printf("%03d buy 10 tickets\n", name);

  if (lock == 1)  
    pthread_rwlock_unlock(&ticket.rwlock);
  else if (lock == 2)
    pthread_mutex_unlock(&ticket.mlock);

  sleep(rand() % 5 + 2); 

  return NULL;
}

int main(int argc, char* argv[]) {
  lock = 0;
  if (argc >= 2) lock = atoi(argv[1]);

  int names[100];
  pthread_t tid[100]; // 之前写成了 pthread_t tid[10],感谢 @juruiyuan111 指出错误
  int i;

  for (i = 0; i < 100; ++i) names[i] = i;

  ticket.remain = 1000;

  printf("remain ticket = %d\n", ticket.remain);

  pthread_rwlock_init(&ticket.rwlock, NULL);
  pthread_mutex_init(&ticket.mlock, NULL);

  for (i = 0; i < 100; ++i) {
    if (i % 10 == 0)
      pthread_create(&tid[i], NULL, buy, (void*)names[i]);
    else
      pthread_create(&tid[i], NULL, query, (void*)names[i]);
  }

  for (i = 0; i < 100; ++i) {
    pthread_join(tid[i], NULL);
  }

  pthread_rwlock_destroy(&ticket.rwlock);
  pthread_mutex_destroy(&ticket.mlock);
  printf("remain ticket = %d\n", ticket.remain);

  return 0;
}

3.2 编译和运行

$ gcc trainticket.c -o trainticket -lpthread
  • 不加锁的运行方式和结果
$ ./trainticket 0

这种方式的运行的结果是错误的,最后打印的余票数量是 990 张:


这里写图片描述
图1 不加锁时的运行结果

  • 使用读写锁的运行结果
$ ./trainticket 1


这里写图片描述
图2 使用读写锁的运行结果

可以发现程序最后的结果是没问题的。

  • 使用互斥量的运行结果
$ ./trainticket 2


这里写图片描述
图3 使用互斥量的运行结果

可以发现,程序最后的结果仍然是正确的。

3.3 结果分析

到此为止,你看文字叙述和图 1、图 2 和图 3 中的结果是看不出来读写锁和互斥锁有什么区别,但是如果你亲自动手做了这个实验,我相信你一辈子也忘不了。

不过对于不想做实验的人,我直接告诉你结果:当使用互斥量的时候,这个程序会运行的非常非常慢,你可能没有耐心等待到它执行完毕……

4. 总结

  • 理解读写锁的使用场合
  • 读写锁有三种状态
  • 读写锁与互斥锁的区别
  • 读写锁是如何防止写饥饿的

练习:复制本文的代码,编译运行。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
FreeRTOS没有提供的标准实现,但可以通过互斥量和信号量来实现是用于访问共享资源的一种同步机制,它允许多个线程同时取共享资源,但只允许一个线程入共享资源。 以下是一个基于FreeRTOS的简单实现: ``` #include "FreeRTOS.h" #include "semphr.h" /* 定义结构体 */ typedef struct { SemaphoreHandle_t mutex; // 互斥量 SemaphoreHandle_t rw_sem; // 信号量 int readers; // 当前者数量 } rwlock_t; /* 初始化 */ void rwlock_init(rwlock_t *rwlock) { rwlock->mutex = xSemaphoreCreateMutex(); rwlock->rw_sem = xSemaphoreCreateBinary(); rwlock->readers = 0; xSemaphoreGive(rwlock->rw_sem); } /* 获取 */ void rwlock_read_lock(rwlock_t *rwlock) { xSemaphoreTake(rwlock->mutex, portMAX_DELAY); rwlock->readers++; if (rwlock->readers == 1) { xSemaphoreTake(rwlock->rw_sem, portMAX_DELAY); } xSemaphoreGive(rwlock->mutex); } /* 释放 */ void rwlock_read_unlock(rwlock_t *rwlock) { xSemaphoreTake(rwlock->mutex, portMAX_DELAY); rwlock->readers--; if (rwlock->readers == 0) { xSemaphoreGive(rwlock->rw_sem); } xSemaphoreGive(rwlock->mutex); } /* 获取 */ void rwlock_write_lock(rwlock_t *rwlock) { xSemaphoreTake(rwlock->rw_sem, portMAX_DELAY); } /* 释放 */ void rwlock_write_unlock(rwlock_t *rwlock) { xSemaphoreGive(rwlock->rw_sem); } ``` 在这个实现中,我们使用了一个互斥量来保证多个线程不会同时访问`readers`计数器。`rw_sem`是一个二元信号量,用于控制者和者之间的访问。当有一个者时,者需要等待,直到者释放信号量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值