前言
在C++ QT中,多线程通信原理主要涉及到信号与槽机制和事件循环机制。
1、信号与槽机制:
在QT中,信号与槽是一种用于对象间通信的机制。对象可以通过发送信号来通知其他对象,其他对象通过连接槽来接收信号并进行相应的处理。这个机制可以跨线程使用,即一个线程可以发送信号,另一个线程可以接收信号。
在使用多线程时,通过将一个对象的信号与另一个对象的槽连接起来,可以在不同线程中进行通信。当信号发出时,QT会在接收对象所在的线程中调用槽函数,从而实现多线程之间的通信。
2、事件循环机制:
在QT中,每个线程都有自己的事件循环。事件循环是处理事件的机制,包括用户输入、定时器事件等。当一个线程启动时,会进入事件循环,等待事件发生并处理事件。
在多线程通信中,通常会涉及到在不同线程之间传递事件。通过在一个线程中发出事件,另一个线程中的事件循环可以接收并处理这个事件,从而实现线程间的通信。
总的来说,C++ QT中的多线程通信原理是通过信号与槽机制和事件循环机制来实现的。通过使用这两种机制,可以在不同线程之间进行通信,并实现多线程之间的协作。但需要注意线程安全性,避免出现竞态条件和线程安全问题。
正文
01-功能演示
如下图所示,第一张图,是经过运行之后,进入主界面,会同时显示后台信息,黑色的控制台界面就是线程运行信息,是否有两个线程也可以查看,主界面可以通过控件控制进程运行状态。
注:系统资源竞态是指多个线程或进程在对共享资源进行访问时,由于执行顺序不确定或未正确同步,导致竞争条件发生,从而导致数据不一致或程序出现错误的现象。
当多个线程同时访问共享资源时,如果没有使用适当的同步机制来确保每个线程对资源的访问顺序,就可能引发系统资源竞态。系统资源竞态会导致一些严重问题,包括但不限于以下几点:
数据不一致性:如果多个线程同时访问共享数据但没有正确同步,可能会导致数据不一致的情况。例如,一个线程正在修改数据的同时,另一个线程也在读取或修改相同的数据,可能会导致数据错误或不一致。
资源泄漏:当多个线程同时竞争资源而没有正确释放资源,可能会导致资源泄漏。例如,并发写入文件时,如果某个线程在写入文件后未正确关闭文件句柄,可能会导致文件描述符泄漏。
死锁:系统资源竞态还可能导致死锁的发生。当两个或多个线程相互等待对方释放资源时,可能导致所有参与的线程无法继续正常执行,从而导致程序挂起。
因此,在采用系统多线程运行时,一定要使用一些方法防止资源竞争,可以采取以下几种措施:a、使用互斥量(Mutex)或信号量(Semaphore)等同步机制,在访问共享资源时确保只有一个线程可以访问;b、 使用临界区(Critical Section)来限制对共享资源的访问,确保线程之间的同步;c、使用条件变量(Condition Variable)在需要等待某个条件满足时进行线程等待和唤醒。
这里的系统使用的是数据库进行资源交换和读取等,线程之间互不干扰,不存在线程竞争问题。
下面是主线程和子线程之间通信的几个案例,也是系统中运行产生的,如下图所示,系统改为手动模式,首先点击打开配置文件按钮,可以看出控制台运行信息为“配置文件config.ini已打开!”,点击平台数据读取按钮,显示下方运行信息,这就是主线程和子线程之间的交互信息,可以通过QT中信号与槽函数的形式实现。
下面重点分析如何实现,并附代码解释。
02-系统多线程实现
这里讲解四个实现函数文件,其实是两个文件:mainwindow.h和mainwindow.cpp文件为主线程文件,Zanj_WFC_ctrl.h和Zanj_WFC_ctrl.cpp文件为子线程文件,下面一一解释。
mainwindow.cpp文件
(1)、该文件用于实现显示主线程中的主界面,也就是运行之后,第一个界面,可以在界面上进行操作,后台信息是由于spdlog日志信息的导入,才会进行信息显示,便于观察,#include "spdlog/spdlog.h",代码中下面这一部分就是建立子线程的核心,QT中可以使用moveToThread()函数建立新线程。具体介绍如下:
a、m_Zanj_WFC_ctrl = new Zanj_WFC_ctrl;// 这个实例要负责预报计算任务
b、m_Zanj_WFC_ctrl_Thread = new QThread; // 子线程,本身不负责预报计算
c、connectZanj_WFC_ctrl(m_Zanj_WFC_ctrl); // 连接信号-槽,复制的开始和取消指令是通过信号发送的
d、m_Zanj_WFC_ctrl->moveToThread(m_Zanj_WFC_ctrl_Thread);// 将实例移动到新的线程,实现多线程运行
e、m_Zanj_WFC_ctrl_Thread->start();
(2)、函数connectZanj_WFC_ctrl()用于连接子线程函数,里面有该函数实现的功能,使用connect()函数编写代码: connect(this, SIGNAL(system_predict_init_link()), m_Zanj_WFC_ctrl, SLOT(HDGL_SYSTEM_Init()));这个函数HDGL_SYSTEM_Init()在子线程中实现,也就是说只要主线程启动,子线程也会跟着启动,可以在该函数中实现子线程的功能。
(3)、上述描述的主线程与子线程通信的案例中的实现过程如下:也是通过信号与槽函数方式,比如打开配置文件函数,首先对该函数进行实现on_Open_config_Btn_clicked(),该函数就是打开配置文件控件对应的函数,该函数实现了点击按钮之后,发送了信号emit send_config();该信号通过connect()函数发送到子线程,代码为connect(this, &MainWindow::send_config, m_Zanj_WFC_ctrl, &Zanj_WFC_ctrl::OnconfigSlot);OnconfigSlot()函数在子线程中实现。
原理就是这样,虽有些繁琐,但是逻辑性强。另一个读取数据按钮原理一样。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "Zanj_WFC_ctrl.h"
#include "qdebug.h"
#include <qstring.h>
#include "cglobal.h"
#include <QElapsedTimer>
#include <iostream>
#include "windows.h"
#pragma comment(lib,".\\lib\\fmtd.lib")
#include "spdlog/fmt/ostr.h"
#include "spdlog/spdlog.h"
#include "spdlog/sinks/daily_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include <QTimer>
#include <time.h>
#include <QLabel>
#include <QValueAxis>
#include <QMargins>
#include <QtWidgets/QApplication>
#include <QtWidgets/QMainWindow>
#include <ctime>
using namespace std::literals;
using namespace std;
namespace spd = spdlog;
auto console_mainWindow = spd::stdout_color_mt("主界面控制台");
auto rotating_logger_mainWindow = spdlog::rotating_logger_mt("CAO8_Winmain_rotating", "logs/CAO8_rotating.logger", 1048576 * 5, 12);
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle(QString::fromLocal8Bit("主系统")); // 此处写应用程序在标题栏上显示的名称
setStyleSheet("background - color:pink; ");
reshow();
data_log_tblview();
console_mainWindow->info("欢迎进入主系统UI线程[Mainwindow]!");
rotating_logger_mainWindow->info("欢迎进入主系统UI线程[Mainwindow]!");
m_Zanj_WFC_ctrl = new Zanj_WFC_ctrl;// 这个实例要负责预报计算任务
m_Zanj_WFC_ctrl_Thread = new QThread; // 子线程,本身不负责预报计算
connectZanj_WFC_ctrl(m_Zanj_WFC_ctrl); // 连接信号-槽,复制的开始和取消指令是通过信号发送的
m_Zanj_WFC_ctrl->moveToThread(m_Zanj_WFC_ctrl_Thread);// 将实例移动到新的线程,实现多线程运行
m_Zanj_WFC_ctrl_Thread->start();
emit system_predict_init_link();
// 向子线程发送打开配置文件信息
connect(this, &MainWindow::send_config, m_Zanj_WFC_ctrl, &Zanj_WFC_ctrl::OnconfigSlot);
// 向子线程发送读取数据信号,子线程接收后,进行数据读取
connect(this, &MainWindow::send_rChk_ToWFC, m_Zanj_WFC_ctrl, &Zanj_WFC_ctrl::OnGETSlot);
}
MainWindow::~MainWindow()
{
delete ui;
}
// 这里连接子线程,进行子线程的开启,子线程连接数据库,连接大数据平台,打开所需文件的信号传递,以及MainWindow界面槽函数处理信号的实现
void MainWindow::connectZanj_WFC_ctrl(Zanj_WFC_ctrl * m_Zanj_WFC_ctrl)
{
connect(this, SIGNAL(system_predict_init_link()), m_Zanj_WFC_ctrl, SLOT(HDGL_SYSTEM_Init()));// 使用信号-槽机制,发出开始指令
}
// 打开配置文件函数
void MainWindow::on_Open_config_Btn_clicked()
{
ui->Iplate4j_IP_QLE->setText("127.0.0.1");
ui->model_PC_IP_QLE->setText("127.0.0.1");
emit send_config();
}
// 由主窗口发送数据读取信号,子线程实现数据读取操作
void MainWindow::on_Restful_data_read_Btn_clicked()
{
emit send_rChk_ToWFC();
}
mainwindow.h文件
该文件就是.cpp文件中众多函数的声明,以及信号的声明,比如读取数据时发送的信号需要在这里声明一下,便于使用。Zanj_WFC_ctrl * m_Zanj_WFC_ctrl; // 预报计算的类
QThread * m_Zanj_WFC_ctrl_Thread; //将被移动到此线程执行,这里也定义了指针,用于在.cpp文件中使用。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include"ui_mainwindow.h"
#include <QMainWindow>
#include <QLabel>
#include <QtSql/QtSql>
#include "spdlog/fmt/ostr.h"
#include "Zanj_WFC_ctrl.h"
#include <QVariant>
#include "wfc_to_main.h"
#include <QtSql/QtSql>
#include <QDateTime>
#include <QTimer>
#include <qtextcodec.h>
#include <QtWidgets/QApplication>
#include <QtWidgets/QMainWindow>
#include <QMouseEvent>
//一定要声明!!!
#include"dialog_data_log.h"
//#include<vector>
//#include "dialog_data_log.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
Zanj_WFC_ctrl * m_Zanj_WFC_ctrl; // 预报计算的类
QThread * m_Zanj_WFC_ctrl_Thread; //m_Mp_Pred将被移动到此线程执行
void connectZanj_WFC_ctrl(Zanj_WFC_ctrl *m_Zanj_WFC_ctrl);
private slots :
void on_Restful_data_read_Btn_clicked(); //点击平台数据读取按钮,实现读取数据信号发送
void on_Open_config_Btn_clicked();
signals:
void system_predict_init_link();//定义向子线程发送的信号,用于实现子线程开启
void send_config(); // 定义向子线程发送打开配置文件的信号
void send_rChk_ToWFC(); // 定义向子线程发送数据读取的信号
private:
Ui::MainWindow *ui;
QLabel * pLabel;
};
#endif // MAINWINDOW_H
Zanj_WFC_ctrl.cpp文件
(1)、该文件中的众多函数就是对主线程发送的信号的实现过程,当接受到主线程的信号之后,如果需要对其进行处理,便可以在这里定义函数实现过程,该HDGL_SYSTEM_Init()函数就是对主线程中connectZanj_WFC_ctrl()连接函数的实现。
(2)、案例中主线程发送读取数据信号,子线程中函数的实现OnGETSlot()在该文件中,这里适用于读取数据函数的实现,可以根据需要进行修改。打开配置文件的实现原理与之一致。
#include "Zanj_WFC_ctrl.h"
#include "wfc_to_main.h"
#include <Python.h>
#include "qdebug.h"
#include <qstring.h>
#include <Python.h>
#pragma comment(lib,"mclmcrrt.lib")
#pragma comment(lib,"libmx.lib")
#pragma comment(lib,"libmat.lib")
#pragma comment(lib,"mclmcr.lib")
#pragma comment(lib,".\\lib\\CGL_Mech_Property_BP_pred.lib")
#pragma comment(lib,".\\lib\\fmtd.lib")
#include <QAxObject>
#include <QStandardPaths>
#include <QEventLoop>
#include <QDesktopWidget>
#include <algorithm>
#include <numeric>
#include <vector>
#include <QVector>
#include <stdlib.h>
#include "spdlog/fmt/ostr.h"
#include "spdlog/spdlog.h"
#include "spdlog/sinks/daily_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/rotating_file_sink.h"
using namespace libxl;
using namespace std::literals;
//using namespace fmt;
using namespace std;
namespace spd = spdlog;
auto console_WFC_thread = spd::stdout_color_mt("WFC计算线程 ");
auto rlogger_WFC_thread = spdlog::rotating_logger_mt("CAO8_WFC_thread_rotating", "logs/CAO8_rotating.logger", 1048576 * 5, 12);
int Zanj_WFC_ctrl::HDGL_SYSTEM_Init()
{
rlogger_WFC_thread->flush_on(spd::level::debug);
console_WFC_thread->info("欢迎进入主系统计算线程");
str_info = QString::fromLocal8Bit("0 应用程序环境路径 ") + path_deploy;
return 1;
}
void Zanj_WFC_ctrl::OnGETSlot()
{
QString strMessage;
QString strResult;
QString strUrl;
QString strInput;
QFile file(QApplication::applicationDirPath() + "/1.json");
if (!file.open(QIODevice::WriteOnly)) {
console_WFC_thread->error("Json file open failed! ");
rlogger_WFC_thread->error("Json file open failed! ");
}
else {
console_WFC_thread->info("Json file open successfully! ");
rlogger_WFC_thread->info("Json File open successfully! ");
}
//QString strInput = ui.textEdit_input->toPlainText();
//strUrl += "http://" + ui.lineEdit_Url->text();
strUrl = serverIP + serviceID_get;
strInput = clientSecret;
SendAndGetText(strUrl, get1, strInput, strMessage, strResult);
QJsonParseError jsonError;
QByteArray byteResult = strResult.toUtf8();//QString转QByteArray
//QJsonDocument json = QJsonDocument::fromJson(byteResult, &jsonError);
json_doc_get = QJsonDocument::fromJson(byteResult, &jsonError);
if (json_doc_get.isNull())
{
console_WFC_thread->critical("json文件为空文档");
//console->critical("jsonError错误类型" + jsonError.errorString().toStdString());
//qDebug() << jsonError.errorString();
rlogger_WFC_thread->critical("json格式错误[json_doc_get_slot]");
//qDebug() << "json格式错误";//注意:如果文档中有中文,就会报格式错误
}
else if (jsonError.error != QJsonParseError::NoError)
{
console_WFC_thread->critical("json格式错误[json_doc_get_slot]" + jsonError.errorString().toStdString());
//qDebug() << jsonError.errorString();
rlogger_WFC_thread->critical("json格式错误[json_doc_get_slot]");
}
else
{
if (json_doc_get.isObject())
{
doc_get_Obj = json_doc_get.object();
}
}
json_doc_get_parse();
// RT_Iplat4j_Communication(); //放在这里可以在主窗口点击数据读取按钮之后,才会提示是否数据平台连接成功
file.write(json_doc_get.toJson(QJsonDocument::Indented)); //Indented:表示自动添加/n回车符
file.close();
}
void Zanj_WFC_ctrl::OnconfigSlot()
{
configuration_read();
}
Zanj_WFC_ctrl.h文件
该文件也是.cpp文件中众多函数的声明,比如处理读取数据时发送的信号的函数需要在这里声明一下,便于使用。
#pragma once
#include <QObject>
#include <QtSql/QtSql>
#include <QVariant>
//#include <connection.h>
//#include <QSql>
#include <QSqlError>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QDebug>
#include <QMessageBox>
#include <QEventLoop>
#include <qstring.h>
#include <string>
#include "snap7.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <Python.h>
class Zanj_WFC_ctrl : public QObject
{
Q_OBJECT
public:
Zanj_WFC_ctrl(QObject *parent=0);
~Zanj_WFC_ctrl();
public slots:
int HDGL_SYSTEM_Init();
void OnGETSlot();
void OnconfigSlot();
总结
同时运行的两个线程,与单线程相比:
提高系统性能:双线程可以并行执行,可以更充分地利用多核处理器的资源,从而提高系统的整体性能。
增加程序响应速度:通过将一些耗时的操作放在单独的线程中执行,可以避免阻塞主线程,提高程序的响应速度和用户体验。
提高系统的并发性:双线程可以同时处理不同的任务,增加系统的并发性,使系统更具有高效性和灵活性。
分解复杂任务:通过将复杂任务拆分成多个线程来执行,可以更容易地管理和维护代码,提高代码的可读性和可维护性。
改善软件的稳定性:通过将不同功能模块分别放在不同的线程中执行,可以避免由于一个线程的错误导致整个系统崩溃的情况,提高软件的稳定性。
总的来说,双线程可以提高系统的性能、响应速度和并发性,同时也能改善软件的稳定性和可维护性,是一种常用的提升程序效率的方法。