在Qt程序中如何优雅地实现线程切换

程序开发过程中,我们避免不了使用线程来执行耗时的操作,最常见的场景是启动线程执行耗时操作同时显示loading画面,当耗时操作完成时关闭loading界面。这样简单的操作中也涉及到了线程切换的动作。首先显示loading画面代码需要执行在ui线程,然后耗时操作执行在子线程。显示loading画面-》切换子线程-》执行耗时操作-》切换ui线程-》关闭loading画面。

Qt中有一个重要的概念是信号和槽,使用信号和槽我们可以实现消息数据的传递,当然这种传递也包括线程间的消息数据传递。槽指的是槽函数,槽函数运行的线程由它所在的QObject绑定的线程决定的。我们可以通过调用moveToThread方法 改变QObject对象绑定的线程。槽通过connect方法连接一个信号后,发射信号和槽函数执行可以分别在两个线程中,进而实现了线程的切换。

我们通过信号和槽可以实现线程的切换,但是在使用的时候还是不够灵活。比如程序中异步执行的操作肯定很多,如果为每个异步操作都定义一份自己的信号与槽,那么信号和槽的数量会特别的多,过多的信号和槽使得程序的执行流程过于混乱。

为了解决异步操作定义导致的信号与槽数量过多的问题,我们需要基于信号与槽的技术定义一个共同的处理流程,避免为异步操作定义定制化的信号与槽。

首先我将操作抽象成一个Worker类,Worker具体的执行内容由 Lambda 函数来指定。

class Worker : public QObject {
  Q_OBJECT
protected:
  Worker(QObject *, std::function<void()> fun, QString name = "Worker");
  Worker(QObject *, QString name = "Worker");
  ~Worker();

public:

  virtual void doWork(std::function<void()> completeFun = nullptr);
}

在创建Worker的时候用户需要传入一个lambda函数,这个函数会在doWork方法中调用。我们看下doWork方法的实现:

void Worker::doWork( std::function<void()> completeFun )
{
    tracker( "Worker", "doWork" );
    if( _fun )
    {
        _fun();
    }
    if( completeFun )
    {
        completeFun();
    }
}

doWork方法的实现比较简单,首先它执行了构造函数传入的lambda函数,然后调用doWork的lambda函数参数。这里的调用都是顺序执行的,没有发生线程的切换。为了支持线程切换,我创建了一个ThreadWorker类继承至Worker类。

class ThreadWorker : public Worker
{
    Q_OBJECT
public:
    ThreadWorker( Worker*, QThread* );
    ~ThreadWorker();
    void doWork( std::function<void()> completeFun = nullptr ) override;
signals:
    void run();

private:
    std::shared_ptr<Runnable> _runnable;
};

ThreadWorker类构造时需要两个参数。第一个参数是一个Worker,ThreadWorker的执行就是调用这个Worker的doWork方法。第二个参数是QThread,ThreadWorker将在这个线程中调用worker的doWork方法来实现线程的切换。我们看下ThreadWorker如何重写的doWork方法。

void ThreadWorker::doWork( std::function<void()> completeFun )
{
    tracker( "ThreadWorker", "doWork" );
    _completeRunnable = std::make_shared<CallbackRunnable>( completeFun, _name + "_CallbackRunnable" );
    connect( _runnable.get(), &Runnable::finished, _completeRunnable.get(), &CallbackRunnable::realRun );
    emit run();
}

doWork方法中为completeFun 函数创建了一个CallbackRunnable,同时CallbackRunnable会连接到Runnable::finished信号。然后发射了一个run()信号。我们发现这里没有执行具体的工作,那么具体工作如何触发执行的呢。我们看下谁处理了run()信号。

ThreadWorker::ThreadWorker( Worker *parentWorker, QThread *outerThread )
    : Worker( parentWorker, "ThreadWorker" )
{
    tracker( "ThreadWorker", "ThreadWorker" );
    auto thread = outerThread == nullptr ? ThreadManager::getSubThread() : outerThread;
    if( thread )
    {
        _runnable = std::make_shared<Runnable>( this->_name + "_Runnable", parentWorker );
        _runnable->moveToThread( thread );
        connect( this, &ThreadWorker::run, _runnable.get(), &Runnable::realRun );
    }
}

在ThreadWorker的构造实现中我们看到了_runnable会处理ThreadWorker::run信号。这里有一点需要注意, _runnable对象被绑定到了子线程,这样_runnable的槽函数都会执行在这个线程了。我们看下Runnable::realRun的实现:

void Runnable::realRun()
{
    tracker( "Runnable", "realRun _originalWorkder " << _originalWorkder );
    if( _originalWorkder )
    {
        _originalWorkder->doWork( [=]() -> void { sendFinish(); } );
    }
}

Runnable::realRun调用了worker的doWork方法,这样保证了worker的工作在子线程中执行的。为了保证ThreadWorker的completeFun函数在doWork方法执行的线程中执行,CallbackRunnable通过槽函数CallbackRunnable::realRun来接收Runnable::finished信号,将子线程的回调切换到ThreadWorker的doWork方法执行线程。

到这里我们把异步执行和线程切换的实现讲解完了,为了让异步执行和线程切换更灵活,我采用了链式调用的方式支持用户灵活切换线程。

class Worker : public QObject
{
    Q_OBJECT
protected:
    Worker( QObject*, std::function<void()> fun, QString name = "Worker" );
    Worker( QObject*, QString name = "Worker" );
    ~Worker();

    Worker* findRootWorker();

public:
    static Worker* workerOf( std::function<void()> fun, QObject* = nullptr, QString name = "workerOf" );

    virtual void doWork( std::function<void()> completeFun = nullptr );

    Worker* concat( std::function<void()> );

    Worker* concat( Worker* );

    Worker* workOnSubThread( QThread* = nullptr );

    Worker* workOnMainThread();

    Worker* endWork();

signals:

protected:
    QString _name;
    std::function<void()> _fun = nullptr;
    std::shared_ptr<CallbackRunnable> _completeRunnable = nullptr;
};
  1. workerOf方法可以认为是一个builder,它使用fun函数创建了一个简单的工作。
  2. concat方法用于连接多个顺序执行的worker。如果直接连接多个worker实际意义不大,但是在连接的多个worker间发生线程切换的话,它就很重要了。
  3. workOnSubThread方法可以切换线程,配合concat方法可以使多个worker执行在不同的线程。
  4. workOnMainThread方法指定worker切换到主线程执行。
  5. endWork方法表示worker执行结束后释放自己。

下面的代码简单测试了worker的主要功能:

Worker::workerOf([]() -> void {
		//运行在线程subThread1
    qDebug() << "worker1 start++++++ " << QThread::currentThread();
    QThread::msleep(1000);
    qDebug() << "worker1 end++++++ " << QThread::currentThread();
  })
      ->workOnSubThread(&subThread1)
      ->concat([]() -> void {
				//运行在线程subThread2
        qDebug() << "worker1 concat work start++++++ "
                 << QThread::currentThread();
        QThread::msleep(1000);
        qDebug() << "worker1  concat work end++++++ "
                 << QThread::currentThread();
      })
      ->workOnSubThread(&subThread2)
			//运行在ThreadManager::getSubThread
      ->concat(generateWorker()->endWork())
      ->workOnSubThread(ThreadManager::getSubThread())
      ->endWork()
      ->doWork([]() -> void {
				//这个函数运行在调用doWork方法的线程
        qDebug() << "worker1 complete thread id " << QThread::currentThread();
      });

这里为了充分展示worker切换线程的灵活性,我通过workOnSubThread方法多次切换线程,注释中标注了各个部分执行在什么线程。

worker也是通过信号与槽实现的线程切换,但是我们不需要为每个异步操作都定义响应的信号与槽。worker内部封装了信号与槽通信的共通部分,同时它还通过lambda函数方式自定义了worker的具体工作内容,我们使用worker实现异步和线程切换更加灵活,并且不需要为新的异步工作定义新的类。下面提供了详细的代码,感兴趣的朋友可以下载下来运行。

git地址:https://github.com/mjlong123123/TestWorker

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Qt Designer实现界面切换,可以通过以下步骤: 1. 在Qt Designer设计两个或多个需要切换的界面,每个界面作为一个QWidget。 2. 在第一个界面添加一个QPushButton,并在其clicked信号上连接一个函数。 3. 在函数使用QStackedWidget控件来实现界面切换。QStackedWidget控件可以在运行时动态地添加和删除QWidget,而且只会显示当前活动的QWidget。 4. 在函数,使用setCurrentIndex()函数设置QStackedWidget控件当前显示的QWidget索引号,以实现界面切换。 以下是一个示例代码,其有两个QWidget界面,分别命名为page1和page2,通过一个QPushButton实现从page1切换到page2: ```python from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QMainWindow, QApplication, QStackedWidget from ui_page1 import Ui_Page1 from ui_page2 import Ui_Page2 class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) # create QStackedWidget and add the pages self.stackedWidget = QStackedWidget() self.page1 = Ui_Page1() self.page2 = Ui_Page2() self.stackedWidget.addWidget(self.page1) self.stackedWidget.addWidget(self.page2) # set the central widget to the stacked widget self.setCentralWidget(self.stackedWidget) # connect button clicked signal to slot function self.page1.pushButton.clicked.connect(self.switchPage) @pyqtSlot() def switchPage(self): # switch to page2 self.stackedWidget.setCurrentIndex(1) ``` 注意,在此示例,使用了两个单独的UI文件,分别为ui_page1.py和ui_page2.py,需要在代码导入这些文件并将它们添加到QStackedWidget

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mjlong123123

你的鼓励时我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值