读者写者问题(写者优先)

本文介绍了使用C++17和C11标准实现读者写者问题的实验,通过类设计和PV操作解决读写互斥和写者优先的问题。实验中,读者和写者类继承自基类,并各自实现了特定的行为。在WindowsAPI下,通过信号量实现线程同步。读者在读取前会先检查是否有其他写者,而写者享有优先权,确保无读者时才能进行写操作。实验结果符合预期,展示出正确的读写互斥和写者优先原则。

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;
}
读者问题中,实现优先的解决方案旨在确保当有者请求访问共享资源时,后续到达的读者必须等待,直到所有者完成操作。这种方式能够避免者长期饥饿,同时在某些应用场景中,如数据库更新、日志入等,操作的及时性非常重要。以下是优先的具体实现方法和关键机制: ### 优先的实现机制 为了实现优先,需要引入多个互斥信号量和计数器变量,以确保对共享资源的访问是互斥的,并且者的请求能够被优先处理。关键变量包括: - `writer_count`:记录当前等待或正在入的者数量。 - `reader_count`:记录当前正在读取的读者数量。 - `wc_mutex`:用于对`writer_count`进行互斥访问的信号量。 - `rc_mutex`:用于对`reader_count`进行互斥访问的信号量。 - `wrt`:用于保证者之间互斥访问共享资源的信号量。 - `read`:用于阻塞读者,当有者在等待或正在入时,新到达的读者必须在此信号量上等待。 #### 者的操作流程 1. 者进入系统后,首先获取`wc_mutex`以更新`writer_count`。 2. 如果`writer_count`为0,说明这是第一个者,需要获取`read`信号量来阻止后续读者进入。 3. 者获取`wrt`信号量以确保没有其他者正在进行操作。 4. 执行操作。 5. 完成操作后,释放`wrt`信号量。 6. 再次获取`wc_mutex`以减少`writer_count`,如果`writer_count`变为0,则释放`read`信号量,允许读者进入。 7. 最后释放`wc_mutex`。 #### 读者的操作流程 1. 读者进入系统后,首先获取`rc_mutex`以更新`reader_count`。 2. 如果`reader_count`为1,说明这是第一个读者,需要获取`wrt`信号量来阻止者进入。 3. 释放`rc_mutex`以便其他读者可以并发读取。 4. 执行读操作。 5. 完成读操作后,再次获取`rc_mutex`以减少`reader_count`。 6. 如果`reader_count`变为0,释放`wrt`信号量,允许者进入。 7. 最后释放`rc_mutex`。 ### 优先的代码示例(C++) 以下是一个简化的优先实现的伪代码示例: ```cpp #include <semaphore.h> #include <pthread.h> sem_t wc_mutex, rc_mutex, wrt, read; int writer_count = 0; int reader_count = 0; // 者操作 void* writer(void* arg) { sem_wait(&wc_mutex); writer_count++; if (writer_count == 1) sem_wait(&read); // 第一个者阻塞读者 sem_post(&wc_mutex); sem_wait(&wrt); // 获取权限 // 操作 sem_post(&wrt); sem_wait(&wc_mutex); writer_count--; if (writer_count == 0) sem_post(&read); // 最后一个者释放读者阻塞 sem_post(&wc_mutex); return NULL; } // 读者操作 void* reader(void* arg) { sem_wait(&read); // 如果有者在等待或正在,阻塞 sem_wait(&rc_mutex); reader_count++; if (reader_count == 1) sem_wait(&wrt); // 第一个读者阻塞者 sem_post(&rc_mutex); // 读操作 sem_wait(&rc_mutex); reader_count--; if (reader_count == 0) sem_post(&wrt); // 最后一个读者释放者阻塞 sem_post(&rc_mutex); sem_post(&read); return NULL; } ``` ### 优先的优缺点 **优点:** - 避免者饥饿,确保操作能够及时完成。 - 在操作频繁的场景下,可以提高系统的整体响应速度。 **缺点:** - 可能导致读者饥饿,尤其是在者频繁到达的情况下。 - 实现较为复杂,涉及多个信号量和计数器变量的管理。 ### 总结 优先读者问题解决方案通过引入多个互斥信号量和计数器变量,确保了者的优先级高于读者。这种机制适用于操作需要快速完成的场景,但需要注意潜在的读者饥饿问题。在实际应用中,可以根据具体需求调整优先级策略,例如采用公平竞争机制来平衡读者者的访问机会。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值