【操作系统】读者-写者问题

问题描述

        读者-写者问题是并发编程中的一个经典问题,描述了一个场景,其中多个读者和多个写者共享一个资源(如文件或数据库)。问题的核心是如何协调读者和写者的访问,以确保:

  1. 多个读者可以同时读取资源,但不允许写者访问。

  2. 写者独占资源,即写者访问时,不允许其他读者或写者访问。

问题的三种变体

  1. 第一种变体:优先考虑读者,允许读者尽可能多地访问资源,但写者可能会饿死(即长时间等待)。

  2. 第二种变体:优先考虑写者,写者一旦准备好就可以立即访问资源,但读者可能会饿死。

  3. 第三种变体:读者和写者都应公平地访问资源,避免饿死。

解决方案

        以下是一个基于POSIX信号量的解决方案,优先考虑读者(第一种变体)。

 伪代码

 读者逻辑(Reader)

初始化信号量:
    mutex = 1  // 用于互斥访问读者计数器
    wrt = 1    // 用于写者访问
    rc = 0     // 读者计数器

读者线程:
    while (true) {
        // 想要读
        P(mutex)  // 进入临界区
        rc++     // 增加读者计数
        if (rc == 1) {
            P(wrt)  // 阻止写者访问
        }
        V(mutex)  // 离开临界区

        // 正在读
        打印 "Reader  is reading"
        等待一段时间 (模拟读取时间)

        // 读完
        P(mutex)  // 进入临界区
        rc--     // 减少读者计数
        if (rc == 0) {
            V(wrt)  // 允许写者访问
        }
        V(mutex)  // 离开临界区

        等待一段时间 (模拟读者等待时间)
    }

写者逻辑(Writer)

初始化信号量:
    wrt = 1  // 用于写者访问

写者线程:
    while (true) {
        // 想要写
        P(wrt)  // 阻止读者和其他写者访问
        打印 "Writer  is writing"
        等待一段时间 (模拟写入时间)
        V(wrt)  // 允许读者和其他写者访问

        等待一段时间 (模拟写者等待时间)
    }

伪代码说明

  1. 信号量操作

    • P(mutex)V(mutex):分别表示对互斥信号量mutex的P操作(等待)和V操作(释放)。

    • P(wrt)V(wrt):分别表示对写者信号量wrt的P操作(等待)和V操作(释放)。

  2. 读者逻辑

    • 读者进入时,增加读者计数器rc

    • 如果是第一个读者,阻塞写者访问。

    • 读者离开时,减少读者计数器rc

    • 如果是最后一个读者,允许写者访问。

  3. 写者逻辑

    • 写者进入时,阻塞读者和其他写者访问。

    • 写者离开时,允许读者和其他写者访问。

伪代码中的符号

  • P(semaphore):表示对信号量semaphore进行P操作(等待)。

  • V(semaphore):表示对信号量semaphore进行V操作(释放)。

  • rc:读者计数器。

  • mutex:互斥信号量,用于保护读者计数器rc

  • wrt:写者信号量,用于控制写者对资源的访问。

 示例代码

 1. 读者代码(reader.c

#include 
#include 
#include 
#include 
#include 

// 信号量定义
sem_t mutex;       // 用于互斥访问读者计数器
sem_t wrt;         // 用于写者访问
int rc = 0;        // 读者计数器

void* reader(void* arg) {
    while (1) {
        // 想要读
        sem_wait(&mutex); // 进入临界区
        rc++;            // 增加读者计数
        if (rc == 1) {   // 如果是第一个读者
            sem_wait(&wrt); // 阻止写者访问
        }
        sem_post(&mutex); // 离开临界区

        // 正在读
        printf("Reader %ld is reading\n", (long)arg);
        sleep(1); // 模拟读取时间

        // 读完
        sem_wait(&mutex); // 进入临界区
        rc--;            // 减少读者计数
        if (rc == 0) {   // 如果是最后一个读者
            sem_post(&wrt); // 允许写者访问
        }
        sem_post(&mutex); // 离开临界区

        sleep(1); // 模拟读者等待时间
    }
}

int main() {
    pthread_t reader_thread1, reader_thread2;

    // 初始化信号量
    sem_init(&mutex, 0, 1);
    sem_init(&wrt, 0, 1);

    // 创建读者线程
    pthread_create(&reader_thread1, NULL, reader, (void*)1);
    pthread_create(&reader_thread2, NULL, reader, (void*)2);

    // 等待线程结束
    pthread_join(reader_thread1, NULL);
    pthread_join(reader_thread2, NULL);

    // 清理信号量
    sem_destroy(&mutex);
    sem_destroy(&wrt);

    return 0;
}

 2. 写者代码(writer.c

#include 
#include 
#include 
#include 
#include 

// 信号量定义
sem_t wrt;         // 用于写者访问

void* writer(void* arg) {
    while (1) {
        // 想要写
        sem_wait(&wrt); // 阻止读者和其他写者访问
        printf("Writer %ld is writing\n", (long)arg);
        sleep(1); // 模拟写入时间
        sem_post(&wrt); // 允许读者和其他写者访问

        sleep(1); // 模拟写者等待时间
    }
}

int main() {
    pthread_t writer_thread1, writer_thread2;

    // 初始化信号量
    sem_init(&wrt, 0, 1);

    // 创建写者线程
    pthread_create(&writer_thread1, NULL, writer, (void*)1);
    pthread_create(&writer_thread2, NULL, writer, (void*)2);

    // 等待线程结束
    pthread_join(writer_thread1, NULL);
    pthread_join(writer_thread2, NULL);

    // 清理信号量
    sem_destroy(&wrt);

    return 0;
}

小结

        第一种变体的实现优先考虑读者,允许多个读者同时访问资源,但写者一旦开始写入,会阻塞所有读者和其他写者。这种策略适合读操作频繁的场景,但可能会导致写者饿死。如果需要更公平的策略,可以调整信号量的逻辑,例如引入额外的信号量来控制写者的优先级。所以可以采用第三种变体,读者和写者都应公平地访问资源,避免饿死。

第三种变体

        第三种变体:读者和写者都应公平地访问资源,避免饿死。

伪代码

读者逻辑

​信号量 mutex = 1;       // 用于互斥访问读者计数器
信号量 wrt = 1;         // 用于写者访问
信号量 can_read = 1;    // 用于控制读者的访问
信号量 can_write = 1;   // 用于控制写者的访问
整数 rc = 0;            // 读者计数器
整数 wc = 0;            // 写者计数器
while (true) {
    // 等待可以读
    P(can_read);

    // 想要读
    P(mutex);  // 进入临界区
    rc++;      // 增加读者计数
    if (rc == 1) {  // 如果是第一个读者
        P(wrt);     // 阻止写者访问
    }
    V(mutex);  // 离开临界区

    // 正在读
    打印 "Reader  is reading";
    等待一段时间 (模拟读取时间);

    // 读完
    P(mutex);  // 进入临界区
    rc--;      // 减少读者计数
    if (rc == 0) {  // 如果是最后一个读者
        V(wrt);     // 允许写者访问
    }
    V(mutex);  // 离开临界区

    // 释放读的权限
    V(can_write);  // 允许写者访问

    等待一段时间 (模拟读者等待时间);
}

写着逻辑 

信号量 wrt = 1;         // 用于写者访问
信号量 can_read = 1;    // 用于控制读者的访问
信号量 can_write = 1;   // 用于控制写者的访问
while (true) {
    // 等待可以写
    P(can_write);

    // 想要写
    P(wrt);  // 阻止读者和其他写者访问
    打印 "Writer  is writing";
    等待一段时间 (模拟写入时间);
    V(wrt);  // 允许读者和其他写者访问

    // 释放写的权限
    V(can_read);  // 允许读者访问

    等待一段时间 (模拟写者等待时间);
}

伪代码说明

  1. 信号量操作

    • P(semaphore):表示对信号量semaphore进行P操作(等待)。

    • V(semaphore):表示对信号量semaphore进行V操作(释放)。

  2. 读者逻辑

    • 读者在访问资源之前,必须等待can_read信号量。

    • 当第一个读者开始读取时,通过P(wrt)阻塞写者。

    • 当最后一个读者结束读取时,通过V(wrt)允许写者访问。

    • 读者完成读取后,释放can_write信号量,允许写者访问。

  3. 写者逻辑

    • 写者在访问资源之前,必须等待can_write信号量。

    • 写者通过P(wrt)阻塞读者和其他写者。

    • 写者完成写入后,通过V(wrt)允许读者和其他写者访问。

    • 写者完成写入后,释放can_read信号量,允许读者访问。

伪代码中的符号

  • P(semaphore):表示对信号量semaphore进行P操作(等待)。

  • V(semaphore):表示对信号量semaphore进行V操作(释放)。

  • rc:读者计数器。

  • wc:写者计数器。

  • mutex:互斥信号量,用于保护读者计数器rc

  • wrt:写者信号量,用于控制写者对资源的访问。

  • can_read:控制读者的访问。

  • can_write:控制写者的访问。

示例代码

读者代码(reader.c

#include 
#include 
#include 
#include 
#include 

// 信号量定义
sem_t mutex;       // 用于互斥访问读者计数器
sem_t wrt;         // 用于写者访问
sem_t can_read;    // 用于控制读者的访问
int rc = 0;        // 读者计数器

void* reader(void* arg) {
    while (1) {
        // 等待可以读
        sem_wait(&can_read);

        // 想要读
        sem_wait(&mutex); // 进入临界区
        rc++;            // 增加读者计数
        if (rc == 1) {   // 如果是第一个读者
            sem_wait(&wrt); // 阻止写者访问
        }
        sem_post(&mutex); // 离开临界区

        // 正在读
        printf("Reader %ld is reading\n", (long)arg);
        sleep(1); // 模拟读取时间

        // 读完
        sem_wait(&mutex); // 进入临界区
        rc--;            // 减少读者计数
        if (rc == 0) {   // 如果是最后一个读者
            sem_post(&wrt); // 允许写者访问
        }
        sem_post(&mutex); // 离开临界区

        // 释放读的权限
        sem_post(&can_read);

        sleep(1); // 模拟读者等待时间
    }
}

int main() {
    pthread_t reader_thread1, reader_thread2;

    // 初始化信号量
    sem_init(&mutex, 0, 1);
    sem_init(&wrt, 0, 1);
    sem_init(&can_read, 0, 1);

    // 创建读者线程
    pthread_create(&reader_thread1, NULL, reader, (void*)1);
    pthread_create(&reader_thread2, NULL, reader, (void*)2);

    // 等待线程结束
    pthread_join(reader_thread1, NULL);
    pthread_join(reader_thread2, NULL);

    // 清理信号量
    sem_destroy(&mutex);
    sem_destroy(&wrt);
    sem_destroy(&can_read);

    return 0;
}

写者代码(writer.c

#include 
#include 
#include 
#include 
#include 

// 信号量定义
sem_t wrt;         // 用于写者访问
sem_t can_read;    // 用于控制读者的访问

void* writer(void* arg) {
    while (1) {
        // 想要写
        sem_wait(&wrt); // 阻止读者和其他写者访问
        printf("Writer %ld is writing\n", (long)arg);
        sleep(1); // 模拟写入时间
        sem_post(&wrt); // 允许读者和其他写者访问

        // 释放读的权限
        sem_post(&can_read);

        sleep(1); // 模拟写者等待时间
    }
}

int main() {
    pthread_t writer_thread1, writer_thread2;

    // 初始化信号量
    sem_init(&wrt, 0, 1);
    sem_init(&can_read, 0, 1);

    // 创建写者线程
    pthread_create(&writer_thread1, NULL, writer, (void*)1);
    pthread_create(&writer_thread2, NULL, writer, (void*)2);

    // 等待线程结束
    pthread_join(writer_thread1, NULL);
    pthread_join(writer_thread2, NULL);

    // 清理信号量
    sem_destroy(&wrt);
    sem_destroy(&can_read);

    return 0;
}

代码说明

  1. 信号量的使用

    • mutex信号量用于保护读者计数器rc,确保读者计数器的增减操作是原子的。

    • wrt信号量用于控制写者对共享资源的访问,确保写者独占资源。

  2. 读者逻辑

    • 当第一个读者开始读取时,通过sem_wait(&wrt)阻塞写者。

    • 当最后一个读者结束读取时,通过sem_post(&wrt)允许写者访问。

    • 多个读者可以同时访问共享资源,因为rc计数器确保了写者不会在读者读取期间访问资源。

  3. 写者逻辑

    • 写者通过sem_wait(&wrt)阻塞读者和其他写者。

    • 写者完成写操作后,通过sem_post(&wrt)允许读者和其他写者访问。

小结

        通过引入can_read信号量,我们确保了读者和写者交替访问资源,避免了读者或写者饿死的问题。这种实现符合第三种变体的要求,即读者和写者都应公平地访问资源。

 

你可能感兴趣的:(Linux,C,linux)