QThread Qt

QThread Qt


函数moveToThread()

函数原型:

void QObject::moveToThread(QThread *targetThread);
该函数用来改变对象的线程依附性,及该对象所属的线程,改变线程后,该对象的事件循环将在目标线程继续运行(对象收到的事件,发送的事件,都将通过目标线程的QThread::exec()事件循环处理),一定要注意,该对象的子对象所属线程也会随次线程改变,另外,调用次函数之前,不能为此对象指定父对象,详细参见Qt Assistant。

验证:

子类化QThread为ProcessThread类,并在其构造函数中new一个MyProcess类对象processMgmt,test对象是processMgmt的子对象,调用moveToThread()函数之后输出所属线程对象的指针:

ProcessThread::ProcessThread(QObject *parent) : QThread(parent)
{
    this->processMgmt = new MyProcess;
    processMgmt->moveToThread(this);
    qDebug() << "processMgmt's thread pointer is " << processMgmt->thread();
    qDebug() << "processMgmt->test's thread pointer is " << processMgmt->test->thread();
    qDebug() << "qApp's thread pointer is " << QApplication::instance()->thread();
    this->start();
}

运行结果为:

processMgmt's thread pointer is  ProcessThread(0x589071f7d8)
processMgmt->test's thread pointer is  ProcessThread(0x589071f7d8)
qApp's thread pointer is  QThread(0x1b40c2dc150)

可以明显看出子对象和父对象的线程为一个线程,并且和主线程不是一个线程,括号中的值是所属线程对象的指针。

另外,请注意一点,如果类中存在成员对象(其他类的对象,但是是该类的成员,上述test对象就是processMgmt的成员对象,但同时test是processMgmt的子对象),且该成员对象不是此类的子对象,则该成员对象的所属线程不变,为了方便说明,我们还是给出test和processMgmt对象所对应的类定义:

myprocess.h文件:

#ifndef MYPROCESS_H
#define MYPROCESS_H

#include <QProcess>

class Test :public QObject
{
    Q_OBJECT
public:
    Test(QObject * parent = NULL):QObject(parent)
    {}
};

class MyProcess : public QProcess
{
    Q_OBJECT
public:
    explicit MyProcess(QObject *parent = 0);
    Test test;
signals:

public slots:
};

#endif // MYPROCESS_H

myprocess.cpp文件:

#include "myprocess.h"

MyProcess::MyProcess(QObject *parent) : QProcess(parent), test(this)
{
}

上述两文件对应第一种情况,test是processMgmt对象的子对象,只需要将myprocess.cpp文件稍作修改,就可验证第二种情况:

#include "myprocess.h"

MyProcess::MyProcess(QObject *parent) : QProcess(parent)
{
}

运行结果为:

processMgmt's thread pointer is  ProcessThread(0x3f0ccff938)
processMgmt->test's thread pointer is  QThread(0x15c3b1c08d0)
qApp's thread pointer is  QThread(0x15c3b1c08d0)

可以看出,当类成员对象不是子对象时,类成员对象的所属线程不会随它所属的类的线程的改变而改变。


线程间信号的传递

调用另一个线程的函数(一般为另一个线程上的对象的成员函数),可以采用信号与槽,QMetaObject::invoke(),connect连接方式采用非阻塞连接时(Qt::QueuedConnection),信号与槽函数参数的传递尽量不要传递引用,或指针,  (因为非阻塞也就是两线程无任何同步机制,同时操作同一块无线程安全保护的内存区域是很危险的);尽量采用值传递(QMetaObject::invoke在非阻塞调用时也要如此),参数为自己定义的对象时,还要使用宏qRegisterMetaType()进行类型注册,详见:http://blog.csdn.net/faith_yu/article/details/53283941;下面我贴出线程间无任何安全措施传递引用的代码:

daemon.h文件:

class Daemon : public QObject
{
    Q_OBJECT
public:
    explicit Daemon(const QStringList &, QObject *parent = 0);
    ~Daemon();
    void setProStatus(bool status) { isRunning = status;}
    bool proStatus() { return isRunning;}


signals:
    void restartFailed(QString&);
    void noPythonEnv();
    void closeFailed(QString&);
    void readyRead(QString&, QString&);
    void startRun(QString &);
    void terminateRun(QString &);

public slots:
    void startAllPro();
    void closeAllPro();
    void setProPaths(const QStringList );
private slots:
    void output();

private:
    QMap<QProcess *, QString> proMap;
    QList<QProcess *> proList;
    time_t * preTime;
    int * crashTimes;
    void proExitHandler(/*int exitCode, QProcess::ExitStatus exitStatus*/);
    bool isRunning = false;
};

daemon类的用途是用来做守护进程,监视其它进程是否crash,如果crash则重新启动它,daemon类对象一般单独放在一个线程,但其所有的槽函数都不是线程安全的,使用时注意调用方式,我们先不关心它的功能,首先注意到它的signals参数全为QString& 引用类型,这注定daemon是一个失败的类,MainWindow类信号连接方式如下:

    connect(daemonThread->daemon, &Daemon::restartFailed, this, &MainWindow::daemonRstFailedHandler);//, Qt::BlockingQueuedConnection);
    connect(daemonThread->daemon, &Daemon::noPythonEnv, this, &MainWindow::daeNoPyHandler);//, Qt::BlockingQueuedConnection);
    connect(daemonThread->daemon, &Daemon::readyRead, this, &MainWindow::outToTE);//, Qt::BlockingQueuedConnection);

注意,connect都是以非阻塞的方式连接(使用阻塞的方式连接可能允许线程间的引用参数传递,因为主线程操作子线程内存区域时,子线程一直处于阻塞状态,但是这样就留下了隐患)

MainWindow槽函数对应如下:

void MainWindow::outToTE(QString & serPath, QString & message)
{
    QPlainTextEdit * temp = mapComb[mapPaths[serPath]]->text;
    QTextCursor textCursor = temp->textCursor();
    textCursor.movePosition(QTextCursor::End);
    textCursor.insertText(message);
    QScrollBar * scrollBar = temp->verticalScrollBar();
    if(scrollBar)
    {
        scrollBar->setSliderPosition(scrollBar->maximum());
    }
}

void MainWindow::daeNoPyHandler()
{
    myMessageBox(this, QMessageBox::Warning, QStringLiteral("警告") ,QStringLiteral("无法启动python"),\
                 QStringLiteral("请检查系统是否安装python并将python放入系统环境变量"));
    qApp->quit();
}

void MainWindow::daemonRstFailedHandler(QString & serverPath)
{
    myMessageBox(this, QMessageBox::Warning, QStringLiteral("警告"), QStringLiteral("已尝试多次重启服务,均已失败,请确认服务路径正确并重启程序"),\
                 QStringLiteral("出错服务为") + serverPath);
    qApp->quit();
}
槽函数中对应的参数也是引用类型,这会导致很严重的后果,但是还好我在遭遇真正的错误之前,被另一个小错误救了一命,运行时并没有出现非法访问内存的情况,而是信号发送失败:
QObject::connect: Cannot queue arguments of type 'QString &' (Make sure 'QString &' is registed using qRegisterMetaType().) 
解决这个问题可以参考上一点所讲,调用qRegisterMetaType()对QString&类型进行注册(qt将引用类型当做自定义类型),但是如果你没有意识到掩藏在这个小错误之后的重大问题,那么程序将会真正崩溃,真正的解决办法只需将槽函数定义和声明以及信号的声明参数类型重QString&改为QString即可。


线程死锁

两个线程之间的信号与槽传递一定要避免出现死锁,着用情况很容易出现,例如Qt::BlockingQueuedConnection下,发送者和接受者在同一个线程时(应该禁用这种方式),会导致线程死锁,另外如果接收信号的对象所在线程没有事件循环,也会导致发送信号对象所在线程死锁,因为发送的信号无法被事件循环处理,而发送信号的线程却在等待处理结果,所以死锁;

我曾经遇见过一种很隐蔽的情况,子线程发送异常信号给主线程(阻塞的方式),主线程收到信号退出程序,但是退出前delte 自定义线程类DaemonThread(它实际属于主线程),MyThread的析构函数中调用了this->quit();this->wati();这两个函数实际作用是向子线程发送退出信号,并等待退出,但此时子线程事件循环正在阻塞,因为它以阻塞方式发送的异常信号给主线程,这就导致了死锁,所以有些死锁的情况是难以发现的,所以使用时要格外小心,上述死锁过程过程代码:

与子线程restartFailed信号对应的主线程的处理函数:

void MainWindow::daemonRstFailedHandler(QString serverPath)
{
    myMessageBox(this, QMessageBox::Warning, QStringLiteral("警告"), QStringLiteral("已尝试多次重启服务,均已失败,请确认服务路径正确并重启程序"),\
                 QStringLiteral("出错服务为") + serverPath);
    delete daemonThread;//导致死锁
    qApp->quit();
}

DaemonThread类的析构函数:

DaemonThread::~DaemonThread()
{
    daemon->deleteLater();
    this->quit();
    this->wait();
}

信号连接:

connect(daemonThread->daemon, &Daemon::restartFailed, this, &MainWindow::daemonRstFailedHandler, \
        Qt::BlockingQueuedConnection);

使用QThread类的两种常用形式

1.使用moveToThread()函数

这种情况主要用于在子线程处理需要事件循环的类对象,此时一般不再重写QThread::run()函数,QThread::run()的原始定义中只有一条语句:

void QThread::run()
{
    (void) exec();
}

它只是打开了线程的事件循环,这种情况下,我们可以子类化QThread类,也可以直接实例化QThread类型的对象,然后将对象作为moveToThread()函数的参数;

a.直接使用QThread实例化对象
class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller() {
        Worker *worker = new Worker;
        worker->moveToThread(&workerThread);
        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork);
        connect(worker, &Worker::resultReady, this, &Controller::handleResults);
        workerThread.start();
    }
    ~Controller() {
        workerThread.quit();
        workerThread.wait();
    }
public slots:
    void handleResults(const QString &);
signals:
    void operate(const QString &);
};

这是一个来自《Qt Creator快速入门》的例子,Worker为笔者自定义的类,以第一个connect语句为例,其实现的效果:当依附于主线程的对象workerThread发出信号finished时worker的槽函数deleteLater()被异步调用,并在子线程上执行,这里要明确一点:workerThread对象是依附于主线程的,只是对象的run()函数运行于子线程,子线程上的run()函数类似于主线程的main()函数;

b.另外也可以子类化QThread对象,但是同样不重写run()函数:

这里使用自己实现的用于守护Python脚本运行的Daemon类作为举例,在讲述线程间的信号传递时用到过bad版Daemon类。注意,Daemon类的除 构造函数的所有函数都不是线程安全的,切勿直接在另一线程直接调用(可以尝试使用QMetaObject::invoke()函数),在另一篇讲述QProcess类的博文中将贴出Daemon的实现,并具体讲述:

子类化QThread类为DaemonThread,daemonthread.文件:

#ifndef DAEMONTHREAD_H
#define DAEMONTHREAD_H

#include <QThread>

class Daemon;

class DaemonThread : public QThread
{
    Q_OBJECT
public:
    explicit DaemonThread(QObject *parent = 0);
    ~DaemonThread();
    Daemon * daemon;

signals:

public slots:
};

#endif // DAEMONTHREAD_H

daemon.cpp

#include "daemonthread.h"
#include <QThread>
#include "daemon.h"

DaemonThread::DaemonThread(QObject *parent) : QThread(parent)
{
    daemon = new Daemon(QStringList());
    daemon->moveToThread(this);
    //daemon->setProPaths(list);//切勿在主线程直接调用依附于子线程的daemon对象的成员函数,可以尝试使用QMetaObject::invoke()函数
    this->start();
}

DaemonThread::~DaemonThread()
{
    daemon->deleteLater();
    this->quit();
    this->wait();
}

这种情况下主要在QThread子类(DaemonThread)的构造函数和析构函数中假如自己的处理,更加方便对依附于子线程的Daemon类对象进行管理。

2.子类化QThread并重写run()函数

这时我们一般不开启子线程的事件循环,我们可以通过其它机制和子线程交互。

这里我们使用《C++ GUI Qt4 Programming》中线程章节给出的框架进行讲解,框架的大致结构是将费时费力的运算打包成一个类,我们称之为算法类,算法类继承于同一个基类,区别在于对apply()虚函数的重写以及类数据成员,子线程中只需调用算法类的apply函数即可实现多态;子线程和主线程交互通过共同维护一个算法类的队列,当队列为空时子线程休眠,当主线程向队列添加新的算法类对象时,子线程被唤醒,处理完毕后接着休眠,当主线程向队列添加的算法类对象包含结束信号时,子线程结束。

transactionthread.h文件:

#ifndef TRANSACTIONTHREAD_H
#define TRANSACTIONTHREAD_H

#include <QMutex>
#include <QQueue>
#include <QThread>
#include <QWaitCondition>

class Transaction
{
public:
    virtual ~Transaction() { }
    virtual void apply() = 0;
private:
};

class LocationTvTransaction : public Transaction
{
private:
    QString openSourceFileName = QString::null;
    QString tempJpgFileName = QString::null;
    QString saveResultFileName = QString::null;
public:
    LocationTvTransaction(QString openSourceFileName, QString tempJgpFileName, QString saveResultFileName);
    void apply();
};

/*class OnlineTvTransaction : public Transaction
{
private:
    QString saveSourceFileName = QString::null;
    QString saveResultFileName = QString::null;
public:
    OnlineTvTransaction(QString saveSourceFileName, QString saveResultFileName);
    void apply();
};*/

class TransactionThread : public QThread
{
    Q_OBJECT

public:
    TransactionThread();
    ~TransactionThread();

    void addTransaction(Transaction *transact);

signals:
//    void transactionStarted(const QString &message);
    void allTransactionsDone(QString);

protected:
    void run();

private:
    QQueue<Transaction *> transactions;
    QWaitCondition transactionAdded;
    QMutex mutex;
};

#endif

Transaction类为算法类的基类,同时也是一个抽象类,LocationTvTransaction类为具体的算法类,主要功能是处理本地视频,OnlineTvTransaction类为处理在线视频的算法类,这里还未实现,TransactionThread类为QThread的子类,其中QQueue<Transaction *>transactions成员就是要维护的算法类对象队列;

#include "transactionthread.h"
#include <string>
#include <QFileInfo>
#include <iostream>
#include <exception>
#include <qDebug>
#include <iostream>

Transaction * const EndTransaction = 0;

void LocationTvTransaction::apply()
{
    bool ret = true;
    ret = false;
    ret = choose_mat(openSourceFileName.toStdString(), tempJpgFileName.toStdString(), 2);
    if(ret == false)
    {
        qCritical("choose_mat failed!");
        return;
    }
    ret = choose_with_color(tempJpgFileName.toStdString(), saveResultFileName.toStdString());
    if(ret == false)
    {
        qCritical("choose_with_color failed!");
    }
}

LocationTvTransaction::LocationTvTransaction(QString openSourceFileName, QString tempJpgFileName, QString saveResultFileName)
{
    this->openSourceFileName = openSourceFileName;
    this->tempJpgFileName = tempJpgFileName;
    this->saveResultFileName = saveResultFileName;
}

TransactionThread::TransactionThread()
{
    start();
}

TransactionThread::~TransactionThread()
{
    {
        QMutexLocker locker(&mutex);
        while (!transactions.isEmpty())
            delete transactions.dequeue();
        transactions.enqueue(EndTransaction);
        transactionAdded.wakeOne();
    }
    wait();
}

void TransactionThread::addTransaction(Transaction *transact)
{
    QMutexLocker locker(&mutex);
    transactions.enqueue(transact);
    transactionAdded.wakeOne();
}

void TransactionThread::run()
{
    Transaction *transact = 0;
    forever
    {
        {
            QMutexLocker locker(&mutex);
            while (transactions.isEmpty())
                transactionAdded.wait(&mutex);
            transact = transactions.dequeue();
            if (transact == EndTransaction)
                break;
        }
        transact->apply();
        delete transact;
        {
            QMutexLocker locker(&mutex);
            if (transactions.isEmpty())
            {
                emit allTransactionsDone("result");
            }
        }
    }
}
LocationTvTransaction类的apply()函数调用了外部库提供的两个处理视频文件的函数,这里主要看Transaction::run()函数和Transaction::addTransaction()函数的实现,主线程和子线程的交互主要在这里实现,run函数中是一个死循环,操作队列之前,先上锁,然后检测队列是否为空,如果为空,调用transactionAdded.wait(&mutex)临时解锁,并休眠线程,等待主线程添加算法类对象到队列,主线程调用addTransaction()函数添加对象到队列后,会调用transaction::wakeOne()函数唤醒子线程,子线程唤醒后先重新获取mutex锁,锁定之后transactionAdded.wait(&mutex)函数才会返回,这样可以保证线程永久安全,返回之后会重新检测是否为空,这里一定不会为空,所以while可以改成if,之后子线程开始算法处理。


 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值