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

author author     2022-12-07     235

关键词:


文章目录

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

一、介绍说明

  • 语言: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++多线程编程》学习笔记(代码片段)

文章目录线程安全的对象生命期管理当析构函数遇上多线程对象的创建对象池线程同步精要互斥器(mutex)条件变量(conditionvariable)慎用读写锁多线程服务器的适合模型与常用编程模型oneloopperthread+threadpool进... 查看详情

多读者多写者

...有写者在写时其他的写者不能再写,当写者全部写完时,读者才能继续读,当读者全部读完时写者才能继续写,写者每个循环写一次,读者每个循环读一次。#include<pthread.h>#include<string.h>#include<stdlib.h>#include<stdio.h&... 查看详情

线程安全的 C++ 堆栈

】线程安全的C++堆栈【英文标题】:Thread-safeC++stack【发布时间】:2010-10-2121:23:48【问题描述】:我是C++新手,正在编写一个多线程应用程序,通过该应用程序不同的编写者将对象推送到堆栈上,而读者将它们从堆栈中拉出(或... 查看详情

“读者-写者问题”的写者优先算法实现(代码片段)

...,一个数据集(如文件或记录)被几个并发进程共享,这些线程分两类,一部分只要求进行复操作,称之为“读者”;另一类要求写或修改操作,我们称之为“写者“。一般而言,对一个数据集,为了保证数据的完整性、正确性,... 查看详情

java线程锁机制-synchronizedlock互斥锁读写锁

...醒时优先考虑写者) 互斥锁特点: 一次只能一个线程拥有互斥锁,其他线程只有等待 所谓互斥锁,指的是一次最多只能有一个线程持有的锁.在jdk1.5之前,我们通 查看详情

经典同步问题总结(代码片段)

...进行操作解法一:读者优先存在的问题:如果有一个读者线程在读,那么之后如果同时来了读线程和写线程,读线程会优先执行      可能会导致写线程长时间等待。1intcount=0;//用于记录当前的读者数量2semaphoremutex=1;//用... 查看详情

多线程编程之读写锁

 在《多线程编程之Linux环境下的多线程(二)》一文中提到了Linux环境下的多线程同步机制之一的读写锁。本文再详细写一下读写锁的概念和原理。一、什么是读写锁  读写锁(也叫共享-独占锁)实际是一种特殊的自旋锁,... 查看详情

c++多线程学习笔记:互斥量概念和用法死锁演示及解决

文章目录​​1.互斥锁(mutex)基本概念​​​​2.互斥量的用法​​​​(1)lock(),unlock()​​​​(2)用lock和unlock改写上一节最后的代码​​​​(3)std::lock_guard类模板​​​​3.死锁​​​​(1)死锁演示​​​​(2)... 查看详情

操作系统学习笔记

...程间的组织进程的特征进程的状态进程控制进程间的通信线程处理机调度调度算法的评价指标FCFS、SJF、HRRN调度算法时间片轮转、优先级、多级反馈队列调度算法进程的同步与互斥进程互斥的软件实现方法进程互斥的硬件实现方... 查看详情

线程同步之读写锁(锁操作的补充)

轻量级的读写锁(SlimReader-Writerlocks):读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发... 查看详情

C ++,两个没有锁的只是编写者线程

】C++,两个没有锁的只是编写者线程【英文标题】:C++,twojustwritersthreadswithoutlock【发布时间】:2018-02-0114:24:20【问题描述】:我有3个线程。线程A和线程B只是编写器。线程C是一个公正的读者。变量是time_t。我需要验证没有数据... 查看详情

ipc之读者-写者问题(代码片段)

问题:  Courtoisetal于1971年提出。  可以多读取,但是写入时不允许读取、写入。  1typedefintsemaphore;2semaphoremutex=1;3semaphoredb=1;4intrc=0;5voidreader(void)67while(TRUE)89down(&mutex);10rc+=1;11if(rc==1)down(&db);12u 查看详情

pthread_rwlock_t

...能会用到该锁,比如定票之类的。二、特性一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。正因为这个特性,当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁... 查看详情

操作系统——读者写者问题(读者优先强写者优先和公平竞争)

操作系统——读者写者问题(读者优先、强写者优先和公平竞争)1.综述  这篇博客写得很好:http://blog.csdn.net/cz_hyf/article/details/4443551  查看详情

读者写者问题(代码片段)

1.读者写者问题读者优先:只要有一个Reader处于活动状态,那么后面来的Reader都会被接纳.若Reader源源不断,那么Writer就会一直处于阻塞状态,即写者被饿死.写者优先:一旦Writer就绪,就会先执行Writer,写者优先级高于读者,若Writer源源不断... 查看详情

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

无论是三种中的哪一种,在没有程序占用临界区时,读者与写者之间的竞争都是公平的,所谓的不公平(优先)是在读者优先和写者优先中,优先方只要占有了临界区,那么之后所有优先方的程序(读者或写者)便占有了临界区的主... 查看详情

多线程编程学习笔记——线程同步

接上文多线程编程学习笔记-基础(一)接上文多线程编程学习笔记-基础(二)接上文多线程编程学习笔记-基础(三)      就如上一篇文章(多线程编程学习笔记-基础(三))中的示例代码十,一样如果... 查看详情