欢迎光临
我们一直在努力

第18讲:读者写者问题与读写锁

目录

  • 读者写者问题[了解]
  • 读写锁
  • 读者优先(Reader-Preference)
  • 写者优先(Writer-Preference)

读者写者问题[了解]

读者写者问题

读者写者问题是一个经典的多线程同步问题:有多个读者线程(只读数据)和多个写者线程(修改数据)。 规则约束:

  • 多个读者可以同时访问共享数据,互不干扰;
  • 写者访问时,必须独占资源,读者、其他写者都不能同时访问;
  • 不能同时出现:一个在写,另一个在读 / 写。

读者写者 vs 生产消费

  • 访问权限不同 生产者 – 消费者:生产者放数据、消费者取数据,双方都要操作同一个缓冲区,不能同时操作缓冲区。 读者 – 写者:读者只读取、不修改,允许多个读者并发读;只有写者需要独占。
  • 并发模型不同 生产者消费者是一对一 / 多对多的生产 – 消费流水线,依赖缓冲区(队列)解耦,靠信号量实现同步。 读者写者是读写并发,核心是区分读操作和写操作,优化读的并发能力。
  • 目的不一样 生产者消费者:解决生产速度和消费速度不匹配的问题,平衡产销节奏。 读者写者:在保证数据安全的前提下,最大化读操作的并发效率。
  • 读者写者问题如何理解

    核心思路:用两个锁 + 读者计数器

  • count_lock:保护 reader_count(记录当前正在读的读者数量),读者修改这个计数器必须先抢这把锁。
  • writer_lock:写锁,写者要修改数据前抢占这把锁;第一个进来的读者会抢占 writer_lock,最后一个离开的读者释放 writer_lock。
  • 流程:

    • 读者:先拿 count_lock,读者数 + 1;如果是第一个读者,就去拿写锁;然后释放 count_lock,开始读。读完后,再次拿 count_lock,读者数 – 1;如果是最后一个读者,释放写锁。
    • 写者:直接申请 writer_lock,拿到之后独占资源写,写完释放锁。

    效果: ✅ 多个读者可以同时读; ✅ 只要有读者在读,写者就拿不到写锁; ✅ 写者写的时候,新读者、新写者都被阻塞。

    下面是一段伪代码,帮助我们理解读者写者的逻辑。

    公共部分

    uint32_t reader_count = 0;
    lock_t count_lock;
    lock_t writer_lock;

    Reader

    // 加锁
    lock(count_lock);
    if (reader_count == 0)
    lock(writer_lock);
    ++reader_count;
    unlock(count_lock);

    // read;

    // 解锁
    lock(count_lock);
    —reader_count;
    if (reader_count == 0)
    unlock(writer_lock);
    unlock(count_lock);

    Writer

    lock(writer_lock);

    // write

    unlock(writer_lock);

    读写锁

    在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高得多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢?

    有,那就是读写锁。

    image.png

    • 注意:写独占,读共享,读锁优先级高。
    读写锁接口
    设置读写优先

    int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

    /*
    pref 共有 3 种选择

    PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况

    PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
    PTHREAD_RWLOCK_PREFER_READER_NP 一致

    PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
    */

    初始化

    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);

    读写锁案例

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    #include <vector>
    #include <cstdlib>
    #include <ctime>

    // 共享资源
    int shared_data = 0;

    // 读写锁
    pthread_rwlock_t rwlock;

    // 读者线程函数
    void *Reader(void *arg)
    {
    // sleep(1); // 读者优先,一旦读者进入 && 读者很多,写者基本就很难进入了
    int number = *(int *)arg;
    while (true)
    {
    pthread_rwlock_rdlock(&rwlock); // 读者加锁
    std::cout << "读者-" << number << " 正在读取数据, 数据是: "
    << shared_data << std::endl;
    sleep(1); // 模拟读取操作
    pthread_rwlock_unlock(&rwlock); // 解锁
    }
    delete (int *)arg;
    return NULL;
    }

    // 写者线程函数
    void *Writer(void *arg)
    {
    int number = *(int *)arg;
    while (true)
    {
    pthread_rwlock_wrlock(&rwlock); // 写者加锁
    shared_data = rand() % 100; // 修改共享数据
    std::cout << "写者- " << number << " 正在写入. 新的数据是: "
    << shared_data << std::endl;
    sleep(2); // 模拟写入操作
    pthread_rwlock_unlock(&rwlock); // 解锁
    }
    delete (int *)arg;
    return NULL;
    }

    int main()
    {
    srand(time(nullptr) ^ getpid());
    pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁

    // 可以更高读写数量配比,观察现象
    const int reader_num = 2;
    const int writer_num = 2;
    const int total = reader_num + writer_num;
    pthread_t threads[total]; // 假设读者和写者数量相等

    // 创建读者线程
    for (int i = 0; i < reader_num; ++i)
    {
    int *id = new int(i);
    pthread_create(&threads[i], NULL, Reader, id);
    }

    // 创建写者线程
    for (int i = reader_num; i < total; ++i)
    {
    int *id = new int(i – reader_num);
    pthread_create(&threads[i], NULL, Writer, id);
    }

    // 等待所有线程完成
    for (int i = 0; i < total; ++i)
    {
    pthread_join(threads[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock); // 销毁读写锁

    return 0;
    }

    Makefile

    reader_writer_lock_test:reader_writer_lock_test.cc
    g++ -o $@ $^ -lpthread
    .PHONY:clean
    clean:
    rm -f reader_writer_lock_test

    部分运行效果

    $ ./reader_writer_lock_test
    写者- 0 正在写入. 新的数据是: 82
    读者-0 正在读取数据, 数据是: 82
    写者- 0 正在写入. 新的数据是: 32
    读者-0 正在读取数据, 数据是: 32
    写者- 0 正在写入. 新的数据是: 30
    读者-0 正在读取数据, 数据是: 30
    写者- 0 正在写入. 新的数据是: 27
    读者-0 正在读取数据, 数据是: 27
    写者- 0 正在写入. 新的数据是: 43
    读者-0 正在读取数据, 数据是: 43
    写者- 0 正在写入. 新的数据是: 47

    读者优先(Reader-Preference)

    在这种策略中,系统会尽可能多地允许多个读者同时访问资源(比如共享文件或数据),而不会优先考虑写者。这意味着当有读者正在读取时,新到达的读者会立即被允许进入读取区,而写者则会被阻塞,直到所有读者都离开读取区。读者优先策略可能会导致写者饥饿(即写者长时间无法获得写入权限),特别是当读者频繁到达时。

    写者优先(Writer-Preference)

    在这种策略中,系统会优先考虑写者。当写者请求写入权限时,系统会尽快地让写者进入写入区,但仍需等待当前读者完成读取。这通常意味着一旦有写者到达,所有后续的读者都会被阻塞,直到写者完成写入并离开写入区。写者优先策略可以减少写者等待的时间,但可能会导致读者饥饿(即读者长时间无法获得读取权限),特别是当写者频繁到达时。

    赞(0)
    未经允许不得转载:171主机测评 » 第18讲:读者写者问题与读写锁
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址