问题描述
读者-写者问题是并发编程中的一个经典问题,描述了一个场景,其中多个读者和多个写者共享一个资源(如文件或数据库)。问题的核心是如何协调读者和写者的访问,以确保:
-
多个读者可以同时读取资源,但不允许写者访问。
-
写者独占资源,即写者访问时,不允许其他读者或写者访问。
问题的三种变体
-
第一种变体:优先考虑读者,允许读者尽可能多地访问资源,但写者可能会饿死(即长时间等待)。
-
第二种变体:优先考虑写者,写者一旦准备好就可以立即访问资源,但读者可能会饿死。
-
第三种变体:读者和写者都应公平地访问资源,避免饿死。
解决方案
以下是一个基于POSIX信号量的解决方案,优先考虑读者(第一种变体)。
伪代码
读者逻辑(Reader)
初始化信号量:mutex = 1 // 用于互斥访问读者计数器wrt = 1 // 用于写者访问rc = 0 // 读者计数器读者线程:while (true) {// 想要读P(mutex) // 进入临界区rc++ // 增加读者计数if (rc == 1) {P(wrt) // 阻止写者访问}V(mutex) // 离开临界区// 正在读打印 "Reader <reader_id> is reading"等待一段时间 (模拟读取时间)// 读完P(mutex) // 进入临界区rc-- // 减少读者计数if (rc == 0) {V(wrt) // 允许写者访问}V(mutex) // 离开临界区等待一段时间 (模拟读者等待时间)}
写者逻辑(Writer)
初始化信号量:wrt = 1 // 用于写者访问写者线程:while (true) {// 想要写P(wrt) // 阻止读者和其他写者访问打印 "Writer <writer_id> is writing"等待一段时间 (模拟写入时间)V(wrt) // 允许读者和其他写者访问等待一段时间 (模拟写者等待时间)}
伪代码说明
-
信号量操作:
-
P(mutex)
和V(mutex)
:分别表示对互斥信号量mutex
的P操作(等待)和V操作(释放)。 -
P(wrt)
和V(wrt)
:分别表示对写者信号量wrt
的P操作(等待)和V操作(释放)。
-
-
读者逻辑:
-
读者进入时,增加读者计数器
rc
。 -
如果是第一个读者,阻塞写者访问。
-
读者离开时,减少读者计数器
rc
。 -
如果是最后一个读者,允许写者访问。
-
-
写者逻辑:
-
写者进入时,阻塞读者和其他写者访问。
-
写者离开时,允许读者和其他写者访问。
-
伪代码中的符号
-
P(semaphore)
:表示对信号量semaphore
进行P操作(等待)。 -
V(semaphore)
:表示对信号量semaphore
进行V操作(释放)。 -
rc
:读者计数器。 -
mutex
:互斥信号量,用于保护读者计数器rc
。 -
wrt
:写者信号量,用于控制写者对资源的访问。
示例代码
1. 读者代码(reader.c
)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>// 信号量定义
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 <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>// 信号量定义
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 <reader_id> 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 <writer_id> is writing";等待一段时间 (模拟写入时间);V(wrt); // 允许读者和其他写者访问// 释放写的权限V(can_read); // 允许读者访问等待一段时间 (模拟写者等待时间);
}
伪代码说明
-
信号量操作:
-
P(semaphore)
:表示对信号量semaphore
进行P操作(等待)。 -
V(semaphore)
:表示对信号量semaphore
进行V操作(释放)。
-
-
读者逻辑:
-
读者在访问资源之前,必须等待
can_read
信号量。 -
当第一个读者开始读取时,通过
P(wrt)
阻塞写者。 -
当最后一个读者结束读取时,通过
V(wrt)
允许写者访问。 -
读者完成读取后,释放
can_write
信号量,允许写者访问。
-
-
写者逻辑:
-
写者在访问资源之前,必须等待
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 <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>// 信号量定义
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 <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>// 信号量定义
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;
}
代码说明
-
信号量的使用:
-
mutex
信号量用于保护读者计数器rc
,确保读者计数器的增减操作是原子的。 -
wrt
信号量用于控制写者对共享资源的访问,确保写者独占资源。
-
-
读者逻辑:
-
当第一个读者开始读取时,通过
sem_wait(&wrt)
阻塞写者。 -
当最后一个读者结束读取时,通过
sem_post(&wrt)
允许写者访问。 -
多个读者可以同时访问共享资源,因为
rc
计数器确保了写者不会在读者读取期间访问资源。
-
-
写者逻辑:
-
写者通过
sem_wait(&wrt)
阻塞读者和其他写者。 -
写者完成写操作后,通过
sem_post(&wrt)
允许读者和其他写者访问。
-
小结
通过引入can_read
信号量,我们确保了读者和写者交替访问资源,避免了读者或写者饿死的问题。这种实现符合第三种变体的要求,即读者和写者都应公平地访问资源。