目录
1 读者-写者问题原理
读者-写者问题是操作系统中经典的进程同步问题之一,它描述的是多个线程或进程对共享资源的并发访问所带来的冲突与控制问题。这个共享资源通常是主存中的某一块区域或者某个文件,允许多个进程对其进行读或写操作。
在该问题中,存在两类进程:
- 读者(Reader):只读取共享资源。
- 写者(Writer):修改共享资源的内容。
系统必须满足以下三个基本约束条件:
- 多个读者可以同时读取共享资源,不会相互干扰;
- 写者在写资源时必须独占资源,即同一时间只能有一个写者对资源进行写操作;
- 写者操作期间禁止任何读者或其他写者访问资源,以防止数据不一致或竞争条件的发生。
为了解决进程间的读写冲突,通常采用同步机制来实现两种不同的优先策略:
- 读者优先:允许读者并发读取,尽可能减少读者等待;写者必须等待读者完成后才能写入。
- 写者优先:优先保证写者能尽快获得写资源,避免写者被长时间延迟甚至“饥饿”的情况发生。
本实验通过信号量机制,分别实现这两种策略,下面对每种策略进行算法分析与说明。
2 读者优先算法描述
在读者优先策略中,允许多个读者同时访问共享资源,只要当前没有写者正在写入。当有读者在读时,新的读者也可立即开始读取;但写者必须等到所有读者完成操作后才能获得资源。这种策略有利于提高读操作的并发性,但可能导致写者长期等待,甚至饥饿。
2.1同步机制设计
- rf_filesrc:一个互斥信号量,用于控制对共享资源的访问,实现读写互斥。
- rf_rcsignal:一个互斥信号量,用于对全局变量 readercount 的保护。
- readercount:记录当前正在读的读者数量。
2.2算法逻辑
- 当一个读者线程开始时,首先通过 rf_rcsignal 加锁,安全地检查 readercount。
如果这是第一个读者(readercount == 0),则它需要获取 rf_filesrc,以阻止写者写入;然后将 readercount++;释放 rf_rcsignal。
- 执行读操作。
- 读操作完成后,再次加锁访问 readercount,将其减一;
如果这是最后一个读者(readercount == 0),释放 rf_filesrc,允许写者操作;最后释放 rf_rcsignal。
- 当写者发出写申请时,必须等待信号量rf_filesrc释放才可以申请资源,以此来实现读写互斥。
2.3伪代码实现
int readercount = 0; // 用于记录当前读者数量
seamphore rf_filesrc = 1; // 互斥信号量,用于控制对共享资源的访问,实现读写互斥
seamphore rf_rcsignal = 1; // 互斥信号量,用于对全局变量readercount的访问保护
writer()
{
while (1)
{
P(rf_filesrc); // 加锁,与读者互斥访问共享文件
写文件操作
V(rf_filesrc); // 释放共享文件
}
}
reader()
{
while (1)
{
P(rf_rcsignal); // 实现对全局变量访问的原子性
if (readercount == 0)
P(rf_filesrc); // 如果是第一个读者,则申请共享资源,加锁阻止写者访问
readercount++;
V(rf_rcsignal);
读文件操作
P(rf_rcsignal); // 实现对全局变量访问的原子性
readercount--;
if (readercount == 0)
V(rf_filesrc); // 如果是最后一个读者,则释放共享资源,允许写者访问
V(rf_rcsignal);
}
}
效果说明:
该机制确保了只要有一个读者在读,写者将被阻塞,后续读者可以继续进入,形成“读者优先”的策略。但在读者频繁出现的场景下,写者可能长期得不到写机会,出现写者饥饿现象。
3 写者优先算法描述
写者优先策略的目标是让写者在有读写冲突时具有更高的优先权,以避免写者长期被延迟。为此,需要在读者到来前检查是否存在正在等待的写者,如果存在,读者必须等待所有写者完成操作后才能继续。
3.1同步机制设计0
- wf_filesrc:一个互斥信号量,控制对共享资源的访问,实现读写互斥。
- wf_rcsignal、wf_wcsignal:用于保护 readercount 和 writercount 两个共享变量的互斥信号量。
- readercount:记录当前读者数量。
- writercount:记录当前写者数量。
- read_s:用于协调读者是否可以继续读取的信号量,实现写者对读者的阻塞功能。
3.2算法逻辑
写者线程:
- 写者线程启动后,先通过 wf_wcsignal 加锁;
- 若是第一个写者(writercount == 0),它会阻塞 read_s,防止新到的读者读操作;
- 然后将 writercount++;
- 释放 wf_wcsignal。
- 请求 wf_filesrc,进行写操作;
- 写操作完成后,再次加锁;
- 将 writercount--;
- 如果这是最后一个写者(writercount == 0),则释放 read_s,允许读者进入;
- 最后释放互斥信号量。
读者线程:
- 读者线程首先请求 read_s(可能被前序写者阻塞);
- 然后请求 wf_rcsignal 访问 readercount;
- 若是第一个读者(readercount == 0),请求 wf_filesrc;
- 将 readercount++;
- 释放互斥信号量;
- 执行读操作;
- 操作完成后,加锁并将 readercount--;
- 如果这是最后一个读者,则释放 wf_filesrc;
- 最后释放互斥信号量;
- 最后释放 read_s,允许其他读者进入。
3.3伪代码实现
int readercount = 0; // 当前读者数量
int writercount = 0; // 当前写者数量
semaphore wf_filesrc = 1; // 控制对共享资源的访问,实现读写互斥
semaphore wf_rcsignal = 1; // 保护 readercount 的互斥信号量
semaphore wf_wcsignal = 1; // 保护 writercount 的互斥信号量
semaphore read_s = 1; // 写者优先时用于阻止读者进入的信号量
writer() {
while (1) {
P(wf_wcsignal); // 对 writercount 加锁
if (writercount == 0)
P(read_s); // 第一个写者阻止新的读者进入
writercount++;
V(wf_wcsignal); // 释放对 writercount 的锁
P(wf_filesrc); // 获取资源锁,写操作开始
写文件操作
V(wf_filesrc); // 写操作完成,释放资源锁
P(wf_wcsignal); // 对 writercount 加锁
writercount--;
if (writercount == 0)
V(read_s); // 最后一个写者释放对读者的阻止
V(wf_wcsignal); // 释放对 writercount 的锁
}
}
reader() {
while (1) {
P(read_s); // 等待写者允许读者进入(可能阻塞)
P(wf_rcsignal); // 对 readercount 加锁
if (readercount == 0)
P(wf_filesrc); // 第一个读者获取资源锁,防止写者写入
readercount++;
V(wf_rcsignal); // 释放对 readercount 的锁
V(read_s); // 允许其他读者尝试进入
读文件操作
P(wf_rcsignal); // 对 readercount 加锁
readercount--;
if (readercount == 0)
V(wf_filesrc); // 最后一个读者释放资源锁,写者可写
V(wf_rcsignal); // 释放对 readercount 的锁
}
}
效果说明:
通过 read_s 信号量的控制,写者在到来时能够有效阻止后续读者插队,从而保障写操作优先完成。该策略能有效防止写者饥饿,但在写者频繁的场景下,读者可能被延迟甚至长期阻塞。
4 测试用例及运行结果
测试用例 1:写者优先验证如表1所示。
表1 写者优先测试用例
编号 | 类型 | 延迟 (s) | 执行时长 (s) |
1 | R | 0 | 2 |
2 | R | 0.5 | 2 |
3 | W | 1 | 3 |
4 | R | 1.5 | 2 |
5 | W | 1.9 | 2 |
Reader #1 和 Reader #2 能顺利并发开始读取数据,因为在它们开始时没有写者等待。
Writer #3 在 Reader #1 和 #2 正在读时到达,必须等待读者完成。但到达之后将阻止新读者进入。
Reader #4 在 Writer #3 到达之后启动,即使 Reader #1 和 #2 已经完成,它也会被 Writer #3 阻塞,直到所有写者完成。
Writer #5 到达时 Writer #3 尚未开始执行,但由于写者优先,它将排在 Writer #3 之后执行,而 Reader #4 仍然被阻塞。
Reader #4 只有在 Writer #3 和 Writer #5 执行完毕之后才能进入读取阶段。
图1 写者优先算法运行状态
算法执行结果如图2所示。
图2 写者优先测试结果
写者到来后会阻止新读者进入(即使系统中暂时没有写者在操作,也不会让新读者插入);写者按顺序执行,避免了写者饥饿;读者必须等待所有排队写者完成后才能进入,可能造成读者延迟或饥饿;同时启动的读者仍然可以并发进行读取操作。
测试用例 2:读者优先验证如表2所示。
表2 读者优先测试用例
编号 | 类型 | 延迟 (s) | 执行时长 (s) |
1 | R | 0 | 2 |
2 | R | 0.5 | 2 |
3 | W | 1 | 3 |
4 | R | 1.5 | 2 |
5 | W | 1.9 | 2 |
Reader #1 和 Reader #2 可以顺利并发地开始读取数据,因为在它们开始时没有写者等待。
Writer #3 在 Reader #1 和 #2 正在读时到达,但由于读者优先,写者会被推迟,必须等待所有读者完成后才能开始写操作。
Reader #4 在 Writer #3 到达时可以继续读取,因为读者优先,新的读者不受写者影响,可以继续进行读取操作,直到最后一个读者完成。
Writer #5 在 Reader #4 正在读取时到达,由于所有读者优先,Writer #5 需要等待所有的读者完成之后才能开始写操作。
读者优先意味着即使有写者在等待,新的读者也会继续进来读取数据;写者必须等待读者全部完成后才能开始写操作,可能导致写者延迟;读者饥饿问题可以避免,所有读者在写者出现前都有平等机会读取;由于读者优先,即使在某些情况下写者到达,读者依然能够继续并发读取,直到所有读者完成,才会开始写操作。
图3 读者优先算法运行状态
算法执行结果如图4所示。
图4 读者优先测试结果
5 程序源代码(C++实现):
#include <iostream>
#include <conio.h>
#include <stdio.h>
#include <Windows.h>
using namespace std;
#define INTE_PER_SEC 1000 // 每秒时钟中断数
#define MAX_THREAD_NUM 64 // 最大线程数量
// 全局变量
int readercount = 0; // 当前读者数
int writercount = 0; // 当前写者数
// 信号量定义
HANDLE rf_rcsignal; // 读者互斥信号量(读者优先)
HANDLE wf_rcsignal; // 读者互斥信号量(写者优先)
HANDLE wf_wcsignal; // 写者互斥信号量
HANDLE rf_filesrc; // 读写互斥信号量(读者优先)
HANDLE wf_filesrc; // 读写互斥信号量(写者优先)
HANDLE read_s; // 写者优先控制信号量
// 线程信息结构体
struct thread_info {
int id; // 线程编号
char type; // 线程类型(R: 读者, W: 写者)
double delay; // 延迟时间(秒)
double lastTime; // 操作持续时间(秒)
};
//=========================//
// 读者优先 - 读者线程
//=========================//
void rp_threadReader(void* p) {
int id = ((thread_info*)p)->id;
DWORD delay = DWORD(((thread_info*)p)->delay * INTE_PER_SEC);
DWORD duration = DWORD(((thread_info*)p)->lastTime * INTE_PER_SEC);
Sleep(delay);
cout << "[读者 #" << id << "] 正在请求读取..." << endl;
WaitForSingleObject(rf_rcsignal, INFINITE);
if (readercount == 0)
WaitForSingleObject(rf_filesrc, INFINITE);
readercount++;
ReleaseSemaphore(rf_rcsignal, 1, NULL);
cout << "[读者 #" << id << "] 开始读取数据..." << endl;
Sleep(duration);
cout << "[读者 #" << id << "] 完成读取。" << endl;
WaitForSingleObject(rf_rcsignal, INFINITE);
readercount--;
ReleaseSemaphore(rf_rcsignal, 1, NULL);
if (readercount == 0)
ReleaseSemaphore(rf_filesrc, 1, NULL);
}
//=========================//
// 读者优先 - 写者线程
//=========================//
void rp_threadWriter(void* p) {
int id = ((thread_info*)p)->id;
DWORD delay = DWORD(((thread_info*)p)->delay * INTE_PER_SEC);
DWORD duration = DWORD(((thread_info*)p)->lastTime * INTE_PER_SEC);
Sleep(delay);
cout << "【写者 #" << id << "】 正在请求写入..." << endl;
WaitForSingleObject(rf_filesrc, INFINITE);
cout << "【写者 #" << id << "】 开始写入数据..." << endl;
Sleep(duration);
cout << "【写者 #" << id << "】 写入完成。" << endl;
ReleaseSemaphore(rf_filesrc, 1, NULL);
}
//=============================//
// 读者优先 - 启动逻辑
//=============================//
void ReaderPriority() {
DWORD n_thread = 0;
DWORD thread_ID;
HANDLE h_Thread[MAX_THREAD_NUM];
thread_info threads[MAX_THREAD_NUM];
readercount = 0;
// 创建信号量
rf_rcsignal = CreateSemaphore(NULL, 1, 1, NULL);
rf_filesrc = CreateSemaphore(NULL, 1, 1, NULL);
cout << "\n===== 模式:读者优先 =====" << endl;
cout << "请输入线程数量: ";
int count;
cin >> count;
cout << "请依次输入:线程编号 线程类型(R/W) 延迟时间(s) 操作时间(s)" << endl;
for (int i = 0; i < count; i++) {
int id;
char type;
double delay, last;
cin >> id >> type >> delay >> last;
threads[n_thread].id = id;
threads[n_thread].type = type;
threads[n_thread].delay = delay;
threads[n_thread].lastTime = last;
n_thread++;
}
for (int i = 0; i < (int)(n_thread); i++)
{
// 如果是读线程
if (threads[i].type == 'R')
{
// 创建读者线程
h_Thread[i] = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)rp_threadReader,
&threads[i], 0, &thread_ID);
}
else
{
// 创建写线程
h_Thread[i] = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)rp_threadWriter,
&threads[i], 0, &thread_ID);
}
}
WaitForMultipleObjects(n_thread, h_Thread, TRUE, INFINITE);
cout << "\n>>> 所有线程已完成操作(读者优先模式)。" << endl;
for (int i = 0; i < (int)n_thread; i++) CloseHandle(h_Thread[i]);
CloseHandle(rf_rcsignal);
CloseHandle(rf_filesrc);
}
//=========================//
// 写者优先 - 读者线程
//=========================//
void wp_threadReader(void* p) {
int id = ((thread_info*)p)->id;
DWORD delay = DWORD(((thread_info*)p)->delay * INTE_PER_SEC);
DWORD duration = DWORD(((thread_info*)p)->lastTime * INTE_PER_SEC);
Sleep(delay);
cout << "[读者 #" << id << "] 正在请求读取..." << endl;
WaitForSingleObject(read_s, INFINITE);
WaitForSingleObject(wf_rcsignal, INFINITE);
if (readercount == 0)
WaitForSingleObject(wf_filesrc, INFINITE);
readercount++;
ReleaseSemaphore(wf_rcsignal, 1, NULL);
ReleaseSemaphore(read_s, 1, NULL);
cout << "[读者 #" << id << "] 开始读取数据..." << endl;
Sleep(duration);
cout << "[读者 #" << id << "] 完成读取。" << endl;
WaitForSingleObject(wf_rcsignal, INFINITE);
readercount--;
ReleaseSemaphore(wf_rcsignal, 1, NULL);
if (readercount == 0)
ReleaseSemaphore(wf_filesrc, 1, NULL);
}
//=========================//
// 写者优先 - 写者线程
//=========================//
void wp_threadWriter(void* p) {
int id = ((thread_info*)p)->id;
DWORD delay = DWORD(((thread_info*)p)->delay * INTE_PER_SEC);
DWORD duration = DWORD(((thread_info*)p)->lastTime * INTE_PER_SEC);
Sleep(delay);
cout << "【写者 #" << id << "】 正在请求写入..." << endl;
WaitForSingleObject(wf_wcsignal, INFINITE);
if (writercount == 0)
WaitForSingleObject(read_s, INFINITE);
writercount++;
ReleaseSemaphore(wf_wcsignal, 1, NULL);
WaitForSingleObject(wf_filesrc, INFINITE);
cout << "【写者 #" << id << "】 开始写入数据..." << endl;
Sleep(duration);
cout << "【写者 #" << id << "】 写入完成。" << endl;
ReleaseSemaphore(wf_filesrc, 1, NULL);
WaitForSingleObject(wf_wcsignal, INFINITE);
writercount--;
ReleaseSemaphore(wf_wcsignal, 1, NULL);
if (writercount == 0)
ReleaseSemaphore(read_s, 1, NULL);
}
//=============================//
// 写者优先 - 启动逻辑
//=============================//
void WriterPriority() {
DWORD n_thread = 0;
DWORD thread_ID;
HANDLE h_Thread[MAX_THREAD_NUM];
thread_info threads[MAX_THREAD_NUM];
readercount = 0;
writercount = 0;
// 创建信号量
wf_rcsignal = CreateSemaphore(NULL, 1, 1, NULL);
wf_wcsignal = CreateSemaphore(NULL, 1, 1, NULL);
wf_filesrc = CreateSemaphore(NULL, 1, 1, NULL);
read_s = CreateSemaphore(NULL, 1, 1, NULL);
cout << "\n===== 模式:写者优先 =====" << endl;
cout << "请输入线程数量: ";
int count;
cin >> count;
cout << "请依次输入:线程编号 线程类型(R/W) 延迟时间(s) 操作时间(s)" << endl;
for (int i = 0; i < count; i++) {
int id;
char type;
double delay, last;
cin >> id >> type >> delay >> last;
threads[n_thread].id = id;
threads[n_thread].type = type;
threads[n_thread].delay = delay;
threads[n_thread].lastTime = last;
n_thread++;
}
for (int i = 0; i < (int)(n_thread); i++)
{
// 如果是读线程
if (threads[i].type == 'R')
{
// 创建读者线程
h_Thread[i] = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)wp_threadReader,
&threads[i], 0, &thread_ID);
}
else
{
// 创建写线程
h_Thread[i] = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)wp_threadWriter,
&threads[i], 0, &thread_ID);
}
}
WaitForMultipleObjects(n_thread, h_Thread, TRUE, INFINITE);
cout << "\n>>> 所有线程已完成操作(写者优先模式)。" << endl;
for (int i = 0; i < (int)n_thread; i++) CloseHandle(h_Thread[i]);
CloseHandle(wf_rcsignal);
CloseHandle(wf_wcsignal);
CloseHandle(wf_filesrc);
CloseHandle(read_s);
}
//=========================//
// 主函数
//=========================//
int main() {
char choice;
while (true) {
cout << "\n======= 读者-写者问题模拟 =======" << endl;
cout << "1. 读者优先模式" << endl;
cout << "2. 写者优先模式" << endl;
cout << "3. 退出程序" << endl;
cout << "请输入选项(1-3):";
do {
choice = _getch();
} while (choice != '1' && choice != '2' && choice != '3');
system("cls");
if (choice == '1') {
ReaderPriority();
system("pause");
}
else if (choice == '2') {
WriterPriority();
system("pause");
}
else {
break;
}
}
return 0;
}