我的QT Creator学习笔记(三十六)——进程和线程

Posted 今天也要努力搬砖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的QT Creator学习笔记(三十六)——进程和线程相关的知识,希望对你有一定的参考价值。

 参考文献:《Qt Creator 快速入门》第三版 霍亚飞编著

一、进程

1.1 进程相关类、接口、信号等

        在当前的应用程序中可以调用外部的程序来实现一些功能,这就会使用到进程。Qt的QProcess类用来启动一个外部程序(进程)并与其进行通信。

        要启动一个进程可以使用QProcess::start()函数,将程序名称和运行这个程序所要使用的命令行参数作为该函数的参数。

        执行完start()函数后QProcess进入Starting状态,当程序已经运行后QProcess就会进入Running状态并发射started()信号。当程序退出后,QProcess重新进入NotRunning状态(初始状态)并发射finished()信号。发射finished()信息提供了进程的退出代码和退出状态,也可以调用exitCode()来查看错误的类型和上次发生的错误。使用exitState()可以查看当前进程的状态。

        QProcess允许将一个进程视为一个顺序I/O设备。可以调用write()、read()\\readLine()和getChar()等标准输入输出进行读写。

1.2 启动一个记事本进程并显示进程各个状态的例子

        该例子的功能是点击按钮后,启动记事本进程,打开桌面上的一个记事本文档。

关联QProcess的readyRead,stateChanged,errorOccurred,finished信号到自定义槽,在自定义槽中,显示进程的各个状态。

头文件源码

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QProcess>
#include <QSharedMemory>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();
    void showResult();
    void showState(QProcess::ProcessState);
    void showError();
    void showFinish(int,QProcess::ExitStatus);

private:
    Ui::MainWindow *ui;
    QProcess myProcess;
};
#endif // MAINWINDOW_H

cpp源码

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QTextCodec>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    connect(&myProcess,&QProcess::readyRead,this,&MainWindow::showResult);
    connect(&myProcess,&QProcess::stateChanged,this,&MainWindow::showState);
    connect(&myProcess,&QProcess::errorOccurred,this,&MainWindow::showError);
    connect(&myProcess,SIGNAL(finished(int,QProcess::ExitStatus)),this,SLOT(showFinish(int,QProcess::ExitStatus)));
}

MainWindow::~MainWindow()
{
    delete ui;
}


void MainWindow::on_pushButton_clicked()
{
    QString program="notepad.exe";
    QStringList arguments;
    arguments<<"C:/Users/Administrator/Desktop/processtest.txt";
    myProcess.start(program,arguments);
}

void MainWindow::showResult()
{
    QTextCodec* codec=QTextCodec::codecForLocale();
    qDebug()<<"showResult:"<<endl<<codec->toUnicode(myProcess.readAll());
}

void MainWindow::showState(QProcess::ProcessState state)
{
    qDebug()<<"showState:";
    if(state==QProcess::NotRunning)
    {
        qDebug()<<"Not Running";
    }
    else if(state==QProcess::Starting)
    {
        qDebug()<<"Not Starting";
    }
    else
    {
        qDebug()<<"Running";
    }
}

void MainWindow::showError()
{
    qDebug()<<"showError:"<<endl<<myProcess.errorString();
}

void MainWindow::showFinish(int exitCode, QProcess::ExitStatus exitStatus)
{
    qDebug()<<"showFinished:"<<endl<<exitCode<<exitStatus;
}

点击按钮运行效果如下,打开了myProcess.start()所用参数指定的记事本文档。

同时输出了进程的各个状态,如下图

关闭记事本后,控制台输出Not Running,showFinished 以及退出状态,如下图。

1.3进程间通信

        Qt提供了多种方法在Qt应用程序中实现进程间通信IPC(Inter-Process Communication):

1)TCP/IP

        跨平台的Qt Network模块提供了众多的类来实现网络编程。它提供了高层的类(比如QNetworkAccessManager等)来使用指定的应用程序级协议,也提供了较低层的类(例如,QTcpSocket、QTcpServer和QSslSocket)来实现相关协议。

2)共享内存

        QSharedMemory是跨平台的共享内存类,提供了访问操作系统共享内存的实现。它允许多个线程和进程安全地访问共享内存段。

3)D-Bus

        D-Bus模块是一个Unix库,可以使用D-Bus协议来实现进程间通信。它将Qt的信号和槽机制扩展到了IPC层面,允许从一个进程发射的信号关联到另一个进程的槽上

4 )QProcess

5)会话管理

        在Linux/X11平台上,Qt提供了对会话管理的支持,会话允许事件传播到进程。

1.4、使用共享内存实现进程间通信示例代码

         该例子的功能是启动两个应用程序(即启动了两个进程),一个程序点击“从文件加载图片”按钮,会将图片加载到共享内存中,同时在label上显示。在另一个应用程序(另一个进程中),点击“从内存加载图片”按钮,从共享内存中读取图片并显示到label上。这样就实现了两个进程间的通信。

 几个重要的接口

1)在使用共享内存以前,需要先指定一个key,系统用它来作为底层共享内存端的标志。

见下面源码构造函数中sharedMemory.setKey("QSharedMemoryExample");代码。

2)调用isATTached()函数来判断进程是否已经连接到共享内存段。

3)使用deatch()接口将进程与共享内存段进行分离。

4)使用create()函数来创建指定大小的共享内存片段,大小单位是字节,该函数还会自动将共享内存段连接到本进程上。

5)在进行共享内存段的操作时迅速要先进行加锁,即调用lock()函数。操作完成后再调用unlock()函数来进行解锁。

6)使用attach函数将进程连接到共享内存段。

2)~5)见下面源码loadFromFile函数。

6)见下面源码loadFromMemory函数。

头文件代码

#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>
#include <QSharedMemory>

QT_BEGIN_NAMESPACE
namespace Ui { class Dialog; }
QT_END_NAMESPACE

class Dialog : public QDialog
{
    Q_OBJECT

public:
    Dialog(QWidget *parent = nullptr);
    ~Dialog();
public slots:
    void loadFromFile();
    void loadFromMemory();
private slots:
    void on_loadFromFileButton_clicked();

    void on_loadFromSharedMemoryButton_clicked();

private:
    void detach();
private:
    Ui::Dialog *ui;
    QSharedMemory sharedMemory;
};
#endif // DIALOG_H

源文件代码

#include "dialog.h"
#include "ui_dialog.h"
#include <QFileDialog>
#include <QBuffer>
#include <QDebug>

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::Dialog)
{
    ui->setupUi(this);
    sharedMemory.setKey("QSharedMemoryExample");

}

Dialog::~Dialog()
{
    delete ui;
}

void Dialog::loadFromFile()
{
    if(sharedMemory.isAttached())
        detach();
    ui->label->setText(QString::fromLocal8Bit("选择一个图片文件!"));
    QString fileName=QFileDialog::getOpenFileName(0,QString(),QString(),tr("Image(*.png *.jpg"));
    QImage image;
    if(!image.load(fileName))
    {
        ui->label->setText(QString::fromLocal8Bit("选择的文件不是图片,请选择图片文件!"));
        return;
    }
    ui->label->setPixmap(QPixmap::fromImage(image));
    //将图片加载到共享内存
    QBuffer buffer;
    buffer.open(QBuffer::ReadWrite);
    QDataStream out(&buffer);
    out<<image;
    int size=buffer.size();
    if(!sharedMemory.create(size))
    {
        ui->label->setText(QString::fromLocal8Bit("无法创建共享内存段!"));
        return;
    }
    sharedMemory.lock();
    char* to=(char*)sharedMemory.data();
    const char* from=buffer.data().data();
    memcpy(to,from,qMin(sharedMemory.size(),size));
    sharedMemory.unlock();
}

void Dialog::loadFromMemory()
{
    if(!sharedMemory.attach())
    {
        ui->label->setText(QString::fromLocal8Bit("无法连接到共享内存片段\\n请先加载一张图片!"));
        return;
    }
    QBuffer buffer;
    QDataStream in(&buffer);
    QImage image;
    sharedMemory.lock();
    buffer.setData((char*)sharedMemory.constData(),sharedMemory.size());
    buffer.open(QBuffer::ReadOnly);
    in>>image;
    sharedMemory.unlock();
    sharedMemory.detach();
    ui->label->setPixmap(QPixmap::fromImage(image));
}

void Dialog::detach()
{
    if(!sharedMemory.detach())
        ui->label->setText(QString::fromLocal8Bit("无法从共享内存中分离!"));
}


void Dialog::on_loadFromFileButton_clicked()
{
    loadFromFile();
}

void Dialog::on_loadFromSharedMemoryButton_clicked()
{
    loadFromMemory();
}

运行效果如下左边是点击“从文件中加载图片”,右边是点击"从共享中显示图片"按钮

二、线程

        Qt提供了对线程的支持,这包括一组与平台无关的线程类、一个线程安全的发送事件的方式以及跨线程的信号-槽的关联。这些使得可以很轻松地开发可移植的多线程Qt应用程序,可以充分利用多处理器的机器。

2.1 使用QThread启动线程

        Qt中的QThread类提供了与平台无关的线程。一个QThread实例代表了一个在应用中可以独立控制的线程。QThread从run()函数开始执行,默认的run通过调用exec来开启事件循环,并在线程内运行一个Qt事件循环。要创建一个线程,需要子类化QThread,并且重新实现run函数。

        调用start()函数来开始执行某个线程,start()默认调用run()函数。当从run函数返回后,线程执行结束。QThread会在开始、结束和终止时发射started(),finished()和terminated()等信号。也可以使用isFinished()和isRunning()来查询线程的状态。可以使用wait()来阻塞直到线程结束。

        静态函数currentThreadId()和currentThread()可以返回当前执行的线程的标识符,前者返回一个特定的ID,后者返回一个QThread指针。

2.2 同步线程

        Qt中的QMutex、QReadWriteLock、QSemaphore和QWaitCondition类提供了同步线程的方法。

        QMutex提供了一个互斥锁(mutex),在任何时间至多只有一个线程可以获得mutex。互斥锁经常用于对共享数据的访问进行保护。

        QReadWriteLock即读写锁,与QMutex很相似,只不过它对共享数据的访问区分为“读”访问和“写”访问,允许多个线程同时对数据进行“读”访问。

        QSemaphore即信号量,是QMutex的一般化,它用来保护一定数量的相同资源,而互斥锁只能保护一个资源。

        QWaitCondition即条件变量,允许一个线程在一些条件满足时唤醒其他的线程。一个或者多个线程可以被阻塞来等待一个QWaitCondition,从而设置一个用于wakeOne()或者wakeAll()的条件。

2.3 使用信号量做线程同步的示例

        这个例子演示了怎样使用QSemaphore信号量来保护对生产者线程和消费者线程共享的环形缓冲区的访问。生产者向缓冲区写入数据,直到它到达缓冲区的终点,这时它会从起点重新开始,覆盖已经存在的数据。消费者线程读取产生的数据,并将其输出。

        代码如下

#include <QtCore>
#include <stdio.h>
#include <stdlib.h>
#include <QDebug>
#include <QThread>

const int DataSize=10;
const int BufferSize=5;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore useBytes;

class Producer : public QThread
{
public:
    void run();
};
void Producer::run()
{
    qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
    for(int i=0;i<DataSize;i++)
    {
        freeBytes.acquire();
        buffer[i%BufferSize]="ACGT"[(int)qrand()%4];
        qDebug()<<QString("producer:%1").arg(buffer[i%BufferSize]);
        useBytes.release();
    }
}

class Customer : public QThread
{
public:
    void run();
};

void Customer::run()
{
    for(int i=0;i<DataSize;i++)
    {
        useBytes.acquire();
        qDebug()<<QString("customer:%1").arg(buffer[i%BufferSize]);
        freeBytes.release();
    }
}

int main(int argc,char* argv[])
{
    QCoreApplication app(argc,argv);
    Producer producer;
    Customer customer;
    producer.start();
    customer.start();
    producer.wait();
    customer.wait();
    return app.exec();
}

        上述代码为了同步生产者和消费者,需要使用两个信号量。其中freeBytes信号量控制缓冲区的空闲区域。useBytes信号量控制已经使用了的缓冲区域。

        QSemaphore::acquire()函数用于获得一定数量的资源,默认获得一个资源。使用QSemaphore::release()函数释放信号量。

        程序执行过程:最初,只有生产者线程可以执行,消费者线程被阻塞来等待useBytes信号量被释放,一旦生产者将一个字节放入缓冲区,freeBytes的可用大小,即freeBytes.availabel()返回BufferSize-1,而useBytes.avaiable()返回1。这时可能发生两件事情:或者消费者线程读取这个字节,或者生产者线程产生第二个字节。两个线程的执行顺序是无法确定的。

        程序运行结构如下

2.4 可重入与线程安全

        1)一个线程安全的函数可以同时被多个线程调用,即便是它们使用了共享数据。因为该共享数据的所有实列都被序列化了。

        2)一个可重入的函数也可以同时被多个线程调用,但是只能是在每个调用使用自己的数据的情况下。

        通俗讲,对于一个类,如果在多个线程中都可以使用这个类,每个线程调用的是不同的实列,则该类是可重入的。如果多个线程使用的是同一个实例,该类的成员函数也可以被多个线程安全地调用,那么这个类被称为线程安全的。

 

以上是关于我的QT Creator学习笔记(三十六)——进程和线程的主要内容,如果未能解决你的问题,请参考以下文章

我的QT Creator学习笔记(三十五)——网络编程之UDP与TCP

我的QT Creator学习笔记(三十五)——网络编程之UDP与TCP

我的QT Creator学习笔记(三十四)——网络编程之HTTP与FTP

我的QT Creator学习笔记(三十四)——网络编程之HTTP与FTP

Zabbix学习笔记(三十六)

C++Widgets编程(《Qt Creator快速入门》 第3版 学习笔记 )