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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现