Qt :线程

在多核时代,随着 CPU 主频的提升,受制于发热量的限制,CPU 散热问题已经进入瓶颈,另辟蹊径地提高程序运行效率就是使用线程,充分利用多核的优势。

Qt 中所有界面都是在 UI 线程中(也被称为主线程,就是执行了QApplication::exec()的线程),在这个线程中执行耗时的操作(比如那个循环),就会阻塞 UI 线程,从而让界面停止响应。

///!!! Qt5
class WorkerThread : public QThread
{
    Q_OBJECT
public:
    WorkerThread(QObject *parent = 0)
        : QThread(parent)
    {
    }
protected:
    void run()
    {
        for (int i = 0; i < 1000000000; i++);
        emit done();
    }
signals:
    void done();
};

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QWidget *widget = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout;
    widget->setLayout(layout);
    lcdNumber = new QLCDNumber(this);
    layout->addWidget(lcdNumber);
    QPushButton *button = new QPushButton(tr("Start"), this);
    layout->addWidget(button);
    setCentralWidget(widget);

    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, [=]() {
        static int sec = 0;
        lcdNumber->display(QString::number(sec++));
    });

    WorkerThread *thread = new WorkerThread(this);
    connect(thread, &WorkerThread::done, timer, &QTimer::stop);
    connect(thread, &WorkerThread::finished, thread, &WorkerThread::deleteLater);
    connect(button, &QPushButton::clicked, [=]() {
        timer->start(1);
        thread->start();
    });
}

注意,增加了一个WorkerThread类。WorkerThread继承自QThread类,重写了其run()函数。我们可以认为,run()函数就是新的线程需要执行的代码。在这里就是要执行这个循环,然后发出计算完成的信号。而在按钮点击的槽函数中,使用QThread::start()函数启动一个线程(注意,这里不是run()函数)。再次运行程序,你会发现现在界面已经不会被阻塞了。另外,我们将WorkerThread::deleteLater()函数与WorkerThread::finished()信号连接起来,当线程完成时,系统可以帮我们清除线程实例。这里的finished()信号是系统发出的,与我们自定义的done()信号无关。

这是 Qt 线程的最基本的使用方式之一。Qt 多线程的优势设计使得它使用起来变得容易,但是坑很多,稍不留神就会被绊住,尤其是涉及到与 QObject 交互的情况。

认识两个术语:

  • 可重入的(Reentrant):如果多个线程可以在同一时刻调用一个类的所有函数,并且保证每一次函数调用都引用一个唯一的数据,就称这个类是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多数 C++ 类都是可重入的。类似的,一个函数被称为可重入的,如果该函数允许多个线程在同一时刻调用,而每一次的调用都只能使用其独有的数据。全局变量就不是函数独有的数据,而是共享的。换句话说,这意味着类或者函数的使用者必须使用某种额外的机制(比如锁)来控制对对象的实例或共享数据的序列化访问。
  • 线程安全(Thread-safe):如果多个线程可以在同一时刻调用一个类的所有函数,即使每一次函数调用都引用一个共享的数据,就说这个类是线程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。如果多个线程可以在同一时刻访问函数的共享数据,就称这个函数是线程安全的。

对于一个类,如果不同的实例可以被不同线程同时使用而不受影响,就说这个类是可重入的;如果这个类的所有成员函数都可以被不同线程同时调用而不受影响,即使这些调用针对同一个对象,那么我们就说这个类是线程安全的。由此可以看出,线程安全的语义要强于可重入。接下来,我们从事件开始讨论。之前我们说过,Qt 是事件驱动的。在 Qt 中,事件由一个普通对象表示(QEvent或其子类)。这是事件与信号的一个很大区别:事件总是由某一种类型的对象表示,针对某一个特殊的对象,而信号则没有这种目标对象。所有QObject的子类都可以通过覆盖QObject::event()函数来控制事件的对象。

事件可以由程序生成,也可以在程序外部生成。例如:

  • QKeyEventQMouseEvent对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;
  • QTimerEvent事件在定时器超时时发送给一个QObject,定时器事件通常由操作系统发出;
  • QChildEvent在增加或删除子对象时发送给一个QObject,这是由 Qt 应用程序自己发出的。

事件循环的伪代码描述大致如下所示:

while (is_active)
{
    while (!event_queue_is_empty) {
        dispatch_next_event();
    }
    wait_for_more_events();
}

猜你喜欢

转载自blog.csdn.net/u013288800/article/details/81385702