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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 多线程学习笔记:读者-写者问题模拟相关的知识,希望对你有一定的参考价值。


文章目录

  • ​​一、介绍说明​​
  • ​​二、使用的语法现象​​
  • ​​三、代码​​
  • ​​四、遇到的问题​​

一、介绍说明

  • 语言: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​​​类:用这个类的对象,可以代替​​lock​​和​​unlock​​操作,可以避免忘记写​​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​​的时候,不是立即去内存取变量值的,而是做了优化用了之前的值?总之不是很确定
  • 最后决定不要在写线程里做数据生成了,生成的数据也最好不要被其他线程覆盖,于是给写者增加了私有数据区和数据生成线程,解决了上述问题。虽然"分组突发"现象依然存在,但是可以确保不同写者写入的数据是不同的了


以上是关于C++ 多线程学习笔记:读者-写者问题模拟的主要内容,如果未能解决你的问题,请参考以下文章

LINUX多线程(线程池,单例模式,线程安全,读者写者模型)

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

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

多线程面试题系列(14):读者写者问题继 读写锁SRWLock

线程同步互斥锁和读写锁的区别和各自适用场景

操作系统-进程PV操作——读者写者问题