OS-读者写者实验
环境
语言标准:/std:c++17,/std:c11
基本逻辑
1.Writer与其他所有Reader、Writer互斥
2.Reader与Writer互斥,但与其他Reader不互斥
类设计
设计Reader、Writer类,包含属性、读写方法、其他方法等。
Myclass.h
参与者的基类
class Myclass
{
public:
string name;
virtual void DoSth() = 0;
virtual void Func() = 0;
virtual void End() = 0;
};
Reader.h
读者类
class Reader : public Myclass
{
public:
Reader()
{
name = "";
}
Reader(string s)
{
name = s;
}
void DoSth()
{
cout << name << " is doing something interesting." << "\n";
}
void Func()
{
cout << name << " is reading now..." << "\n";
}
void End()
{
cout << name << " has left" << "\n";
}
}readers[5];
Writer.h
写者类
class Writer : public Myclass
{
public:
Writer()
{
name = "";
}
Writer(string s)
{
name = s;
}
void DoSth()
{
cout << name << " is doing something interesting." << "\n";
}
void Func()
{
cout << name << " is writing something, please wait for a while..." << "\n";
}
void End()
{
cout << name << " has left" << "\n";
}
}writers[5];
PV操作
PV.h
P
void P(void* S) //P操作,传入的参数是信号量
{
WaitForSingleObject(S, INFINITE);
}
Windows API函数WaitForSingleObject,即等待线程结束,信号量S表示要等待的线程,即HANDLE。
参数dwMilliseconds是等待时间,为保证模拟真实,此处设定为INFINITE,即读者或写者会无限地等待下去。
V
void V(void* S) //V操作,传入的参数是信号量
{
ReleaseSemaphore(S, 1, NULL);
}
传入目标线程的HANDLE,调用Windows API函数ReleaseSemaphore,这一函数通过句柄释放资源。
参数1 hSemaphore为目标线程句柄。
参数2 IReleaseCount为要增加信号量的数量,增加1,表示当前互斥量未被占用,与互斥量被创建时设置的初始信号量相同,表示资源未被占用。
参数3 lpPreviousCount返回前一次信号量,此处不需要,设为NULL即可。
互斥量
void* mutex = CreateSemaphoreW(NULL, 1, 1, NULL); //防止Count系数同时被多个Reader访问,保证Count数值正确,当mutex信号占用,当前的reader应当等待
void* rw = CreateSemaphoreW(NULL, 1, 1, NULL); //保证读写互斥,当rw信号占用,当前的reader和writer都必须等待
void* w = CreateSemaphoreW(NULL, 1, 1, NULL); //保证写优先,当w信号占用,当前的Reader应当等待
Windows API函数CreateSemephoreW,创建信号量。
参数1 IpSemaphoreAttributes是信号量的安全属性,此处设为NULL即可。
参数2 IInitialCount是初始化信号量,此处设为1,后续V操作也应当将信号量回归这一初始值,表示资源的释放。
参数3 IMaximumCount是信号量允许增加到的最大值。
参数4 IpName为信号量名称,设为NULL。
注意:其中Count系数用于记录当前程序中读者的数量,为保证每个读者获取或改变Count数值时是都是该时刻的真实值,应该保证Count系数被互斥访问。
Reader方法
是读者线程的线程函数。
读者线程函数包含三个部分:Beginning of Read,Read,End of Read
Beginning of Read:首先等待互斥量w、mutex,等待写者结束写以及其他读者结束对Count的操作。如果该读者是当前程序中唯一的读者,则占用互斥量rw,保证读写互斥。将主线程Sleep0.1秒,模拟Beginning of Read的过程。在这一过程中,程序中有且仅有当前读者占用了互斥量,即处在运行状态,其余线程应当均处在等待状态。更新Count数值,释放互斥量w、mutex,Beginning of Read结束。Beginning of Read过程与其余处在非Read状态的Reader线程以及Writer线程互斥。
Read:模拟读者阅读过程,调用当前线程读者对象的功能方法,将主线程Sleep1秒,模拟阅读的过程。在这一过程中,当前程序中的互斥量被释放,可能存在多个读者并行。Read过程与写者线程互斥,但与其他读者不互斥。
End of Read:首先等待互斥量mutex,等待其他读者结束对Count的操作。如果当前读者是程序中最后一位读者,那么释放互斥量rw,允许写者操作,更新count数值。模拟读者离开过程,将主线程Sleep0.1秒,模拟End of Read的过程。在这一过程中,程序中有且仅有当前读者占用了互斥量,即处在运行状态,其余线程应当均处在等待状态。释放mutex,End of Read结束。End of Read过程与处在非Read状态的Reader线程以及Writer线程互斥。
unsigned long _stdcall Readers(int i)
{
while (true)
{
//Beginning of Read
P(w); //等待写进程结束,保证writer优先
P(mutex); //等待其他Reader处理Count系数,保证Count数值正确
Sleep(100);
if (count == 0) //仅当前第一个Reader用于增加阻塞Writer的信号
{
P(rw);
}
count++; //当前Reader数量+1
V(mutex); //Beginning of Read结束,释放mutex,count数值可以由下一个reader操作
V(w); //释放w,即使此处释放了w,信号rw都必然存在,保证read过程中没有write出现。
//Reading
readers[i].Func(); //执行读者功能
//Reader::f();
//Reader::DoSth(); //读者其他功能
Sleep(1000); //Wait for 1 second,体现reader的read过程
//End of Read
P(mutex); //等待其他Reader处理Count系数,保证Count数值正确
readers[i].End();
Sleep(100);
count--; //读者数量-1
if (count == 0) //如果此时没有Reader,那么释放rw,允许Writer执行写者功能
{
V(rw);
}
V(mutex); //End of Read结束,释放count
}
return (unsigned long)0;
}
Writer方法
是写者线程的线程函数。
写者线程函数包含三个部分:Beginning of Write,Write,End of Write
Beginning of Write:首先等待互斥量w、rw,保证写者间互斥、读写互斥。
Write:占用资源后,执行写者功能,将主线程Sleep1秒,模拟写的过程。
End of Write:释放互斥量w、rw,将主线程Sleep0.1秒,模拟写者离开的过程。
易得在写过程中,写线程全程与其余所有非主线程互斥。
unsigned long _stdcall Writers(int i)
{
while (true)
{
//Beginning of Write
P(w); //等待其他写者
P(rw); //等待当前所有线程结束
//Writing
writers[i].Func(); //执行写者功能
//Writer::f();
//Writer::DoSth(); //写者其他功能
Sleep(1000); //wait for 1 second,体现writer的write过程
//End of Write
writers[i].End();
Sleep(100);
V(w); //释放w,完成写
V(rw); //释放rw
}
return (unsigned long)0;
}
实验设计
创建对象
分别创建5个Reader、Writer对象。
for (int i = 0; i < 5; i++) //自定义五个reader writer
{
string s = "Reader " + to_string(i + 1);
readers[i] = { s };
s = "Writer " + to_string(i + 1);
writers[i] = { s };
}
创建线程
为五个Reader、Writer对象创建线程
#define LSR LPTHREAD_START_ROUTINE
for (int i = 0; i < 5; i++) //为五个reader writer创建线程
{
reader[i] = CreateThread(NULL, 0, (LSR)Readers, (void*)i, 0, NULL);
}
for (int i = 0; i < 5; i++)
{
writer[i] = CreateThread(NULL, 0, (LSR)Writers, (void*)i, 0, NULL);
}
Windows API函数CreateThread,创建一个在调用进程的虚拟地址空间内执行的线程。
参数1 lpThreadAttributes 是线程安全属性,设为NULL,默认线程获取安全描述符,子进程将无法继承返回的句柄。
参数2 dwStackSize 指堆栈初始大小,设为0,则新线程使用可执行文件的默认大小。
参数3 lpStartAddress 是指向线程函数的指针,类型为LPTHREAD_START_ROUTINE。
参数4 IpParameter 是指向传递给线程函数的参数的指针。
参数5 dwCreationFlags 控制线程创建的数值,设为0,则该线程在创建后立即执行。若设置为CREATE_SUSPENDED(#define CREATE_SUSPENDED 0x00000004),则线程在执行ResumeThread函数前不会执行。
根据参数5可以重新设计创建线程的过程,线程的加入与否更加可控:
for (int i = 0; i < 5; i++) //为五个reader writer创建线程
{
reader[i] = CreateThread(NULL, 0, (LSR)Readers, (void*)i, CREATE_SUSPENDED, NULL);
}
for (int i = 0; i < 5; i++)
{
writer[i] = CreateThread(NULL, 0, (LSR)Writers, (void*)i, CREATE_SUSPENDED, NULL);
}
for (int i = 0; i < 5; i++) //激活线程
{
ResumeThread(reader[i]);
ResumeThread(writer[i]);
}
参数6 IpThreadId 指向线程标识符变量的指针,设为NULL则不返回线程标识符。
线程并发
while (true) //线程并发运行
{
for (int i = 0; i < 5; i++) //并发五个reader、writer
{
WaitForSingleObject(reader[i], INFINITE); //相当于p操作
WaitForSingleObject(writer[i], INFINITE);
}
}
预期结果
根据线程函数中的分析设计:Reader方法中,在beginning与end过程中reader线程与其余线程互斥,互斥过程主线程Sleep0.1秒,在read过程中reader间不互斥,read过程主线程Sleep1秒。Write方法中,writer全程与其余线程互斥,互斥过程主线程Sleep0.1秒,write过程中主线程Sleep1秒,write结束时主线程Sleep0.1秒。
那么实验预期结果应该符合以下情况:Writer与其余线程互斥,应当逐个执行,每个writer占用主线程一秒,根据写者优先,只有在一个writer结束后才会有其他线程。Reader在beginning过程中逐个执行,每个reader占用主线程0.1秒。read过程占用时间为1秒,这一过程中reader间不互斥,则可能出现多个reader同时执行,现象是:多个reader每隔0.1秒分别执行beginning,然后同时出现并占用1秒时间执行read。Reader在End过程中逐个执行,每个reader占用主线程0.1秒。且writer一定在所有reader和writer都执行end之后才执行。
整理得到:
1.读写互斥:只有reader全部结束才有writer的执行,writer未结束前不会有reader执行。
2.写者优先:只有当前writer结束后才会有其他线程。
3.Count占用互斥,reader不会同时出现,当前reader会在前一个reader结束beginning或结束end过程后再执行beginning或end过程。
运行代码,可见运行结果符合预期。
对于连续的reader1 2 3 4 5,每隔0.1秒出现一行,然后共同等待1秒,然后每隔0.1秒出现一行end指令。对于writer,每个writer单独等待1秒,等待后执行end指令,只有end后才会有其他指令。且writer之前的所有reader均执行end指令。
实验优化
1.创建线程时设置参数dwCreationFlags为CREATE_SUSPENDED,在后续过程中通过ResumeThread方法控制个别线程是否加入并行。
2.设计随机数,在模拟read或模拟write过程设置随机Sleep时间,使得实验更加拟真。
附录
pch.h
#pragma once
#include <iostream>
#include <thread>
#include <iomanip>
#include <vector>
#include <Windows.h>
#include <stdlib.h>
#include <string>
#define count Count
#define LSR LPTHREAD_START_ROUTINE
#ifndef INFINITE
0xFFFFFFFF
#endif
#ifndef NULL
0
#endif
using namespace std;
int Count = 0;
main.cpp
#include "pch.h"
#include "Writer.h"
#include "Reader.h"
#include "PV.h"
int main()
{
ios::sync_with_stdio(false);
HANDLE reader[5];
HANDLE writer[5];
unsigned int X = std::thread::hardware_concurrency();
cout << "The maximum of concurrent threads of the hardware in current device: ";
cout << std::thread::hardware_concurrency() << endl;
//cout << "The main thread will PAUSE..." << endl;
//Sleep(2000);
cout << "Start simulating the reader/writer process." << endl;
system("pause");
for (int i = 0; i < 5; i++) //自定义五个reader writer
{
string s = "Reader " + to_string(i + 1);
readers[i] = { s };
s = "Writer " + to_string(i + 1);
writers[i] = { s };
}
for (int i = 0; i < 5; i++) //为五个reader writer创建线程
{
reader[i] = CreateThread(NULL, 0, (LSR)Readers, (void*)i, 0, NULL);
}
for (int i = 0; i < 5; i++)
{
writer[i] = CreateThread(NULL, 0, (LSR)Writers, (void*)i, 0, NULL);
}
/*for (int i = 0; i < 5; i++)
{
ResumeThread(reader[i]);
ResumeThread(writer[i]);
}*/
while (true) //线程并发运行
{
for (int i = 0; i < 5; i++)
{
WaitForSingleObject(reader[i], INFINITE);
WaitForSingleObject(writer[i], INFINITE);
}
}
return 0;
}