在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
    评论
### 回答1: 在Qt,UDP客户端可以通过多线程实现Qt提供了一个QThread类,可以用于创建多线程。可以通过继承QThread类并重写其run()函数来实现自定义的线程行为。在UDP客户端,可以使用一个独立的线程来处理UDP消息的接收和发送操作。 具体实现步骤如下: 1. 创建一个自定义的线程类,继承QThread。 2. 在该类重写run()函数,在该函数实现UDP消息的接收和发送逻辑。 3. 在主线程,创建该自定义线程类的对象,并调用其start()函数启动线程。 4. 在主线程,通过信号机制与该线程类进行通信,以实现消息接收和发送的交互。 使用多线程的好处是,可以避免在主线程进行耗时的UDP操作,保持主线程的响应能力。同时,多线程还可以提高程序的并发处理能力,更好地利用计算机的多核处理能力。 需要注意的是,在多线程编程,要注意线程之间的数据共享和同步问题,以避免出现并发访问导致的数据竞争或不一致的情况。可以使用Qt提供的线程同步机制,如互斥锁、信号量等,来确保线程安全性。 总而言之,UDP客户端在Qt是可以通过多线程实现的,这能够提高程序的并发处理能力和响应性能。 ### 回答2: 在Qt,UDP客户端可以实现线程Qt是一个跨平台的应用程序开发框架,提供了丰富的多线程支持。使用QtQThread类,我们可以创建和管理多个线程。 在实现UDP客户端的多线程,可以通过创建一个新的QThread对象作为每个客户端的线程,并在该线程执行UDP通信操作。我们可以通过继承QThread类并重写其run()函数来实现自定义的线程逻辑。 在每个线程,我们可以使用QUdpSocket类实现UDP客户端的功能。QUdpSocket提供了发送和接收UDP数据报的方法,我们可以在每个线程分别创建一个QUdpSocket对象并使用该对象与服务器进行通信。 通过将UDP通信逻辑封装在独立的线程,可以实现多个UDP客户端同时与服务器进行通信,而不会阻塞主线程或其他客户端的通信。每个线程可以独立处理与服务器的通信,提高了系统的并发性和响应性。 需要注意的是,多线程编程需要考虑线程的同步和资源管理。Qt提供了一些用于线程间通信和同步的机制,如信号机制、互斥锁和条件变量等。通过合理使用这些机制,可以确保在多线程环境下的UDP客户端的正确运行。 总结而言,在Qt,我们可以使用多线程实现UDP客户端,并通过创建独立的线程和使用QUdpSocket类来实现多个UDP客户端的并发通信。多线程能够提高系统性能和响应性,但同时也需要注意线程间的同步和资源管理。 ### 回答3: 在Qt,UDP客户端可以通过多线程进行实现Qt框架提供了QThread类,可以用来创建和管理多线程。使用多线程的好处是可以同时处理多个UDP套接字的数据收发,提高程序的并发性能。 实现UDP客户端的多线程可以通过创建一个继承自QThread的子类,在子类重写run()函数来实现具体的UDP操作。首先,在子类的构造函数可以初始化UDP套接字,并设置套接字的一些属性,如端口号、目标地址等。然后,在run()函数,通过调用套接字的相关函数来进行数据的发送和接收操作。 在多线程,可以创建多个UDP客户端对象,并分配给不同的线程来执行任务。每个线程可以独立地发送和接收数据,互不干扰。当需要进行大量的数据传输时,使用多线程可以提高整体的传输速度,减少阻塞和等待时间。此外,多线程还可以保持UI界面的流畅性,让用户可以随时进行其他操作。 需要注意的是,在多线程编程要注意线程安全性,避免出现竞争条件和数据一致性问题。可以通过使用互斥锁(QMutex)来对共享资源进行保护,确保在不同线程对数据的访问是安全的。 总之,UDP客户端在Qt是可以实现线程的,通过多线程可以提高程序的并发性能和用户体验。通过合理的线程设计和资源保护措施,可以编写出高效、稳定的UDP客户端。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

mjlong123123

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

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

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

打赏作者

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

抵扣说明:

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

余额充值