现在有这么一个场景,有多个数据发送线程发送数据,我们想要将这些数据都记录在一个文件中,考虑了如下这样一种设计:先实现一个数据发送线程,用向量管理起来。
我们在写文件的时候就是要写磁盘,那么I/O速率一般是慢于内存操作速率的,所以可以考虑使用双缓冲的机制来缓存文件,设计如下:实现一个Manager管理类,里边包含两个线程,一个线程负责接收发送线程的数据存入内存,另一个线程等到缓冲区满时直接写数据。代码结构如下:
数据发送线程DataSendThread实现如下:
#ifndef DATASENDTHREAD_H
#define DATASENDTHREAD_H
#include <QVector>
#include <QThread>
#include <QDebug>
#include <QElapsedTimer>
class DataSendThread: public QThread
{
Q_OBJECT
public:
explicit DataSendThread(QObject* parent = nullptr, int start = 0);
protected:
void run() override;
private:
QVector<int> _data;
int _start;
signals:
void emitSendData(int val);
};
#endif // DATASENDTHREAD_H
其中,_data是我们要发送的数据,_start负责初始化起始的发送数据。成员函数实现如下:
#include "DataSendThread.h"
DataSendThread::DataSendThread(QObject* parent, int start):
_start(start)
{
/* 初始化要发送的数据 */
for(int i = start; i < start + 20; i++)
_data.push_back(i);
}
void DataSendThread::run()
{
for(auto& data: _data){
QElapsedTimer t; t.restart();
while(t.elapsed() < 50);
emit emitSendData(data);
}
}
通过Qt的emit信号发送数据,发送间隔为50ms. 实现一个线程管理类DataReceiveManager,实现如下:
#ifndef DATARECEIVEMANAGER_H
#define DATARECEIVEMANAGER_H
#include <QMutex>
#include <QMutexLocker>
#include <QObject>
#include <QThread>
#include <QVector>
#include <QSharedPointer>
#include <QQueue>
#include <QScopedPointer>
#include "DataReceiveThread.h"
#include "DataWriteThread.h"
class DataReceiveManager: public QObject
{
Q_OBJECT
public:
explicit DataReceiveManager(QObject* parent = nullptr);
void checkResult();
public slots:
void saveData(int i);
private:
QSharedPointer<QVector<int>> _buffer_1; // 缓冲1
QSharedPointer<QVector<int>> _buffer_2; // 缓存2
QSharedPointer<QVector<int>> _result_vector; // 汇总所有的结果,最后用来打印验证
QScopedPointer<DataReceiveThread> _receive_thread; // 数据接收线程
QScopedPointer<DataWriteThread> _data_write_thread; // 数据写文件线程
int _cache_size;
QMutex _mutex; // 加锁
};
#endif // DATARECEIVEMANAGER_H
首先使用智能指针管理两块缓冲区,用_result_vector记录最终的收数结果;定义一个收数的接口,负责从DataSendThread接收数据;_receive_thread负责接收数据写内存,_data_write_thread负责模拟写磁盘;_cache_size是内存缓冲区大小。
#include "DataReceiveManager.h"
DataReceiveManager::DataReceiveManager(QObject* parent)
: _buffer_1(new QVector<int>)
, _buffer_2(new QVector<int>)
, _result_vector(new QVector<int>)
, _receive_thread(new DataReceiveThread)
, _data_write_thread(new DataWriteThread)
, _cache_size(4)
{
_receive_thread->resetBuffer(_buffer_1);
}
void DataReceiveManager::checkResult()
{
QString temp;
for(int i = 0; i < _result_vector.data()->size(); i++){
temp += (QString::number(_result_vector->data()[i]) + " ");
}
qDebug() << "Curr Result: " << temp;
qDebug() << "Manager: total receive " << _result_vector->size();
qDebug() << "Buffer_1 Size: " << QString::number(_buffer_1->size())
<< "| Buffer_2 Size: " << QString::number(_buffer_2->size());
}
void DataReceiveManager::saveData(int i)
{
QMutexLocker lock(&_mutex);
if(_buffer_1->size() > _cache_size){
/* 通过智能指针快速交换数据地址 */
_buffer_2.data()->clear();
_buffer_1.swap(_buffer_2);
_receive_thread->resetBuffer(_buffer_1);
/* 缓冲区满,使用一个线程模拟磁盘操作 */
_data_write_thread->saveDataToDisk(_buffer_2, _result_vector);
}
/* 使用一个线程写入缓冲区 */
_receive_thread->saveData(i);
this->checkResult();
}
因为有多个线程会同时访问saveData接口,那么就需要加锁,加锁后,判断缓冲区有没有满,满了通过智能指针swap后,写内存的线程继续在空的缓存位置写数据,满的缓冲区交给另一个线程保存。
现在实现数据写内存线程DataReceiveThread:
#ifndef DATARECEIVETHREAD_H
#define DATARECEIVETHREAD_H
#include <QVector>
#include <QThread>
#include <QQueue>
#include <QMutex>
#include <QDebug>
#include <QSharedPointer>
#include <QElapsedTimer>
class DataReceiveThread: public QThread
{
Q_OBJECT
public:
DataReceiveThread();
void resetBuffer(QSharedPointer<QVector<int>>& buffer);
void saveData(int i);
protected:
void run() override;
private:
QSharedPointer<QVector<int>> _buffer;
QSharedPointer<QQueue<int>> _queue;
QQueue<int> _receive_queue;
QMutex _mutex;
};
#endif // DATARECEIVETHREAD_H
成员变量和缓冲区是绑定的,用智能指针来进行设置赋值操作比直接复制原数据节省时间。
#include "DataReceiveThread.h"
DataReceiveThread::DataReceiveThread()
{
}
void DataReceiveThread::resetBuffer(QSharedPointer<QVector<int> > &buffer)
{
_buffer = buffer;
}
void DataReceiveThread::saveData(int i)
{
QMutexLocker lock(&_mutex);
if(_receive_queue.size() < 20){
_receive_queue.enqueue(i);
}
if(!this->isRunning() && !_receive_queue.isEmpty()){
this->start();
}
}
void DataReceiveThread::run()
{
while(!_receive_queue.isEmpty()){
QMutexLocker lock(&_mutex);
auto data = _receive_queue.dequeue();
lock.unlock();
QElapsedTimer t; t.restart();
while(t.elapsed() < 8);
_buffer->append(data);
qDebug() << "Receive queue size: " << QString::number(_receive_queue.size())
<< " Try save an element: " << QString::number(data)
<< " Curr Buffer Size: " << QString::number(_buffer->size());
}
}
DataReceiveThread接收数据时都是通过接口saveData进行的,写数据到队列后直接启动线程。在run()函数中,模拟内存的耗时操作为8ms.
实现写磁盘线程DataWriteThread类:
#ifndef DATAWRITETHREAD_H
#define DATAWRITETHREAD_H
#include <QDebug>
#include <QThread>
#include <QVector>
#include <QElapsedTimer>
#include <QSharedPointer>
class DataWriteThread: public QThread
{
Q_OBJECT
public:
DataWriteThread();
void saveDataToDisk(QSharedPointer<QVector<int>>& data, QSharedPointer<QVector<int>>& result);
protected:
void run() override;
private:
QSharedPointer<QVector<int>> _data;
QSharedPointer<QVector<int>> _result;
QString _temp;
int _receive_count;
};
#endif // DATAWRITETHREAD_H
写磁盘都是通过saveDataToDisk来模拟的,具体实现如下:
#include "DataWriteThread.h"
DataWriteThread::DataWriteThread()
: _receive_count(0)
{
}
void DataWriteThread::saveDataToDisk(QSharedPointer<QVector<int> > &data, QSharedPointer<QVector<int>>& result)
{
// 设置存储的位置
_data = data;
_result = result;
if(!this->isRunning())
this->start();
}
void DataWriteThread::run()
{
for(int i = 0; i < _data->size(); i++)
_result->append(_data->data()[i]);
// 模拟耗时操作
QElapsedTimer t; t.restart();
while(t.elapsed() < 25);
for(int i = 0; i < _data->size(); i++){
_temp += (QString::number(_data->data()[i]) + " ");
_receive_count++;
}
qDebug() << "Save Data to Disk. Tolal Receive: " <<QString::number(_receive_count) << " Curr Result: " << _temp;
}
在run()函数中,模拟耗时操作为25ms.
现在再实现一个Handler来模拟一个发送数据接收数据的过程,实现如下:
#ifndef HANDLER_H
#define HANDLER_H
#include <QObject>
#include "DataReceiveManager.h"
#include "DataSendThread.h"
class Handler: public QObject
{
Q_OBJECT
public:
Handler();
~Handler();
void start();
DataReceiveManager* _manager;
QVector<DataSendThread*> data_send_threads;
};
#endif // HANDLER_H
成员函数实现如下:
#include "Handler.h"
Handler::Handler()
: _manager(new DataReceiveManager(this))
{
QVector<int> init_data{1,21,41,61,81};
for(auto& data: init_data){
DataSendThread* _thread = new DataSendThread(this, data);
/* 初始化所有的信号连接 */
connect(_thread, &DataSendThread::emitSendData,
_manager, &DataReceiveManager::saveData);
data_send_threads.append(_thread);
}
}
Handler::~Handler()
{
if(_manager)
delete _manager;
}
void Handler::start()
{
qDebug() << "Start All DataSendThreads.";
for(auto th: data_send_threads)
th->start();
}
其中,init_data负责模拟发送数据的链路数,加入我们有5路数据需要接收;在堆上分配5个数据发送线程,同时绑定到管理类的数据接收接口;通过start()函数启动所有发送线程。主函数调用如下:
#include <QCoreApplication>
#include <QDebug>
#include <QObject>
#include "Handler.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Handler hander;
hander.start();
qDebug() << "Finished!";
return a.exec();
}
执行主函数结果如下:
观察最后的执行结果:Manager: totol receive 90,说明已经收到了90个数据,剩下的10个观察还没写入的缓存区,在最后一行:Curr Buffer Size: 10,总和正好是我们发送的100个数据,说明没有丢数。
注意事项
1. 数据接收线程对数据的处理速度一定要快,加入你现在同时监听5路数据,每一路的发送频率是50ms一包,那么这些线程抢占锁导致互斥,写内存的速率尽量要再 50ms / 5 = 10 ms 上下,否则会丢数据。
2. 写磁盘的速率一定要快于缓冲区填满的速率,不然也会丢数据。
3. 关于信号与槽:我在实现connect函数时,发现线程的信号不能触发DataReceiveManager管理类的saveData函数,在第一次实现主函数时,我是这样写的:
#include <QCoreApplication>
#include <QDebug>
#include <QObject>
#include "Handler.h"
int main(int argc, char *argv[])
{
// QCoreApplication a(argc, argv);
Handler hander;
hander.start();
qDebug() << "Finished!";
// return a.exec();
return 0;
}
相当于没有执行主函数的事件循环,那么通过队列连接的信号就无法在主函数触发相应的槽,修正后就可以监听到了;当然也可以用直接连接的方式DirectConnection,就不用进队列了,不过有风险就是了。最后总结一下,锁互斥是很占用时间和资源的,如果可以尽量还是分开记录数据比较好一点。
以上就是平时的一些学习与实践,欢迎多多交流~