Qt之Threads和QObjects

简述

QThread继承自QObject,它发射信号(signals)以表明线程执行开始或结束,并提供了一些槽函数(slots)。

更有趣的是,QObjects可以在多线程中使用,发射信号以在其它线程中调用槽函数,并且向“存活”于其它线程中的对象发送事件(post events)。这是可能的,因为每一个线程都拥有它自身的事件循环(event loop)。

QObject可重入性

QObject是可重入的。它的大多数非GUI子类,例如:QTimer、QTcpSocket、QUdpSocket和QProcess,也都是可重入的,这使得在多线程中同时使用这些类成为可能。注意:这些类被设计成在单一线程中创建和使用的,在一个线程中创建一个对象而在另一个线程中调用该对象的函数,不保证能行得通。需要注意有三个约束:

  • 一个QObject类型的孩子必须总是被创建在它的父亲所被创建的线程中。这意味着,除了别的以外,永远不要把QThread对象(this)作为该线程中创建的一个对象的父亲(因为QThread对象自身被创建在另外一个线程中)。

  • 事件驱动的对象可能只能被用在一个单线程中。特别是,这适用于计时器机制(timer mechanism)和网络模块。例如:你不能在不属于这个对象的线程中启动一个定时器或连接一个socket,必须保证在删除QThread之前删除所有创建在这个线程中的对象。在run()函数的实现中,通过在栈中创建这些对象,可以轻松地做到这一点。

  • 虽然QObject是可重入的,但GUI类,尤其是QWidget及其所有子类都不是可重入的,它们只能被用在主线程中。如前面所述,QCoreApplication::exec()必须也从那个线程被调用。

在实践中,只能在主线程而非其它线程中使用GUI的类这一点,可以很轻易地被解决:将耗时操作放在一个单独的worker线程中,当worker线程结束后在主线程中由屏幕显示结果。这个方法被用来实现Mandelbrot Example和the Blocking Fortune Client Example。

一般来说,在QApplication之前就创建QObject是不行的,会导致奇怪的崩溃或退出,取决于平台。这意味着,也不支持QObject的静态实例。一个单线程或多线程的应用程序应该先创建QApplication,并最后销毁QObject。

每个线程的事件循环

每个线程都有它自己的事件循环。初始线程通过QCoreApplication::exec()来启动它的事件循环, 或者对于单对话框的GUI应用程序,有些时候用QDialog::exec(),其它线程可以用QThread::exec()来启动事件循环。就像QCoreApplication,QThread提供一个exit(int)函数和quit()槽函数.

一个线程中的事件循环使得该线程可以利用一些非GUI的、要求有事件循环存在的Qt类(例如:QTimer、QTcpSocket、和QProcess)。它也使得连接一些线程的信号到一个特定线程的槽函数成为可能。这一点将会在下面的“跨线程的信号和槽”有详细介绍。

这里写图片描述

一个QObject实例被称为存活于它所被创建的线程中。关于这个对象的事件被分发到该线程的事件循环中。可以用QObject::thread()方法获取一个QObject所处的线程。

QObject::moveToThread()函数改变一个对象和它的孩子的线程所属性。(如果该对象有父亲的话,它不能被移动到其它线程中)。

从另一个线程(不是该QObject对象所属的线程)对该QObject对象调用delete方法是不安全的,除非你能保证该对象在那个时刻不处理事件,使用QObejct::deleteLater()更好。一个DeferredDelete类型的事件将被提交(posted),而该对象的线程的事件循环最终会处理这个事件。默认情况下,拥有一个QObject的线程就是创建QObject的那个线程,而不是QObject::moveToThread()被调用后的。

如果没有事件循环运行,事件将不会传递给对象。例如:你在一个线程中创建了一个QTimer对象,但从没有调用exec(),那么,QTimer就永远不会发射timeout()信号,即使调用deleteLater()也不行。(这些限制也同样适用于主线程)。

利用线程安全的方法QCoreApplication::postEvent(),你可以在任何时刻给任何线程中的任何对象发送事件,这些事件将自动被分发到该对象所被创建的线程事件循环中。

所有的线程都支持事件过滤器,而限制是监控对象必须和被监控对象存在于相同的线程中。类似的,QCoreApplication::sendEvent()(不同于postEvent())只能将事件分发到和该函数调用者相同的线程中的对象。

从其它线程访问QObject子类

QObject及其所有子类都不是线程安全的。这包含了整个事件交付系统。重要的是,切记事件循环可能正在向你的QObject子类发送事件,当你从另一个线程访问该对象时。

如果你正在调用一个QObject子类的函数,而该子类对象并不存活于当前线程中,并且该对象是可以接收事件的,那么你必须用一个mutex保护对该QObject子类的内部数据的所有访问,否则,就有可能发生崩溃和非预期的行为。

同其它对象一样,QThread对象存活于该对象被创建的线程中 – 而并非是在QThread::run()被调用时所在的线程。一般来说,在QThread子类中提供槽函数是不安全的,除非用一个mutex保护成员变量。

另一方面,可以在QThread::run()的实现中安全地发射信号,因为信号发射是线程安全的。

跨线程的信号和槽

Qt支持如下的信号-槽连接类型:

  • Auto Connection(默认):如果信号在接收者所依附的线程内发射,则等同于Direct Connection。否则,等同于Queued Connection。

  • Direct Connection:当信号发射后,槽函数立即被调用。槽函数在信号发射者所在的线程中执行,而未必需要在接收者的线程中。

  • Queued Connection:当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接收者的线程中执行。

  • Blocking Queued Connection:槽函数的调用情形和Queued Connection相同,不同的是当前的线程会阻塞住,直到槽函数返回。
    注意:在同一个线程中使用这种类型进行连接会导致死锁。

  • Unique Connection:行为与Auto Connection相同,但是连接只会在“不会与已存在的连接相同”时建立,也就是:如果相同的信号已经被连接到相同的槽函数,那么连接就不会被再次建立,并且connect()会返回false。

通过传递一个参数给connect()来指定连接类型。要知道,如果一个事件循环运行在接收者的线程中,而发送者和接收者位于不同的线程时,使用Direct Connection是不安全的。同样的原因,调用存活于另一个线程中的对象的任何函数都是不安全的。

QObject::connect()本身是线程安全的。

Mandelbrot Example使用了Queued Connection来连接一个worker线程和主线程。为了避免冻结主线程的事件循环(即避免因此而冻结了应用的UI),所有的曼德尔布罗特分形计算(Mandelbrot fractal computation)都是在一个单独的worker线程中完成的,线程结束一个计算时发射一个信号。

同样,Blocking Fortune Client Example使用了一个单独的线程来和TCP server进行异步通信。

<think>我们正在讨论Qt框架中常量的声明使用。在C++中,常量通常使用`const`关键字或预处理器宏`#define`来定义。然而,在Qt中,我们也可以使用Qt特有的机制,比如使用命名空间枚举。此外,由于Qt是C++的扩展,所以C++中定义常量的方法在Qt中同样适用。 下面我们将介绍几种在Qt中声明使用常量的方法: ### 1. 使用`const`关键字 这是C++中最常用的定义常量的方法。在Qt中,我们同样可以在类中、函数中或全局作用域中定义`const`常量。 **示例:** ```cpp // 全局常量 const int MAX_WIDTH = 1024; const int MAX_HEIGHT = 768; class MyWidget : public QWidget { public: // 类内常量 static const int DEFAULT_WIDTH = 800; static const int DEFAULT_HEIGHT = 600; void myFunction() { // 局部常量 const int localConst = 100; // ... } }; ``` 注意:对于类内的静态常量(static const),如果类型是整数(如int, char, bool等)或枚举类型,可以在类内直接初始化(如上面的例子)。对于非整数类型(如double)或者需要取地址的情况,则需要在类外进行定义(但不需再次初始化,除非是C++17以后的版本,内联变量允许在类内定义)。 ### 2. 使用枚举 在类中,我们可以使用枚举类型来定义一组相关的常量。 **示例:** ```cpp class MyClass { public: enum { MaxCount = 100, BufferSize = 1024 }; // 或者使用强类型枚举(C++11) enum class Limits : int { MaxCount = 100, BufferSize = 1024 }; }; ``` ### 3. 使用`#define`宏 虽然`#define`是预处理器宏,不是类型安全的,但在某些情况下(如条件编译)仍然被使用。在Qt中,我们也可以使用它来定义常量。 **示例:** ```cpp #define MAX_WIDTH 1024 #define MAX_HEIGHT 768 ``` 但是,由于宏没有作用域,且容易引起命名冲突,所以通常推荐使用`const`常量。 ### 4. 使用`constexpr`(C++11起) C++11引入了`constexpr`关键字,用于定义编译期常量。在Qt中,如果使用C++11或更高版本,我们可以使用`constexpr`。 **示例:** ```cpp constexpr int MAX_WIDTH = 1024; constexpr int MAX_HEIGHT = 768; class Circle { public: constexpr static double PI = 3.1415926535; }; ``` ### 5. 使用Qt的命名空间 Qt本身在全局命名空间中定义了一些常量,例如在`Qt`命名空间中有一些枚举值(如`Qt::AlignLeft`)。我们也可以在自己的命名空间中定义常量。 **示例:** ```cpp namespace MyConstants { const int MAX_WIDTH = 1024; const int MAX_HEIGHT = 768; } ``` ### 6. 使用Q_GLOBAL_STATIC宏(用于全局静态对象) 如果常量是一个对象(比如QString),并且我们希望它是全局唯一的,可以使用`Q_GLOBAL_STATIC`来创建线程安全的全局静态对象。 **示例:** ```cpp Q_GLOBAL_STATIC(QString, globalString) // 使用 QString str = *globalString(); ``` 但是,对于常量来说,通常我们使用`const`修饰的全局变量即可,因为它是只读的,所以线程安全。 ### 在Qt中使用常量 在Qt中,我们可以在任何需要的地方使用这些常量,比如在设置窗口大小时: ```cpp MyWidget::MyWidget(QWidget *parent) : QWidget(parent) { resize(MyWidget::DEFAULT_WIDTH, MyWidget::DEFAULT_HEIGHT); } ``` ### 注意事项 - 避免在头文件中定义非静态的全局常量(除非是内部链接的,如加上`static`或放在匿名命名空间内),否则当多个源文件包含该头文件时会导致重复定义。 - 推荐在头文件中使用`constexpr`、`static const`成员、枚举或者内联函数来定义常量。 - 对于字符串常量,可以使用`const char*`、`const QString`或者`QLatin1String`(用于字符串字面量)等。 ### 总结 在Qt中声明使用常量与标准C++基本相同,主要使用`const`、`constexpr`、枚举`static`成员等方式。根据常量的作用域类型选择合适的方式,并注意避免重复定义命名冲突。 §§相关问题§§ 1. Qt中如何定义全局常量? 2. 在Qt的类中,如何声明一个常量成员? 3. Qt中`constexpr``const`有什么区别? 4. 如何在Qt中使用枚举类型定义常量? 5. Qt中定义字符串常量有哪些方法?</think>### Qt框架中声明使用常量的方法 在Qt框架中声明使用常量主要遵循标准C++语法规则,同时结合Qt特有的机制。以下是几种常见方法: #### 1. **使用`const`关键字**(推荐) ```cpp // 声明常量 const int MAX_WIDTH = 1024; const double PI = 3.1415926; const QString APP_NAME = "MyQtApp"; // 类内常量 class MyClass : public QObject { Q_OBJECT public: static const int MAX_COUNT = 100; // 整型常量可直接初始化 static const QString DEFAULT_NAME; }; // 类外定义(非整型需在.cpp中定义) const QString MyClass::DEFAULT_NAME = "Default"; ``` #### 2. **使用`constexpr`**(C++11及以上) ```cpp constexpr int BUFFER_SIZE = 512; // 编译期常量 constexpr QSize DEFAULT_WINDOW_SIZE(800, 600); ``` #### 3. **使用枚举** ```cpp enum Limits { MAX_USERS = 10, TIMEOUT_MS = 5000 }; // C++11强类型枚举 enum class Config : int { MAX_THREADS = 8, CACHE_SIZE = 1024 }; ``` #### 4. **命名空间常量** ```cpp namespace AppConstants { const int VERSION_MAJOR = 1; const int VERSION_MINOR = 0; const QString LOG_PATH = "/var/log"; } // 使用 int ver = AppConstants::VERSION_MAJOR; ``` #### 5. **Qt特有的常量机制** ```cpp // Q_GLOBAL_STATIC创建线程安全单例常量 Q_GLOBAL_STATIC(QString, globalConfigPath) void initApp() { *globalConfigPath() = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); } ``` #### 使用场景示例 ```cpp // 在Qt组件中使用 void MainWindow::initUI() { resize(AppConstants::DEFAULT_WIDTH, AppConstants::DEFAULT_HEIGHT); statusBar()->showMessage(tr("Version: %1").arg(AppConstants::VERSION)); } // 信号槽连接中使用 connect(button, &QPushButton::clicked, [MAX = Limits::MAX_USERS]() { qDebug() << "Max users allowed:" << MAX; }); ``` #### 最佳实践建议: 1. **优先选择`const`或`constexpr`**:类型安全且支持作用域控制 2. **避免`#define`宏常量**:缺乏类型检查且易污染全局命名空间 3. **类内常量使用`static const`**:确保每个类只有一份实例 4. **复杂常量使用单例模式**:通过`Q_GLOBAL_STATIC`管理初始化顺序 5. **字符串常量使用`QLatin1String`**:减少内存分配(适用于静态字符串) ```cpp constexpr auto DB_NAME = QLatin1String("app_database"); ``` #### 注意事项: - 在头文件中声明常量时,若非整型需在实现文件中定义 - 线程间共享的常量需确保线程安全性(使用原子操作或互斥锁) - Qt元对象系统(如Q_PROPERTY)要求常量在编译时已知 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值