C++ 多线程学习笔记(6):读者-写者问题模拟

一、介绍说明

  • 语言:C++11
  • 题目:读者-写者问题模拟
  • 背景:
    • 2个读者5个写者,操作一个共享的数据区(用一个string字符串代表)
    • 写者和其他写者、读者是互斥的
    • 读者和其他写者是互斥的,和其他读者是不互斥的
  • 编程思路
    • 做一个临界资源类,包含读者写者共同共享数据区,和对这个数据的读写操作
    • 利用C++11提供的 mutex 类,用 “使用成员函数指针作为线程函数” 的方法建立多个读者写者线程
    • 为了自动生成读者的数据,给每个写者一个私有数据区,并单独开一个数据生成线程,此线程不断生成随机字符串填入写者的私有数据区中。当某个写者拿到写 “ 读者-写者共享数据区” 的权限后,从其私有数据区中取出数据生成线程生成的随机字符串写入。
  • 进程同步分析
    • 从写者角度看
      • 写者和其他写者、读者都是互斥的
      • 临界资源是 “读者-写者共享数据区”,给它设置一个写互斥量Wmutex:=1
    • 从读者角度看
      • 读者和写者之间互斥(可以用上面的 Wmutex 处理)
      • 读者和读者之间不互斥
      • 关键在于,要知道当前有没有读者在读,否则没法确定 signal(Wmutex) 的时机。因此我们可以设置一个计数值RCount:=0 描述当前访问临界资源的读者个数,这个值可以被所有读者互斥访问,设置一个互斥信号量 Rmutex 来控制读者的互斥访问
    • 针对写者的数据生成线程
      • 数据生成线程不断访问写者的私有数据区,向其中填入随机数据
      • 在写者写 “读者-写者共享数据区” 时,写线程要访问写者的私有数据区
      • 因此每个写者的私有数据区是临界资源,数据生成线程和写线程应该互斥地访问,设置互斥信号量 GENmutex 来控制

二、使用的语法现象

  • 利用C++11标准的 thread 类创建线程

    • 使用成员函数指针创建线程
      • std::thread mytobj(&类名::成员函数名, &类对象, 参数列表); 这行代码,以指定类对象的指定函数作为线程的起始函数,创建一个子线程
    • .join()方法
      • 这是thread 类的一个方法,其作用是阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合,再往下执行,以防主线程先于子线程结束,导致子线程被操作系统强制结束
  • 互斥量mutex

    • mutex是一个类对象,提供了多个对互斥量(信号量)的操作
    • lock() 方法:给互斥量 “加锁”,相当于P操作
    • unlock()方法:给互斥量 “解锁”,相当于V操作
  • this_thread命名空间

    • this_thread::sleep_for(时间):令此线程睡眠一段时间,期间不参与互斥量争用
    • this_thread::get_id():获取当前线程的id
  • 其他

    • std::lock_guard类:用这个类的对象,可以代替lockunlock操作,可以避免忘记写unlock。原理是在这个对象构造时lock(),在它析构时unlock()。这次的作业里没用到

    • std::lock()函数:用这个函数,可以同时给多个互斥量加锁。当某处需要同时请求多个互斥量时,此函数从第一个互斥量开始尝试上锁,如果lock()成功,就继续尝试下一个互斥量;一旦有一个互斥量锁不上,立即释放已经锁住的所有互斥量,从第一个互斥量开始重新尝试。相当于课上的AND信号量集

    • 一个技巧

      • 这样写可以同时lock()多个信号量,并自动unlock()
      • 下面代码的92~102行可以用这个方法改进
    //用lock类同时锁俩信号量
    std::lock(my_mutex1, my_mutex2);	
    //用lock_guard对象来unlock,adopt_lock用来避免重复lock
    std::lock_guard<std::mutex> threadGuard1(my_mutex1, std::adopt_lock);
    std::lock_guard<std::mutex> threadGuard2(my_mutex2, std::adopt_lock);		

三、代码

//读者写者问题,请在release状态下执行,因为debug状态要求线程A进行的lock必须由线程A来unlock,
//而读者写者问题中,某个读者对写互斥量的lock可能是由其他读者unlock的。如果在debug状态运行,会报错unlock of unowned mutex(stl::mutex)

#include "pch.h"	//vs2017自带的空编译头
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
#include <cstdlib>
#include <ctime>
#include <string>
#include <cstdio>
#include <windows.h>

using namespace std;

//写者类
class writer
{
private:
	string data;		//写者准备的数据(数据生成线程的临界资源)
	thread *Wthread;	//写线程指针
	thread GENthread;	//数据生成线程


public:
	mutex GENmutex;		//数据生成互斥量

	//构造函数
	writer()
	{
		cout << "构造" << endl;
		GENthread = thread(&writer::genData, this);	//创建一个数据生成线程
	}

    //关联写线程
	void setThread(thread *p)
	{
		Wthread = p;
	}
	
    //设置子线程为join方式
	void join()
	{
		Wthread->join();
		GENthread.join();
	}

	//生成写者的数据,十个随机大写字母
	void genData()
	{
		while (1)
		{
			GENmutex.lock();

			data.clear();
			for (int i = 0; i < 10; i++)
				data.insert((string::size_type)0, 1, rand() % 26 + 'A');

			GENmutex.unlock();
		}
	}

	//获取写者数据
	string getData()
	{
		return data;
	}
};


//读者-写者 临界资源类
class CriticalResource
{
private:
	mutex Wmutex;	//写互斥量,临界资源空闲
	mutex Rmutex;	//读互斥量,RCount访问空闲
	int RCount;		//读者计数值

	string str;		//临界资源(读者写者共享数据区)

public:
	//用构造函数赋初值
	CriticalResource() {	RCount = 0;	}

	//写线程
	void Write(writer *w)
	{
		while (1)
		{
			//请求空闲的临界资源,加锁
			Wmutex.lock();

			//写入随机生成的数据
			w->GENmutex.lock();		//先申请访问写者的临界资源data
			str = w->getData();		//写入临界资源
			w->GENmutex.unlock();	//释放写者临界资源互斥量GENmutex

			cout << endl << "写者" << this_thread::get_id() << "写入:" << str <<"----------------------------------"<< endl;
			
			//解锁,释放写互斥量
			Wmutex.unlock();
			//隔一段随机时间请求一次
			this_thread::sleep_for(chrono::seconds(rand() % 3));
		}
	}

	//读线程
	void Read()
	{ 
		while (1)
		{
			//请求访问RCount,加锁
			Rmutex.lock();
			//如果当前没有渎者,请求空闲的临界资源(避免干扰写者)
			if (RCount == 0)
				Wmutex.lock();
							
			//读者数+1
			RCount++;

			//释放RCount访问互斥量
			Rmutex.unlock();

			//读
			cout << "读者" << this_thread::get_id() << "读取:" << str << ",当前有" << RCount << "个读者正在访问" << endl;

			//请求访问RCount,加锁
			Rmutex.lock();

			//读者数+1
			RCount--;

			//临界资源空闲了,写者可以写了,释放写互斥量
			if (RCount == 0)
				Wmutex.unlock();

			//释放RCount访问互斥量
			Rmutex.unlock();

			//隔一段随机时间请求一次
			this_thread::sleep_for(chrono::seconds(rand() % 3));
		}
	}
};


int main()
{
	srand((int)time(0));

	vector <thread> readerThreads;
	vector <thread> writerThreads;
	writer W[20];	//最多20个写者

	CriticalResource CR;

	//创建5个写线程,子线程入口是CriticalResource类函数Wiite
	for (int i = 0; i < 2; i++)
	{
		writerThreads.push_back(thread(&CriticalResource::Write, &CR, &W[i]));
		W[i].setThread(&writerThreads.back());
	}

	//创建5个读线程,子线程入口是CriticalResource类函数Read
	for (int i = 0; i < 5; i++)
		readerThreads.push_back(std::thread(&CriticalResource::Read, &CR));

	//所有线程都设置成join模式,主线程要等待子线程结束才能退出,防止主线程提前退出
	for (auto iter = readerThreads.begin(); iter != readerThreads.end(); ++iter)
		iter->join();

	for (int i = 0; i < 2; i++)
		W[i].join();

	return  0;
}


  • 这个程序是死循环运行的,这里截取了一段输出
  写者2604写入:WPIACTWNOU----------------------------------
  
  写者26320写入:JFRPIAIZXX----------------------------------
  读者27172读取:JFRPIAIZXX,当前有3个读者正在访问
  读者28956读取:JFRPIAIZXX,当前有2个读者正在访问
  读者27504读取:JFRPIAIZXX,当前有1个读者正在访问
  读者1512读取:JFRPIAIZXX,当前有2个读者正在访问
  读者32716读取:JFRPIAIZXX,当前有1个读者正在访问
  
  写者2604写入:LXPNIQLOHB----------------------------------
  
  写者26320写入:YAZTWLTIGL----------------------------------
  读者28956读取:YAZTWLTIGL,当前有1个读者正在访问
  读者27172读取:读者27504读取:YAZTWLTIGL,当前有2个读者正在访问
  YAZTWLTIGL,当前有1个读者正在访问
  读者1512读取:YAZTWLTIGL,当前有2个读者正在访问
  读者32716读取:YAZTWLTIGL,当前有1个读者正在访问
  
  写者2604写入:KTLXOJFAMF----------------------------------
  读者28956读取:KTLXOJFAMF,当前有1个读者正在访问
  读者27504读取:KTLXOJFAMF,当前有2个读者正在访问
  读者27172读取:KTLXOJFAMF,当前有1个读者正在访问
  
  写者26320写入:GTCWCHIFON----------------------------------
  
  写者2604写入:WZHLSHRWFH----------------------------------
  读者1512读取:WZHLSHRWFH,当前有2个读者正在访问读者28956读取:WZHLSHRWFH,当前有5个读者正在访问
  
  读者32716读取:WZHLSHRWFH,当前有3个读者正在访问
  读者27172读取:WZHLSHRWFH,当前有2个读者正在访问
  读者27504读取:WZHLSHRWFH,当前有1个读者正在访问
  
  写者26320写入:ZEFCTZRYDQ----------------------------------
  
  写者2604写入:RZATXQKCBK----------------------------------
  读者读者28956读取:RZATXQKCBK,当前有5个读者正在访问
  1512读取:RZATXQKCBK,当前有4个读者正在访问
  读者27172读取:RZATXQKCBK,当前有3个读者正在访问
  读者27504读取:RZATXQKCBK,当前有2个读者正在访问
  读者32716读取:RZATXQKCBK,当前有1个读者正在访问
  
  写者26320写入:TFLBUPLSTF----------------------------------
  读者1512读取:TFLBUPLSTF,当前有2个读者正在访问
  读者32716读取:TFLBUPLSTF,当前有1个读者正在访问
  • 结果分析

    • 由于写者向控制台打印的那行代码不在lock()区域内,有可能被打断,可以看到有些读者的数据数据被打断了
    • 可以看到两个写者不断写入数据,每次写入后,直到下一次写入数据之前,所有读者读取的数据都和最近写入的一致,而且总是在没有读者时才会发生写入,符合读者-写者问题要求
    • 可以看出,各个子线程的运行是不可预测的

四、遇到的问题

  • 如果用的是VS,一定要在release状态下执行,因为debug状态要求线程A进行的lock必须由线程A来unlock,而读者写者问题中,某个读者对写互斥量的lock可能是由其他读者unlock的。如果在debug状态运行,会报错unlock of unowned mutex(stl::mutex) 这个问题调试了很久

  • 所有的子线程的.join()方法调用,应当统一放在主线程最后,否则在第一个.join()位置主线程就会被阻塞,子线程结束前,后面的其他代码都不能执行。

  • 一开始没有设置单独的数据生成线程,而是在写线程中现场准备随机数。但是在加上 this_thread::sleep_for 延时后,我发现以下问题

    • 经过随机延时,每个写者进程发起请求的时机不同,按理说,应当看到控制台上不时出现一个写者的打印提示,并且相邻两个打印提示中写者写入的数据应该不同(因为每个线程里都是写入前临时随机生成的数据)。但事实上控制台的打印数据是 ”分组突发“ 形式的,往往是半天没有打印,然后一下打印好多行。根据打印的线程id,可以确定这些提示是由不同的写进程打印的,但它们打印的写入内容却有很多重复

    • 这个问题查了挺久的,没有解决,也不知道为什么会这样,我怀疑可能是编译器针对cout做了什么优化导致"分组突发"现象,而数据重复问题可能是cout语句里直接打印共享数据str造成的?可能cout的时候,不是立即去内存取变量值的,而是做了优化用了之前的值?总之不是很确定

    • 最后决定不要在写线程里做数据生成了,生成的数据也最好不要被其他线程覆盖,于是给写者增加了私有数据区和数据生成线程,解决了上述问题。虽然"分组突发"现象依然存在,但是可以确保不同写者写入的数据是不同的了

  • 7
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云端FFF

所有博文免费阅读,求打赏鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值