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多线程(线程池,单例模式,线程安全,读者写者模型)