QT thread学习2

Choosing between using QThreadPool and QThread

Qt框架提供了许多用于多线程的工具。 一开始选择正确的工具可能很有挑战性,但实际上,决策树只包含两个选项:您希望Qt为您管理线程,或者您希望自己管理线程。 然而,还有其他重要的标准:  

1、不需要事件循环的任务。 具体来说,就是在任务执行过程中不使用信号/槽机制的任务。  

使用:QtConcurrent和QThreadPool + QRunnable。   

2、使用信号/槽的任务,因此需要事件循环。  

使用:Worker对象移动到 + QThread。  

Qt框架的巨大灵活性允许你解决“缺少事件循环”的问题,并添加一个到QRunnable:  

class MyTask : public QObject, public QRunnable
{
    Q_OBJECT
    
public:
    void MyTask::run() {
        _loop.exec();  
    }
    
public slots:
    // you need a signal connected to this slot to exit the loop,
    // otherwise the thread running the loop would remain blocked...
    void finishTask() {
        _loop.exit();
    }
    
private:
    QEventLoop _loop;
}

但是,要尽量避免这种“变通方法”,因为这很危险,而且效率不高:如果线程池中的一个线程(运行MyTask)由于等待信号而被阻塞,那么它就不能执行池中的其他任务。  

 

你也可以通过重写QThread::run()方法在不发生任何事件循环的情况下运行QThread,只要你知道你在做什么,这是完全可以的。 例如,不要期望方法quit()在这种情况下工作。  

 

Running one task instance at a time

假设您需要确保一次只能执行一个任务实例,并且运行同一任务的所有挂起请求都在某个队列上等待。 当任务正在访问独占资源时,例如写入相同的文件或使用TCP套接字发送数据包时,通常需要这样做。   

让我们暂时忘记计算机科学和生产者-消费者模式,考虑一些琐碎的事情; 在真正的项目中很容易找到的东西。   

这个问题的一个原语解决方案可以使用QMutex。 在任务函数中,您可以简单地获取互斥量,有效地序列化所有试图运行任务的线程。 这将保证一次只有一个线程可以运行该函数。 但是,这个解决方案会引入高争用问题,从而影响性能,因为所有这些线程在可以继续进行之前都会被阻塞(在互斥对象上)。 如果有许多线程在积极地使用这样的任务,并在其间做一些有用的工作,那么所有这些线程在大部分时间都将处于睡眠状态。

void logEvent(const QString & event) {  
    static QMutex lock;  
    QMutexLocker locker(& lock);   // high contention!  
    logStream << event;            // exclusive resource  
}  

为了避免争用,我们需要一个队列和一个在自己的线程中处理该队列的worker。 这是典型的生产者-消费者模式。 worker (consumer)将从队列中一个接一个地挑选请求,而每个producer可以简单地将其请求添加到队列中。 一开始听起来很简单,你可能会想到使用QQueue和QWaitCondition,但是等一下,让我们看看我们是否可以在没有这些原语的情况下实现目标:  

  • 我们可以使用QThreadPool,因为它有一个挂起的任务队列  

或   

  • 我们可以使用默认的QThread::run(),因为它有QEventLoop   

第一个选项是使用QThreadPool。 我们可以创建一个QThreadPool实例,并使用QThreadPool::setMaxThreadCount(1)。 然后我们可以使用QtConcurrent::run()来调度请求:  

class Logger: public QObject
{
public:
    explicit Logger(QObject *parent = nullptr) : QObject(parent) {
        threadPool.setMaxThreadCount(1);
    }

    void logEvent(const QString &event) {
        QtConcurrent::run(&threadPool, [this, event]{
            logEventCore(event);
        });
    }

private:
    void logEventCore(const QString &event) {
        logStream << event;
    }

    QThreadPool threadPool;
};

这个解决方案有一个好处:QThreadPool::clear()允许您立即取消所有挂起的请求,例如当您的应用程序需要快速关闭时。 然而,连接到线程私有的logEventCore函数也有一个显著的缺点:从调用到调用,logEventCore函数可能会在不同的线程中执行。 我们知道Qt有一些类需要线程关联:QTimer, QTcpSocket和其他一些类。  

Qt规范中关于线程相关性的说明:定时器在一个线程中启动,不能在另一个线程中停止。 只有拥有套接字实例的线程才能使用这个套接字。 这意味着任何正在运行的线程中你必须停止启动它们的计时器,并且你必须在拥有该套接字的线程中调用QTcpSocket::close()。 这两个例子通常都在析构函数中执行。   

更好的解决方案依赖于使用QThread提供的QEventLoop。 这个想法很简单:我们使用信号/槽机制来发出请求,并且线程内部运行的事件循环将作为一个队列,每次只允许执行一个槽。  

// the worker that will be moved to a thread
class LogWorker: public QObject
{
    Q_OBJECT

public:
    explicit LogWorker(QObject *parent = nullptr);

public slots:
    // this slot will be executed by event loop (one call at a time)
    void logEvent(const QString &event);
};

LogWorker构造函数和logEvent的实现很简单,因此这里没有提供。 现在我们需要一个服务来管理线程和worker实例:  

// interface
class LogService : public QObject
{
    Q_OBJECT
    
public:
    explicit LogService(QObject *parent = nullptr);
    ~LogService();

signals:
    // to use the service, just call this signal to send a request:
    // logService->logEvent("event");
    void logEvent(const QString &event);

private:
    QThread *thread;
    LogWorker *worker;
};

// implementation
LogService::LogService(QObject *parent) : QObject(parent) {
    thread = new QThread(this);
    worker = new LogWorker;
    worker->moveToThread(thread);
    connect(this, &LogService::logEvent, worker, &LogWorker::logEvent);
    connect(thread, &QThread::finished, worker, &QObject::deleteLater);
    thread->start();
}

LogService::~LogService() {
    thread->quit();
    thread->wait();
}

 

 

 让我们来讨论一下这段代码是如何工作的:   

  • 在构造函数中,我们创建了一个thread和worker实例。 注意,worker没有接收父线程,因为它将被移动到新线程。 因此,Qt不能自动释放worker的内存,因此,我们需要通过连接QThread::finished信号来实现这一点。 我们还将代理方法LogService::logEvent()连接到LogWorker::logEvent(),因为不同的线程,LogWorker::logEvent()将使用Qt::QueuedConnection模式。  
  • 在析构函数中,我们将退出事件放入事件循环的队列中。 此事件将在所有其他事件处理之后进行处理。 例如,如果我们在调用析构函数之前调用了数百个logEvent(),日志记录器将在获取退出事件之前处理所有这些调用。 当然,这需要时间,所以我们必须wait()直到事件循环退出。 值得注意的是,退出事件之后发布的所有日志请求将永远不会被处理。  
  • 日志记录本身(LogWorker::logEvent)总是在同一个线程中完成,因此这种方法对于需要线程相关性的类来说工作得很好。 同时,LogWorker构造函数和析构函数是在主线程中执行的(特别是运行LogService的线程),因此,您需要非常小心在主线程中运行的是什么代码。 特别地,不要在worker的析构函数中停止计时器或使用套接字,除非你可以在同一个线程中运行析构函数!  

 

Executing worker’s destructor in the same thread 

如果你的worker处理的是定时器或套接字,你需要确保析构函数在同一个线程中执行(你为worker创建的线程和你移动worker的线程)。 最明显的支持方法是继承QThread并在QThread::run()方法中删除worker。 考虑以下模板:  

template <typename TWorker>
class Thread : QThread
{
public:
    explicit Thread(TWorker *worker, QObject *parent = nullptr)
        : QThread(parent), _worker(worker) {
        _worker->moveToThread(this);
        start();
    }

    ~Thread() {
        quit();
        wait();
    }

    TWorker worker() const {
        return _worker;
    }

protected:
    void run() override {
        QThread::run();
        delete _worker;
    }

private:
    TWorker *_worker;
};

使用这个模板,我们从前面的例子中重新定义LogService:  

// interface
class LogService : public Thread<LogWorker>
{
    Q_OBJECT

public:
    explicit LogService(QObject *parent = nullptr);

signals:
    void **logEvent**(const QString &event);
};

// implementation
LogService::**LogService**(QObject *parent)
    : Thread<LogWorker>(new LogWorker, parent) {
    connect(this, &LogService::logEvent, worker(), &LogWorker::logEvent);
}

让我们来讨论一下这是如何工作的:   

  • 我们将LogService作为QThread对象,因为我们需要实现定制的run()函数。 我们使用私有子类来防止访问QThread的函数,因为我们想要在内部控制线程的生命周期。  
  • 在Thread::run()函数中,我们通过调用默认的QThread::run()实现来运行事件循环,并在事件循环退出后立即销毁工作实例。 注意worker的析构函数是在同一个线程中执行的。  
  • LogService::logEvent()是一个代理函数(信号),它将日志记录事件发布到线程的事件队列中。  

 

Pausing and resuming the threads

另一个有趣的机会是能够暂停和恢复我们的定制线程。 假设您的应用程序正在进行一些处理,当应用程序最小化、锁定或失去网络连接时,这些处理需要暂停。 这可以通过构建一个自定义的异步队列来实现,该队列将保存所有挂起的请求,直到worker被恢复。 然而,由于我们正在寻找最简单的解决方案,我们将(再次)使用事件循环的队列来实现同样的目的。   

为了挂起一个线程,我们显然需要它在一个特定的等待条件下等待。 如果线程以这种方式被阻塞,它的事件循环不会处理任何事件,Qt必须将keep放入队列中。 一旦恢复,事件循环将处理所有累积的请求。 对于等待条件,我们只需使用同样需要QMutex的QWaitCondition对象。 为了设计一个可以被任何工作人员重用的通用解决方案,我们需要将所有挂起/恢复逻辑放到一个可重用的基类中。 我们叫它SuspendableWorker。 这样的类应该支持两个方法:  

  • Suspend()是一个阻塞调用,它设置线程等待一个等待条件。 这可以通过将一个挂起请求提交到队列中并等待它得到处理来实现。 非常类似于QThread::quit() + wait()。  
  • Resume()将向等待条件发出信号,以唤醒处于睡眠状态的线程继续执行。  

让我们回顾一下接口和实现:  

// interface
class SuspendableWorker : public QObject
{
    Q_OBJECT

public:
    explicit SuspendableWorker(QObject *parent = nullptr);
    ~SuspendableWorker();

    // resume() must be called from the outer thread.
    void resume();

    // suspend() must be called from the outer thread.
    // the function would block the caller's thread until
    // the worker thread is suspended.
    void suspend();

private slots:
    void suspendImpl();

private:
    QMutex _waitMutex;
    QWaitCondition _waitCondition;
};
// implementation
SuspendableWorker::SuspendableWorker(QObject *parent) : QObject(parent) {
    _waitMutex.lock();
}

SuspendableWorker::~SuspendableWorker() {
    _waitCondition.wakeAll();
    _waitMutex.unlock();
}

void SuspendableWorker::resume() {
    _waitCondition.wakeAll();
}

void SuspendableWorker::suspend() {
    QMetaObject::invokeMethod(this, &SuspendableWorker::suspendImpl);
    // acquiring mutex to block the calling thread
    _waitMutex.lock();
    _waitMutex.unlock();
}

void SuspendableWorker::suspendImpl() {
    _waitCondition.wait(&_waitMutex);
}

记住,挂起的线程永远不会收到退出事件。 由于这个原因,我们不能在普通的QThread中安全地使用它,除非我们在发布退出之前恢复线程。 让我们将其集成到自定义的Thread<T>模板中。  

template <typename TWorker>
class Thread : QThread
{
public:
    explicit Thread(TWorker *worker, QObject *parent = nullptr)
        : QThread(parent), _worker(worker) {
        _worker->moveToThread(this);
        start();
    }

    ~Thread() {
        resume();
        quit();
        wait();
    }

    void suspend() {
        auto worker = qobject_cast<SuspendableWorker*>(_worker);
        if (worker != nullptr) {
            worker->suspend();
        }
    }

    void resume() {
        auto worker = qobject_cast<SuspendableWorker*>(_worker);
        if (worker != nullptr) {
            worker->resume();
        }
    }

    TWorker worker() const {
        return _worker;
    }

protected:
    void run() override {
        QThread::*run*();
        delete _worker;
    }

private:
    TWorker *_worker;
};

有了这些改变,我们将在发布退出事件之前恢复线程。 另外,线程<TWorker>仍然允许传入任何类型的worker,无论它是否是一个SuspendableWorker。   

用法如下:  

LogService logService;
logService.logEvent("processed event");
logService.suspend();
logService.logEvent("queued event");
logService.resume();
// "queued event" is now processed.

 

volatile vs atomic

这是一个经常被误解的话题。 大多数人认为,volatile变量可以用于为多个线程访问的某些标志提供服务,这样可以避免数据竞争条件。 这是假的,QAtomic*类(或std::atomic)必须用于此目的。   

让我们考虑一个现实的例子:一个TcpConnection连接类,它工作在一个专用的线程中,我们希望这个类导出一个线程安全的方法:bool isConnected()。 在内部,类将监听套接字事件:connected和disconnected,以维护一个内部布尔值标志:  

// pseudo-code, won't compile
class TcpConnection : QObject
{
    Q_OBJECT 

public:
    // this is not thread-safe!
    bool isConnected() const {
        return _connected;
    }
    
private slots:
    void handleSocketConnected() {
        _connected = true;
    }
    
    void handleSocketDisconnected() {
        _connected = false;
    }
    
private:
    bool _connected;
}

让_connected成员volatile不会解决这个问题,也不会让isConnected()成为线程安全的。 这个方法在99%的情况下都有效,但是剩下的1%会让你的生活变成一场噩梦。 要解决这个问题,我们需要保护变量访问不受多线程的影响。 我们使用QReadWriteLocker来实现这个目的:  

// pseudo-code, won't compile
class TcpConnection : QObject
{
    Q_OBJECT 

public:
    bool isConnected() const {
        QReadLocker locker(&_lock);
        return _connected;
    }
    
private slots:
    void handleSocketConnected() {
        QWriteLocker locker(&_lock);
        _connected = true;
    }
    
    void handleSocketDisconnected() {
        QWriteLocker locker(&_lock);
        _connected = false;
    }
    
private:
    QReadWriteLocker _lock;
    bool _connected;
}

 这可以可靠地工作,但不如使用“无锁”原子操作快。 第三种解决方案既快又线程安全(例子是使用std::atomic而不是QAtomicInt,但它们在语义上是相同的):  

// pseudo-code, won't compile
class TcpConnection : QObject
{
    Q_OBJECT 

public:
    bool isConnected() const {
        return _connected;
    }
    
private slots:
    void handleSocketConnected() {
        _connected = true;
    }
    
    void handleSocketDisconnected() {
        _connected = false;
    }
    
private:
    std::atomic<bool> _connected;
}
posted @ 2022-04-07 09:03  菜鸟_IceLee  阅读(103)  评论(0编辑  收藏  举报